Sample Java Code to decrypt PKPaymentToken

Document created by gjsissons on Aug 16, 2016
Version 1Show Document
  • View in full screen mode

This sample code provided by Apple illustrates how a merchant can decrypt the Apple PKPaymentToken.

 

Download the java source file here

 

//
//  App.java
//
//  Copyright (c) 2014 Apple, Inc. All rights reserved.
//

package com.apple.merchant;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.bouncycastle.asn1.ASN1UTCTime;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.cms.CMSAttributes;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.Store;
import org.bouncycastle.util.encoders.Hex;

import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import static org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;

import java.io.*;
import java.nio.charset.Charset;
import java.security.*;
import java.security.cert.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;

public class App {
    public static final String PAYMENT_VERSION = "version";
    public static final String PAYMENT_DATA = "data";
    public static final String PAYMENT_HEADER = "header";
    public static final String PAYMENT_SIGNATURE = "signature";
    public static final String PAYMENT_HEADER_APPLICATION_DATA = "applicationData";
    public static final String PAYMENT_HEADER_EPHEMERAL_PUBLIC_KEY = "ephemeralPublicKey";
    public static final String PAYMENT_HEADER_PUBLIC_KEY_HASH = "publicKeyHash";
    public static final String PAYMENT_HEADER_TRANSACTION_ID = "transactionId";

    public static final String MERCHANT_ID = "mock-1";

    private static KeyStore keyStore;
    private static PrivateKey merchantPrivateKey;

    static {
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(new BouncyCastleProvider());
        }
    }

    public static void main(String[] args) throws Exception {
        // Load merchant private key
        byte[] merchantPrivateKeyBytes = IOUtils.toByteArray(App.class.getResource("/merchant_key.pk8"));
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(merchantPrivateKeyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("EC", PROVIDER_NAME);
        merchantPrivateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);

        // Load Apple root certificate
        keyStore = KeyStore.getInstance("BKS");
        keyStore.load(App.class.getResourceAsStream("/AppleRootCA.keystore"), "apple123".toCharArray());

        // Load JSON sample payloads
        File jsonDirectory = new File(App.class.getResource("/json").toURI());
        File[] jsonFiles = jsonDirectory.listFiles();
        if (jsonFiles != null) {
            for (final File entry : jsonFiles) {
                Reader reader = new FileReader(entry);
                JsonObject jsonObject = new Gson().fromJson(reader, JsonObject.class);
                reader.close();
                unwrap(jsonObject);
            }
        }
    }

    public static void unwrap(JsonObject jsonObject) throws Exception {
        // Merchants should use the 'version' field to determine how to verify and decrypt the message payload.
        // At this time the only published version is 'EC_v1' which is demonstrated here.
        String version = jsonObject.get(PAYMENT_VERSION).getAsString();

        byte[] signatureBytes = Base64.decodeBase64(jsonObject.get(PAYMENT_SIGNATURE).getAsString());
        byte[] dataBytes = Base64.decodeBase64(jsonObject.get(PAYMENT_DATA).getAsString());
        JsonObject headerJsonObject = jsonObject.get(PAYMENT_HEADER).getAsJsonObject();
        byte[] transactionIdBytes = Hex.decode(headerJsonObject.get(PAYMENT_HEADER_TRANSACTION_ID).getAsString());
        byte[] ephemeralPublicKeyBytes = Base64.decodeBase64(headerJsonObject.get(PAYMENT_HEADER_EPHEMERAL_PUBLIC_KEY).getAsString());

        // Merchants that have more than one certificate may use the 'publicKeyHash' field to determine which
        // certificate was used to encrypt this payload.
        byte[] publicKeyHash = Base64.decodeBase64(headerJsonObject.get(PAYMENT_HEADER_PUBLIC_KEY_HASH).getAsString());

        // Application data is a conditional field, present when the merchant has supplied it to the iOS SDK.
        byte[] applicationDataBytes = null;
        if (headerJsonObject.has(PAYMENT_HEADER_APPLICATION_DATA)) {
            applicationDataBytes = Hex.decode(headerJsonObject.get(PAYMENT_HEADER_APPLICATION_DATA).getAsString());
            // The include test payloads were created by passing a UTF8 encoded string "TEST" to the SDK.
            // Payload field "applicationData" contains the SHA256 hash of that encoded string.
            // Verify that they match.
            //
            // If you didn't set the "applicationData" property of your PKPaymentRequest, you may remove
            // these lines.
            MessageDigest messageDigest = MessageDigest.getInstance("SHA256", PROVIDER_NAME);
            byte[] sdkApplicationDataHash = messageDigest.digest("TEST".getBytes("UTF-8"));
            if (!Arrays.equals(sdkApplicationDataHash, applicationDataBytes)) {
                 throw new InvalidParameterException("applicationData mismatch");
            }
        }

        // PKCS#7 signature over (ephemeralPublicKey . data  . transactionId . applicationData )
        byte[] signedBytes = ArrayUtils.addAll(ephemeralPublicKeyBytes, dataBytes);
        signedBytes = ArrayUtils.addAll(signedBytes, transactionIdBytes);
        signedBytes = ArrayUtils.addAll(signedBytes, applicationDataBytes);

        CMSSignedData signedData = new CMSSignedData(new CMSProcessableByteArray(signedBytes), signatureBytes);

        // Check certificate path
        Store certificateStore = signedData.getCertificates();
        List<X509Certificate> certificates = new ArrayList<X509Certificate>();
        JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
        certificateConverter.setProvider(PROVIDER_NAME);
        for (Object o : certificateStore.getMatches(null)) {
            X509CertificateHolder certificateHolder = (X509CertificateHolder) o;
            certificates.add(certificateConverter.getCertificate(certificateHolder));
        }
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509", PROVIDER_NAME);
        CertPath certificatePath = certificateFactory.generateCertPath(certificates);

        PKIXParameters params = new PKIXParameters(keyStore);
        params.setRevocationEnabled(false);

        // TODO: Test certificate has no CRLs.  Merchants must perform revocation checks in production.
        // TODO: Verify certificate attributes per instructions at
        // https://developer.apple.com/library/ios/documentation/PassKit/Reference/PaymentTokenJSON/PaymentTokenJSON.html#//apple_ref/doc/uid/TP40014929

        CertPathValidator validator = CertPathValidator.getInstance("PKIX", PROVIDER_NAME);
        PKIXCertPathValidatorResult result = (PKIXCertPathValidatorResult)validator.validate(certificatePath, params);
        System.out.println(result);

        // Verify signature
        SignerInformationStore signerInformationStore = signedData.getSignerInfos();
        boolean verified = false;
        for (Object o : signerInformationStore.getSigners()) {
            SignerInformation signer = (SignerInformation) o;
            Collection matches = certificateStore.getMatches(signer.getSID());
            if (!matches.isEmpty()) {
                X509CertificateHolder certificateHolder = (X509CertificateHolder) matches.iterator().next();
                if (signer.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider(PROVIDER_NAME).build(certificateHolder))) {
                    DERSequence sequence = (DERSequence) signer.getSignedAttributes().get(CMSAttributes.signingTime).toASN1Primitive();
                    DERSet set = (DERSet) sequence.getObjectAt(1);
                    ASN1UTCTime signingTime = (ASN1UTCTime) set.getObjectAt(0).toASN1Primitive();
                    // Merchants can check the signing time of this payment to determine its freshness.
                    System.out.println("Signature verified.  Signing time is " + signingTime.getDate());
                    verified = true;
                }
            }
        }

        if (verified) {
            // Ephemeral public key
            KeyFactory keyFactory = KeyFactory.getInstance("EC", PROVIDER_NAME);
            PublicKey ephemeralPublicKey = keyFactory.generatePublic(new X509EncodedKeySpec(ephemeralPublicKeyBytes));

            // Key agreement
            String asymmetricKeyInfo = "ECDH";
            KeyAgreement agreement = KeyAgreement.getInstance(asymmetricKeyInfo, PROVIDER_NAME);
            agreement.init(merchantPrivateKey);
            agreement.doPhase(ephemeralPublicKey, true);
            byte[] sharedSecret = agreement.generateSecret();

            // KDF
            // An alternative approach for determining MERCHANT_ID is to use the publicKeyHash field to look up
            // the relevant public key certificate, and retrieve the DER-encoded byte sequence from OID
            // 1.2.840.113635.100.6.32.  This is the SHA256 hash of the merchant identifier.
            byte[] derivedSecret = performKDF(sharedSecret, MERCHANT_ID);

            // Decrypt the payment data
            String symmetricKeyInfo = "AES/GCM/NoPadding";
            Cipher cipher = Cipher.getInstance(symmetricKeyInfo, PROVIDER_NAME);

            SecretKeySpec key = new SecretKeySpec(derivedSecret, cipher.getAlgorithm());
            cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(new byte[16]));
            byte[] decryptedPaymentData = cipher.doFinal(dataBytes);

            // JSON payload
            System.out.println(new String(decryptedPaymentData, "UTF-8"));
        }
    }

    private static final byte [] APPLE_OEM            = "Apple".getBytes(Charset.forName("US-ASCII"));
    private static final byte [] COUNTER              = { 0x00, 0x00, 0x00, 0x01 };
    private static final byte [] ALG_IDENTIFIER_BYTES = "id-aes256-GCM".getBytes(Charset.forName("US-ASCII"));

    /**
     * 00000001_16 || sharedSecret || length("AES/GCM/NoPadding") || "AES/GCM/NoPadding" || "Apple" || merchantID
     */
    private static byte[] performKDF(byte[] sharedSecret, byte[] merchantId) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        baos.write(COUNTER);
        baos.write(sharedSecret);
        baos.write(ALG_IDENTIFIER_BYTES.length);
        baos.write(ALG_IDENTIFIER_BYTES);
        baos.write(APPLE_OEM);
        baos.write(merchantId);
        MessageDigest messageDigest = MessageDigest.getInstance("SHA256", PROVIDER_NAME);
        return messageDigest.digest(baos.toByteArray());
    }

    private static byte[] performKDF(byte[] sharedSecret, String merchantId)  throws Exception {
        MessageDigest messageDigest = MessageDigest.getInstance("SHA256", PROVIDER_NAME);
        return performKDF(sharedSecret, messageDigest.digest(merchantId.getBytes("UTF-8")));
    }
}

Attachments

    Outcomes