You can now accept Android Pay in your app or mobile site using the Google Payment API.

Payment Token Cryptography

This page provides information on generating a public key to request an Android Pay payment token along with steps to decrypt the token.

Encryption Scheme specification

Android Pay uses Elliptic Curve Integrated Encryption Scheme (ECIES) to secure the payment method token returned in the full Wallet response, with the following parameters:

  1. Key encapsulation method used is ECIES-KEM, as defined in ISO 18033-2
    • Elliptic curve: NIST P-256 (also known in openssl as prime256v1)
    • CheckMode, OldCofactorMode, SingleHashMode and CofactorMode are 0
    • Point format is uncompressed
  2. Key Derivation Function
    • HKDFwithSHA256, as described in the RFC, using the parameters below:
      1. salt should not be provided
      2. info should be Android, encoded in ASCII
    • 128 bits should be derived for the AES128 key and another 128 bits should be derived for the HMAC_SHA256 key
  3. For the symmetric encryption algorithm, use DEM2 from ISO 18033-2 with the following parameters:
    • encryption algorithm: AES128 CTR with zero IV and no padding
    • mac algorithm: HMAC_SHA256 using a key of 128bits as derived in 2)

Setting a public key

Your app provides an Elliptic Curve Public Key as part of the MaskedWalletRequest. You (or the processor on behalf of you) will generate and manage the key pair. The public key should be in the uncompressed point format and encoded as a base64 string. For generating such a public/private key pair, see the example in Using OpenSSL to Generate and Format a Key.

Retrieving the encrypted payload

The encrypted payload returned by Android Pay in the FullWallet response should be decrypted on the merchant server or forwarded to processor for decryption.

The returned token will be a UTF-8 encoded serialized JSON dictionary with the following keys:

Name Type Description
encryptedMessage string (base64) The encrypted message
ephemeralPublicKey string (base64) The ephemeral public key associated with the private key to encrypt the message
tag string (base64) MAC of encryptedMessage

Example payment method token response in JSON:

{
  “encryptedMessage”: “ZW5jcnlwdGVkTWVzc2FnZQ==”,
  “ephemeralPublicKey”: “ZXBoZW1lcmFsUHVibGljS2V5”,
  "tag": ”c2lnbmF0dXJl”
}
  

The decrypted payment credential (the result of decrypting encryptedMessage) is a UTF-8 encoded, serialized JSON dictionary with the following keys:

Name Type Description
dpan string (digits only) The device-specific personal account number (i.e., token)
expirationMonth number The expiration month of the dpan (1 = January, 2 = February, etc.)
expirationYear number The four-digit expiration year of the dpan (e.g., 2015)
authMethod string The constant “3DS”. This may change in the future.
3dsCryptogram string 3DSecure cryptogram
3dsEciIndicator string (optional) ECI indicator per 3DSecure specification

Example in JSON:

{
  “dpan”: “4444444444444444”,
  “expirationMonth”: 10,
  “expirationYear”: 2015,
  “authMethod”: “3DS”,
  “3dsCryptogram”: “AAAAAA...”,
  “3dsEciIndicator”: “eci indicator”
}
  

After decrypting the token bundle (either on the merchant server or by a processor), a transaction request gets submitted to the processor to charge the network token.

Decrypting the payment token

To decrypt the token, follow these steps:

  1. Using your private key and the given ephemeralPublicKey, derive a 256 bit long shared key using ECIES-KEM, as defined in ISO 18033-2, using the following parameters:
    • Elliptic curve: NIST P-256 (also known in OpenSSL as prime256v1)
    • CheckMode, OldCofactorMode, SingleHashMode and CofactorMode are 0
    • Encoding function: Uncompressed Point format
    • Key Derivation Function: HKDFwithSHA256, as described in RFC 5689, using the following parameters:
      • salt should not be provided (per the RFC, this should be equivalent to a salt of 32 zeroed bytes)
      • info should be Android, encoded in ASCII
  2. Split the generated key into two 128-bit-long keys, symmetricEncryptionKey and macKey
  3. Verify that the tag field is a valid MAC for encryptedMessage:
    • For generating the expected MAC, use HMAC (RFC 5869) with hash function SHA256 and the macKey obtained above
    • Remember to use a constant time array comparison to avoid timing attacks
  4. Decrypt encryptedMessage using AES128 CTR mode with a zero IV, no padding, and the symmetricEncryptionKey derived above

Example: Using OpenSSL to generate and format a public key

The following example generates an Elliptic Curve private key suitable for use with NIST P-256 and writes it to merchant-key.pem:

openssl ecparam -name prime256v1 -genkey -noout -out merchant-key.pem

To view both the private and public key, you may use the following command:

openssl ec -in merchant-key.pem -pubout -text -noout

This should produce output similar to this:

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

The private and public key that is generated in the above example is HEX-encoded. In order to get a Base64-encoded public key, use the following command:

openssl ec -in merchant-key.pem -pubout -text -noout | grep "pub:" -A5 | xxd -r -p | base64 | paste -sd '' -

This should produce output similar to this:

read EC key
BOdoXP+9Aq473SnGwg3JU1aiNpsd9vH2ognq4PtDtlLGa3Kj8TPf+jaQNPyDSkh3JUhiS0KyrrlWhAgNZKHYF2Y=

You can then pass the base64 string in the PaymentMethodTokenizationParameters as the publicKey parameter in the MaskedWallet request.

Example: Token decryption

See the decryption sample:

  
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
import org.bouncycastle.crypto.params.HKDFParameters;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.ECPointUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex;
import org.json.JSONException;
import org.json.JSONObject;

import java.nio.charset.Charset;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyAgreement;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/** Utility for decrypting encrypted network tokens as per Android Pay InApp spec. */
class NetworkTokenDecryptionUtil {

  private static final String SECURITY_PROVIDER = "BC";
  private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
  private static final String ASYMMETRIC_KEY_TYPE = "EC";
  private static final String KEY_AGREEMENT_ALGORITHM = "ECDH";
  /** OpenSSL name of the NIST P-126 Elliptic Curve */
  private static final String EC_CURVE = "prime256v1";
  private static final String SYMMETRIC_KEY_TYPE = "AES";
  private static final String SYMMETRIC_ALGORITHM = "AES/CTR/NoPadding";
  private static final byte[] SYMMETRIC_IV = Hex.decode("00000000000000000000000000000000");
  private static final int SYMMETRIC_KEY_BYTE_COUNT = 16;
  private static final String MAC_ALGORITHM = "HmacSHA256";
  private static final int MAC_KEY_BYTE_COUNT = 16;
  private static final byte[] HKDF_INFO = "Android".getBytes(DEFAULT_CHARSET);
  private static final byte[] HKDF_SALT = null /* equivalent to a zeroed salt of hashLen */;

  private PrivateKey privateKey;

  private NetworkTokenDecryptionUtil(PrivateKey privateKey) {
    if (!ASYMMETRIC_KEY_TYPE.equals(privateKey.getAlgorithm())) {
      throw new IllegalArgumentException("Unexpected type of private key");
    }
    this.privateKey = privateKey;
  }

  public static NetworkTokenDecryptionUtil createFromPkcs8EncodedPrivateKey(
      byte[] pkcs8PrivateKey) {
    PrivateKey privateKey;
    try {
      KeyFactory asymmetricKeyFactory =
          KeyFactory.getInstance(ASYMMETRIC_KEY_TYPE, SECURITY_PROVIDER);
      privateKey = asymmetricKeyFactory.generatePrivate(
          new PKCS8EncodedKeySpec(pkcs8PrivateKey));
    } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException e) {
      throw new RuntimeException("Failed to create NetworkTokenDecryptionUtil", e);
    }
    return new NetworkTokenDecryptionUtil(privateKey);
  }

  /**
   * Sets up the {@link #SECURITY_PROVIDER} if not yet set up.
   *
   *You must call this method at least once before using this class.
   */
  public static void setupSecurityProviderIfNecessary() {
    if (Security.getProvider(SECURITY_PROVIDER) == null) {
      Security.addProvider(new BouncyCastleProvider());
    }
  }

  /**
   * Verifies then decrypts the given payload according to the Android Pay Network Token
   * encryption spec.
   */
  public String verifyThenDecrypt(String encryptedPayloadJson) {
    try {
      JSONObject object = new JSONObject(encryptedPayloadJson);
      byte[] ephemeralPublicKeyBytes =
          Base64.decode(object.getString("ephemeralPublicKey"));
      byte[] encryptedMessage = Base64.decode(object.getString("encryptedMessage"));
      byte[] tag = Base64.decode(object.getString("tag"));

      // Parsing public key.
      ECParameterSpec asymmetricKeyParams = generateECParameterSpec();
      KeyFactory asymmetricKeyFactory =
          KeyFactory.getInstance(ASYMMETRIC_KEY_TYPE, SECURITY_PROVIDER);
      PublicKey ephemeralPublicKey = asymmetricKeyFactory.generatePublic(
        new ECPublicKeySpec(
              ECPointUtil.decodePoint(asymmetricKeyParams.getCurve(), ephemeralPublicKeyBytes),
              asymmetricKeyParams));

      // Deriving shared secret.
      KeyAgreement keyAgreement =
          KeyAgreement.getInstance(KEY_AGREEMENT_ALGORITHM, SECURITY_PROVIDER);
      keyAgreement.init(privateKey);
      keyAgreement.doPhase(ephemeralPublicKey, true);
      byte[] sharedSecret = keyAgreement.generateSecret();

      // Deriving encryption and mac keys.
      HKDFBytesGenerator hkdfBytesGenerator = new HKDFBytesGenerator(new SHA256Digest());
      byte[] khdfInput = ByteUtils.concatenate(ephemeralPublicKeyBytes, sharedSecret);
      hkdfBytesGenerator.init(new HKDFParameters(khdfInput, HKDF_SALT, HKDF_INFO));
      byte[] encryptionKey = new byte[SYMMETRIC_KEY_BYTE_COUNT];
      hkdfBytesGenerator.generateBytes(encryptionKey, 0, SYMMETRIC_KEY_BYTE_COUNT);
      byte[] macKey = new byte[MAC_KEY_BYTE_COUNT];
      hkdfBytesGenerator.generateBytes(macKey, 0, MAC_KEY_BYTE_COUNT);

      // Verifying Message Authentication Code (aka mac/tag)
      Mac macGenerator = Mac.getInstance(MAC_ALGORITHM, SECURITY_PROVIDER);
      macGenerator.init(new SecretKeySpec(macKey, MAC_ALGORITHM));
      byte[] expectedTag = macGenerator.doFinal(encryptedMessage);
      if (!isArrayEqual(tag, expectedTag)) {
        throw new RuntimeException("Bad Message Authentication Code!");
      }

      // Decrypting the message.
      Cipher cipher = Cipher.getInstance(SYMMETRIC_ALGORITHM);
      cipher.init(
          Cipher.DECRYPT_MODE,
          new SecretKeySpec(encryptionKey, SYMMETRIC_KEY_TYPE),
          new IvParameterSpec(SYMMETRIC_IV));
      return new String(cipher.doFinal(encryptedMessage), DEFAULT_CHARSET);
    } catch (JSONException | NoSuchAlgorithmException | NoSuchProviderException
        | InvalidKeySpecException | InvalidKeyException | NoSuchPaddingException
        | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
      throw new RuntimeException("Failed verifying/decrypting message", e);
    }
  }

  private ECNamedCurveSpec generateECParameterSpec() {
    ECNamedCurveParameterSpec bcParams = ECNamedCurveTable.getParameterSpec(EC_CURVE);
    ECNamedCurveSpec params = new ECNamedCurveSpec(bcParams.getName(), bcParams.getCurve(),
        bcParams.getG(), bcParams.getN(), bcParams.getH(), bcParams.getSeed());
    return params;
  }

  /**
   * Fixed-timing array comparison.
   */
  public static boolean isArrayEqual(byte[] a, byte[] b) {
    if (a.length != b.length) {
      return false;
    }

    int result = 0;
    for (int i = 0; i < a.length; i++) {
      result |= a[i] ^ b[i];
    }
    return result == 0;
  }
}
  

See the unit test sample:

  
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import com.google.common.io.BaseEncoding;

import org.bouncycastle.util.encoders.Base64;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Unit tests for {@link NetworkTokenDecryptionUtil}. */
@RunWith(JUnit4.class)
public class NetworkTokenDecryptionUtilTest {

  /**
   * Created with:
   * openssl pkcs8 -topk8 -inform PEM -outform PEM -in merchant-key.pem -nocrypt
   */
  private static final String MERCHANT_PRIVATE_KEY_PKCS8_BASE64 =
      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgCPSuFr4iSIaQprjj"
      + "chHPyDu2NXFe0vDBoTpPkYaK9dehRANCAATnaFz/vQKuO90pxsINyVNWojabHfbx"
      + "9qIJ6uD7Q7ZSxmtyo/Ez3/o2kDT8g0pIdyVIYktCsq65VoQIDWSh2Bdm";

  private static final String ENCRYPTED_PAYLOAD = "{"
      + "\"encryptedMessage\":\"PHxZxBQvVWwP\","
      + "\"ephemeralPublicKey\":\"BPhVspn70Zj2Kkgu9t8+ApEuUWsI\\/zos5whGCQBlgOkuYagOis7qsrcbQrcpr"
      + "jvTZO3XOU+Qbcc28FSgsRtcgQE=\","
      + "\"tag\":\"TNwa3Q2WiyGi\\/eDA4XYVklq08KZiSxB7xvRiKK3H7kE=\"}";

  private NetworkTokenDecryptionUtil util;

  @Before
  public void setUp() {
    NetworkTokenDecryptionUtil.setupSecurityProviderIfNecessary();
    util = NetworkTokenDecryptionUtil.createFromPkcs8EncodedPrivateKey(
        BaseEncoding.base64().decode(MERCHANT_PRIVATE_KEY_PKCS8_BASE64));
  }

  @Test
  public void testShouldDecrypt() {
    assertEquals("plaintext", util.verifyThenDecrypt(ENCRYPTED_PAYLOAD));
  }

  @Test
  public void testShouldFailIfBadTag() throws Exception {
    JSONObject payload = new JSONObject(ENCRYPTED_PAYLOAD);
    byte[] tag = Base64.decode(payload.getString("tag"));
    // Messing with the first byte
    tag[0] = (byte) ~tag[0];
    payload.put("tag", new String(Base64.encode(tag)));

    try {
      util.verifyThenDecrypt(payload.toString());
      fail();
    } catch (RuntimeException e) {
      assertEquals("Bad Message Authentication Code!", e.getMessage());
    }
  }

  @Test
  public void testShouldFailIfEncryptedMessageWasChanged() throws Exception {
    JSONObject payload = new JSONObject(ENCRYPTED_PAYLOAD);
    byte[] encryptedMessage = Base64.decode(payload.getString("encryptedMessage"));
    // Messing with the first byte
    encryptedMessage[0] = (byte) ~encryptedMessage[0];
    payload.put("encryptedMessage", new String(Base64.encode(encryptedMessage)));

    try {
      util.verifyThenDecrypt(payload.toString());
      fail();
    } catch (RuntimeException e) {
      assertEquals("Bad Message Authentication Code!", e.getMessage());
    }
  }
}
  

Kirim masukan tentang...

Android Pay API
Butuh bantuan? Kunjungi halaman dukungan kami.