diff --git a/WORKSPACE b/WORKSPACE index 8a5b9eb213a17d02388b1bf78a72abeec7672cf6..a3158d2a8f3b8ec805ae291bf6d8d313ad4dd4b1 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -177,6 +177,12 @@ maven_jar( sha1 = "07c10d545325e3a6e72e06381afe469fd40eb701", ) +maven_jar( + name = "json", + artifact = "org.json:json:20170516", + sha1 = "949abe1460757b8dc9902c562f83e49675444572", +) + maven_jar( name = "junit_junit_4", artifact = "junit:junit:jar:4.12", diff --git a/apps/BUILD b/apps/BUILD new file mode 100644 index 0000000000000000000000000000000000000000..f631b6df06d13b4ecf09aed3d810f02b996f197e --- /dev/null +++ b/apps/BUILD @@ -0,0 +1,3 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) # Apache 2.0 diff --git a/apps/README.md b/apps/README.md new file mode 100644 index 0000000000000000000000000000000000000000..699896edf606b649ea89e30229fc114aefbe6fed --- /dev/null +++ b/apps/README.md @@ -0,0 +1 @@ +This folder contains applications that depend on or extend Tink's functionality. diff --git a/apps/googlepayments/BUILD b/apps/googlepayments/BUILD new file mode 100644 index 0000000000000000000000000000000000000000..bd5dcff956db777aab1463ecb0a8a0f53935d51c --- /dev/null +++ b/apps/googlepayments/BUILD @@ -0,0 +1,43 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) # Apache 2.0 + +java_library( + name = "payment_token_method", + srcs = glob([ + "src/main/**/*.java", + ]), + deps = [ + "//java", + "//java:subtle", + "@com_google_guava//jar", + "@json//jar", + ], +) + +# Tests + +load("//tools:gen_java_test_rules.bzl", "gen_java_test_rules") + +java_library( + name = "generator_test", + testonly = 1, + srcs = glob([ + "src/test/**/*.java", + ]), + deps = [ + ":payment_token_method", + "//java:testonly", + "@json//jar", + "@junit_junit_4//jar", + ], +) + +gen_java_test_rules( + test_files = glob([ + "src/test/**/*Test.java", + ]), + deps = [ + ":generator_test", + ], +) diff --git a/apps/googlepayments/README.md b/apps/googlepayments/README.md new file mode 100644 index 0000000000000000000000000000000000000000..bd264aab2b11896f8da2aef6546cf211841303bf --- /dev/null +++ b/apps/googlepayments/README.md @@ -0,0 +1,3 @@ + # An implementation of [Google Payment Method Token](https://developers.google.com/android-pay/integration/payment-token-cryptography). + + This implementation supports version ECv1 or newer. diff --git a/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenConstants.java b/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..220fbb43d5da6b6eef73cf3aa53c45d3ab0bc9f9 --- /dev/null +++ b/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenConstants.java @@ -0,0 +1,46 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.payments; + +import com.google.crypto.tink.subtle.EcUtil; +import java.nio.charset.StandardCharsets; + +/** + * Various constants. + */ +public final class PaymentMethodTokenConstants { + public static final String GOOGLE_SENDER_ID = "Google"; + public static final String HMAC_SHA256_ALGO = "HmacSha256"; + public static final byte[] HKDF_EMPTY_SALT = new byte[0]; + public static final byte[] GOOGLE_CONTEXT_INFO_ECV1 = "Google".getBytes(StandardCharsets.UTF_8); + public static final String AES_CTR_ALGO = "AES/CTR/NoPadding"; + public static final int AES_CTR_KEY_SIZE = 16; + // Zero IV is fine here because each encryption uses a unique key. + public static final byte[] AES_CTR_ZERO_IV = new byte[16]; + public static final int HMAC_SHA256_KEY_SIZE = 16; + public static final EcUtil.PointFormatEnum UNCOMPRESSED_POINT_FORMAT = + EcUtil.PointFormatEnum.UNCOMPRESSED; + public static final String PROTOCOL_VERSION_EC_V1 = "ECv1"; + public static final String ECDSA_SHA256_SIGNING_ALGO = "SHA256WithECDSA"; + + public static final String JSON_ENCRYPTED_MESSAGE_KEY = "encryptedMessage"; + public static final String JSON_TAG_KEY = "tag"; + public static final String JSON_EPHEMERAL_PUBLIC_KEY = "ephemeralPublicKey"; + public static final String JSON_SIGNATURE_KEY = "signature"; + public static final String JSON_SIGNED_MESSAGE_KEY = "signedMessage"; + public static final String JSON_PROTOCOL_VERSION_KEY = "protocolVersion"; +} diff --git a/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenHybridDecrypt.java b/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenHybridDecrypt.java new file mode 100644 index 0000000000000000000000000000000000000000..86191af6941c51d9f5763e287a66a5703f8a2a96 --- /dev/null +++ b/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenHybridDecrypt.java @@ -0,0 +1,84 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.payments; + +import com.google.crypto.tink.HybridDecrypt; +import com.google.crypto.tink.subtle.EciesHkdfRecipientKem; +import com.google.crypto.tink.subtle.SubtleUtil; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.interfaces.ECPrivateKey; +import java.util.Arrays; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * An implementation of Google Payment Method Token. + * See {@link https://developers.google.com/android-pay/integration/payment-token-cryptography}. + */ +class PaymentMethodTokenHybridDecrypt implements HybridDecrypt { + private final EciesHkdfRecipientKem recipientKem; + + public PaymentMethodTokenHybridDecrypt(final ECPrivateKey recipientPrivateKey) + throws GeneralSecurityException { + this.recipientKem = new EciesHkdfRecipientKem(recipientPrivateKey); + } + + @Override + public byte[] decrypt(final byte[] ciphertext, final byte[] contextInfo) + throws GeneralSecurityException { + try { + JSONObject json = new JSONObject(new String(ciphertext, StandardCharsets.UTF_8)); + validate(json); + byte[] kem = PaymentMethodTokenUtil.BASE64.decode(json.getString(PaymentMethodTokenConstants.JSON_EPHEMERAL_PUBLIC_KEY)); + int symmetricKeySize = PaymentMethodTokenConstants.AES_CTR_KEY_SIZE + + PaymentMethodTokenConstants.HMAC_SHA256_KEY_SIZE; + byte[] demKey = recipientKem.generateKey( + kem, + PaymentMethodTokenConstants.HMAC_SHA256_ALGO, + PaymentMethodTokenConstants.HKDF_EMPTY_SALT, + contextInfo, + symmetricKeySize, + PaymentMethodTokenConstants.UNCOMPRESSED_POINT_FORMAT); + byte[] hmacSha256Key = Arrays.copyOfRange( + demKey, PaymentMethodTokenConstants.AES_CTR_KEY_SIZE, symmetricKeySize); + byte[] encryptedMessage = PaymentMethodTokenUtil.BASE64.decode( + json.getString(PaymentMethodTokenConstants.JSON_ENCRYPTED_MESSAGE_KEY)); + byte[] computedTag = PaymentMethodTokenUtil.hmacSha256(hmacSha256Key, + encryptedMessage); + byte[] expectedTag = PaymentMethodTokenUtil.BASE64.decode(json.getString(PaymentMethodTokenConstants.JSON_TAG_KEY)); + if (!SubtleUtil.arrayEquals(expectedTag, computedTag)) { + throw new GeneralSecurityException("cannot decrypt; invalid MAC"); + } + byte[] aesCtrKey = Arrays.copyOfRange(demKey, 0, + PaymentMethodTokenConstants.AES_CTR_KEY_SIZE); + return PaymentMethodTokenUtil.aesCtr(aesCtrKey, encryptedMessage); + } catch (JSONException e) { + throw new GeneralSecurityException("cannot decrypt; failed to parse JSON", e); + } + } + + private void validate(final JSONObject payload) throws GeneralSecurityException { + if (!payload.has(PaymentMethodTokenConstants.JSON_ENCRYPTED_MESSAGE_KEY) + || !payload.has(PaymentMethodTokenConstants.JSON_TAG_KEY) + || !payload.has(PaymentMethodTokenConstants.JSON_EPHEMERAL_PUBLIC_KEY) + || payload.length() != 3) { + throw new GeneralSecurityException( + "The payload must contain exactly encryptedMessage, tag and ephemeralPublicKey"); + } + } +} diff --git a/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenHybridEncrypt.java b/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenHybridEncrypt.java new file mode 100644 index 0000000000000000000000000000000000000000..0e8108cc9be15e5739a2286389e7bc4750764763 --- /dev/null +++ b/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenHybridEncrypt.java @@ -0,0 +1,68 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.payments; + +import com.google.crypto.tink.HybridEncrypt; +import com.google.crypto.tink.subtle.EcUtil; +import com.google.crypto.tink.subtle.EciesHkdfSenderKem; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.interfaces.ECPublicKey; +import java.util.Arrays; +import org.json.JSONObject; + +/** + * An implementation of Google Payment Method Token. + * See {@link https://developers.google.com/android-pay/integration/payment-token-cryptography}. + */ +class PaymentMethodTokenHybridEncrypt implements HybridEncrypt { + private final EciesHkdfSenderKem senderKem; + + public PaymentMethodTokenHybridEncrypt(final ECPublicKey recipientPublicKey) + throws GeneralSecurityException { + EcUtil.checkPublicKey(recipientPublicKey); + this.senderKem = new EciesHkdfSenderKem(recipientPublicKey); + } + + @Override + public byte[] encrypt(final byte[] plaintext, final byte[] contextInfo) + throws GeneralSecurityException { + int symmetricKeySize = PaymentMethodTokenConstants.AES_CTR_KEY_SIZE + + PaymentMethodTokenConstants.HMAC_SHA256_KEY_SIZE; + EciesHkdfSenderKem.KemKey kemKey = senderKem.generateKey( + PaymentMethodTokenConstants.HMAC_SHA256_ALGO, + PaymentMethodTokenConstants.HKDF_EMPTY_SALT, + contextInfo, + symmetricKeySize, + PaymentMethodTokenConstants.UNCOMPRESSED_POINT_FORMAT); + byte[] aesCtrKey = Arrays.copyOfRange(kemKey.getSymmetricKey(), 0, + PaymentMethodTokenConstants.AES_CTR_KEY_SIZE); + byte[] ciphertext = PaymentMethodTokenUtil.aesCtr(aesCtrKey, plaintext); + byte[] hmacSha256Key = Arrays.copyOfRange( + kemKey.getSymmetricKey(), PaymentMethodTokenConstants.AES_CTR_KEY_SIZE, symmetricKeySize); + byte[] tag = PaymentMethodTokenUtil.hmacSha256(hmacSha256Key, ciphertext); + byte[] ephemeralPublicKey = kemKey.getKemBytes(); + return new JSONObject() + .put(PaymentMethodTokenConstants.JSON_ENCRYPTED_MESSAGE_KEY, + PaymentMethodTokenUtil.BASE64.encode(ciphertext)) + .put(PaymentMethodTokenConstants.JSON_TAG_KEY, PaymentMethodTokenUtil.BASE64.encode(tag)) + .put(PaymentMethodTokenConstants.JSON_EPHEMERAL_PUBLIC_KEY, + PaymentMethodTokenUtil.BASE64.encode(ephemeralPublicKey)) + .toString() + .getBytes(StandardCharsets.UTF_8); + } +} diff --git a/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenRecipient.java b/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenRecipient.java new file mode 100644 index 0000000000000000000000000000000000000000..477adaf537d3abff31d61eeb65468d7f57e59ab4 --- /dev/null +++ b/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenRecipient.java @@ -0,0 +1,282 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.payments; + +import com.google.crypto.tink.HybridDecrypt; +import com.google.crypto.tink.PublicKeyVerify; +import com.google.crypto.tink.subtle.EcdsaVerifyJce; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.util.ArrayList; +import java.util.List; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * An implementation of the recipient side of Google Payment Method Token. + * See {@link https://developers.google.com/android-pay/integration/payment-token-cryptography}. + * This implementation supports only version ECv1. + */ +public final class PaymentMethodTokenRecipient { + private final String protocolVersion; + private final List<PublicKeyVerify> verifiers = new ArrayList<PublicKeyVerify>(); + private final List<HybridDecrypt> hybridDecrypters = new ArrayList<HybridDecrypt>(); + private final String senderId; + private final String recipientId; + + PaymentMethodTokenRecipient( + String protocolVersion, + final List<ECPublicKey> senderVerifyingKeys, + String senderId, + final List<ECPrivateKey> recipientPrivateKeys, + String recipientId) + throws GeneralSecurityException { + if (!protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)) { + throw new IllegalArgumentException("invalid version: " + protocolVersion); + } + this.protocolVersion = protocolVersion; + if (senderVerifyingKeys == null || senderVerifyingKeys.isEmpty()) { + throw new IllegalArgumentException( + "must set at least one sender's verifying key using Builder.senderVerifyingKeys"); + } + for (ECPublicKey publicKey : senderVerifyingKeys) { + verifiers.add(new EcdsaVerifyJce(publicKey, + PaymentMethodTokenConstants.ECDSA_SHA256_SIGNING_ALGO)); + } + this.senderId = senderId; + + if (recipientPrivateKeys == null || recipientPrivateKeys.isEmpty()) { + throw new IllegalArgumentException( + "must add at least one recipient's decrypting key using Builder.addRecipientPrivateKey"); + } + for (ECPrivateKey privateKey : recipientPrivateKeys) { + hybridDecrypters.add(new PaymentMethodTokenHybridDecrypt(privateKey)); + } + if (recipientId == null) { + throw new IllegalArgumentException( + "must set recipient Id using Builder.recipientId"); + } + this.recipientId = recipientId; + } + + private PaymentMethodTokenRecipient(Builder builder) throws GeneralSecurityException { + this(builder.protocolVersion, + builder.senderVerifyingKeys, + builder.senderId, + builder.recipientPrivateKeys, + builder.recipientId); + } + + /** + * Builder for PaymentMethodTokenRecipient. + */ + public static class Builder { + private String protocolVersion = PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1; + private String senderId = PaymentMethodTokenConstants.GOOGLE_SENDER_ID; + private String recipientId = null; + private final List<ECPublicKey> senderVerifyingKeys = new ArrayList<ECPublicKey>(); + private final List<ECPrivateKey> recipientPrivateKeys = new ArrayList<ECPrivateKey>(); + + public Builder() { + } + + /** + * Sets the protocolVersion. + */ + public Builder protocolVersion(String val) { + protocolVersion = val; + return this; + } + + /** + * Sets the sender Id. + */ + public Builder senderId(String val) { + senderId = val; + return this; + } + + /** + * Sets the recipient Id. + */ + public Builder recipientId(String val) { + recipientId = val; + return this; + } + + /** + * Sets the trusted verifying public keys of the sender. + * + * <p>The given string is a JSON object formatted like the following: + * + * <pre> + * { + * "keys": [ + * { + * "keyValue": "encoded public key", + * "protocolVersion": "ECv1" + * }, + * { + * "keyValue": "encoded public key", + * "protocolVersion": "ECv1" + * }, + * ], + * } + * </pre> + * + * <p>Each public key will be a base64 (no wrapping, padded) version of the key encoded in ASN.1 + * type SubjectPublicKeyInfo defined in the X.509 standard. + */ + public Builder senderVerifyingKeys(String trustedSigningKeysJson) + throws GeneralSecurityException { + senderVerifyingKeys.clear(); + try { + JSONArray keys = new JSONObject(trustedSigningKeysJson).getJSONArray("keys"); + for (int i = 0; i < keys.length(); i++) { + JSONObject key = keys.getJSONObject(i); + if (protocolVersion.equals(key.getString(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY))) { + senderVerifyingKeys.add( + PaymentMethodTokenUtil.x509EcPublicKey(key.getString("keyValue"))); + } + } + } catch (JSONException e) { + throw new RuntimeException("failed to extract trusted signing public keys", e); + } + if (senderVerifyingKeys.isEmpty()) { + throw new IllegalArgumentException( + "no trusted keys are available for this protocol version"); + } + return this; + } + + /** + * Adds a verifying public key of the sender. + * + * <p>The public key is a base64 (no wrapping, padded) version of the key encoded in ASN.1 + * type SubjectPublicKeyInfo defined in the X.509 standard. + * + * <p>Multiple keys may be added. This utility will then verify any message signed with any of + * the private keys corresponding to the public keys added. Adding multiple keys is useful for + * handling key rotation. + */ + public Builder addSenderVerifyingKey(String val) throws GeneralSecurityException { + senderVerifyingKeys.add(PaymentMethodTokenUtil.x509EcPublicKey(val)); + return this; + } + + public Builder addSenderVerifyingKey(ECPublicKey val) throws GeneralSecurityException { + senderVerifyingKeys.add(val); + return this; + } + + /** + * Sets the decryption private key of the recipient. + * + * <p>It must be base64 encoded PKCS8 private key. + */ + public Builder addRecipientPrivateKey(String val) throws GeneralSecurityException { + recipientPrivateKeys.add(PaymentMethodTokenUtil.pkcs8EcPrivateKey(val)); + return this; + } + + public Builder addRecipientPrivateKey(ECPrivateKey val) throws GeneralSecurityException { + recipientPrivateKeys.add(val); + return this; + } + + public PaymentMethodTokenRecipient build() throws GeneralSecurityException { + return new PaymentMethodTokenRecipient(this); + } + } + + public String unseal(final String sealedMessage) throws GeneralSecurityException { + try { + if (protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)) { + return unsealV1(sealedMessage); + } + throw new IllegalArgumentException("unsupported version: " + protocolVersion); + } catch (JSONException e) { + throw new GeneralSecurityException("cannot unseal; invalid JSON message", e); + } + } + + private String unsealV1(final String sealedMessage) + throws GeneralSecurityException, JSONException { + JSONObject jsonMsg = new JSONObject(sealedMessage); + validateV1(jsonMsg); + byte[] signature = PaymentMethodTokenUtil.BASE64.decode( + jsonMsg.getString(PaymentMethodTokenConstants.JSON_SIGNATURE_KEY)); + String signedMessage = jsonMsg.getString(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY); + byte[] signedBytes = PaymentMethodTokenUtil.toLengthValue( + // The order of the parameters matters. + senderId, + recipientId, + PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1, + signedMessage); + verify(signature, signedBytes); + return decrypt(signedMessage); + } + + private void verify(final byte[] signature, + final byte[] message) throws GeneralSecurityException { + boolean verified = false; + for (PublicKeyVerify verifier : verifiers) { + try { + verifier.verify(signature, message); + // No exception means the signature is valid. + verified = true; + } catch (GeneralSecurityException e) { + // ignored, try again + } + } + if (!verified) { + throw new GeneralSecurityException("cannot verify signature"); + } + } + + private String decrypt(String ciphertext) + throws GeneralSecurityException { + for (HybridDecrypt hybridDecrypter : hybridDecrypters) { + try { + byte[] cleartext = hybridDecrypter.decrypt( + ciphertext.getBytes(StandardCharsets.UTF_8), + PaymentMethodTokenConstants.GOOGLE_CONTEXT_INFO_ECV1); + return new String(cleartext, StandardCharsets.UTF_8); + } catch (GeneralSecurityException e) { + // ignored, try again + } + } + throw new GeneralSecurityException("cannot decrypt"); + } + + private void validateV1(final JSONObject jsonMsg) throws GeneralSecurityException { + if (!jsonMsg.has(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY) + || !jsonMsg.has(PaymentMethodTokenConstants.JSON_SIGNATURE_KEY) + || !jsonMsg.has(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY) + || jsonMsg.length() != 3) { + throw new GeneralSecurityException( + "ECv1 message must contain exactly protocolVersion, signature and signedMessage"); + } + String version = jsonMsg.getString(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY); + if (!version.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)) { + throw new GeneralSecurityException("invalid version: " + version); + } + } +} diff --git a/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenSender.java b/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenSender.java new file mode 100644 index 0000000000000000000000000000000000000000..09aa242bc055bccf081cd16202c67b887057cf60 --- /dev/null +++ b/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenSender.java @@ -0,0 +1,195 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.payments; + +import com.google.crypto.tink.HybridEncrypt; +import com.google.crypto.tink.PublicKeySign; +import com.google.crypto.tink.subtle.EcdsaSignJce; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import org.json.JSONObject; + +/** + * An implementation of the sender side of Google Payment Method Token. + * See {@link https://developers.google.com/android-pay/integration/payment-token-cryptography}. + * This implementation supports only version ECv1. + */ +public final class PaymentMethodTokenSender { + private final String protocolVersion; + private final PublicKeySign signer; + private final HybridEncrypt hybridEncrypter; + private final String senderId; + private final String recipientId; + + PaymentMethodTokenSender( + final String protocolVersion, + final ECPrivateKey senderSigningKey, + String senderId, + final ECPublicKey recipientPublicKey, + String recipientId) + throws GeneralSecurityException { + if (!protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)) { + throw new IllegalArgumentException("invalid version: " + protocolVersion); + } + this.protocolVersion = protocolVersion; + if (senderSigningKey == null) { + throw new IllegalArgumentException( + "must set sender's signing key using Builder.senderSigningKey"); + } + this.signer = new EcdsaSignJce(senderSigningKey, + PaymentMethodTokenConstants.ECDSA_SHA256_SIGNING_ALGO); + this.senderId = senderId; + if (recipientPublicKey == null) { + throw new IllegalArgumentException( + "must set recipient's public key using Builder.recipientPublicKey"); + } + this.hybridEncrypter = new PaymentMethodTokenHybridEncrypt(recipientPublicKey); + if (recipientId == null) { + throw new IllegalArgumentException( + "must set recipient Id using Builder.recipientId"); + } + this.recipientId = recipientId; + } + + private PaymentMethodTokenSender(Builder builder) + throws GeneralSecurityException { + this(builder.protocolVersion, + builder.senderSigningKey, + builder.senderId, + builder.recipientPublicKey, + builder.recipientId); + } + + /** + * Builder for PaymentMethodTokenSender. + */ + public static class Builder { + private String protocolVersion = PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1; + private String senderId = PaymentMethodTokenConstants.GOOGLE_SENDER_ID; + private String recipientId = null; + private ECPrivateKey senderSigningKey = null; + private ECPublicKey recipientPublicKey = null; + + public Builder() { + } + + /** + * Sets the protocolVersion. + */ + public Builder protocolVersion(String val) { + protocolVersion = val; + return this; + } + + /** + * Sets the sender Id. + */ + public Builder senderId(String val) { + senderId = val; + return this; + } + + /** + * Sets the recipient Id. + */ + public Builder recipientId(String val) { + recipientId = val; + return this; + } + + /** + * Sets the signing key of the sender. + * + * <p>It must be base64 encoded PKCS8 private key. + */ + public Builder senderSigningKey(String val) throws GeneralSecurityException { + senderSigningKey = PaymentMethodTokenUtil.pkcs8EcPrivateKey(val); + return this; + } + + public Builder senderSigningKey(ECPrivateKey val) throws GeneralSecurityException { + senderSigningKey = val; + return this; + } + + /** + * Sets the encryption public key of the recipient. + * + * <p>The public key is a base64 (no wrapping, padded) version of the key encoded in ASN.1 + * type SubjectPublicKeyInfo defined in the X.509 standard. + */ + public Builder recipientPublicKey(String val) throws GeneralSecurityException { + recipientPublicKey = PaymentMethodTokenUtil.x509EcPublicKey(val); + return this; + } + + /** + * Sets the encryption public key of the recipient. + * + * <p>The public key must be formatted as base64 encoded uncompressed point format. This format + * is described in more detail in "Public Key Cryptography For The Financial Services Industry: + * The Elliptic Curve Digital Signature Algorithm (ECDSA)", ANSI X9.62, 1998 + */ + public Builder rawUncompressedRecipientPublicKey(String val) throws GeneralSecurityException { + recipientPublicKey = PaymentMethodTokenUtil.rawUncompressedEcPublicKey(val); + return this; + } + + public Builder recipientPublicKey(ECPublicKey val) throws GeneralSecurityException { + recipientPublicKey = val; + return this; + } + + public PaymentMethodTokenSender build() throws GeneralSecurityException { + return new PaymentMethodTokenSender(this); + } + } + + /** + * Seals the input message according to the Payment Method Token specification. + */ + public String seal(final String message) throws GeneralSecurityException { + if (protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)) { + return sealv1(message); + } + throw new GeneralSecurityException("Unsupported version: " + protocolVersion); + } + + private String sealv1(final String message) throws GeneralSecurityException { + String signedMessage = new String( + hybridEncrypter.encrypt( + message.getBytes(StandardCharsets.UTF_8), + PaymentMethodTokenConstants.GOOGLE_CONTEXT_INFO_ECV1), + StandardCharsets.UTF_8); + byte[] toSignBytes = PaymentMethodTokenUtil.toLengthValue( + // The order of the parameters matters. + senderId, + recipientId, + PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1, + signedMessage); + byte[] signature = signer.sign(toSignBytes); + return new JSONObject() + .put(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY, signedMessage) + .put(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, + PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1) + .put(PaymentMethodTokenConstants.JSON_SIGNATURE_KEY, + PaymentMethodTokenUtil.BASE64.encode(signature)) + .toString(); + } +} diff --git a/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenUtil.java b/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..1debb04ce5754232b1d2611fef3cd737ec24886a --- /dev/null +++ b/apps/googlepayments/src/main/java/com/google/payments/PaymentMethodTokenUtil.java @@ -0,0 +1,96 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.payments; + +import com.google.common.io.BaseEncoding; +import com.google.crypto.tink.subtle.EcUtil; +import com.google.crypto.tink.subtle.EngineFactory; +import com.google.crypto.tink.subtle.SubtleUtil; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * Various helpers. + */ +public final class PaymentMethodTokenUtil { + + public static ECPublicKey rawUncompressedEcPublicKey(String rawUncompressedPublicKey) + throws GeneralSecurityException { + ECPoint point = EcUtil.ecPointDecode(EcUtil.getNistP256Params().getCurve(), + EcUtil.PointFormatEnum.UNCOMPRESSED, BASE64.decode(rawUncompressedPublicKey)); + ECPublicKeySpec pubSpec = new ECPublicKeySpec(point, EcUtil.getNistP256Params()); + KeyFactory kf = EngineFactory.KEY_FACTORY.getInstance("EC"); + return (ECPublicKey) kf.generatePublic(pubSpec); + } + + public static ECPublicKey x509EcPublicKey(String x509PublicKey) + throws GeneralSecurityException { + KeyFactory kf = EngineFactory.KEY_FACTORY.getInstance("EC"); + return (ECPublicKey) kf.generatePublic(new X509EncodedKeySpec(BASE64.decode( + x509PublicKey))); + } + + public static ECPrivateKey pkcs8EcPrivateKey(String pkcs8PrivateKey) + throws GeneralSecurityException { + KeyFactory kf = EngineFactory.KEY_FACTORY.getInstance("EC"); + return (ECPrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec( + BASE64.decode(pkcs8PrivateKey))); + } + + public static byte[] toLengthValue(String... chunks) throws GeneralSecurityException { + byte[] out = new byte[0]; + for (String chunk : chunks) { + byte[] bytes = chunk.getBytes(StandardCharsets.UTF_8); + out = SubtleUtil.concat(out, SubtleUtil.intToByteArray(4, bytes.length)); + out = SubtleUtil.concat(out, bytes); + } + return out; + } + + static byte[] aesCtr(final byte[] encryptionKey, final byte[] message) + throws GeneralSecurityException { + Cipher cipher = EngineFactory.CIPHER.getInstance(PaymentMethodTokenConstants.AES_CTR_ALGO); + cipher.init( + Cipher.ENCRYPT_MODE, + new SecretKeySpec(encryptionKey, "AES"), + new IvParameterSpec(PaymentMethodTokenConstants.AES_CTR_ZERO_IV)); + return cipher.doFinal(message); + } + + static byte[] hmacSha256(final byte[] macKey, final byte[] encryptedMessage) + throws GeneralSecurityException { + SecretKeySpec key = new SecretKeySpec(macKey, + PaymentMethodTokenConstants.HMAC_SHA256_ALGO); + Mac mac = EngineFactory.MAC.getInstance( + PaymentMethodTokenConstants.HMAC_SHA256_ALGO); + mac.init(key); + return mac.doFinal(encryptedMessage); + } + + public static final BaseEncoding BASE64 = BaseEncoding.base64(); +} diff --git a/apps/googlepayments/src/test/java/com/google/payments/PaymentMethodTokenHybridDecryptTest.java b/apps/googlepayments/src/test/java/com/google/payments/PaymentMethodTokenHybridDecryptTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ef5b34b2805a6b158b3696d9b983fa59ea53ba11 --- /dev/null +++ b/apps/googlepayments/src/test/java/com/google/payments/PaymentMethodTokenHybridDecryptTest.java @@ -0,0 +1,112 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.payments; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; + +import com.google.crypto.tink.HybridDecrypt; +import com.google.crypto.tink.HybridEncrypt; +import com.google.crypto.tink.subtle.EcUtil; +import com.google.crypto.tink.subtle.Random; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.util.Arrays; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Unit tests for {@code PaymentMethodTokenHybridDecrypt}. + */ +@RunWith(JUnit4.class) +public class PaymentMethodTokenHybridDecryptTest { + @Test + public void testModifyDecrypt() throws Exception { + ECParameterSpec spec = EcUtil.getNistP256Params(); + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); + keyGen.initialize(spec); + KeyPair recipientKey = keyGen.generateKeyPair(); + ECPublicKey recipientPublicKey = (ECPublicKey) recipientKey.getPublic(); + ECPrivateKey recipientPrivateKey = (ECPrivateKey) recipientKey.getPrivate(); + + HybridEncrypt hybridEncrypt = new PaymentMethodTokenHybridEncrypt(recipientPublicKey); + HybridDecrypt hybridDecrypt = new PaymentMethodTokenHybridDecrypt(recipientPrivateKey); + testModifyDecrypt(hybridEncrypt, hybridDecrypt); + } + + public void testModifyDecrypt(HybridEncrypt hybridEncrypt, HybridDecrypt hybridDecrypt) + throws Exception { + byte[] plaintext = Random.randBytes(111); + byte[] context = "context info".getBytes(StandardCharsets.UTF_8); + + byte[] ciphertext = hybridEncrypt.encrypt(plaintext, context); + byte[] decrypted = hybridDecrypt.decrypt(ciphertext, context); + assertArrayEquals(plaintext, decrypted); + + JSONObject json = new JSONObject(new String(ciphertext, StandardCharsets.UTF_8)); + + // Modify public key. + byte[] kem = PaymentMethodTokenUtil.BASE64.decode(json.getString(PaymentMethodTokenConstants.JSON_EPHEMERAL_PUBLIC_KEY)); + for (int bytes = 0; bytes < kem.length; bytes++) { + for (int bit = 0; bit < 8; bit++) { + byte[] modifiedPublicKey = Arrays.copyOf(kem, kem.length); + modifiedPublicKey[bytes] ^= (byte) (1 << bit); + json.put(PaymentMethodTokenConstants.JSON_EPHEMERAL_PUBLIC_KEY, + PaymentMethodTokenUtil.BASE64.encode(modifiedPublicKey)); + try { + hybridDecrypt.decrypt(json.toString().getBytes(StandardCharsets.UTF_8), context); + fail("Invalid ciphertext, should have thrown exception"); + } catch (GeneralSecurityException expected) { + // Expected + } + } + } + + // Modify payload. + byte[] payload = PaymentMethodTokenUtil.BASE64.decode(json.getString( + PaymentMethodTokenConstants.JSON_ENCRYPTED_MESSAGE_KEY)); + for (int bytes = 0; bytes < payload.length; bytes++) { + for (int bit = 0; bit < 8; bit++) { + byte[] modifiedPayload = Arrays.copyOf(payload, payload.length); + modifiedPayload[bytes] ^= (byte) (1 << bit); + json.put(PaymentMethodTokenConstants.JSON_ENCRYPTED_MESSAGE_KEY, + PaymentMethodTokenUtil.BASE64.encode(modifiedPayload)); + try { + hybridDecrypt.decrypt(json.toString().getBytes(StandardCharsets.UTF_8), context); + fail("Invalid ciphertext, should have thrown exception"); + } catch (GeneralSecurityException expected) { + // Expected + } + } + } + + // Modify context. + try { + hybridDecrypt.decrypt(ciphertext, Arrays.copyOf(context, context.length - 1)); + fail("Invalid context, should have thrown exception"); + } catch (GeneralSecurityException expected) { + // Expected + } + } +} diff --git a/apps/googlepayments/src/test/java/com/google/payments/PaymentMethodTokenHybridEncryptTest.java b/apps/googlepayments/src/test/java/com/google/payments/PaymentMethodTokenHybridEncryptTest.java new file mode 100644 index 0000000000000000000000000000000000000000..fa1413a96222234962ad7e0783a99b0721d8013e --- /dev/null +++ b/apps/googlepayments/src/test/java/com/google/payments/PaymentMethodTokenHybridEncryptTest.java @@ -0,0 +1,75 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.payments; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import com.google.crypto.tink.HybridDecrypt; +import com.google.crypto.tink.HybridEncrypt; +import com.google.crypto.tink.subtle.EcUtil; +import com.google.crypto.tink.subtle.Random; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.util.Set; +import java.util.TreeSet; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Unit tests for {@code PaymentMethodTokenHybridEncrypt}. + */ +@RunWith(JUnit4.class) +public class PaymentMethodTokenHybridEncryptTest { + @Test + public void testBasicMultipleEncrypts() throws Exception { + ECParameterSpec spec = EcUtil.getNistP256Params(); + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); + keyGen.initialize(spec); + KeyPair recipientKey = keyGen.generateKeyPair(); + ECPublicKey recipientPublicKey = (ECPublicKey) recipientKey.getPublic(); + ECPrivateKey recipientPrivateKey = (ECPrivateKey) recipientKey.getPrivate(); + + HybridEncrypt hybridEncrypt = new PaymentMethodTokenHybridEncrypt(recipientPublicKey); + HybridDecrypt hybridDecrypt = new PaymentMethodTokenHybridDecrypt(recipientPrivateKey); + testBasicMultipleEncrypts(hybridEncrypt, hybridDecrypt); + } + + public void testBasicMultipleEncrypts(HybridEncrypt hybridEncrypt, HybridDecrypt hybridDecrypt) + throws Exception { + byte[] plaintext = Random.randBytes(111); + byte[] context = "context info".getBytes(StandardCharsets.UTF_8); + // Makes sure that the encryption is randomized. + Set<String> ciphertexts = new TreeSet<String>(); + for (int j = 0; j < 100; j++) { + byte[] ciphertext = hybridEncrypt.encrypt(plaintext, context); + if (ciphertexts.contains(new String(ciphertext, StandardCharsets.UTF_8))) { + throw new GeneralSecurityException("Encryption is not randomized"); + } + ciphertexts.add(new String(ciphertext, StandardCharsets.UTF_8)); + byte[] decrypted = hybridDecrypt.decrypt(ciphertext, context); + assertArrayEquals(plaintext, decrypted); + } + assertEquals(100, ciphertexts.size()); + } +} diff --git a/apps/googlepayments/src/test/java/com/google/payments/PaymentMethodTokenRecipientTest.java b/apps/googlepayments/src/test/java/com/google/payments/PaymentMethodTokenRecipientTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5b4771bcaca2f134bf55141443f443702bb29a1b --- /dev/null +++ b/apps/googlepayments/src/test/java/com/google/payments/PaymentMethodTokenRecipientTest.java @@ -0,0 +1,311 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.payments; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.security.GeneralSecurityException; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Unit tests for {@code PaymentMethodTokenRecipient}. + */ +@RunWith(JUnit4.class) +public class PaymentMethodTokenRecipientTest { + /** + * Created with: + * + * <pre> + * openssl pkcs8 -topk8 -inform PEM -outform PEM -in merchant-key.pem -nocrypt + * </pre> + */ + private static final String MERCHANT_PRIVATE_KEY_PKCS8_BASE64 = + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgCPSuFr4iSIaQprjj" + + "chHPyDu2NXFe0vDBoTpPkYaK9dehRANCAATnaFz/vQKuO90pxsINyVNWojabHfbx" + + "9qIJ6uD7Q7ZSxmtyo/Ez3/o2kDT8g0pIdyVIYktCsq65VoQIDWSh2Bdm"; + + private static final String ALTERNATE_MERCHANT_PRIVATE_KEY_PKCS8_BASE64 = + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgOUIzccyJ3rTx6SVm" + + "XrWdtwUP0NU26nvc8KIYw2GmYZKhRANCAAR5AjmTNAE93hQEQE+PryLlgr6Q7FXyN" + + "XoZRk+1Fikhq61mFhQ9s14MOwGBxd5O6Jwn/sdUrWxkYk3idtNEN1Rz"; + + /** + * Public key value created with: + * + * <pre> + * openssl ec -in google-key.pem -inform PEM -outform PEM -pubout + * </pre> + */ + private static final String GOOGLE_VERIFYING_PUBLIC_KEYS_JSON = + "{\n" + + " \"keys\": [\n" + + " {\n" + + " \"keyValue\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPYnHwS8uegWAewQtlxizmLFynw" + + "HcxRT1PK07cDA6/C4sXrVI1SzZCUx8U8S0LjMrT6ird/VW7be3Mz6t/srtRQ==\",\n" + + " \"protocolVersion\": \"ECv1\"\n" + + " },\n" + + " ],\n" + + "}"; + + private static final String RECIPIENT_ID = "someRecipient"; + + private static final String PLAINTEXT = "plaintext"; + + /** + * The result of {@link #PLAINTEXT} encrypted with {@link #MERCHANT_PRIVATE_KEY_PKCS8_BASE64} + * and signed with the only key in {@link #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON}. + */ + private static final String CIPHERTEXT = + "{" + + "\"protocolVersion\":\"ECv1\"," + + "\"signedMessage\":" + + ("\"{" + + "\\\"tag\\\":\\\"ZVwlJt7dU8Plk0+r8rPF8DmPTvDiOA1UAoNjDV+SqDE\\\\u003d\\\"," + + "\\\"ephemeralPublicKey\\\":\\\"BPhVspn70Zj2Kkgu9t8+ApEuUWsI/zos5whGCQBlgOkuYagOis7" + + "qsrcbQrcprjvTZO3XOU+Qbcc28FSgsRtcgQE\\\\u003d\\\"," + + "\\\"encryptedMessage\\\":\\\"12jUObueVTdy\\\"}\",") + + "\"signature\":\"MEQCIDxBoUCoFRGReLdZ/cABlSSRIKoOEFoU3e27c14vMZtfAiBtX3pGMEpnw6mSAbnagC" + + "CgHlCk3NcFwWYEyxIE6KGZVA\\u003d\\u003d\"}"; + + private static final String ALTERNATE_PUBLIC_SIGNING_KEY = + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEU8E6JppGKFG40r5dDU1idHRN52NuwsemFzXZh1oUqh3bGUPgPioH+RoW" + + "nmVSUQz1WfM2426w9f0GADuXzpUkcw=="; + + @Test + public void testShouldDecryptV1() throws Exception { + PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder() + .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON) + .recipientId(RECIPIENT_ID) + .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64) + .build(); + + assertEquals(PLAINTEXT, recipient.unseal(CIPHERTEXT)); + } + + @Test + public void testShouldTryAllKeysToDecryptV1() throws Exception { + PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder() + .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON) + .recipientId(RECIPIENT_ID) + .addRecipientPrivateKey(ALTERNATE_MERCHANT_PRIVATE_KEY_PKCS8_BASE64) + .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64) + .build(); + + assertEquals(PLAINTEXT, recipient.unseal(CIPHERTEXT)); + } + + @Test + public void testShouldFailIfDecryptingWithDifferentKeyV1() throws Exception { + PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder() + .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON) + .recipientId(RECIPIENT_ID) + .addRecipientPrivateKey(ALTERNATE_MERCHANT_PRIVATE_KEY_PKCS8_BASE64) + .build(); + + try { + recipient.unseal(CIPHERTEXT); + fail("Expected GeneralSecurityException"); + } catch (GeneralSecurityException e) { + assertEquals("cannot decrypt", e.getMessage()); + } + } + + @Test + public void testShouldFailIfVerifyingWithDifferentKeyV1() throws Exception { + JSONObject trustedKeysJson = new JSONObject(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON); + trustedKeysJson + .getJSONArray("keys") + .getJSONObject(0) + .put("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY); + + PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder() + .senderVerifyingKeys(trustedKeysJson.toString()) + .recipientId(RECIPIENT_ID) + .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64) + .build(); + + try { + recipient.unseal(CIPHERTEXT); + fail("Expected GeneralSecurityException"); + } catch (GeneralSecurityException e) { + assertEquals("cannot verify signature", e.getMessage()); + } + } + + @Test + public void testShouldTryAllKeysToVerifySignatureV1() throws Exception { + JSONObject trustedKeysJson = new JSONObject(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON); + JSONArray keys = trustedKeysJson.getJSONArray("keys"); + JSONObject correctKey = new JSONObject(keys.getJSONObject(0).toString()); + JSONObject wrongKey = + new JSONObject(keys.getJSONObject(0).toString()) + .put("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY); + trustedKeysJson.put("keys", new JSONArray().put(wrongKey).put(correctKey)); + + PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder() + .senderVerifyingKeys(trustedKeysJson.toString()) + .recipientId(RECIPIENT_ID) + .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64) + .build(); + + assertEquals(PLAINTEXT, recipient.unseal(CIPHERTEXT)); + } + + @Test + public void testShouldFailIfSignedV1WithKeyForWrongProtocolVersion() throws Exception { + JSONObject trustedKeysJson = new JSONObject(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON); + JSONArray keys = trustedKeysJson.getJSONArray("keys"); + JSONObject correctKeyButWrongProtocol = new JSONObject(keys.getJSONObject(0).toString()) + .put(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, "ECv2"); + JSONObject wrongKeyButRightProtocol = + new JSONObject(keys.getJSONObject(0).toString()) + .put("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY) + .put(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, + PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1); + trustedKeysJson.put( + "keys", new JSONArray().put(correctKeyButWrongProtocol).put(wrongKeyButRightProtocol)); + + PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder() + .senderVerifyingKeys(trustedKeysJson.toString()) + .recipientId(RECIPIENT_ID) + .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64) + .build(); + + try { + recipient.unseal(CIPHERTEXT); + fail("Expected GeneralSecurityException"); + } catch (GeneralSecurityException e) { + assertEquals("cannot verify signature", e.getMessage()); + } + } + + @Test + public void testShouldFailIfNoSigningKeysForProtocolVersion() throws Exception { + JSONObject trustedKeysJson = new JSONObject(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON); + JSONArray keys = trustedKeysJson.getJSONArray("keys"); + JSONObject key1 = new JSONObject(keys.getJSONObject(0).toString()) + .put(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, "ECv2"); + JSONObject key2 = + new JSONObject(keys.getJSONObject(0).toString()) + .put("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY) + .put(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, "ECv3"); + trustedKeysJson.put("keys", new JSONArray().put(key1).put(key2)); + + try { + PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder() + .senderVerifyingKeys(trustedKeysJson.toString()) + .recipientId(RECIPIENT_ID) + .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64) + .build(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals("no trusted keys are available for this protocol version", e.getMessage()); + } + } + + @Test + public void testShouldFailIfSignedMessageWasChangedInV1() throws Exception { + PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder() + .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON) + .recipientId(RECIPIENT_ID) + .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64) + .build(); + JSONObject payload = new JSONObject(CIPHERTEXT); + payload.put(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY, + payload.getString(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY) + " "); + try { + recipient.unseal(payload.toString()); + fail("Expected GeneralSecurityException"); + } catch (GeneralSecurityException e) { + assertEquals("cannot verify signature", e.getMessage()); + } + } + + @Test + public void testShouldFailIfWrongRecipientInV1() throws Exception { + PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder() + .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON) + .recipientId("not " + RECIPIENT_ID) + .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64) + .build(); + try { + recipient.unseal(CIPHERTEXT); + fail("Expected GeneralSecurityException"); + } catch (GeneralSecurityException e) { + assertEquals("cannot verify signature", e.getMessage()); + } + } + + @Test + public void testShouldFailIfV1SetsWrongProtocolVersion() throws Exception { + PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder() + .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON) + .recipientId(RECIPIENT_ID) + .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64) + .build(); + JSONObject payload = new JSONObject(CIPHERTEXT); + String invalidVersion = "ECv2"; + payload.put(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, invalidVersion); + try { + recipient.unseal(payload.toString()); + fail("Expected GeneralSecurityException"); + } catch (GeneralSecurityException e) { + assertEquals("invalid version: " + invalidVersion, e.getMessage()); + } + } + + @Test + public void testShouldFailIfProtocolSetToAnInt() throws Exception { + PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder() + .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON) + .recipientId(RECIPIENT_ID) + .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64) + .build(); + JSONObject payload = new JSONObject(CIPHERTEXT); + payload.put(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, 1); + System.out.println(payload); + try { + recipient.unseal(payload.toString()); + fail("Expected GeneralSecurityException"); + } catch (GeneralSecurityException e) { + assertEquals("cannot unseal; invalid JSON message", e.getMessage()); + } + } + + @Test + public void testShouldFailIfProtocolSetToAnFloat() throws Exception { + PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder() + .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON) + .recipientId(RECIPIENT_ID) + .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64) + .build(); + JSONObject payload = new JSONObject(CIPHERTEXT); + payload.put(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, 1.1); + System.out.println(payload); + try { + recipient.unseal(payload.toString()); + fail("Expected GeneralSecurityException"); + } catch (GeneralSecurityException e) { + assertEquals("cannot unseal; invalid JSON message", e.getMessage()); + } + } +} diff --git a/apps/googlepayments/src/test/java/com/google/payments/PaymentMethodTokenSenderTest.java b/apps/googlepayments/src/test/java/com/google/payments/PaymentMethodTokenSenderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..dc851f0aa4138d886ad603cb601a2f2ad77f708f --- /dev/null +++ b/apps/googlepayments/src/test/java/com/google/payments/PaymentMethodTokenSenderTest.java @@ -0,0 +1,131 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.payments; + +import static org.junit.Assert.assertEquals; + +import com.google.crypto.tink.subtle.EcUtil; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Unit tests for {@code PaymentMethodTokenSender}. + */ +@RunWith(JUnit4.class) +public class PaymentMethodTokenSenderTest { + private static final String MERCHANT_PUBLIC_KEY_BASE64 = + "BOdoXP+9Aq473SnGwg3JU1aiNpsd9vH2ognq4PtDtlLGa3Kj8TPf+jaQNPyDSkh3JUhiS0KyrrlWhAgNZKHYF2Y="; + /** + * Created with: + * + * <pre> + * openssl pkcs8 -topk8 -inform PEM -outform PEM -in merchant-key.pem -nocrypt + * </pre> + */ + private static final String MERCHANT_PRIVATE_KEY_PKCS8_BASE64 = + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgCPSuFr4iSIaQprjj" + + "chHPyDu2NXFe0vDBoTpPkYaK9dehRANCAATnaFz/vQKuO90pxsINyVNWojabHfbx" + + "9qIJ6uD7Q7ZSxmtyo/Ez3/o2kDT8g0pIdyVIYktCsq65VoQIDWSh2Bdm"; + /** + * Created with: + * + * <pre> + * openssl pkcs8 -topk8 -inform PEM -outform PEM -in google-key.pem -nocrypt + * </pre> + */ + private static final String GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64 = + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZj/Dldxz8fvKVF5O" + + "TeAtK6tY3G1McmvhMppe6ayW6GahRANCAAQ9icfBLy56BYB7BC2XGLOYsXKfAdzF" + + "FPU8rTtwMDr8LixetUjVLNkJTHxTxLQuMytPqKt39Vbtt7czPq3+yu1F"; + /** + * Public key value created with: + * + * <pre> + * openssl ec -in google-key.pem -inform PEM -outform PEM -pubout + * </pre> + */ + private static final String GOOGLE_VERIFYING_PUBLIC_KEYS_JSON = + "{\n" + + " \"keys\": [\n" + + " {\n" + + " \"keyValue\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPYnHwS8uegWAewQtlxizmLFynw" + + "HcxRT1PK07cDA6/C4sXrVI1SzZCUx8U8S0LjMrT6ird/VW7be3Mz6t/srtRQ==\",\n" + + " \"protocolVersion\": \"ECv1\"\n" + + " },\n" + + " ],\n" + + "}"; + + private static final String RECIPIENT_ID = "someRecipient"; + + @Test + public void testWithPrecomputedKeys() throws Exception { + PaymentMethodTokenSender sender = new PaymentMethodTokenSender.Builder() + .senderSigningKey(GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64) + .recipientId(RECIPIENT_ID) + .rawUncompressedRecipientPublicKey(MERCHANT_PUBLIC_KEY_BASE64) + .build(); + + PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder() + .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON) + .recipientId(RECIPIENT_ID) + .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64) + .build(); + + String plaintext = "blah"; + assertEquals(plaintext, recipient.unseal(sender.seal(plaintext))); + } + + @Test + public void testSendReceive() throws Exception { + ECParameterSpec spec = EcUtil.getNistP256Params(); + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); + keyGen.initialize(spec); + String senderId = "foo"; + String recipientId = "bar"; + + KeyPair senderKey = keyGen.generateKeyPair(); + ECPublicKey senderPublicKey = (ECPublicKey) senderKey.getPublic(); + ECPrivateKey senderPrivateKey = (ECPrivateKey) senderKey.getPrivate(); + + KeyPair recipientKey = keyGen.generateKeyPair(); + ECPublicKey recipientPublicKey = (ECPublicKey) recipientKey.getPublic(); + ECPrivateKey recipientPrivateKey = (ECPrivateKey) recipientKey.getPrivate(); + + PaymentMethodTokenSender sender = new PaymentMethodTokenSender.Builder() + .senderId(senderId) + .senderSigningKey(senderPrivateKey) + .recipientId(recipientId) + .recipientPublicKey(recipientPublicKey) + .build(); + + PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder() + .senderId(senderId) + .addSenderVerifyingKey(senderPublicKey) + .recipientId(recipientId) + .addRecipientPrivateKey(recipientPrivateKey) + .build(); + + String plaintext = "blah"; + assertEquals(plaintext, recipient.unseal(sender.seal(plaintext))); + } +} diff --git a/java/src/main/java/com/google/crypto/tink/subtle/SubtleUtil.java b/java/src/main/java/com/google/crypto/tink/subtle/SubtleUtil.java index 405866c565c19b4dfa21e00b1fee751c44c476f3..e13b7e6e09f88650c626d278ae6908951caa9e34 100644 --- a/java/src/main/java/com/google/crypto/tink/subtle/SubtleUtil.java +++ b/java/src/main/java/com/google/crypto/tink/subtle/SubtleUtil.java @@ -23,6 +23,7 @@ import java.security.GeneralSecurityException; /** * Helper methods. */ +// TODO(thaidn): tests these functions. public final class SubtleUtil { /** * Best effort fix-timing array comparison. @@ -145,4 +146,54 @@ public final class SubtleUtil { System.err.print(String.format("Error: %s\n", error)); System.exit(1); } + + // TODO(thaidn): add checks for boundary conditions/overflows. + /** + * Transforms a passed value to a LSB first byte array with the size of + * the specified capacity + * + * @param capacity size of the resulting byte array + * @param value that should be represented as a byte array + */ + public static byte[] intToByteArray(int capacity, int value) { + final byte[] result = new byte[capacity]; + for (int i = 0; i < capacity; i++) { + result[i] = (byte) ((value >> (8 * i)) & 0xFF); + } + return result; + } + + /** + * Transforms a passed LSB first byte array to an int + * + * @param bytes that should be transformed to a byte array + */ + public static int byteArrayToInt(byte[] bytes) { + return byteArrayToInt(bytes, bytes.length); + } + + /** + * Transforms a passed LSB first byte array to an int + * + * @param bytes that should be transformed to a byte array + * @param length amount of the passed {@code bytes} that should be transformed + */ + public static int byteArrayToInt(byte[] bytes, int length) { + return byteArrayToInt(bytes, 0, length); + } + + /** + * Transforms a passed LSB first byte array to an int + * + * @param bytes that should be transformed to a byte array + * @param offset start index to start the transformation + * @param length amount of the passed {@code bytes} that should be transformed + */ + public static int byteArrayToInt(byte[] bytes, int offset, int length) { + int value = 0; + for (int i = 0; i < length; i++) { + value += (bytes[i + offset] & 0xFF) << (i * 8); + } + return value; + } }