> ## Documentation Index
> Fetch the complete documentation index at: https://docs.clinkbill.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 解密

> 使用解密范例读取支付凭证，并填写商户结账所需的卡信息。

`get_credential` 返回 `credential.key` 与 `credential.payload` 时，使用本页范例。先按[云端平台接入](/cn/guides/agent/platform_integration)线下联系 Clink 配置平台公钥，再使用对应私钥解密。

如果响应已含 `credential.card`，直接继续 [Browser Pay](/cn/guides/agent/browser_pay) 流程，无需解密。

## 解密凭证

复制下方折叠区中的函数，传入私钥及同一次响应中的 `key`、`payload` 原值。返回的 `body` 包含支付信息和结果上报所需的标识。

```python theme={null}
body = decrypt_credential(
    private_key_pem,
    credential["key"],
    credential["payload"],
)
```

<Accordion title="完整 Python 解密范例">
  使用 Python 3.10+，先安装依赖：

  ```bash theme={null}
  python -m pip install cryptography
  ```

  `private_key_pem` 接受未加密的 RSA PEM 私钥字节。密钥加载方式按平台现有实现调整。函数解密失败时抛出异常，不返回支付数据。

  ```python theme={null}
  import base64
  import binascii
  import json
  from typing import Any

  from cryptography.exceptions import InvalidTag, UnsupportedAlgorithm
  from cryptography.hazmat.primitives import hashes, serialization
  from cryptography.hazmat.primitives.asymmetric import padding, rsa
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM


  def _decode_base64(value: str) -> bytes:
      if not isinstance(value, str):
          raise TypeError("Expected a Base64 string")
      return base64.b64decode("".join(value.split()), validate=True)


  def decrypt_credential(
      private_key_pem: bytes, key: str, payload: str
  ) -> dict[str, Any]:
      try:
          private_key = serialization.load_pem_private_key(
              private_key_pem, password=None
          )
          if not isinstance(private_key, rsa.RSAPrivateKey):
              raise ValueError("Expected an RSA private key")

          aes_key_text = private_key.decrypt(
              _decode_base64(key),
              padding.OAEP(
                  mgf=padding.MGF1(algorithm=hashes.SHA1()),
                  algorithm=hashes.SHA256(),
                  label=b"",
              ),
          )
          aes_key = _decode_base64(aes_key_text.decode("ascii"))
          if len(aes_key) != 32:
              raise ValueError("Expected a 32-byte AES key")

          envelope = _decode_base64(payload)
          if len(envelope) <= 12 + 16:
              raise ValueError("Invalid payload length")
          iv = envelope[:12]
          ciphertext_and_tag = envelope[12:]
          plaintext = AESGCM(aes_key).decrypt(iv, ciphertext_and_tag, None)
          body = json.loads(plaintext.decode("utf-8"))
          if not isinstance(body, dict):
              raise ValueError("Expected a JSON object")
          return body
      except (
          ValueError,
          TypeError,
          binascii.Error,
          InvalidTag,
          UnsupportedAlgorithm,
      ):
          raise ValueError("Credential decryption failed") from None
  ```
</Accordion>

## 读取支付信息

使用 `body` 中实际返回的字段：

| 字段 | 用途 |
| :- | :- |
| `orderId` | 传入 `confirm_result.orderId`。 |
| `paymentExecutionDetailId` | 传入 `confirm_result.paymentExecutionDetailId`。 |
| `amount`、`currencyCode` | 核对本次结账的金额与币种。 |
| `cardholderName`、`billingAddress` | 返回时用于填写对应商户字段。`billingAddress` 可能是 JSON 字符串，需要先解析。 |
| `status` | 凭证签发状态，不代表商户付款成功。 |

同时保留 `get_credential` 响应外层的 `operation_id`，用于 `confirm_result.operationId`。

<Note>
  解密后的 `amount` 使用主货币单位，例如 USD 12.50；`get_credential.amount` 和 `confirm_result.paidAmount` 使用最小货币单位，对应为 `1250`。上报实际付款金额时按币种精度转换，不要直接复制解密后的金额。
</Note>

## 填写卡信息

解密后的 `body` 包含 `signedPayload` 时，使用下面的辅助函数读取其中的卡字段。先复制折叠区中的函数，再调用：

```python theme={null}
dynamic = read_dynamic_data(body["signedPayload"])
```

<Accordion title="卡字段读取范例">
  与上方解密范例共用导入。此函数只读取 `signedPayload` 中的字段，不验证 JWT 签名。

  ```python theme={null}
  def read_dynamic_data(signed_payload: str) -> dict[str, Any]:
      # Decodes JWT claims; does not verify the JWT signature.
      try:
          parts = signed_payload.split(".")
          if len(parts) != 3:
              raise ValueError("Expected a JWT")
          claims_bytes = base64.b64decode(
              parts[1] + "=" * (-len(parts[1]) % 4),
              altchars=b"-_",
              validate=True,
          )
          claims = json.loads(claims_bytes.decode("utf-8"))
          dynamic_data = claims["dynamicData"][0]
          if not isinstance(dynamic_data, dict):
              raise ValueError("Expected dynamicData")
          return dynamic_data
      except (ValueError, TypeError, KeyError, IndexError, binascii.Error):
          raise ValueError("Invalid credential claims") from None
  ```
</Accordion>

| `dynamic` 内的字段 | 商户结账用途 |
| :- | :- |
| `token.paymentToken` | 卡号 |
| `token.tokenExpirationMonth` | 到期月份，补齐为两位数 |
| `token.tokenExpirationYear` | 到期年份；表单要求 `MM/YY` 时取年份后两位 |
| `dynamicDataValue` | 安全码 |
| `dynamicDataExpiration` | 凭证到期时间，使用 Unix 秒 |

提交前检查卡号、安全码和到期字段是否齐全，并在凭证有效期内使用。后续提交与结果上报见 [Browser Pay](/cn/guides/agent/browser_pay)。

## 处理解密失败

无法解密时，先检查私钥是否匹配当前平台及环境配置的公钥，以及 `key`、`payload` 是否来自同一完整响应。仍然失败或缺少支付字段时，停止提交并联系 Clink 排查。

已签发凭证但未提交支付时，使用可取得的订单与 Operation 标识调用 `confirm_result`，上报 `NOT_SUBMITTED`；需要提供 `failureReason` 时使用 `OTHER`。标识不可用或需要再次获取凭证时，按 [Browser Pay 恢复流程](/cn/guides/agent/browser_pay)处理。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.