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;
+  }
 }