Google Pay API 会在经过签名和加密的 PaymentMethodToken 载荷中返回付款方式。返回的付款方式是由 PAN 组成的卡,或由设备 PAN 和密文组成的令牌化卡。
载荷中含有一个名为 protocolVersion 的字段,该字段会告诉载荷接收方哪些加密基元正在使用中以及预期的格式。
本指南介绍了如何生成公钥来请求由 Google 签名和加密的付款方式令牌,并详述了验证和解密该令牌的步骤。
本指南仅适用于 protocolVersion = ECv2。
由于您直接接收支付卡信息,因此请确保您的应用符合 PCI DSS,并确保您的服务器具有所需的基础架构,可在您继续操作之前安全地处理用户的付款凭据。
以下步骤概述了集成商必须完成哪些操作才能使用 Google Pay API 的 ECv2 PaymentMethodToken 载荷:
- 获取 Google 根签名密钥。
- 通过任何未过期的根签名密钥,验证中间签名密钥的签名是否有效。
- 验证载荷的中间签名密钥是否未过期。
- 通过中间签名密钥,验证载荷的签名是否有效。
- 在验证签名后解密载荷的内容。
- 验证消息是否未过期。为此,您需要检查当前时间是否早于经过解密的内容中的
messageExpiration字段。 - 使用经过解密的内容中的付款方式并从中扣取费用。
Tink 库中的示例代码执行的是第 1-6 步。
付款方式令牌的结构
Google 在 PaymentData 响应中返回的消息是一个采用 UTF-8 编码的序列化 JSON 对象,包含下表中指定的密钥:
| 名称 | 类型 | 说明 |
|---|---|---|
protocolVersion |
字符串 | 标识创建消息时所使用的加密或签名架构。如果需要,您可以通过此参数让协议随时间演进。 |
signature |
字符串 | 用于验证消息是否来自 Google。此参数采用 Base64 编码,并通过中间签名密钥使用 ECDSA 创建而成。 |
intermediateSigningKey |
对象 | 含有 Google 所提供的中间签名密钥的 JSON 对象。它包含带 keyValue、keyExpiration 和 signatures 的 signedKey。它已经过序列化处理,可简化中间签名密钥的签名验证过程。 |
signedMessage |
字符串 | 已序列化为 HTML 安全字符串的 JSON 对象,其中包含 encryptedMessage、ephemeralPublicKey 和 tag。它已经过序列化处理,可简化签名验证过程。 |
示例
以下是采用 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 |
字符串 | 中间密钥的到期日期和时间(采用 UTC 时区,以自纪元起经过的毫秒数来表示)。集成商会拒绝任何已过期的密钥。 |
已签名的消息
signedMessage 是一个采用 UTF-8 编码且包含以下值的序列化 JSON 对象:
| 名称 | 类型 | 说明 |
|---|---|---|
encryptedMessage |
字符串 | 采用 Base64 编码的加密消息,其中包含付款信息和一些附加的安全字段。 |
ephemeralPublicKey |
字符串 | 与私钥相关联、采用 Base64 编码的临时公钥,用于以未压缩的点格式对消息加密。如需了解详情,请参阅加密公钥格式。 |
tag |
字符串 | encryptedMessage 的 MAC,采用 Base64 编码。 |
加密的消息
解密后的 encryptedMessage 是一个采用 UTF-8 编码的序列化 JSON 对象。该 JSON 共有两层。外层包含元数据和出于安全目的而加入的字段,而内层则是另一个代表实际付款凭据的 JSON 对象。
如需详细了解 encryptedMessage,请参阅以下表格和 JSON 对象示例:
| 名称 | 类型 | 说明 |
|---|---|---|
messageExpiration |
字符串 | 消息的到期日期和时间(采用 UTC 时区,以自纪元以来的毫秒数表示)。集成商应拒绝所有过期的消息。 |
messageId |
字符串 | 用于标识消息的唯一 ID,可在日后需要撤消或定位消息时使用。 |
paymentMethod |
字符串 | 付款凭据的类型。 目前只支持 CARD。
|
paymentMethodDetails |
对象 | 付款凭据本身。此对象的格式由 paymentMethod 决定,详情请见下表。 |
卡
以下属性构成了 CARD 付款方式的付款凭据:
| 名称 | 类型 | 说明 |
|---|---|---|
pan |
字符串 | 扣费的个人账号。该字符串仅包含数字。 |
expirationMonth |
数字 | 卡的到期月份,其中 1 代表 1 月,2 代表 2 月,依此类推。 |
expirationYear |
数字 | 卡的四位数到期年份,例如 2020。 |
authMethod |
字符串 | 卡交易的身份验证方法。 |
PAN_ONLY
下例中的 JSON 代码段就属于采用 PAN_ONLY authMethod 的 CARD paymentMethod 的完整 encryptedMessage。
{ "paymentMethod": "CARD", "paymentMethodDetails": { "authMethod": "PAN_ONLY", "pan": "1111222233334444", "expirationMonth": 10, "expirationYear": 2025 }, "gatewayMerchantId": "some-merchant-id", "messageId": "some-message-id", "messageExpiration": "1759309000000" }
CRYPTOGRAM_3DS
采用 3-D 安全密文 (CRYPTOGRAM_3DS authMethod) 进行身份验证的 CARD。它包含以下额外字段:
| 名称 | 类型 | 说明 |
|---|---|---|
cryptogram |
字符串 | 3-D 安全密文。 |
eciIndicator |
字符串 | 该字符串并不总是存在。它仅针对 Android 上经过身份验证的设备令牌交易返回 (CRYPTOGRAM_3DS)。此值必须在付款处理流程中向下传递。 |
下例中的 JSON 代码段就属于采用 CRYPTOGRAM_3DS authMethod 的 CARD paymentMethod 的完整 encryptedMessage:
{ "paymentMethod": "CARD", "paymentMethodDetails": { "authMethod": "CRYPTOGRAM_3DS", "pan": "1111222233334444", "expirationMonth": 10, "expirationYear": 2025, "cryptogram": "AAAAAA...", "eciIndicator": "eci indicator" }, "messageId": "some-message-id", "messageExpiration": "1759309000000" }
eciIndicator
支付卡网络可能会针对经过身份验证的设备令牌交易 (CRYPTOGRAM_3DS) 提供 eciIndicator。
您必须在发生授权交易时传递 eciIndicator 值,且不得对其进行更改或硬编码,否则交易将失败。下表详细介绍了 eciIndicator 的值。
| eciIndicator 值 | 支付卡网络 | 责任方 | authMethod |
|---|---|---|---|
""(empty) |
Mastercard 卡 | 商家/收单机构 | CRYPTOGRAM_3DS |
02 |
Mastercard | 发卡机构 | CRYPTOGRAM_3DS |
06 |
Mastercard | 商家/收单机构 | CRYPTOGRAM_3DS |
05 |
Visa | 发卡机构 | CRYPTOGRAM_3DS |
07 |
Visa | 商家/收单机构 | CRYPTOGRAM_3DS |
""(empty) |
其他网络 | 商家/收单机构 | CRYPTOGRAM_3DS |
系统不会返回此表中未出现的任何其他适用于 VISA 和 Mastercard 的 ECI 值。
签名验证
要验证签名(包括中间密钥签名和消息签名),必须具备以下资源:
- 创建签名所用的算法。
- 创建签名所用的字节串。
- 与创建签名所用的私钥相对应的公钥。
- 签名本身。
签名算法
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_id、protocolVersion、signedKey)都必须采用 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_version 为 ECv2。
如果 sender_id 为 Google,则 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_id、recipient_id、protocolVersion、signedMessage)都必须采用 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_id 为 merchant:merchantId。merchantId 与具有正式版访问权限的商家的 Google Pay & Wallet Console 中的值相一致。
如果 sender_id 为 Google,且 recipient_id 为 merchant: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 中所定义)。
|
| 密钥推导函数 | 基于 HMAC 并采用 SHA-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
如何解密付款方式令牌
要解密令牌,请按以下步骤操作:
- 使用您的私钥和所提供的
ephemeralPublicKey推导出长度为 512 位且采用 ECIES-KEM 的共享密钥。请使用以下参数: - 椭圆曲线:NIST P-256(在 OpenSSL 中也称为 prime256v1)。
CheckMode、OldCofactorMode、SingleHashMode和CofactorMode为0。- 编码函数:未压缩的点格式。
- 密钥推导函数:HKDFwithSHA256(如 RFC 5869 中所述),它具有以下参数:
- 请勿提供盐。根据 RFC,这必须等同于 32 个零字节的盐。
- 将生成的密钥拆分为两个长度为 256 位的密钥:
symmetricEncryptionKey和macKey。 验证
tag字段是否为encryptedMessage的有效 MAC。要生成预期的 MAC,请将 HMAC (RFC 5869) 与哈希函数 SHA256 以及在第 2 步中获得的
macKey搭配使用。使用符合以下条件的 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() 获取该编码。
测试环境和生产环境的网址如下:
- 测试环境网址:
https://payments.developers.google.com/paymentmethodtoken/test/keys.json - 正式版:
https://payments.developers.google.com/paymentmethodtoken/keys.json
密钥轮替
对于直接集成,如果您在服务器上直接解密付款方式令牌,则必须每年轮替一次密钥。
要轮替加密密钥,请完成以下步骤:
- 使用 OpenSSL 生成新密钥对。
- 登录您之前用来在 Google Pay 中注册为开发者的 Google 账号 ,然后打开 Google Pay 和钱包控制台。
- 在 Google Pay API 标签页中的直接集成窗格下,点击现有公钥旁边的管理。点击添加其他密钥。
- 选择公开加密密钥文本输入字段,然后添加新生成的采用未压缩点格式及 Base64 编码的公钥。
- 点击保存加密密钥。
为确保无缝轮替密钥,请在转换密钥时,同时支持新旧私钥的解密。
如果使用 Tink 库来解密令牌,请使用以下 Java 代码来支持多个私钥:
String decryptedMessage = new PaymentMethodTokenRecipient.Builder() .addRecipientPrivateKey(newPrivateKey) .addRecipientPrivateKey(oldPrivateKey);
请确保将解密代码部署至正式版,且解密取得成功。
更改代码中使用的公钥。
替换
PaymentMethodTokenizationSpecificationparameters属性中的publicKey属性的值:const tokenizationSpecification = { "type": "DIRECT", "parameters": { "protocolVersion": "ECv2", "publicKey": "BOdoXP1aiNp.....kh3JUhiSZKHYF2Y=" } }
- 将第 4 步中的代码部署到生产环境。部署代码后,加密和解密交易将使用新的密钥对。
确认旧公钥不再用于加密任何交易。
- 移除旧的私钥。
- 登录您之前用来在 Google Pay 中注册为开发者的 Google 账号,然后打开 Google Pay 和钱包控制台。
- 在 Google Pay API 标签页中的直接集成窗格下,点击现有公钥旁边的管理。点击旧公钥旁边的删除,然后点击保存加密密钥。
Google 将使用 PaymentMethodTokenizationSpecification parameters 对象的 publicKey 属性所指定的密钥,如下例所示:
{
"protocolVersion": "ECv2",
"publicKey": "BOdoXP+9Aq473SnGwg3JU1..."
}使用 Tink 库管理加密响应
如要验证签名和解密消息,请使用 Tink paymentmethodtoken 库。 此库仅适用于 Java。如需使用该工具,请完成以下步骤:
在您的
pom.xml中,添加 Tinkpaymentmethodtoken应用作为依赖项:<dependencies> <!-- other dependencies ... --> <dependency> <groupId>com.google.crypto.tink</groupId> <artifactId>apps-paymentmethodtoken</artifactId> <version>1.9.1</version> <!-- or latest version --> </dependency> </dependencies>在服务器启动时,预先获取 Google 签名密钥,让密钥保存在内存中。此操作可防止用户在解密过程中因获取密钥而经历网络延迟。
GooglePaymentsPublicKeysManager.INSTANCE_PRODUCTION.refreshInBackground();
使用以下代码解密消息(该代码假定
paymentMethodToken存储在encryptedMessage变量中),并根据您的情况替换粗体部分。对于非生产测试,请将
INSTANCE_PRODUCTION替换为INSTANCE_TEST;如果您的集成处于非活动状态或未配置加密密钥,请将 [YOUR MERCHANT ID] 替换为12345678901234567890。- 有效
- 是否已启用 DIRECT 集成
- 是否已配置加密密钥
不替换 [YOUR MERCHANT ID]。
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);
请将
PrivateKey1替换为与您在准备密钥并向 Google 注册步骤中向 Google 注册的公钥值相关联的相应私钥值。当您以后需要通过 Google 轮替密钥时,您可以添加多个其他私钥值。变量可以是采用 base64 编码的 PKCS8 字符串或者ECPrivateKey对象。如需详细了解如何生成采用 base64 编码的 PKCS8 私钥,请参阅准备密钥并向 Google 注册。如果无法在每次解密密钥时都调用 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 签名密钥并将其缓存在内存中
- 签名验证
- 解密