针对商家的付款数据加密

Google Pay API 会以签名的加密 PaymentMethodToken 载荷返回付款方式。返回的付款方式是由 PAN 组成的卡,或由设备 PAN 和密码组成的令牌化卡。

负载中含有一个名为 protocolVersion 的字段,该字段会告诉负载接收者正在使用哪些加密基元以及预期的格式。

本指南介绍了如何生成公钥以请求由 Google 签名和加密的付款方式令牌,并详述了验证和解密令牌的步骤。

本指南仅适用于 protocolVersion = ECv2

由于您直接接收支付卡信息,因此请确保您的应用符合 PCI DSS,并确保您的服务器具有所需的基础架构,可在您继续操作之前安全地处理用户的付款凭据。

以下步骤概述了集成商在使用 Google Pay API ECv2 PaymentMethodToken 载荷时必须执行的操作:

  1. 获取 Google 根签名密钥
  2. 通过任何未到期的根签名密钥,验证中间签名密钥的签名是否有效。
  3. 验证负载的中间签名密钥是否到期。
  4. 通过中间签名密钥,验证载荷的签名是否有效。
  5. 在验证签名后解密载荷的内容。
  6. 验证消息是否到期。您需要检查当前时间是否早于解密内容中的 messageExpiration 字段。
  7. 使用解密内容中的付款方式并扣取费用。

Tink 库中的示例代码可执行第 1-6 步。

付款方式令牌结构

Google 在 PaymentData 响应中返回的消息是 UTF-8 编码的序列化 JSON 对象,具有下表中指定的键:

名称 类型 说明
protocolVersion 字符串 标识创建消息时所使用的加密或签名架构。如果需要,您可以通过此参数让协议随时间演进。
signature 字符串 验证消息是否来自 Google。此参数采用 Base64 编码,并通过中间签名密钥使用 ECDSA 创建而成。
intermediateSigningKey 对象 含有 Google 所提供的中间签名密钥的 JSON 对象。它包含带 keyValuekeyExpirationsignaturessignedKey。已经过序列化处理,可简化中间签名密钥的签名验证过程。
signedMessage 字符串 已序列化为 HTML 安全字符串的 JSON 对象,其中包含 encryptedMessageephemeralPublicKeytag。它已经过序列化处理,可简化签名验证过程。

示例

以下是采用 JSON 格式的付款方式令牌响应:

{
  "protocolVersion":"ECv2",
  "signature":"MEQCIH6Q4OwQ0jAceFEkGF0JID6sJNXxOEi4r+mA7biRxqBQAiAondqoUpU/bdsrAOpZIsrHQS9nwiiNwOrr24RyPeHA0Q\u003d\u003d",
  "intermediateSigningKey":{
    "signedKey": "{\"keyExpiration\":\"1542323393147\",\"keyValue\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw\\u003d\\u003d\"}",
    "signatures": ["MEYCIQCO2EIi48s8VTH+ilMEpoXLFfkxAwHjfPSCVED/QDSHmQIhALLJmrUlNAY8hDQRV/y1iKZGsWpeNmIP+z+tCQHQxP0v"]
  },
  "signedMessage":"{\"tag\":\"jpGz1F1Bcoi/fCNxI9n7Qrsw7i7KHrGtTf3NrRclt+U\\u003d\",\"ephemeralPublicKey\":\"BJatyFvFPPD21l8/uLP46Ta1hsKHndf8Z+tAgk+DEPQgYTkhHy19cF3h/bXs0tWTmZtnNm+vlVrKbRU9K8+7cZs\\u003d\",\"encryptedMessage\":\"mKOoXwi8OavZ\"}"
}

中间签名密钥

intermediateSigningKey 是一个采用 UTF-8 编码且包含以下值的序列化 JSON 对象:

名称 类型 说明
signedKey 字符串 采用 Base64 编码的消息,其中包含该密钥的付款说明。
signatures 字符串 验证中间签名密钥是否来自 Google。它采用 Base64 编码并使用 ECDSA 创建而成。

已签名的密钥

signedKey 是一个采用 UTF-8 编码且包含以下值的序列化 JSON 对象:

名称 类型 说明
keyValue 字符串 以 ASN.1 类型编码的 Base64 版密钥。SubjectPublicKeyInfo 按照 X.509 标准进行定义。
keyExpiration 字符串 中间密钥到期的日期和时间(采用世界协调时间,以毫秒为单位,从 Epoch 起算)。集成商拒绝任何已过期的密钥。

已签名的消息

signedMessage 是一个采用 UTF-8 编码且包含以下值的序列化 JSON 对象:

名称 类型 说明
encryptedMessage 字符串 采用 Base64 编码的加密消息,其中包含付款信息和一些附加的安全字段
ephemeralPublicKey 字符串 与私钥相关联、采用 Base64 编码的临时公钥,用于以未压缩的点格式对消息加密。如需了解详情,请参阅加密公钥格式
tag 字符串 encryptedMessage 的 MAC,采用 Base64 编码。

加密的消息

解密后的 encryptedMessage 是一个采用 UTF-8 编码的序列化 JSON 对象。该 JSON 共有两层。外层包含元数据和出于安全目的而加入的字段,而内层则是另一个代表实际付款凭据的 JSON 对象。

如需详细了解 encryptedMessage,请参阅以下表格和 JSON 对象示例:

名称 类型 说明
gatewayMerchantId 字符串

商家的唯一标识符,处理方将解读并使用该标识符来验证消息是否针对提出请求的商家。该标识符由处理方创建,并由商家在 AndroidWeb 上利用 PaymentMethodTokenizationSpecification 传递给 Google。

messageExpiration 字符串 消息到期的日期和时间(采用世界协调时间,以毫秒为单位,从 Epoch 起算)。集成商应拒绝所有过期的消息。
messageId 字符串 用于标识消息的唯一 ID,可在日后需要撤消或定位消息时使用。
paymentMethod 字符串 付款凭据的类型。目前只支持 CARD
paymentMethodDetails 对象 付款凭据本身。此对象的格式由 paymentMethod 决定,详情请见下表。

以下属性构成了 CARD 付款方式的付款凭据:

名称 类型 说明
pan 字符串 扣费的个人帐号。该字符串仅包含数字。
expirationMonth 数字 卡的到期月份,其中 1 代表 1 月,2 代表 2 月,依此类推。
expirationYear 数字 卡的四位数到期年份,例如 2020。
authMethod 字符串 卡交易的身份验证方法。
assuranceDetails AssuranceDetailsSpecifications 此对象可提供对返回的付款凭据执行验证的相关信息。

PAN_ONLY

以下 JSON 代码段示例展示了针对包含 PAN_ONLY authMethodCARD paymentMethod 的完整 encryptedMessage

  "paymentMethod": "CARD",
  "paymentMethodDetails": {
    "authMethod": "PAN_ONLY",
    "pan": "1111222233334444",
    "expirationMonth": 10,
    "expirationYear": 2020
  },  "gatewayMerchantId": "some-merchant-id",  "messageId": "some-message-id",
  "messageExpiration": "1577862000000"
}

CRYPTOGRAM_3DS

采用 3D 安全密文 (CRYPTOGRAM_3DS authMethod) 进行身份验证的 CARD。它包含以下额外字段:

名称 类型 说明
cryptogram 字符串 3D 安全密文。
eciIndicator 字符串 不一定提供。仅会针对 Visa 卡网络上的令牌返回该字符串。此值通过付款授权请求传递。

以下 JSON 代码段示例展示了针对包含 CRYPTOGRAM_3DS authMethodCARD paymentMethod 的完整 encryptedMessage

{
  "paymentMethod": "CARD",
  "paymentMethodDetails": {
    "authMethod": "CRYPTOGRAM_3DS",
    "pan": "1111222233334444",
    "expirationMonth": 10,
    "expirationYear": 2020,
    "cryptogram": "AAAAAA...",
    "eciIndicator": "eci indicator"
    
  },
  
  "messageId": "some-message-id",
  "messageExpiration": "1577862000000"
}

签名验证

要验证签名(包括中间密钥签名和消息签名),必须具备以下资源:

  • 创建签名所用的算法。
  • 创建签名所用的字节串。
  • 与创建签名所用的私钥相对应的公钥。
  • 签名本身。

签名算法

Google 使用椭圆曲线数字签名算法 (ECDSA) 以及以下参数对消息进行签名:基于 NIST P-256 的 ECDSA 且以 SHA-256 为散列函数(如 FIPS 186-4 中所定义)。

签名

签名包含在消息的最外层。它采用 Base64 进行编码,格式为 ASN.1 字节。如需详细了解 ASN.1,请参阅 IETF 工具附录 A。签名由 ECDSA 整数 r 和 s 组成。如需了解详情,请参阅签名生成算法

以下是上述 ASN.1 字节格式的示例,它是由 Java Cryptography Extension (JCE) ECDSA 实现所生成的标准格式。

ECDSA-Sig-Value :: = SEQUENCE {
 r INTEGER,
 s INTEGER
}

如何构造中间签名密钥签名的字节串

要验证付款方式令牌示例中的中间签名密钥签名,请使用以下公式构造 signedStringForIntermediateSigningKeySignature

signedStringForIntermediateSigningKeySignature =
length_of_sender_id || sender_id || length_of_protocol_version || protocol_version || length_of_signed_key || signed_key

“||”符号表示并置。每个组件(sender_idprotocolVersionsignedKey)都必须采用 UTF-8 编码。signedKey 必须是 intermediateSigningKey.signedKey 的字符串。每个组件的字节长度为 4 个字节(采用小端格式)。

示例

此示例使用以下付款方式令牌示例:

{
  "protocolVersion":"ECv2",
  "signature":"MEQCIH6Q4OwQ0jAceFEkGF0JID6sJNXxOEi4r+mA7biRxqBQAiAondqoUpU/bdsrAOpZIsrHQS9nwiiNwOrr24RyPeHA0Q\u003d\u003d",
  "intermediateSigningKey":{
    "signedKey": "{\"keyExpiration\":\"1542323393147\",\"keyValue\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw\\u003d\\u003d\"}",
    "signatures": ["MEYCIQCO2EIi48s8VTH+ilMEpoXLFfkxAwHjfPSCVED/QDSHmQIhALLJmrUlNAY8hDQRV/y1iKZGsWpeNmIP+z+tCQHQxP0v"]
  },
  "signedMessage":"{\"tag\":\"jpGz1F1Bcoi/fCNxI9n7Qrsw7i7KHrGtTf3NrRclt+U\\u003d\",\"ephemeralPublicKey\":\"BJatyFvFPPD21l8/uLP46Ta1hsKHndf8Z+tAgk+DEPQgYTkhHy19cF3h/bXs0tWTmZtnNm+vlVrKbRU9K8+7cZs\\u003d\",\"encryptedMessage\":\"mKOoXwi8OavZ\"}"
}

sender_id 始终为 Google,而 protocol_versionECv2

如果 sender_idGoogle,则 signedString 会以下列示例中的形式显示:

signedStringForIntermediateSigningKeySignature =
\x06\x00\x00\x00 || Google || | \x04\x00\x00\x00 || ECv2 || \xb5\x00\x00\x00 || {"keyExpiration":"1542323393147","keyValue":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw\u003d\u003d"}

如何验证 signedStringForIntermediateSigningKeySignature 上的签名

在组合中间签名密钥签名的签名字符串时,会使用标准 ECDSA 验证算法。对于 ECv2 协议,您需要遍历 intermediateSigningKey.signatures 中的所有签名,并尝试使用 keys.json 中未到期的 Google 签名密钥来验证每个签名。只要有一个签名验证有效,即可将此验证视为已完成。稍后使用 intermediateSigningKey.signedKey.keyValue 验证 signedStringForMessageSignature。Google 强烈建议您使用现有的加密库,而不是自己的验证码。

如何构造消息签名的字节串

要验证付款方式令牌示例中的签名,请使用以下公式构造 signedStringForMessageSignature

signedStringForMessageSignature =
length_of_sender_id || sender_id || length_of_recipient_id || recipient_id || length_of_protocolVersion || protocolVersion || length_of_signedMessage || signedMessage

“||”符号表示并置。每个组件(sender_idrecipient_idprotocolVersionsignedMessage)都必须采用 UTF-8 编码。每个组件的字节长度为 4 个字节(采用小端格式)。构建字节字符串时,请勿解析或修改 signedMessage。例如,请勿将 \u003d 替换为 = 字符。

示例

以下示例是一个付款方式令牌示例:

{
  "protocolVersion":"ECv2",
  "signature":"MEQCIH6Q4OwQ0jAceFEkGF0JID6sJNXxOEi4r+mA7biRxqBQAiAondqoUpU/bdsrAOpZIsrHQS9nwiiNwOrr24RyPeHA0Q\u003d\u003d",
  "intermediateSigningKey":{
    "signedKey": "{\"keyExpiration\":\"1542323393147\",\"keyValue\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw\\u003d\\u003d\"}",
    "signatures": ["MEYCIQCO2EIi48s8VTH+ilMEpoXLFfkxAwHjfPSCVED/QDSHmQIhALLJmrUlNAY8hDQRV/y1iKZGsWpeNmIP+z+tCQHQxP0v"]
  },
  "signedMessage":"{\"tag\":\"jpGz1F1Bcoi/fCNxI9n7Qrsw7i7KHrGtTf3NrRclt+U\\u003d\",\"ephemeralPublicKey\":\"BJatyFvFPPD21l8/uLP46Ta1hsKHndf8Z+tAgk+DEPQgYTkhHy19cF3h/bXs0tWTmZtnNm+vlVrKbRU9K8+7cZs\\u003d\",\"encryptedMessage\":\"mKOoXwi8OavZ\"}"
}

sender_id 始终为 Google,而 recipient_idmerchant:merchantIdmerchantId 与具有生产环境访问权限的商家的 Google Pay 业务控制台中的值一致。

如果 sender_idGoogle,且 recipient_idmerchant:12345,则 signedString 会以下列示例中的形式显示:

signedStringForMessageSignature =
\x06\x00\x00\x00 || Google || \x0e\x00\x00\x00 || merchant:12345 || | \x04\x00\x00\x00 || ECv2 || \xd2\x00\x00\x00 || {"tag":"jpGz1F1Bcoi/fCNxI9n7Qrsw7i7KHrGtTf3NrRclt+U\u003d","ephemeralPublicKey":"BJatyFvFPPD21l8/uLP46Ta1hsKHndf8Z+tAgk+DEPQgYTkhHy19cF3h/bXs0tWTmZtnNm+vlVrKbRU9K8+7cZs\u003d","encryptedMessage":"mKOoXwi8OavZ"}

如何验证 signedStringForMessageSignature 上的签名

在组合签名字符串时,会使用标准 ECDSA 验证算法。在上一步中验证的 intermediateSigningKey.signedKey.keyValue 将用来验证 signedMessage。Google 强烈建议您使用现有的加密库,而不是自己的验证码。

加密架构规范

Google 使用椭圆曲线集成加密架构 (ECIES) 来保护在 Google Pay API 响应中返回的付款方式令牌。该加密架构采用了以下参数:

参数 定义
密钥封装方法

ECIES-KEM(如 ISO 18033-2 中所定义)。

  • 椭圆曲线:NIST P-256(在 OpenSSL 中也称为 prime256v1)。
  • CheckModeOldCofactorModeSingleHashModeCofactorMode 为 0。
  • 点格式未压缩。
密钥推导函数

基于 HMAC 并采用 SHA-256 (HKDFwithSHA256)。

  • 请勿提供盐 (Salt)。
  • 对于协议版本 ECv2,信息必须由 Google 采用 ASCII 码进行编码。
  • 必须为 AES256 密钥推导出 256 位并为 HMAC_SHA256 密钥推导出另外 256 位。
对称加密算法

DEM2(如 ISO 18033-2 中所定义)

加密算法:IV 为零且未填充的 AES-256-CTR。

MAC 算法 HMAC_SHA256,采用从密钥推导函数推导出的 256 位密钥。

加密公钥格式

加密公钥和 Google 载荷中返回的 ephemeralPublicKey 都是采用未压缩点格式的 Base64 版密钥。它包含以下两个元素:

  • 用于指定格式的幻数 (0x04)。
  • 两个 32 字节的大整数,表示椭圆曲线中的 X 和 Y 坐标。

如需详细了解这种格式,请参阅“金融服务行业的公钥密码学:椭圆曲线数字签名算法(ECDSA)”,ANSI X9.62,1998 年。

使用 OpenSSL 生成公钥

第 1 步:生成私钥

以下示例会生成适用于 NIST P-256 的椭圆曲线私钥,并将其写入 key.pem

openssl ecparam -name prime256v1 -genkey -noout -out key.pem

可选:查看私钥和公钥

使用以下命令查看私钥和公钥:

openssl ec -in key.pem -pubout -text -noout

该命令会产生类似于以下内容的输出:

read EC key
Private-Key: (256 bit)
priv:
    08:f4:ae:16:be:22:48:86:90:a6:b8:e3:72:11:cf:
    c8:3b:b6:35:71:5e:d2:f0:c1:a1:3a:4f:91:86:8a:
    f5:d7
pub:
    04:e7:68:5c:ff:bd:02:ae:3b:dd:29:c6:c2:0d:c9:
    53:56:a2:36:9b:1d:f6:f1:f6:a2:09:ea:e0:fb:43:
    b6:52:c6:6b:72:a3:f1:33:df:fa:36:90:34:fc:83:
    4a:48:77:25:48:62:4b:42:b2:ae:b9:56:84:08:0d:
    64:a1:d8:17:66
ASN1 OID: prime256v1

第 2 步:生成采用 Base64 编码的公钥

在上一个可选步骤示例中生成的私钥和公钥采用了十六进制编码。若想获取采用未压缩点格式进行 Base64 编码的公钥,请使用以下命令:

openssl ec -in key.pem -pubout -text -noout 2> /dev/null | grep "pub:" -A5 | sed 1d | xxd -r -p | base64 | paste -sd "\0" - | tr -d '\n\r ' > publicKey.txt

该命令会生成一个 publicKey.txt 文件,其内容是采用未压缩点格式的 Base64 版密钥,类似于以下内容:

BOdoXP+9Aq473SnGwg3JU1aiNpsd9vH2ognq4PtDtlLGa3Kj8TPf+jaQNPyDSkh3JUhiS0KyrrlWhAgNZKHYF2Y=

文件内容不含任何额外的空格或回车符。要验证这一点,请在 Linux 或 MacOS 中运行以下命令:

od -bc publicKey.txt

第 3 步:生成采用 PKCS #8 格式及 Base64 编码的私钥

Tink 库期望您的私钥以 PKCS #8 格式进行 Base64 编码。要根据第一步中生成的私钥生成此格式的私钥,请使用以下命令:

openssl pkcs8 -topk8 -inform PEM -outform DER -in key.pem -nocrypt | base64 | paste -sd "\0" -

该命令会产生类似于以下内容的输出:

MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWV4oK8c/MZkCLk4qSCNjW0Zm6H0CBCtSYxkXkC9FBHehRANCAAQPldOnhO2/oXjdJD1dwlFPiNs6fcdoRgFu3/Z0iKj24SjTGyLRGAtYWLGXBZcDdPj3T2bJRHRVhE8Bc2AjkT7n

如何解密付款方式令牌

要解密令牌,请按以下步骤操作:

  1. 使用您的私钥和所提供的 ephemeralPublicKey 推导出长度为 512 位且采用 ECIES-KEM 的共享密钥。请使用以下参数:
    • 椭圆曲线:NIST P-256(在 OpenSSL 中也称为 prime256v1)。
    • CheckModeOldCofactorModeSingleHashModeCofactorMode0
    • 编码函数:未压缩的点格式。
    • 密钥推导函数:HKDFwithSHA256(如 RFC 5869 中所述),它具有以下参数:
      • 请勿提供盐。根据 RFC,这必须等同于 32 个零字节的盐。
  2. 将生成的密钥拆分为两个长度为 256 位的密钥:symmetricEncryptionKeymacKey
  3. 验证 tag 字段是否为 encryptedMessage 的有效 MAC。

    要生成预期的 MAC,请使用采用散列函数 SHA256 的 HMAC (RFC 5869) 以及在第 2 步中取得的 macKey

  4. 使用符合以下条件的 AES-256-CTR 模式解密 encryptedMessage

    • 零 IV。
    • 未填充。
    • 第 2 步中推导出的 symmetricEncryptionKey

密钥管理

商家加密密钥

商家应根据加密架构规范所述的规格生成公钥。

Google 根签名密钥

Google 发布了一组当前有效的根签名公钥,这些公钥可通过一个公共网址获取。该网址返回的 HTTP 缓存标头会指示密钥的有效期限。这些密钥会被缓存起来,直到到期,而到期日由 keyExpiration 字段决定。如果获取的密钥到期,我们建议再次从该公共网址获取密钥以接收当前有效的密钥列表。

ECv2 协议的例外情况:如果您无法在运行时从 Google 获取密钥,请通过我们的生产环境网址获取 keys.json 并将其保存到您的系统中,然后定期手动刷新。正常情况下,Google 会在有效期最长的密钥到期的五年前,为 ECv2 发布新的根签名密钥。如果密钥被破解,Google 会通过自助门户网站中提供的联系信息通知所有商家,以便商家更快请求重新加载 keys.json。为确保您不会错过定期轮替,我们建议选择在 keys.json 内容中保存 Google 密钥的商家,在其每年的密钥轮替工作中,每年刷新一次该文件。

通过公开网址提供的密钥采用以下格式进行映射:

{
  "keys": [
    {
      "keyValue": "encoded public key",
      "protocolVersion": "ECv2"
      "keyExpiration":"2000000000000"
    },
    {
      "keyValue": "encoded public key",
      "protocolVersion": "ECv2"
      "keyExpiration":"3000000000000"
    }
  ]
}

keyValue 是采用 X.509 标准中定义的 ASN.1 类型 SubjectPublicKeyInfo 编码的 Base64 版密钥(未换行或填充)。在 Java 中,上述 ASN.1 编码将由 X509EncodedKeySpec 类表示。您可以通过 ECPublicKey.getEncoded() 获取该编码。

测试环境和生产环境的网址如下:

密钥轮替

对于直接集成,如果您在服务器上直接解密付款方式令牌,则必须每年轮替一次密钥。

要轮替加密密钥,请完成以下步骤:

  1. 使用 OpenSSL 生成新密钥对
  2. 登录您之前用来在 Google Pay 中注册为开发者的 Google 帐号 之前用于通过 Google Play 管理您的应用。
  3. Google Pay API 标签页中的直接集成窗格下,点击现有公钥旁边的管理。点击添加其他密钥
  4. 选择公开加密密钥文本输入字段,然后添加新生成的采用未压缩点格式及 Base64 编码的公钥。
  5. 点击保存加密密钥
  6. 为确保无缝轮替密钥,请在转换密钥时,同时支持新旧私钥的解密。

    如果使用 Tink 库来解密令牌,请使用以下 Java 代码来支持多个私钥:

    String decryptedMessage =
        new PaymentMethodTokenRecipient.Builder()
            .addRecipientPrivateKey(newPrivateKey)
            .addRecipientPrivateKey(oldPrivateKey);

    请确保将解密代码部署至生产环境,且解密取得成功。

  7. 更改代码中使用的公钥。

    替换 PaymentMethodTokenizationSpecification parameters 属性中的 publicKey 特性的值:

    /**
     * @param publicKey public key retrieved from your server
     */
    private static JSONObject getTokenizationSpecification(String publicKey) {
      JSONObject tokenizationSpecification = new JSONObject();
      tokenizationSpecification.put("type", "DIRECT");
      tokenizationSpecification.put(
        "parameters",
        new JSONObject()
            .put("protocolVersion", "ECv2")
            .put("publicKey", publicKey));
      return tokenizationSpecification;
    }
  8. 将第 4 步中的代码部署到生产环境。部署代码后,加密和解密交易将使用新的密钥对。
  9. 确认旧公钥不再用于加密任何交易。

  10. 移除旧的私钥。
  11. 登录您之前用来在 Google Pay 中注册为开发者的 Google 帐号,然后打开 Google Pay 商家控制台
  12. Google Pay API 标签页中的直接集成窗格下,点击现有公钥旁边的管理。点击旧公钥旁边的删除,然后点击保存加密密钥

Google 将使用 PaymentMethodTokenizationSpecification parameters 对象的 publicKey 属性所指定的密钥,如下例所示:

{
  "protocolVersion": "ECv2",
  "publicKey": "BOdoXP+9Aq473SnGwg3JU1..."
}

使用 Tink 库管理加密响应

如要验证签名和解密消息,请使用 Tink 加密库。要与 Tink 集成并执行验证和解密,请完成以下步骤:

  1. 在您的 pom.xml 中,添加 Tink paymentmethodtoken 应用作为依赖项:

    <dependencies>
      <!-- other dependencies ... -->
      <dependency>
        <groupId>com.google.crypto.tink</groupId>
        <artifactId>apps-paymentmethodtoken</artifactId>
        <version>1.2.0</version>
      </dependency>
    </dependencies>
    
  2. 在服务器启动时,预先获取 Google 签名密钥,让密钥保存在内存中。此操作可防止用户在解密过程中因获取密钥而经历网络延迟。

    GooglePaymentsPublicKeysManager.INSTANCE_PRODUCTION.refreshInBackground();
  3. 使用以下代码解密密钥(该代码假定 paymentMethodToken 存储在 encryptedMessage 变量中),并根据您的情况替换粗体部分。

    对于环境测试,请将 INSTANCE_PRODUCTION 替换为 INSTANCE_TEST,将 [YOUR MERCHANT ID] 替换为 12345678901234567890

    String decryptedMessage =
        new PaymentMethodTokenRecipient.Builder()
        .fetchSenderVerifyingKeysWith(
            GooglePaymentsPublicKeysManager.INSTANCE_PRODUCTION)
        .recipientId("merchant:YOUR_MERCHANT_ID")
        // This guide applies only to protocolVersion = ECv2
        .protocolVersion("ECv2")
        // Multiple private keys can be added to support graceful
        // key rotations.
        .addRecipientPrivateKey(PrivateKey1)
        .addRecipientPrivateKey(PrivateKey2)
        .build()
        .unseal(encryptedMessage);
    
  4. PrivateKey1PrivateKey2 替换为您自己的密钥。变量可以是采用 Base64 编码的 PKCS8 字符串或者 ECPrivateKey 对象。如需详细了解如何生成采用 Base64 编码的 PKCS8 私钥,请参阅使用 OpenSSL 生成密钥对

  5. 如果无法每次解密密钥时都调用 Google 服务器,请使用以下代码进行解密,并根据您的情况替换粗体部分。

    String decryptedMessage =
        new PaymentMethodTokenRecipient.Builder()
        .addSenderVerifyingKey("ECv2 key fetched from test or production url")
        .recipientId("merchant:YOUR_MERCHANT_ID")
        // This guide applies only to protocolVersion = ECv2
        .protocolVersion("ECv2")
        // Multiple private keys can be added to support graceful
        // key rotations.
        .addRecipientPrivateKey(PrivateKey1)
        .addRecipientPrivateKey(PrivateKey2)
        .build()
        .unseal(encryptedMessage);
    

    在正常情况下,只要密钥未被破解,生产环境中的当前密钥在 2038 年 4 月 14 日之前都将保持有效。如果密钥被破解,Google 会通过自助门户网站中提供的联系信息通知所有商家,以便商家更快地请求重新加载 keys.json

    上述代码段会处理以下安全细节,从而让您能够专心处理载荷:

    • 获取 Google 签名密钥并将其缓存在内存中
    • 签名验证
    • 解密