Apple's own reference page for decrypting an Apple Pay token currently gets the key derivation wrong. The KDF table lists the hash function where the shared secret should be, and it has dropped the id-aes256-GCM algorithm ID bytes altogether; the archived version of the same page has the correct values. If you are building a decryptor from the live docs, you will get a key that never opens the ciphertext, and nothing in the error will tell you why.
That is the level most "decrypt Apple Pay token" guides stop above. They show you a library call, or a PSP's CSR upload screen, and move on. This guide covers the actual formats for Apple Pay (EC_v1 and RSA_v1) and Google Pay (ECv2), the signature checks people skip, the certificate rotation rules that cause outages, and the more important question: whether you should be decrypting at all.
What Is Inside an Apple Pay Payment Token?
When the payment sheet completes, PKPayment.token.paymentData (or the ApplePayPaymentToken on the web) hands you a JSON object with four keys:
{
"version": "EC_v1",
"data": "",
"signature": "",
"header": {
"ephemeralPublicKey": "",
"publicKeyHash": "",
"transactionId": "",
"applicationData": ""
}
}
version is EC_v1 almost everywhere. Apple says other regions use RSA_v1 "if ECC encryption is unavailable due to regulatory concerns", and its March 2026 certificate technote (TN3206) makes the mapping clear without quite saying it: the Payment Processing CSR is a 256-bit ECC key globally and a 2048-bit RSA key for China mainland. An RSA_v1 header carries wrappedKey instead of ephemeralPublicKey.
publicKeyHash matters more than it looks. It is the only field that tells you which of your private keys the token was encrypted to, and you will need it during every certificate rotation.
How to Decrypt an Apple Pay Token (EC_v1)
The flow for EC_v1 is ECDH, then a single-pass NIST SP 800-56A concatenation KDF, then AES-GCM:
1. Select the key. Match header.publicKeyHash against the SHA-256 of each Payment Processing certificate's public key you hold.
2. Agree a secret. ECDH between your private key and ephemeralPublicKey. Apple only says "256-bit ECC key pair"; every open-source decryptor treats it as P-256.
3. Derive the key. SHA-256 over counter (0x00000001) || Z || AlgorithmID || PartyUInfo || PartyVInfo, where:
- Z is the ECDH shared secret
- AlgorithmID is the byte 0x0D followed by the ASCII string id-aes256-GCM (the 0x0D is a length prefix: 13 characters)
- PartyUInfo is the ASCII string Apple
- PartyVInfo is the SHA-256 of your merchant identifier, either hashed from the UID in the certificate subject or hex-decoded from the extension at OID 1.2.840.113635.100.6.32
4. Decrypt. AES-256-GCM over data, IV of 16 zero bytes, no associated data. The last 16 bytes of data are the GCM tag.
One SHA-256 iteration produces exactly 32 bytes, so there is no loop. The live Apple page shows Z as "SHA-256" and the Algorithm ID as "the shared secret"; use the archived Payment Token Format Reference for those two rows.
For RSA_v1, you skip the KDF. Decrypt wrappedKey with RSA/ECB/OAEPWithSHA256AndMGF1Padding to get the symmetric key, then decrypt with AES-128-GCM, same zero IV.
What comes out:
{
"applicationPrimaryAccountNumber": "4817499999999999",
"applicationExpirationDate": "291231",
"currencyCode": "826",
"transactionAmount": 1999,
"deviceManufacturerIdentifier": "040010030273",
"paymentDataType": "3DSecure",
"paymentData": {
"onlinePaymentCryptogram": "",
"eciIndicator": ""
}
}
The account number is a device-specific DPAN, not the card PAN. The expiry is YYMMDD, not MMYY. currencyCode is ISO 4217 numeric as a string, so 826 is GBP and a leading zero survives. eciIndicator is optional, and Apple is blunt about it: if you receive one, pass it to your processor, "otherwise, the transaction fails". Newer tokens can also carry merchantTokenIdentifier and merchantTokenMetadata for Apple Pay merchant tokens, and authenticationResponses for multi-merchant payments.
How to Verify the Apple Pay Token Signature
This is the step open-source code skips most often. The most-starred Node library for Apple Pay decryption, apple-pay-decrypt (65 stars), does not appear to verify the signature at all. Decryption without verification means you trust any blob encrypted to your public key, and your public key is, by definition, public.
Apple's seven-step check:
1. Confirm the leaf certificate carries OID 1.2.840.113635.100.6.29 and the intermediate carries 1.2.840.113635.100.6.2.14. Only their presence matters, not their values.
2. Confirm the chain runs leaf → intermediate → Apple Root CA - G3. Pin the root; do not accept whatever the CMS blob ships.
3. Verify the ECDSA-SHA256 signature over ephemeralPublicKey || data || transactionId || applicationData (for RSA_v1, wrappedKey replaces the first element).
4. Check the CMS signing time. Apple's current wording: if the signing time and the transaction time "differ by more than 5 minutes, the token may be a replay attack". The old docs said "a few minutes".
5. Deduplicate on transactionId within that window.
6. After decryption, check transactionAmount, currencyCode and the applicationData hash against what you actually asked the customer to pay.
7. If anything fails, ignore the transaction.
Step 6 is the one I would insist on in code review. The amount in the token is signed by Apple and bound to what the customer saw on the sheet. Comparing it to your basket is the cheapest integrity check in the whole flow.
How Google Pay ECv2 Token Decryption Works
Google's format looks similar and is different in almost every detail:
{
"protocolVersion": "ECv2",
"signature": "",
"intermediateSigningKey": {
"signedKey": "{\"keyValue\":\"...\",\"keyExpiration\":\"...\"}",
"signatures": [""]
},
"signedMessage": "{\"encryptedMessage\":\"...\",\"ephemeralPublicKey\":\"...\",\"tag\":\"...\"}"
}
Note that signedKey and signedMessage are JSON strings, not objects. You verify them byte for byte, so never parse and re-serialise before checking signatures; Google specifically warns you to leave escapes such as = untouched.
Verification is a two-level chain with no X.509 in sight. Google publishes root signing keys at payments.developers.google.com/paymentmethodtoken/keys.json (the current ECv2 root expires on 14 April 2038, served with a seven-day cache header). The root signs the intermediate key over a length-prefixed string: len("Google") || "Google" || len("ECv2") || "ECv2" || len(signedKey) || signedKey, each length as 4 bytes little-endian. The intermediate then signs the message over the same shape with your recipient ID inserted: merchant:. Then check that neither keyExpiration nor messageExpiration has passed.
Decryption is ECIES-KEM on P-256 with HKDF-SHA256 (no salt, info string Google) deriving 64 bytes: a 32-byte AES-256-CTR key and a 32-byte HMAC-SHA256 key. Check the HMAC tag in constant time before decrypting, with a zero IV. The detail that breaks hand-rolled implementations: the HKDF input keying material is ephemeralPublicKey || sharedSecret, not the shared secret alone. That follows from the ISO 18033-2 mode flags Google specifies, and you only find it by reading Tink's source.
The decrypted payload:
{
"gatewayMerchantId": "...",
"messageExpiration": "1790000000000",
"messageId": "...",
"paymentMethod": "CARD",
"paymentMethodDetails": {
"pan": "4111111111111111",
"expirationMonth": 12,
"expirationYear": 2029,
"authMethod": "CRYPTOGRAM_3DS",
"cryptogram": "",
"eciIndicator": "05"
}
}
authMethod is the field that decides your risk and compliance posture. CRYPTOGRAM_3DS means an Android device token with a cryptogram. PAN_ONLY means a card stored on the customer's Google account, which decrypts to the real card number and, in the EEA, may still need 3D Secure. Google's SCA guidance tells you to step up when assuranceDetails.cardHolderAuthenticated is false. (If SCA exemptions are on your mind, I covered the EU and UK rules for 3DS2 separately.)
Google publishes the ECI meanings Apple does not: Visa 05 issuer liable, 07 merchant liable; Mastercard 02 issuer liable, 06 or empty merchant liable. Like Apple, it says pass the value unaltered or the transaction fails.
Apple Pay vs Google Pay Decryption: Side by Side
Apple Pay EC_v1 | Google Pay ECv2 | |
|---|---|---|
| Trust anchor | X.509 chain to Apple Root CA - G3 | Root keys in keys.json, no certificates |
| Signature | Detached CMS/PKCS #7 | Two-level ECDSA over length-prefixed strings |
| Key agreement | ECDH, 256-bit EC | ECIES-KEM, P-256 |
| KDF | SP 800-56A concat, SHA-256 | HKDF-SHA256, info Google |
| Cipher | AES-256-GCM, zero IV | AES-256-CTR + HMAC-SHA256, zero IV |
| Freshness | CMS signing time, 5 minutes | messageExpiration (no production window published) |
| Recipient binding | SHA-256 of merchant ID in KDF | merchant: in signed string |
| Credential | Always a DPAN (or merchant token) | DPAN (CRYPTOGRAM_3DS) or real PAN (PAN_ONLY) |
| Key lifetime | Certificate valid 25 months | Rotate yearly with PCI attestation |
| Official library | None | Tink, Java only |
The practical difference is where each scheme puts trust. Apple binds the token to your merchant identity inside the key derivation, so a token for another merchant simply will not decrypt. Google binds it in the signature, so a token for another merchant decrypts fine if someone has your key but fails verification. Skip Google's signature check and you lose the recipient binding entirely.
Library support is thin on both sides. Google's Tink apps-paymentmethodtoken (v1.15.0, August 2026) is Java only; everything else is third party (r2d2 in Ruby, google-pay-decryptor in Go, google-pay-token-decryption in Python). For Apple Pay, the best-known PHP extension from Etsy has been archived since 2019, and I could not find a Go or Java library with meaningful adoption.
Should You Decrypt Apple Pay and Google Pay Tokens Yourself?
My position: no, unless you need something your PSP cannot give you, such as routing the same wallet payment across two acquirers, or sending your own 3DS data. Everything else is cost.
The PCI argument is subtler than most vendor pages make it. PCI SSC FAQ 1326 says an EMVCo payment token used per the specification, outside the token service provider's environment, "is not considered Account Data". On that reading, an Apple Pay DPAN alone could sit out of scope. In practice, the gatekeepers do not accept that. Google restricts DIRECT integration to merchants whose PCI DSS compliance is "validated by a Qualified Security Assessor", demands a fresh attestation with every annual key rotation, and bars PSPs from using DIRECT on behalf of merchants. Adyen says that if you are not PCI compliant you must let Adyen decrypt. Braintree calls self-decryption not recommended because it typically requires SAQ D. And Google's PAN_ONLY path hands you a real card number, which is Account Data under any reading. (My PCI DSS 4.0 guide covers what SAQ D actually costs.)
Then there is operational risk. Apple allows one active Payment Processing certificate at a time. Activating a new one is "immediate and irreversible", and tokens encrypted to the old key keep arriving while the change propagates. Unless your service holds both private keys and selects by publicKeyHash, a routine renewal becomes a checkout outage. Google's rotation is gentler but longer: register the new key, deploy code that decrypts with both, switch the publicKey in your request, and keep the old private key working for eight days after removing it.
Which PSPs Accept Decrypted Apple Pay and Google Pay Data?
If you do decrypt, the DPAN, cryptogram and ECI go to the acquirer in a PSP-specific shape. As of September 2026:
| PSP | Own Apple Pay cert | Decrypted wallet fields |
|---|---|---|
| Stripe | No, Dashboard CSR | No public API that I could find |
| Adyen | Yes (required for native iOS) | type: "scheme", brand: "applepay", mpiData.cavv / mpiData.eci |
| Checkout.com | No, CKO generates the CSR | source.type: "network_token", token_type: "applepay", cryptogram, eci |
| Braintree | No, Braintree CSR | applePayCard { number, cryptogram, eciIndicator } |
| Worldpay Access | No, Worldpay CSR | card/networkToken+applepay, authentication.networkToken.cryptogram (max 40 chars) |
| Cybersource | Yes, "merchant decryption" mode | tokenizedCard { cryptogram, transactionType: "1" }, paymentSolution: "001" |
Two details catch people. Adyen does not use a network-token type for decrypted Apple Pay; it treats the DPAN as a card number with 3DS-style MPI data. Cybersource has no ECI field in its Apple Pay examples at all: the commerceIndicator (vbv for Visa, spa for Mastercard) carries it. If you are thinking about token portability more broadly, the network tokenisation guide explains why wallet DPANs are not the same as the card-on-file tokens a PSP provisions.
What This Means for Payments Teams in the UK
- Default to gateway mode. Send the encrypted token to your PSP (
applePayTokenat Adyen,POST /tokensat Checkout.com,walletTokenat Worldpay). You get the DPAN benefits without the key custody. - If you must decrypt, build the verification first. Pin Apple Root CA - G3, enforce the 5-minute signing window, dedupe
transactionId, and compare the signed amount with the basket. For Google, verify both signature levels against the raw strings before touching the ciphertext. - Treat certificate renewal as a deployment. Hold old and new private keys, select by
publicKeyHash, and delete the old key only once its hash stops appearing in traffic. Apple's reminder emails come at 30, 15 and 7 days; put the 25-month expiry in your own alerting instead of trusting an inbox. - Never hardcode ECI. Pass through what the wallet returns, empty values included.
- Read
authMethodon every Google Pay payload.PAN_ONLYfrom a UK or EEA card is a candidate for 3DS, not a pre-authenticated token. - Test against the archived Apple spec. Until Apple fixes its KDF table, cross-check your implementation with a known-good library or the 2017 reference.