Integrate Apple Pay - Decrypt the Apple Pay Payload - Decryption Step

Decryption

  1. Restore the symmetric key - steps described here

    1. Create a function restoreSymmetricKey. It takes 3 arguments: the ephemeral public key, your merchant id cert, and your payment processor cert

      const restoreSymmetricKey = (ephemeralPublicKey, merchantCert, paymentProcessorCert) => {
      
      };
      
    2. Use the merchant private key and the ephemeral public key to generate the shared secret using Elliptic Curve Diffie-Hellman (id-ecDH 1.3.132.1.12)

    3. Create a function generateSharedSecret. It will take 2 arguments: merchantPrivateKey and ephemeralPublicKey.
      The private key comes from your merchant identifier certificate. The ephemeral public key comes from your Apple Pay encrypted token.

    4. First, we want to create a new ECkey instance from the merchantPrivateKey. It is in pem format.

      const privateKey = new ECKey(merchantPrivateKey, 'pem');
      
    5. Next, create a new ECkey instance from ephemeralPublicKey. It is in base-64 spki format

      const publicKey = new ECKey(ephemeralPublicKey, 'spki');
      
    6. Lastly, compute the secret in hex format and return that value

      return privateKey.computeSecret(publicKey).toString('hex');
      
    7. Your final generateSharedSecret function is complete

      // generateSharedSecret -
      const generateSharedSecret = (merchantPrivateKey, ephemeralPublicKey) => {
        //===================================
        // Use private key from payment processing certificate and the ephemeral public key to generate 
        // the shared secret using Elliptic Curve Diffie*Hellman (ECDH)
        //===================================
      	const privateKey = new ECKey(merchantPrivateKey, 'pem'); 
      	const publicKey = new ECKey(ephemeralPublicKey, 'spki'); 
        //-----------------------------------
        // Return
        //-----------------------------------
      	return privateKey.computeSecret(publicKey).toString('hex'); 
        //-----------------------------------
      }
      
    8. In restoreSymmetricKey function, set a variable called sharedSecret. You will need the shared secret to get your symmetric key.

      const sharedSecret = generateSharedSecret(paymentProcessorCert, ephemeralPublicKey);
      
    9. Next, we need the merchant identifier field of the public key certificate (your merchant identifier certificate)

    10. Create a function called extractMerchantID. It should take your merchant identifier certificate as an argument.

    11. Within extractMerchantID, we first need to extract the certificate information. We can do so using node-forge

      const info = forge.pki.certificateFromPem(merchantCert);
      
    12. Next, we need to look for OID 1.2.840.113635.100.6.32. We can do so by accessing info['extensions'], and filtering by 1.2.840.113635.100.6.32.

      const result = info['extensions'].filter(d => d.id === MERCHANT_ID_FIELD_OID);
      

      Note: MERCHANT_ID_FIELD_OID was a global variable set to 1.2.840.113635.100.6.32 to improve readability.

    13. Convert the value of result to a string, then remove the first two characters.

      return result[0].value.toString().substring(2);
      
    14. The final extractMerchantID definition should be

      // extractMerchantID -
      const extractMerchantID = (merchantCert) => {
        //===================================
        // Extract merchant identification from public key certificate
        //===================================
        try {
      		const info = forge.pki.certificateFromPem(merchantCert);
          const result = info['extensions'].filter(d => d.id === MERCHANT_ID_FIELD_OID);
          //-----------------------------------
          // Return
          //-----------------------------------
          return result[0].value.toString().substring(2);
        } catch (err) {
          throw new Error(`Unable to extract merchant ID from certificate: ${err}`);
        }
      }
      
    15. You need to use your merchant identifier field and shared secret to derive your symmetric key. Create a function called getSymmetricKey. It should take 2 arguments: merchantId and sharedSecret.

    16. We need to use the Node.js crypto.createHash() method.

    17. We need the Z set to SHA-256:

      const hash = cryptoNative.createHash('sha256');
      
    18. We need to add 2 hex buffers. We do so by using hash.update

      hash.update(Buffer.from('000000', 'hex'));
      hash.update(Buffer.from('01', 'hex'));
      
    19. Add your shared secret in hex format to the hash

      hash.update(Buffer.from(sharedSecret, 'hex'));
      
    20. You need to define a variable called KDF_INFO. It will be a concatenation of multiple variables:

      const KDF_ALGORITHM = '\x0did-aes256-GCM'; // The byte (0x0D) followed by the ASCII string "id-aes256-GCM". The first byte of this value is an unsigned integer that indicates the string’s length in bytes; the remaining bytes are a string.
      const KDF_PARTY_V = Buffer.from(merchantId, 'hex').toString('binary'); // The SHA-256 hash of your merchant ID string literal; 32 bytes in size.
      const KDF_PARTY_U = 'Apple'; // The ASCII string "Apple". This value is a fixed-length string.
      const KDF_INFO = KDF_ALGORITHM + KDF_PARTY_U + KDF_PARTY_V;
      
    21. KDF_ALGORITHM is the byte (0x0D) followed by the ASCII string "id-aes256-GCM". The first byte of this value is an unsigned integer that indicates the string’s length in bytes; the remaining bytes are a constiable-length string.

    22. KDF_PARTY_V is the SHA-256 hash of your merchant ID string literal, as specified in the Apple docs

    23. KDF_PARTY_U is the ASCII string “Apple”, as specified in the Apple docs

    24. KDF_INFO is the sum of all these 3 variables.

    25. Lastly, update your hash with the binary format of KDF_INFO

      hash.update(KDF_INFO, 'binary');
      
    26. Return your hash in hex

      return hash.digest('hex');
      
    27. getSymmetricKey is complete:

      // getSymmetricKey -
      const getSymmetricKey = (merchantId, sharedSecret) => {
        //===================================
        // Get KDF_Info as defined from Apple Pay documentation
        //===================================
        const KDF_ALGORITHM = '\x0did*aes256*GCM'; 
        const KDF_PARTY_V = Buffer.from(merchantId, 'hex').toString('binary'); 
        const KDF_PARTY_U = 'Apple'; 
        const KDF_INFO = KDF_ALGORITHM + KDF_PARTY_U + KDF_PARTY_V;
        //-----------------------------------
        // Create hash
        //-----------------------------------
        const hash = cryptoNative.createHash('sha256');
        hash.update(Buffer.from('000000', 'hex'));
        hash.update(Buffer.from('01', 'hex'));
        hash.update(Buffer.from(sharedSecret, 'hex'));
        hash.update(KDF_INFO, 'binary');
        //-----------------------------------
        // Return
        //-----------------------------------
        return hash.digest('hex');
        //-----------------------------------
      }
      
    28. Return the value of getSymmetricKey in restoreSymmetricKey.

    29. restoreSymmetricKey is complete

      // restoreSymmetricKey -
      const restoreSymmetricKey = (ephemeralPublicKey, merchantCert, paymentProcessorCert) => {
        //===================================
        // 3.a Use the payment processor private key and the ephemeral public key, to generate the shared secret
        //===================================
        const sharedSecret = generateSharedSecret(paymentProcessorCert, ephemeralPublicKey);
        //-----------------------------------
        // 3.b Use the merchant identifier of the public key certificate and the shared secret, to derive the symmetric key
        //-----------------------------------
        const merchantId = extractMerchantID(merchantCert);
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // Return
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        return getSymmetricKey(merchantId, sharedSecret);
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      }
      
  2. Use the symmetric key to decrypt the value of the data key - reference step 4

    1. Create a function called decryptCiphertextfunc. It will take 2 arguments: your symmetric key and the token encrypted data

    2. We will use the Node.js crypto.createDecipheriv() method. It requires 3 parameters as mentioned here:
      algorithm: this should be 'aes-256-gcm' as specified in the Apple docs
      key: this should be your symmetric key
      iv: this is the initialization vector of 16 null bytes

    3. First, let’s prepare our encrypted data using a Node buffer. It initially is in base64 format

      const buf = Buffer.from(encryptedData, 'base64');
      
    4. Extract all the raw binary data from the start up till the last 16th element

      const CIPHERTEXT = buf.slice(0, -16);
      
    5. Prepare the symmetric key using a Node buffer. It is initially in hex format

      const SYMMETRIC_KEY = Buffer.from(symmetricKey, 'hex');
      
    6. Create the initialization vector of 16 null bytes

      const IV = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
      
    7. Create the createDecipheriv object

      const decipher = cryptoNative.createDecipheriv(
        'aes-256-gcm',
        SYMMETRIC_KEY,
        IV
      );
      
    8. Create and set the auth tag for the decipher object

      const tag = buf.slice(-16, buf.length);
      decipher.setAuthTag(tag);
      
    9. Update the encrypted text using decipher.update

      let decrypted = decipher.update(CIPHERTEXT);
      
    10. Decrypt the text

      decrypted += decipher.final();
      
    11. Return the decrypted text

      return decrypted;
      
    12. Your decryptCiphertextFunc is complete

      // decryptCiphertextFunc -
      const decryptCiphertextFunc = (symmetricKey, encryptedData) => {
        //===================================
        // Get symmetric key and initialization vector
        //===================================
        const buf = Buffer.from(encryptedData, 'base64');
        const SYMMETRIC_KEY = Buffer.from(symmetricKey, 'hex');
        const IV = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); // Initialization vector of 16 null bytes
        const CIPHERTEXT = buf.slice(0, -16);
        //-----------------------------------
        // Create and return a Decipher object that uses the given algorithm and password (key)
        //-----------------------------------
        const decipher = cryptoNative.createDecipheriv(
          'aes*256*gcm',
          SYMMETRIC_KEY,
          IV
        );
        const tag = buf.slice(-16, buf.length);
        decipher.setAuthTag(tag);
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // Load encrypted token into Decipher object
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        let decrypted = decipher.update(CIPHERTEXT);
        decrypted += decipher.final();
        //:::::::::::::::::::::::::::::::::::
        // Return
        //:::::::::::::::::::::::::::::::::::
        return decrypted;
        //:::::::::::::::::::::::::::::::::::
      }
      
  3. Prepare TabaPay token

    1. The TabaPay API expects specific keys from the decrypted Apple Pay token. (See Create Transaction for more reference)

    2. The decrypted token does not have all the required fields such as transactionID, network, or type. Thus, we need to get that info from the encrypted token. Below is the function that bundles all the required data together:

      1. // prepTabaPayToken -
        const prepTabaPayToken = (encryptedToken, decryptedToken) => {
          let preppedToken = {};
          preppedToken['accountNumber'] = decryptedToken["applicationPrimaryAccountNumber"]
          preppedToken['expirationDate'] = "20" + decryptedToken["applicationExpirationDate"].substring(0,4);
          preppedToken['cryptogram'] = decryptedToken["paymentData"]["onlinePaymentCryptogram"];
          preppedToken['transactionID'] = encryptedToken["transactionIdentifier"];
          //===================================
          // eciIndicator will not be present if card is not Visa
          //===================================
          preppedToken['eciIndicator'] = decryptedToken["paymentData"]["eciIndicator"];
          preppedToken['network'] = encryptedToken["paymentMethod"]["network"];
          preppedToken['type'] = encryptedToken["paymentMethod"]["type"];
          //===================================
          // Return
          //===================================
          return preppedToken;
          //===================================
        }
        
  4. Putting it all together

    1. Within our decryption endpoint, we need to verify the signature, restore the symmetric key, and use the symmetric key to decrypt the data.

    2. We also need to ensure we access the merchant identification certificate and the payment processor certificate

    3. Additionally, we need to create a simple function that gathers the required TabaPay API fields.

    4. Lastly, we want to send the decrypted token back to the frontend.

    5. Your endpoint logic should be as follows

      //===================================
      // Endpoint for Apple Pay Token Decryption 
      //===================================
      app.post('/decryptToken', (req,res) => {
        const token = req.body; 
        const ephemeralPublicKey = token['paymentData']['header']['ephemeralPublicKey']
        const encryptedData = token['paymentData']['data'];
        //===================================
        // Import certs
        //===================================
      	const merchantCert = fs.readFileSync('/path/to/cert/merchantIdentifier.pem', 'utf8') 
      	const paymentProcessorCert = fs.readFileSync('/path/to/cert/paymentProcessor.pem', 'utf8') 
        //===================================
      	// Verify signature
        //===================================
      	verifySignature(token);
        //===================================
      	// Restore the symmetric key
        //===================================
      	let symmetricKey = '';
      	try {
      		symmetricKey = restoreSymmetricKey(ephemeralPublicKey, merchantCert, paymentProcessorCert);
      	} catch (err) {
      		throw new Error(`Restore symmetric key failed: ${err.message}`);
      	}
      	try {
          //-----------------------------------
      		// Use the symmetric key to decrypt the value of the data key
          //-----------------------------------
      		const decrypted = JSON.parse(decryptCiphertextFunc(symmetricKey, encryptedData));
          const preppedToken = prepTabaPayToken(token, decrypted)
          //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          // Send decrypted token back to frontend
          //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      		res.send(preppedToken);
      	} catch (err) {
      		throw new Error(`Decrypt cipher data failed: ${err.message}`);
      	}
      });
      
    6. And that’s it! If you fire up your server, press the button, and check your console, you will see your decrypted Apple Pay token! Below is the completed code for the backend (note: this code expands on previous example)

      //***********************************
      // Copyright (c) 2022.   TabaPay, Inc.   All Rights Reserved.
      //***********************************
      
      //**************************** 
      // Apple Pay Express Server
      //**************************** 
      let https;
      try {
      	https = require('https');
      } catch (err) {
      	console.log('https support is disabled');
      }
      
      //********************************************************
      // Imports for system file reading and server implementation
      //******************************************************** 
      const fs = require('fs');
      const express = require('express');
      const cors = require('cors');
      const app = express();
      const axios = require('axios').default;
      
      //**************************** 
      // Imports for token decryption 
      //**************************** 
      const cryptoNative = require('crypto');
      const forge = require('node*forge');
      const ECKey = require('ec*key');
      const asn1js = require('asn1js');
      const pkijs = require('pkijs');
      const Crypto = require('node*webcrypto*ossl');
      
      //************************************************************* 
      // Variables used in signature varification and token decryption 
      //************************************************************* 
      const crypto = new Crypto.Crypto()
      pkijs.setEngine(
        'newEngine',
        crypto,
        new pkijs.CryptoEngine({ name: '', crypto, subtle: crypto.subtle })
      );
      
      //===================================
      // Should be set to 5 minutes (300000 ms) per apple
      //===================================
      const TOKEN_EXPIRE_WINDOW = 300000; 
      
      //===================================
      // OIDs as defined by Apple Pay documentation
      //===================================
      const LEAF_CERTIFICATE_OID = '1.2.840.113635.100.6.29';
      const INTERMEDIATE_CA_OID = '1.2.840.113635.100.6.2.14';
      const SIGNINGTIME_OID = '1.2.840.113549.1.9.5';
      const MERCHANT_ID_FIELD_OID = '1.2.840.113635.100.6.32';
      
      //===================================
      // Retrieve Apple Root CA
      //===================================
      const AppleRootCABuffer = fs.readFileSync('/path/to/cert/AppleRootCA*G3.cer');
      const AppleRootCAASN1 = asn1js.fromBER(new Uint8Array(AppleRootCABuffer).buffer);
      const AppleRootCA = new pkijs.Certificate({ schema: AppleRootCAASN1.result });
      
      // **************************** //
      // SSL Certs to establish HTTPS 
      // **************************** //
      const credentials = {
        key: fs.readFileSync('/path/to/cert/<FQDN>.key'),
        cert: fs.readFileSync('/path/to/cert/<FQDN>.crt')
      };
      
      // **************************** //
      // To enable CORS 
      // **************************** //
      app.use(cors({
      	origin: 'https://<FQDN>.com'
      }));
      
      // ******************************************** //
      // To parse incoming requests with JSON payloads  
      // ******************************************** //
      app.use(express.json());
      
      // **************************** //
      // Server listening on port 8001 
      // **************************** //
      https.createServer(credentials, app).listen(8001);
      console.log('Listening on HTTPS, port 8001');
      
      // ********************************************** //
      // API ENDPOINTS
      // ********************************************** //
      
      //===================================
      // Endpoint for Apple Pay Merchant Verification 
      //===================================
      app.post('/merchantVerification', (req,res) => {
        //-----------------------------------
        // Certs required for TLS Handshake between Backend and Apple Pay Servers
        //-----------------------------------
      	const agent= new https.Agent({
      		cert: fs.readFileSync('/path/to/cert/merchantIdentifier.pem'),
      		key: fs.readFileSync('/path/to/cert/merchantIdentifier.pem'),
      	});
        //-----------------------------------
        // POST to Apple Pay servers
        //-----------------------------------
      	axios({
      		"url": req.body.validationURL, 
      		"httpsAgent": agent,	
      		"method": "post",
      		"headers": {
      			'Accept': 'application/json',
            'Content*Type': 'application/json;charset=UTF*8'
          },
      		"data": {
      			"merchantIdentifier": "Your Merchant Identifier",
      			"displayName": "Whatever you want to display on Touch Bar",
      			"initiative": "web",
      			"initiativeContext": "<FQDN>"
      		}
      	})
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // Send response back to frontend
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      	.then(response => res.send(response.data))
      	.catch(err => console.error(err));
      });
      
      //===================================
      // Endpoint for Apple Pay Token Decryption 
      //===================================
      app.post('/decryptToken', (req,res) => {
        const token = req.body; 
        const ephemeralPublicKey = token['paymentData']['header']['ephemeralPublicKey']
        const encryptedData = token['paymentData']['data'];
        //===================================
        // Import certs
        //===================================
      	const merchantCert = fs.readFileSync('/path/to/cert/merchantIdentifier.pem', 'utf8') 
      	const paymentProcessorCert = fs.readFileSync('/path/to/cert/paymentProcessor.pem', 'utf8') 
        //===================================
      	// Verify signature
        //===================================
      	verifySignature(token);
        //===================================
      	// Restore the symmetric key
        //===================================
      	let symmetricKey = '';
      	try {
      		symmetricKey = restoreSymmetricKey(ephemeralPublicKey, merchantCert, paymentProcessorCert);
      	} catch (err) {
      		throw new Error(`Restore symmetric key failed: ${err.message}`);
      	}
      	try {
          //-----------------------------------
      		// Use the symmetric key to decrypt the value of the data key
          //-----------------------------------
      		const decrypted = JSON.parse(decryptCiphertextFunc(symmetricKey, encryptedData));
          const preppedToken = prepTabaPayToken(token, decrypted)
          //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          // Send decrypted token back to frontend
          //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      		res.send(preppedToken);
      	} catch (err) {
      		throw new Error(`Decrypt cipher data failed: ${err.message}`);
      	}
      });
      
      // ********************************************** //
      // SIGNATURE VERIFICATION
      // ********************************************** //
      
      // verifySignature - 
      const verifySignature = async (token) => {
        //===================================
        // Extract data from token
        //===================================
        const p1 = Buffer.from(token.paymentData.header.ephemeralPublicKey, 'base64');
        const p2 = Buffer.from(token.paymentData.data, 'base64');
        const p3 = Buffer.from(token.paymentData.header.transactionId, 'hex');
        const signedData = Buffer.concat([p1, p2, p3]);
        //-----------------------------------
        // Create CMS Signed Data
        //-----------------------------------
        const cmsSignedBuffer = Buffer.from(token.paymentData.signature, 'base64');
        const cmsSignedASN1 = asn1js.fromBER(new Uint8Array(cmsSignedBuffer).buffer);
        const cmsContentSimpl = new pkijs.ContentInfo({
          schema: cmsSignedASN1.result,
        });
        const cmsSignedData = new pkijs.SignedData({
          schema: cmsContentSimpl.content,
        });
        const signerInfo = cmsSignedData.signerInfos[0];
        //-----------------------------------
      	// 1.a Ensure that the certificates contain the correct custom OIDs: 1.2.840.113635.100.6.29
      	// for the leaf certificate and 1.2.840.113635.100.6.2.14 for the intermediate CA
        //-----------------------------------
        checkCertificates(cmsSignedData.certificates);
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      	// 1.b Ensure that the root CA is the Apple Root CA * G3 - root CA downloaded from Apple web site so this is satisfied
      	// 1.c Ensure that there is a valid X.509 chain of trust from the signature to the root CA
        // 1.d Validate the token’s signature
        // PKI.js can check chain of trust and verify on one shot, so 1.c and 1.d can be done together
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        const ret = await validateSignature(cmsSignedData, AppleRootCA, signedData);
        if (!ret.signatureVerified) {
          throw new Error('CMS signed data verification failed');
        }
        //:::::::::::::::::::::::::::::::::::
      	// 1.e Inspect the CMS signing time of the signature
        //:::::::::::::::::::::::::::::::::::
        checkSigningTime(signerInfo);
      };
      
      // checkCertificates - 
      const checkCertificates = (certificates) => {
        //===================================
        // Check certificates contain correct OIDs
        //===================================
        if (certificates.length !== 2) {
          throw new Error(
            `Signature certificates number error: expected 2 but got ${certificates.length}`
          );
        }
        if (
          !certificates[0].extensions.find(x => x.extnID === LEAF_CERTIFICATE_OID)
        ) {
          throw new Error(
            `Leaf certificate doesn't have extension: ${LEAF_CERTIFICATE_OID}`
          );
        }
        if (!certificates[1].extensions.find(x => x.extnID === INTERMEDIATE_CA_OID)) {
          throw new Error(
            `Intermediate certificate doesn't have extension: ${INTERMEDIATE_CA_OID}`
          );
        }
      }
      
      // validateSignature -
      const validateSignature = (cmsSignedData, rootCA, signedData) => {
        return cmsSignedData.verify({
          //===================================
          // Should only contain 1 signer, verify with it
          //===================================
          signer: 0, 
          trustedCerts: [rootCA],
          data: signedData,
          //===================================
          // Check x509 chain of trust
          //===================================
          checkChain: true, 
          //===================================
          // Enable to show signature validation result
          //===================================
          extendedMode: true, 
        });
      };
      
      // checkSigningTime -
      const checkSigningTime = (signerInfo) => {
        //===================================
        // Inspect the CMS signing time of the signature 
        //===================================
        const signerInfoAttrs = signerInfo.signedAttrs.attributes;
        const attr = signerInfoAttrs.find(x => x.type === SIGNINGTIME_OID);
        const signedTime = new Date(attr.values[0].toDate());
        const now = new Date();
        if (now * signedTime > TOKEN_EXPIRE_WINDOW) {
          throw new Error('Signature has expired');
        }
      }
      
      // ********************************************** //
      // TOKEN DECRYPTION
      // ********************************************** //
      
      // extractMerchantID -
      const extractMerchantID = (merchantCert) => {
        //===================================
        // Extract merchant identification from public key certificate
        //===================================
        try {
      		const info = forge.pki.certificateFromPem(merchantCert);
          const result = info['extensions'].filter(d => d.id === MERCHANT_ID_FIELD_OID);
          //-----------------------------------
          // Return
          //-----------------------------------
          return result[0].value.toString().substring(2);
        } catch (err) {
          throw new Error(`Unable to extract merchant ID from certificate: ${err}`);
        }
      }
      
      // generateSharedSecret -
      const generateSharedSecret = (merchantPrivateKey, ephemeralPublicKey) => {
        //===================================
        // Use private key from payment processing certificate and the ephemeral public key to generate 
        // the shared secret using Elliptic Curve Diffie*Hellman (ECDH)
        //===================================
      	const privateKey = new ECKey(merchantPrivateKey, 'pem'); 
      	const publicKey = new ECKey(ephemeralPublicKey, 'spki'); 
        //-----------------------------------
        // Return
        //-----------------------------------
      	return privateKey.computeSecret(publicKey).toString('hex'); 
        //-----------------------------------
      }
      
      // getSymmetricKey -
      const getSymmetricKey = (merchantId, sharedSecret) => {
        //===================================
        // Get KDF_Info as defined from Apple Pay documentation
        //===================================
        const KDF_ALGORITHM = '\x0did*aes256*GCM'; 
        const KDF_PARTY_V = Buffer.from(merchantId, 'hex').toString('binary'); 
        const KDF_PARTY_U = 'Apple'; 
        const KDF_INFO = KDF_ALGORITHM + KDF_PARTY_U + KDF_PARTY_V;
        //-----------------------------------
        // Create hash
        //-----------------------------------
        const hash = cryptoNative.createHash('sha256');
        hash.update(Buffer.from('000000', 'hex'));
        hash.update(Buffer.from('01', 'hex'));
        hash.update(Buffer.from(sharedSecret, 'hex'));
        hash.update(KDF_INFO, 'binary');
        //-----------------------------------
        // Return
        //-----------------------------------
        return hash.digest('hex');
        //-----------------------------------
      }
      
      // restoreSymmetricKey -
      const restoreSymmetricKey = (ephemeralPublicKey, merchantCert, paymentProcessorCert) => {
        //===================================
        // 3.a Use the payment processor private key and the ephemeral public key, to generate the shared secret
        //===================================
        const sharedSecret = generateSharedSecret(paymentProcessorCert, ephemeralPublicKey);
        //-----------------------------------
        // 3.b Use the merchant identifier of the public key certificate and the shared secret, to derive the symmetric key
        //-----------------------------------
        const merchantId = extractMerchantID(merchantCert);
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // Return
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        return getSymmetricKey(merchantId, sharedSecret);
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      }
      
      // decryptCiphertextFunc -
      const decryptCiphertextFunc = (symmetricKey, encryptedData) => {
        //===================================
        // Get symmetric key and initialization vector
        //===================================
        const buf = Buffer.from(encryptedData, 'base64');
        const SYMMETRIC_KEY = Buffer.from(symmetricKey, 'hex');
        const IV = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); // Initialization vector of 16 null bytes
        const CIPHERTEXT = buf.slice(0, -16);
        //-----------------------------------
        // Create and return a Decipher object that uses the given algorithm and password (key)
        //-----------------------------------
        const decipher = cryptoNative.createDecipheriv(
          'aes*256*gcm',
          SYMMETRIC_KEY,
          IV
        );
        const tag = buf.slice(-16, buf.length);
        decipher.setAuthTag(tag);
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // Load encrypted token into Decipher object
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        let decrypted = decipher.update(CIPHERTEXT);
        decrypted += decipher.final();
        //:::::::::::::::::::::::::::::::::::
        // Return
        //:::::::::::::::::::::::::::::::::::
        return decrypted;
        //:::::::::::::::::::::::::::::::::::
      }
      
      // prepTabaPayToken -
      const prepTabaPayToken = (encryptedToken, decryptedToken) => {
        let preppedToken = {};
        preppedToken['accountNumber'] = decryptedToken["applicationPrimaryAccountNumber"]
        preppedToken['expirationDate'] = "20" + decryptedToken["applicationExpirationDate"].substring(0,4);
        preppedToken['cryptogram'] = decryptedToken["paymentData"]["onlinePaymentCryptogram"];
        preppedToken['transactionID'] = encryptedToken["transactionIdentifier"];
        //===================================
        // eciIndicator will not be present if card is not Visa
        //===================================
        preppedToken['eciIndicator'] = decryptedToken["paymentData"]["eciIndicator"];
        preppedToken['network'] = encryptedToken["paymentMethod"]["network"];
        preppedToken['type'] = encryptedToken["paymentMethod"]["type"];
        //===================================
        // Return
        //===================================
        return preppedToken;
        //===================================
      }