Sample Java Code to Decrypt PKPaymentToken

Document created by gjsissons on Aug 16, 2016Last modified by andrew.harris on May 14, 2018
Version 5Show Document
  • View in full screen mode

When developing for mobile and Apple Pay, developers can use the following sample Java code to process the user payments credentials properly.

 

A payment token is an instance of the PKPaymentToken class. The value of its paymentData property is a JSON dictionary, which holds information used for validation as well as encrypted payment data. 

 

Visit the Apple Community page on Vantiv O.N.E. for more documentation, APIs and guides for developing mobile and digital wallets to Apple Pay.

 

App.java source

 

//
//  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