|
@@ -0,0 +1,382 @@
|
|
|
|
|
+package com.train.service;
|
|
|
|
|
+
|
|
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import okhttp3.MediaType;
|
|
|
|
|
+import okhttp3.OkHttpClient;
|
|
|
|
|
+import okhttp3.Request;
|
|
|
|
|
+import okhttp3.RequestBody;
|
|
|
|
|
+import okhttp3.Response;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.PostConstruct;
|
|
|
|
|
+import javax.crypto.Cipher;
|
|
|
|
|
+import javax.crypto.spec.GCMParameterSpec;
|
|
|
|
|
+import javax.crypto.spec.SecretKeySpec;
|
|
|
|
|
+import java.io.BufferedReader;
|
|
|
|
|
+import java.io.File;
|
|
|
|
|
+import java.io.FileInputStream;
|
|
|
|
|
+import java.io.InputStream;
|
|
|
|
|
+import java.io.InputStreamReader;
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.security.KeyFactory;
|
|
|
|
|
+import java.security.PrivateKey;
|
|
|
|
|
+import java.security.Signature;
|
|
|
|
|
+import java.security.spec.PKCS8EncodedKeySpec;
|
|
|
|
|
+import java.util.Base64;
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+import java.util.UUID;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 微信支付 V3 客户端(对齐 cfc E:\cfc 的 PaymentService 约定)。
|
|
|
|
|
+ * <p>API 版本:微信支付 API v3(JSON + RSA 签名 + AES-256-GCM 回调解密)。
|
|
|
|
|
+ * <p>test-mode=true 时跳过真实微信 API 调用(doPost 返回 mock prepay_id),
|
|
|
|
|
+ * 生产对接只需在 application.yml 配置商户号/密钥/证书后关闭 test-mode。
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Component
|
|
|
|
|
+public class WechatPayClient {
|
|
|
|
|
+
|
|
|
|
|
+ private static final String WECHAT_API_BASE = "https://api.mch.weixin.qq.com";
|
|
|
|
|
+ private static final MediaType JSON_MEDIA = MediaType.parse("application/json; charset=utf-8");
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${wechat.appid}")
|
|
|
|
|
+ private String appid;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${wechat.mch-id:}")
|
|
|
|
|
+ private String mchId;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${wechat.api-v3-key:}")
|
|
|
|
|
+ private String apiV3Key;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${wechat.mch-serial-no:}")
|
|
|
|
|
+ private String mchSerialNo;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${wechat.private-key-path:}")
|
|
|
|
|
+ private String privateKeyPath;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${wechat.notify-url:}")
|
|
|
|
|
+ private String notifyUrl;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${wechat.test-mode:true}")
|
|
|
|
|
+ private boolean testMode;
|
|
|
|
|
+
|
|
|
|
|
+ private PrivateKey privateKey;
|
|
|
|
|
+ private final OkHttpClient httpClient;
|
|
|
|
|
+
|
|
|
|
|
+ public WechatPayClient() {
|
|
|
|
|
+ httpClient = new OkHttpClient.Builder()
|
|
|
|
|
+ .connectTimeout(10, TimeUnit.SECONDS)
|
|
|
|
|
+ .readTimeout(10, TimeUnit.SECONDS)
|
|
|
|
|
+ .writeTimeout(10, TimeUnit.SECONDS)
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @PostConstruct
|
|
|
|
|
+ public void init() {
|
|
|
|
|
+ if (testMode) {
|
|
|
|
|
+ log.info("微信支付使用测试模式,跳过商户私钥加载");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (privateKeyPath == null || privateKeyPath.isEmpty()) {
|
|
|
|
|
+ log.warn("微信支付私钥路径未配置(wechat.private-key-path),仅 test-mode 可用");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ try (InputStream is = loadPrivateKeyStream()) {
|
|
|
|
|
+ if (is == null) {
|
|
|
|
|
+ log.error("微信支付私钥文件未找到: {}", privateKeyPath);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
|
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
|
|
+ String line;
|
|
|
|
|
+ while ((line = reader.readLine()) != null) {
|
|
|
|
|
+ if (!line.startsWith("-----")) {
|
|
|
|
|
+ sb.append(line.trim());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ byte[] keyBytes = Base64.getDecoder().decode(sb.toString());
|
|
|
|
|
+ PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
|
|
|
|
|
+ KeyFactory kf = KeyFactory.getInstance("RSA");
|
|
|
|
|
+ privateKey = kf.generatePrivate(spec);
|
|
|
|
|
+ log.info("微信支付商户私钥加载成功");
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("加载微信支付私钥失败: {}", e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 私钥加载:优先 classpath,其次绝对路径文件 */
|
|
|
|
|
+ private InputStream loadPrivateKeyStream() throws Exception {
|
|
|
|
|
+ String normal = privateKeyPath.replace("\\", "/");
|
|
|
|
|
+ InputStream is = getClass().getClassLoader().getResourceAsStream(normal);
|
|
|
|
|
+ if (is != null) {
|
|
|
|
|
+ return is;
|
|
|
|
|
+ }
|
|
|
|
|
+ File f = new File(privateKeyPath);
|
|
|
|
|
+ if (f.exists()) {
|
|
|
|
|
+ return new FileInputStream(f);
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信 JSAPI 下单(v3 /v3/pay/transactions/jsapi)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param orderNo 商户订单号
|
|
|
|
|
+ * @param description 商品描述
|
|
|
|
|
+ * @param totalFee 金额(分)
|
|
|
|
|
+ * @param openid 支付用户 openid(trade_type=JSAPI 必填)
|
|
|
|
|
+ * @return 小程序发起支付所需参数:appId/timeStamp/nonceStr/package/signType/paySign
|
|
|
|
|
+ */
|
|
|
|
|
+ public Map<String, Object> createJsapiPrepay(String orderNo, String description, Integer totalFee, String openid) {
|
|
|
|
|
+ JSONObject body = new JSONObject();
|
|
|
|
|
+ body.put("appid", appid);
|
|
|
|
|
+ body.put("mchid", mchId);
|
|
|
|
|
+ body.put("description", description);
|
|
|
|
|
+ body.put("out_trade_no", orderNo);
|
|
|
|
|
+ body.put("notify_url", notifyUrl);
|
|
|
|
|
+ JSONObject payer = new JSONObject();
|
|
|
|
|
+ payer.put("openid", openid);
|
|
|
|
|
+ body.put("payer", payer);
|
|
|
|
|
+ JSONObject amount = new JSONObject();
|
|
|
|
|
+ amount.put("total", totalFee);
|
|
|
|
|
+ amount.put("currency", "CNY");
|
|
|
|
|
+ body.put("amount", amount);
|
|
|
|
|
+
|
|
|
|
|
+ String respBody;
|
|
|
|
|
+ try {
|
|
|
|
|
+ respBody = doPost("/v3/pay/transactions/jsapi", body.toJSONString());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("微信统一下单请求异常: orderNo={}, err={}", orderNo, e.getMessage());
|
|
|
|
|
+ throw new RuntimeException("微信统一下单请求异常: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ JSONObject resp = JSON.parseObject(respBody);
|
|
|
|
|
+ String prepayId = resp.getString("prepay_id");
|
|
|
|
|
+ if (prepayId == null) {
|
|
|
|
|
+ log.error("微信统一下单失败: {}", respBody);
|
|
|
|
|
+ throw new RuntimeException("微信统一下单失败: "
|
|
|
|
|
+ + resp.getString("code") + " " + resp.getString("message"));
|
|
|
|
|
+ }
|
|
|
|
|
+ String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
|
|
|
|
|
+ String nonceStr = UUID.randomUUID().toString().replace("-", "");
|
|
|
|
|
+ String packageStr = "prepay_id=" + prepayId;
|
|
|
|
|
+ String paySign = generatePaySign(appid, timeStamp, nonceStr, packageStr);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("appId", appid);
|
|
|
|
|
+ result.put("timeStamp", timeStamp);
|
|
|
|
|
+ result.put("nonceStr", nonceStr);
|
|
|
|
|
+ result.put("package", packageStr);
|
|
|
|
|
+ result.put("signType", "RSA");
|
|
|
|
|
+ result.put("paySign", paySign);
|
|
|
|
|
+ log.info("微信预支付订单创建成功: orderNo={}, prepayId={}", orderNo, prepayId);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信支付回调解析(V3:AES-256-GCM 解密 resource)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param requestBody 回调 JSON body
|
|
|
|
|
+ * @return {outTradeNo, transactionId};解析失败返回 null
|
|
|
|
|
+ */
|
|
|
|
|
+ public Map<String, String> handleNotify(String requestBody) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ JSONObject body = JSON.parseObject(requestBody);
|
|
|
|
|
+ JSONObject resource = body == null ? null : body.getJSONObject("resource");
|
|
|
|
|
+ if (resource == null) {
|
|
|
|
|
+ log.error("微信通知数据缺少resource字段");
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ String ciphertext = resource.getString("ciphertext");
|
|
|
|
|
+ String associatedData = resource.getString("associated_data");
|
|
|
|
|
+ String nonce = resource.getString("nonce");
|
|
|
|
|
+ String plaintext = decryptAes256Gcm(ciphertext, associatedData, nonce);
|
|
|
|
|
+ JSONObject payResult = JSON.parseObject(plaintext);
|
|
|
|
|
+ Map<String, String> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("outTradeNo", payResult.getString("out_trade_no"));
|
|
|
|
|
+ result.put("transactionId", payResult.getString("transaction_id"));
|
|
|
|
|
+ log.info("微信支付通知解析: orderNo={}, transactionId={}",
|
|
|
|
|
+ result.get("outTradeNo"), result.get("transactionId"));
|
|
|
|
|
+ return result;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("处理微信支付通知异常: {}", e.getMessage());
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查询订单支付状态(v3 /v3/pay/transactions/out-trade-no/{no})。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return trade_state:SUCCESS / NOTPAY / CLOSED / REFUND 等;查询失败返回 null
|
|
|
|
|
+ */
|
|
|
|
|
+ public String queryTradeState(String outTradeNo) {
|
|
|
|
|
+ if (testMode) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ String path = "/v3/pay/transactions/out-trade-no/" + outTradeNo + "?mchid=" + mchId;
|
|
|
|
|
+ String respBody = doGet(path);
|
|
|
|
|
+ if (respBody == null || respBody.isEmpty()) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ JSONObject resp = JSON.parseObject(respBody);
|
|
|
|
|
+ return resp.getString("trade_state");
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("查询微信支付状态异常: orderNo={}, err={}", outTradeNo, e.getMessage());
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信退款(v3 /v3/refund/domestic/refunds)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param orderNo 原商户订单号
|
|
|
|
|
+ * @param outRefundNo 商户退款单号
|
|
|
|
|
+ * @param refundAmount 退款金额(分)
|
|
|
|
|
+ * @param totalAmount 原订单金额(分)
|
|
|
|
|
+ * @return 退款状态 SUCCESS/PROCESSING/CLOSED;失败抛异常
|
|
|
|
|
+ */
|
|
|
|
|
+ public String refund(String orderNo, String outRefundNo, Integer refundAmount, Integer totalAmount) {
|
|
|
|
|
+ JSONObject body = new JSONObject();
|
|
|
|
|
+ body.put("out_trade_no", orderNo);
|
|
|
|
|
+ body.put("out_refund_no", outRefundNo);
|
|
|
|
|
+ JSONObject amount = new JSONObject();
|
|
|
|
|
+ amount.put("refund", refundAmount);
|
|
|
|
|
+ amount.put("total", totalAmount);
|
|
|
|
|
+ amount.put("currency", "CNY");
|
|
|
|
|
+ body.put("amount", amount);
|
|
|
|
|
+ String respBody;
|
|
|
|
|
+ try {
|
|
|
|
|
+ respBody = doPost("/v3/refund/domestic/refunds", body.toJSONString());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("微信退款请求异常: orderNo={}, err={}", orderNo, e.getMessage());
|
|
|
|
|
+ throw new RuntimeException("微信退款请求异常: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ JSONObject resp = JSON.parseObject(respBody);
|
|
|
|
|
+ String status = resp.getString("status");
|
|
|
|
|
+ if (status == null) {
|
|
|
|
|
+ log.error("微信退款失败: orderNo={}, response={}", orderNo, respBody);
|
|
|
|
|
+ throw new RuntimeException("微信退款失败: " + resp.getString("code") + " " + resp.getString("message"));
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("微信退款结果: orderNo={}, outRefundNo={}, status={}", orderNo, outRefundNo, status);
|
|
|
|
|
+ return status;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public boolean isTestMode() {
|
|
|
|
|
+ return testMode;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public boolean isConfigured() {
|
|
|
|
|
+ return !testMode && mchId != null && !mchId.isEmpty();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String doGet(String path) throws Exception {
|
|
|
|
|
+ if (testMode) {
|
|
|
|
|
+ log.info("【测试模式】跳过微信API请求: GET {}", path);
|
|
|
|
|
+ return "{\"code\":\"TEST\",\"message\":\"mock\"}";
|
|
|
|
|
+ }
|
|
|
|
|
+ String url = WECHAT_API_BASE + path;
|
|
|
|
|
+ String auth = buildAuthHeader("GET", path, "");
|
|
|
|
|
+ Request request = new Request.Builder()
|
|
|
|
|
+ .url(url)
|
|
|
|
|
+ .header("Authorization", auth)
|
|
|
|
|
+ .header("Accept", "application/json")
|
|
|
|
|
+ .header("User-Agent", "train-backend")
|
|
|
|
|
+ .get()
|
|
|
|
|
+ .build();
|
|
|
|
|
+ try (Response response = httpClient.newCall(request).execute()) {
|
|
|
|
|
+ String respBody = response.body() != null ? response.body().string() : "";
|
|
|
|
|
+ if (!response.isSuccessful()) {
|
|
|
|
|
+ log.error("微信API GET请求失败: status={}, path={}, body={}", response.code(), path, respBody);
|
|
|
|
|
+ }
|
|
|
|
|
+ return respBody;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String doPost(String path, String body) throws Exception {
|
|
|
|
|
+ if (testMode) {
|
|
|
|
|
+ log.info("【测试模式】跳过微信API请求: POST {}", path);
|
|
|
|
|
+ return "{\"code\":\"TEST\",\"message\":\"mock\",\"prepay_id\":\"mock_prepay_id_" + System.currentTimeMillis() + "\"}";
|
|
|
|
|
+ }
|
|
|
|
|
+ String url = WECHAT_API_BASE + path;
|
|
|
|
|
+ String auth = buildAuthHeader("POST", path, body);
|
|
|
|
|
+ Request request = new Request.Builder()
|
|
|
|
|
+ .url(url)
|
|
|
|
|
+ .header("Authorization", auth)
|
|
|
|
|
+ .header("Accept", "application/json")
|
|
|
|
|
+ .header("Content-Type", "application/json")
|
|
|
|
|
+ .header("User-Agent", "train-backend")
|
|
|
|
|
+ .post(RequestBody.create(JSON_MEDIA, body))
|
|
|
|
|
+ .build();
|
|
|
|
|
+ try (Response response = httpClient.newCall(request).execute()) {
|
|
|
|
|
+ String respBody = response.body() != null ? response.body().string() : "";
|
|
|
|
|
+ if (!response.isSuccessful()) {
|
|
|
|
|
+ log.error("微信API POST请求失败: status={}, path={}, body={}", response.code(), path, respBody);
|
|
|
|
|
+ }
|
|
|
|
|
+ return respBody;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String buildAuthHeader(String method, String path, String body) throws Exception {
|
|
|
|
|
+ if (privateKey == null) {
|
|
|
|
|
+ log.warn("微信支付私钥未配置,使用模拟认证头");
|
|
|
|
|
+ return "WECHATPAY2-SHA256-RSA2048 mchid=\"" + mchId
|
|
|
|
|
+ + "\",nonce_str=\"mock\",timestamp=\"0\",serial_no=\"mock\",signature=\"mock\"";
|
|
|
|
|
+ }
|
|
|
|
|
+ String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
|
|
|
|
|
+ String nonce = UUID.randomUUID().toString().replace("-", "");
|
|
|
|
|
+ String message = method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + body + "\n";
|
|
|
|
|
+ Signature sign = Signature.getInstance("SHA256withRSA");
|
|
|
|
|
+ sign.initSign(privateKey);
|
|
|
|
|
+ sign.update(message.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ String signature = Base64.getEncoder().encodeToString(sign.sign());
|
|
|
|
|
+ return "WECHATPAY2-SHA256-RSA2048 "
|
|
|
|
|
+ + "mchid=\"" + mchId + "\","
|
|
|
|
|
+ + "nonce_str=\"" + nonce + "\","
|
|
|
|
|
+ + "timestamp=\"" + timestamp + "\","
|
|
|
|
|
+ + "serial_no=\"" + mchSerialNo + "\","
|
|
|
|
|
+ + "signature=\"" + signature + "\"";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String generatePaySign(String appId, String timeStamp, String nonceStr, String packageStr) {
|
|
|
|
|
+ if (privateKey == null) {
|
|
|
|
|
+ return "mock_pay_sign_" + System.currentTimeMillis();
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ String message = appId + "\n" + timeStamp + "\n" + nonceStr + "\n" + packageStr + "\n";
|
|
|
|
|
+ Signature sign = Signature.getInstance("SHA256withRSA");
|
|
|
|
|
+ sign.initSign(privateKey);
|
|
|
|
|
+ sign.update(message.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ return Base64.getEncoder().encodeToString(sign.sign());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("生成支付签名失败", e);
|
|
|
|
|
+ throw new RuntimeException("生成支付签名失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 微信支付 API v3 回调解密(APIv3 密钥作为 AES-256 key,需恰好 32 字节) */
|
|
|
|
|
+ public String decryptAes256Gcm(String ciphertext, String associatedData, String nonce) throws Exception {
|
|
|
|
|
+ if (apiV3Key == null || apiV3Key.isEmpty()) {
|
|
|
|
|
+ log.warn("API V3密钥未配置,使用模拟解密");
|
|
|
|
|
+ return "{\"out_trade_no\":\"mock\",\"transaction_id\":\"mock\",\"trade_state\":\"SUCCESS\"}";
|
|
|
|
|
+ }
|
|
|
|
|
+ byte[] aesKey = apiV3Key.getBytes(StandardCharsets.UTF_8);
|
|
|
|
|
+ if (aesKey.length != 32) {
|
|
|
|
|
+ log.warn("API V3密钥长度不为32字节,实际长度={}", aesKey.length);
|
|
|
|
|
+ }
|
|
|
|
|
+ SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
|
|
|
|
|
+ GCMParameterSpec gcmSpec = new GCMParameterSpec(128, Base64.getDecoder().decode(nonce));
|
|
|
|
|
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
|
|
|
|
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
|
|
|
|
|
+ if (associatedData != null && !associatedData.isEmpty()) {
|
|
|
|
|
+ cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ }
|
|
|
|
|
+ byte[] plaintext = cipher.doFinal(Base64.getDecoder().decode(ciphertext));
|
|
|
|
|
+ return new String(plaintext, StandardCharsets.UTF_8);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|