Merge commit '053b5b8c4214ec379daac2cc2bf208c51cc1cd66' into main

Current Tink mirror in Fuchsia has missing include issue with the latest version of Clang. The missing include is #include <cstdlib> (https://github.com/tink-crypto/tink/blob/master/cc/util/secret_data_internal.h#L21)

Tink introduced it in https://github.com/tink-crypto/tink/commit/053b5b8c4214ec379daac2cc2bf208c51cc1cd66
We should at least uprev Tink to this commit to unblock the issue.

Command to uprev Tink:
```
git checkout origin/main -b ${USER}-merge
git merge 053b5b8c4214ec379daac2cc2bf208c51cc1cd66
python3 ./tools/convert_for_cobalt
fx format-code --files=${gn build files that convert_for_cobalt wrote}
git commit --no-verify
```

Locally patch to include missing absl/strings/str_cat.h header:
cc/mac/hmac_parameters.cc
cc/mac/aes_cmac_parameters.cc

Bug: 376297654

Change-Id: I7cc47556e5ddec6be01bd7a523f527abea47e20a
Reviewed-on: https://fuchsia-review.googlesource.com/c/third_party/tink/+/1153894
Commit-Queue: Anivia Li <aniviali@google.com>
Reviewed-by: Alex Pankhurst <pankhurst@google.com>
diff --git a/.bazelversion b/.bazelversion
index ac14c3d..09b254e 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 66f2ffc..2f37316 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,7 +1,6 @@
 ---
 name: Bug report
 about: Create a report to help us improve Tink
-assignees: 'thaidn, chuckx'
 
 ---
 
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 486da6f..100d12c 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,7 +1,6 @@
 ---
 name: Feature request
 about: Suggest an idea for Tink
-assignees: 'thaidn, chuckx'
 
 ---
 
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 49711e3..49269d9 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -32,7 +32,7 @@
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
       with:
         # We must fetch at least the immediate parents so that if this is
         # a pull request then we can checkout the head.
@@ -45,7 +45,7 @@
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v1
+      uses: github/codeql-action/init@v2
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +56,7 @@
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
     - name: Autobuild
-      uses: github/codeql-action/autobuild@v1
+      uses: github/codeql-action/autobuild@v2
 
     # ℹī¸ Command-line programs to run using the OS shell.
     # 📚 https://git.io/JvXDl
@@ -70,4 +70,4 @@
     #   make release
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v1
+      uses: github/codeql-action/analyze@v2
diff --git a/CMakeLists.txt b/CMakeLists.txt
index dff0033..2e4f78f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,5 +1,5 @@
-cmake_minimum_required(VERSION 3.5)
-project(Tink VERSION 1.7.0 LANGUAGES CXX)
+cmake_minimum_required(VERSION 3.13)
+project(Tink VERSION 2.0.0 LANGUAGES CXX)
 
 list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
 
@@ -7,8 +7,6 @@
 option(TINK_USE_SYSTEM_OPENSSL "Build Tink linking to OpenSSL installed in the system" OFF)
 option(TINK_USE_INSTALLED_ABSEIL "Build Tink linking to Abseil installed in the system" OFF)
 option(TINK_USE_INSTALLED_GOOGLETEST "Build Tink linking to GTest installed in the system" OFF)
-option(TINK_USE_ABSL_STATUS "Compile Tink with absl::Status" OFF)
-option(TINK_USE_ABSL_STATUSOR "Compile Tink with absl::StatusOr" OFF)
 option(USE_ONLY_FIPS "Enables the FIPS only mode in Tink" OFF)
 
 set(CPACK_GENERATOR TGZ)
diff --git a/README.md b/README.md
index 74035b9..3ff7337 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,74 @@
 # Tink
 
-*A multi-language, cross-platform library that provides cryptographic APIs that are secure, easy to use correctly, and hard(er) to misuse.*
+*A multi-language, cross-platform library that provides cryptographic APIs that
+are secure, easy to use correctly, and hard(er) to misuse. See also:
+https://developers.google.com/tink*.
 
-https://developers.google.com/tink
+> **NOTE**: **Tink is moving!**
+>
+> As part of our roadmap we are splitting Tink into
+> [multiple GitHub repositories][split_repo_roadmap_url] that will be hosted at
+> [github.com/tink-crypto](https://github.com/tink-crypto) and will be
+> independently versioned.
+>
+> Roughly, we are going to create one repository per language, library extension
+> such as KMS (except Tink Python), and tools.
+>
+> A few important highlights:
+>
+> -   The migration will be done gradually over the course of 2023 with a new
+>     release from each of the new repositories. Releases will be announced in
+>     our [mailing list][tink_mailing_list_url].
+> -   We will keep updating each implementation/tool in
+>     [github.com/google/tink](https://github.com/google/tink) for a specified
+>     amount of time; migrated implementations/tools will eventually stop being
+>     updated on [github.com/google/tink](https://github.com/google/tink). The
+>     support window depends on the specific implementation, as shown in the
+>     table below.
+> -   New issues and pull requests should be created in the new repos.
+>
+> Below is the list of resulting repositories, migration timeline and expected
+> end of support.
+>
+> Tink implementation/extension         | New repository                                                                            | Migration status               | End of support in google/tink
+> ------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------ | -----------------------------
+> Tink Java                             | [tink-crypto/tink-java](https://github.com/tink-crypto/tink-java)                         | Complete (Q1 2023)             | Q3 2023
+> Tink Java AWS KMS extension           | [tink-crypto/tink-java-awskms](https://github.com/tink-crypto/tink-java-awskms)           | Complete (Q1 2023)             | Q3 2023
+> Tink Java Google Cloud KMS extension  | [tink-crypto/tink-java-gcpkms](https://github.com/tink-crypto/tink-java-gcpkms)           | Complete (Q1 2023)             | Q3 2023
+> Tink Java apps extension              | [tink-crypto/tink-java-apps](https://github.com/tink-crypto/tink-java-apps)               | Complete (Q1 2023)             | Q3 2023
+> Tink C++                              | [tink-crypto/tink-cc](https://github.com/tink-crypto/tink-cc)                             | Complete (Q2 2023)             | Q4 2023
+> Tink C++ AWS KMS extension            | [tink-crypto/tink-cc-awskms](https://github.com/tink-crypto/tink-cc-awskms)               | Complete (Q2 2023)             | Q4 2023
+> Tink C++ Google Cloud KMS extension   | [tink-crypto/tink-cc-gcpkms](https://github.com/tink-crypto/tink-cc-gcpkms)               | Complete (Q2 2023)             | Q4 2023
+> Tink Python                           | [tink-crypto/tink-py](https://github.com/tink-crypto/tink-py)                             | Not started (expected Q3 2023) | TBA
+> Tink Go                               | [tink-crypto/tink-go](https://github.com/tink-crypto/tink-go)                             | In progress (expected Q2 2023) | TBA
+> Tink Go AWS KMS extension             | [tink-crypto/tink-go-awskms](https://github.com/tink-crypto/tink-go-awskms)               | In progress (expected Q2 2023) | TBA
+> Tink Go Google Cloud KMS extension    | [tink-crypto/tink-go-gcpkms](https://github.com/tink-crypto/tink-go-gcpkms)               | In progress (expected Q2 2023) | TBA
+> Tink Go HashiCorp Vault KMS extension | [tink-crypto/tink-go-hcvault](https://github.com/tink-crypto/tink-go-hcvault)             | In progress (expected Q2 2023) | TBA
+> Tink Obj-C                            | [tink-crypto/tink-objc](https://github.com/tink-crypto/tink-objc)                         | Not started (expected Q4 2023) | TBA
+> Tink Tinkey                           | [tink-crypto/tink-tinkey](https://github.com/tink-crypto/tink-tinkey)                     | Complete (Q2 2023)             | Q4 2023
+> Tink cross language tests             | [tink-crypto/tink-cross-lang-tests](https://github.com/tink-crypto/tink-cross-lang-tests) | Not started (expected Q4 2023) | TBA
 
-**`Ubuntu`**                        | **`macOS`**
------------------------------------ | ---------------------------------
-[![Kokoro Ubuntu][ubuntu_badge]](#) | [![Kokoro macOS][macos_badge]](#)
+> **NOTE**: **We are removing Tink for JavaScript/TypeScript**
+>
+> We are removing the Tink JavaScript/TypeScript library from our current Github
+> repository (master branch). As part of our effort to migrate Tink to
+> https://github.com/tink-crypto, we will not release an individual
+> JavaScript/Typescript repository. Furthermore, the JavaScript/TypeScript
+> [directory](https://github.com/google/tink/tree/master/javascript) in the
+> current release branch (v1.7.0) will no longer be actively supported.
+>
+> _We aim to remove the JS/TS directory from the current Tink Github repository
+> (master branch) on **June 22, 2023**. We will also deprecate the Tink npm
+> package on this date._
+>
+> See [this](https://github.com/google/tink/issues/689) tracking issue for more
+> details.
+>
+> Feel free to use our [mailing list][tink_mailing_list_url] to raise any
+> questions, issues or concerns.
 
-[ubuntu_badge]: https://storage.googleapis.com/tink-kokoro-build-badges/tink-ubuntu.png
-[macos_badge]: https://storage.googleapis.com/tink-kokoro-build-badges/tink-macos.png
+[split_repo_roadmap_url]: https://developers.google.com/tink/roadmap#splitting_tink_into_multiple_github_repositories
+[tink_mailing_list_url]: https://groups.google.com/forum/#!forum/tink-users
 
 ## Index
 
@@ -25,9 +84,10 @@
 Using crypto in your application [shouldn't have to][devs_are_users_too_slides]
 feel like juggling chainsaws in the dark. Tink is a crypto library written by a
 group of cryptographers and security engineers at Google. It was born out of our
-extensive experience working with Google's product teams, [fixing weaknesses in
-implementations](https://github.com/google/wycheproof), and providing simple
-APIs that can be used safely without needing a crypto background.
+extensive experience working with Google's product teams,
+[fixing weaknesses in implementations](https://github.com/google/wycheproof),
+and providing simple APIs that can be used safely without needing a crypto
+background.
 
 Tink provides secure APIs that are easy to use correctly and hard(er) to misuse.
 It reduces common crypto pitfalls with user-centered design, careful
@@ -52,6 +112,15 @@
 released on 2022-08-09.
 
 Javascript/Typescript is in an alpha state and should only be used for testing.
+Please see the intent to remove statement
+[here](https://github.com/google/tink/issues/689).
+
+**`Ubuntu`**                        | **`macOS`**
+----------------------------------- | ---------------------------------
+[![Kokoro Ubuntu][ubuntu_badge]](#) | [![Kokoro macOS][macos_badge]](#)
+
+[ubuntu_badge]: https://storage.googleapis.com/tink-kokoro-build-badges/tink-ubuntu.png
+[macos_badge]: https://storage.googleapis.com/tink-kokoro-build-badges/tink-macos.png
 
 ## Getting started
 
@@ -130,12 +199,11 @@
 
 ## Contact and mailing list
 
-If you want to contribute, please read [CONTRIBUTING](docs/CONTRIBUTING.md)
-and send us pull requests. You can also report bugs or file feature requests.
+If you want to contribute, please read [CONTRIBUTING](docs/CONTRIBUTING.md) and
+send us pull requests. You can also report bugs or file feature requests.
 
 If you'd like to talk to the developers or get notified about major product
-updates, you may want to subscribe to our
-[mailing list](https://groups.google.com/forum/#!forum/tink-users).
+updates, you may want to subscribe to our [mailing list][tink_mailing_list_url].
 
 ## Maintainers
 
@@ -167,3 +235,5 @@
 -   Enzo Puig
 -   Veronika Slívová
 -   Paula Vidas
+-   Cathie Yun
+-   Federico Zalcberg
diff --git a/WORKSPACE b/WORKSPACE
deleted file mode 100644
index 5a9185b..0000000
--- a/WORKSPACE
+++ /dev/null
@@ -1,9 +0,0 @@
-workspace(name = "tink_base")
-
-load("@tink_base//:tink_base_deps.bzl", "tink_base_deps")
-
-tink_base_deps()
-
-load("@tink_base//:tink_base_deps_init.bzl", "tink_base_deps_init")
-
-tink_base_deps_init()
diff --git a/apps/.bazelversion b/apps/.bazelversion
deleted file mode 100644
index ac14c3d..0000000
--- a/apps/.bazelversion
+++ /dev/null
@@ -1 +0,0 @@
-5.1.1
diff --git a/apps/BUILD.bazel b/apps/BUILD.bazel
deleted file mode 100644
index 1a1b3a3..0000000
--- a/apps/BUILD.bazel
+++ /dev/null
@@ -1,3 +0,0 @@
-package(default_visibility = ["//:__subpackages__"])
-
-licenses(["notice"])
diff --git a/apps/README.md b/apps/README.md
deleted file mode 100644
index 70b329f..0000000
--- a/apps/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Tink Java Apps
-
-It contains extensions and applications of Tink Java.
diff --git a/apps/WORKSPACE b/apps/WORKSPACE
deleted file mode 100644
index 27aaa48..0000000
--- a/apps/WORKSPACE
+++ /dev/null
@@ -1,22 +0,0 @@
-workspace(name = "tink_apps")
-
-local_repository(
-    name = "tink_java",
-    path = "../java_src",
-)
-
-load("@tink_java//:tink_java_deps.bzl", "tink_java_deps", "TINK_MAVEN_ARTIFACTS")
-tink_java_deps()
-
-load("@tink_java//:tink_java_deps_init.bzl", "tink_java_deps_init")
-tink_java_deps_init()
-
-load("@rules_jvm_external//:defs.bzl", "maven_install")
-
-maven_install(
-    artifacts = TINK_MAVEN_ARTIFACTS,
-    repositories = [
-        "https://maven.google.com",
-        "https://repo1.maven.org/maven2",
-    ],
-)
diff --git a/apps/paymentmethodtoken/BUILD.bazel b/apps/paymentmethodtoken/BUILD.bazel
deleted file mode 100644
index f601945..0000000
--- a/apps/paymentmethodtoken/BUILD.bazel
+++ /dev/null
@@ -1,26 +0,0 @@
-load("@tink_java//tools:gen_maven_jar_rules.bzl", "gen_maven_jar_rules")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-gen_maven_jar_rules(
-    name = "maven",
-    doctitle = "Tink Cryptography API for Google Payment Method Token",
-    manifest_lines = [
-        "Automatic-Module-Name: com.google.crypto.tink.apps.paymentmethodtoken",
-    ],
-    root_packages = ["com.google.crypto.tink.apps.paymentmethodtoken"],
-    deps = [
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:google_payments_public_keys_manager",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_constants",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_hybrid_decrypt",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_hybrid_encrypt",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_recipient",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_recipient_kem",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_recipient_key_gen",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_sender",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_util",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:sender_intermediate_cert_factory",
-    ],
-)
diff --git a/apps/paymentmethodtoken/README.md b/apps/paymentmethodtoken/README.md
deleted file mode 100644
index 936026c..0000000
--- a/apps/paymentmethodtoken/README.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# An implementation of [Google Payment Method Token](https://developers.google.com/pay/api/payment-data-cryptography).
-
-## Latest release
-
-The most recent release is
-[1.7.0](https://github.com/google/tink/releases/tag/v1.7.0), released
-2022-08-09. API docs can be found
-[here](https://google.github.io/tink/javadoc/apps-paymentmethodtoken/1.7.0).
-
-The Maven group ID is `com.google.crypto.tink`, and the artifact ID is
-`apps-paymentmethodtoken`.
-
-To add a dependency using Maven:
-
-```xml
-<dependency>
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>apps-paymentmethodtoken</artifactId>
-  <version>1.7.0</version>
-</dependency>
-```
-
-## Snapshots
-
-Snapshots of this app built from the master branch are available through Maven
-using version `HEAD-SNAPSHOT`. API docs can be found
-[here](https://google.github.io/tink/javadoc/apps-paymentmethodtoken/HEAD-SNAPSHOT).
-
-To add a dependency using Maven:
-
-```xml
-<repositories>
-<repository>
-  <id>sonatype-snapshots</id>
-  <name>sonatype-snapshots</name>
-  <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
-  <snapshots>
-    <enabled>true</enabled>
-    <updatePolicy>always</updatePolicy>
-  </snapshots>
-  <releases>
-    <updatePolicy>always</updatePolicy>
-  </releases>
-</repository>
-</repositories>
-
-<dependency>
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>apps-paymentmethodtoken</artifactId>
-  <version>HEAD-SNAPSHOT</version>
-</dependency>
-```
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/BUILD.bazel b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/BUILD.bazel
deleted file mode 100644
index 434db56..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/BUILD.bazel
+++ /dev/null
@@ -1,125 +0,0 @@
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-java_library(
-    name = "google_payments_public_keys_manager",
-    srcs = ["GooglePaymentsPublicKeysManager.java"],
-    deps = [
-        "@maven//:com_google_http_client_google_http_client",
-        "@tink_java//src/main/java/com/google/crypto/tink/util:keys_downloader",
-    ],
-)
-
-java_library(
-    name = "payment_method_token_hybrid_decrypt",
-    srcs = ["PaymentMethodTokenHybridDecrypt.java"],
-    deps = [
-        ":payment_method_token_constants",
-        ":payment_method_token_recipient_kem",
-        ":payment_method_token_util",
-        "@maven//:com_google_code_gson_gson",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_decrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:bytes",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:hkdf",
-    ],
-)
-
-java_library(
-    name = "payment_method_token_sender",
-    srcs = ["PaymentMethodTokenSender.java"],
-    deps = [
-        ":payment_method_token_constants",
-        ":payment_method_token_hybrid_encrypt",
-        ":payment_method_token_util",
-        "@maven//:com_google_code_gson_gson",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink:public_key_sign",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:ecdsa_sign_jce",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-    ],
-)
-
-java_library(
-    name = "payment_method_token_recipient_key_gen",
-    srcs = ["PaymentMethodTokenRecipientKeyGen.java"],
-    deps = [
-        ":payment_method_token_constants",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-    ],
-)
-
-java_library(
-    name = "payment_method_token_constants",
-    srcs = ["PaymentMethodTokenConstants.java"],
-    deps = [
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:enums",
-    ],
-)
-
-java_library(
-    name = "payment_method_token_recipient_kem",
-    srcs = ["PaymentMethodTokenRecipientKem.java"],
-)
-
-java_library(
-    name = "payment_method_token_hybrid_encrypt",
-    srcs = ["PaymentMethodTokenHybridEncrypt.java"],
-    deps = [
-        ":payment_method_token_constants",
-        ":payment_method_token_util",
-        "@maven//:com_google_code_gson_gson",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:ecies_hkdf_sender_kem",
-    ],
-)
-
-java_library(
-    name = "payment_method_token_recipient",
-    srcs = ["PaymentMethodTokenRecipient.java"],
-    deps = [
-        ":google_payments_public_keys_manager",
-        ":payment_method_token_constants",
-        ":payment_method_token_hybrid_decrypt",
-        ":payment_method_token_recipient_kem",
-        ":payment_method_token_util",
-        "@maven//:com_google_code_gson_gson",
-        "@maven//:joda_time_joda_time",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_decrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:ecdsa_verify_jce",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-    ],
-)
-
-java_library(
-    name = "sender_intermediate_cert_factory",
-    srcs = ["SenderIntermediateCertFactory.java"],
-    deps = [
-        ":payment_method_token_constants",
-        ":payment_method_token_util",
-        "@maven//:com_google_code_gson_gson",
-        "@tink_java//src/main/java/com/google/crypto/tink:public_key_sign",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:ecdsa_sign_jce",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-    ],
-)
-
-java_library(
-    name = "payment_method_token_util",
-    srcs = ["PaymentMethodTokenUtil.java"],
-    deps = [
-        ":payment_method_token_constants",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:bytes",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
-    ],
-)
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/GooglePaymentsPublicKeysManager.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/GooglePaymentsPublicKeysManager.java
deleted file mode 100644
index 0da08d2..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/GooglePaymentsPublicKeysManager.java
+++ /dev/null
@@ -1,144 +0,0 @@
-// 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.crypto.tink.apps.paymentmethodtoken;
-
-import com.google.api.client.http.HttpTransport;
-import com.google.api.client.http.javanet.NetHttpTransport;
-import com.google.crypto.tink.util.KeysDownloader;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-/**
- * Thread-safe Google Payments public key manager.
- *
- * <p>For best performance, use the {@link GooglePaymentsPublicKeysManager#INSTANCE_PRODUCTION} for
- * production environment or {@link GooglePaymentsPublicKeysManager#INSTANCE_TEST} for test
- * environment.
- *
- * <p>If you need extra customizations for your use, we recommend you to use {@link
- * GooglePaymentsPublicKeysManager.Builder} to construct an instance and keep it as a singleton in a
- * static final variable across requests.
- *
- * <p>When initializing your server, we also recommend that you call {@link #refreshInBackground()}
- * to proactively fetch the keys.
- *
- * @since 1.0.0
- */
-public class GooglePaymentsPublicKeysManager {
-  /** Default HTTP transport used by this class. */
-  public static final NetHttpTransport DEFAULT_HTTP_TRANSPORT =
-      new NetHttpTransport.Builder().build();
-  /** URL to fetch keys for environment production. */
-  public static final String KEYS_URL_PRODUCTION =
-      "https://payments.developers.google.com/paymentmethodtoken/keys.json";
-  /** URL to fetch keys for environment test. */
-  public static final String KEYS_URL_TEST =
-      "https://payments.developers.google.com/paymentmethodtoken/test/keys.json";
-
-  private static final Executor DEFAULT_BACKGROUND_EXECUTOR = Executors.newCachedThreadPool();
-
-  private final KeysDownloader downloader;
-
-  /**
-   * Instance configured to talk to fetch keys from production environment (from {@link
-   * GooglePaymentsPublicKeysManager#KEYS_URL_PRODUCTION}).
-   */
-  public static final GooglePaymentsPublicKeysManager INSTANCE_PRODUCTION =
-      new GooglePaymentsPublicKeysManager(
-          DEFAULT_BACKGROUND_EXECUTOR, DEFAULT_HTTP_TRANSPORT, KEYS_URL_PRODUCTION);
-  /**
-   * Instance configured to talk to fetch keys from test environment (from {@link
-   * GooglePaymentsPublicKeysManager#KEYS_URL_TEST}).
-   */
-  public static final GooglePaymentsPublicKeysManager INSTANCE_TEST =
-      new GooglePaymentsPublicKeysManager(
-          DEFAULT_BACKGROUND_EXECUTOR, DEFAULT_HTTP_TRANSPORT, KEYS_URL_TEST);
-
-  GooglePaymentsPublicKeysManager(
-      Executor backgroundExecutor, HttpTransport httpTransport, String keysUrl) {
-    this.downloader =
-        new KeysDownloader.Builder()
-            .setUrl(keysUrl)
-            .setExecutor(backgroundExecutor)
-            .setHttpTransport(httpTransport)
-            .build();
-  }
-
-  HttpTransport getHttpTransport() {
-    return downloader.getHttpTransport();
-  }
-
-  String getUrl() {
-    return downloader.getUrl();
-  }
-
-  /**
-   * Returns a string containing a JSON with the Google public signing keys.
-   *
-   * <p>Meant to be called by {@link PaymentMethodTokenRecipient}.
-   */
-  String getTrustedSigningKeysJson() throws IOException {
-    return this.downloader.download();
-  }
-
-  /** Fetches keys in the background. */
-  public void refreshInBackground() {
-    downloader.refreshInBackground();
-  }
-
-  /**
-   * Builder for {@link GooglePaymentsPublicKeysManager}.
-   *
-   * @since 1.0.0
-   */
-  public static class Builder {
-    private HttpTransport httpTransport = DEFAULT_HTTP_TRANSPORT;
-    private String keysUrl = KEYS_URL_PRODUCTION;
-
-    public Builder setKeysUrl(String keysUrl) {
-      this.keysUrl = keysUrl;
-      return this;
-    }
-
-    /**
-     * Sets the HTTP transport.
-     *
-     * <p>You generally should not need to set a custom transport as the default transport {@link
-     * GooglePaymentsPublicKeysManager#DEFAULT_HTTP_TRANSPORT} should be suited for most use cases.
-     */
-    public Builder setHttpTransport(HttpTransport httpTransport) {
-      this.httpTransport = httpTransport;
-      return this;
-    }
-
-    public GooglePaymentsPublicKeysManager build() {
-      // If all parameters are equal to the existing singleton instances, returning them instead.
-      // This is more a safe guard if users of this class construct a new class and forget to
-      // save in a singleton.
-      for (GooglePaymentsPublicKeysManager instance :
-          Arrays.asList(INSTANCE_PRODUCTION, INSTANCE_TEST)) {
-        if (instance.getHttpTransport() == httpTransport && instance.getUrl().equals(keysUrl)) {
-          return instance;
-        }
-      }
-      return new GooglePaymentsPublicKeysManager(
-          DEFAULT_BACKGROUND_EXECUTOR, httpTransport, keysUrl);
-    }
-  }
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenConstants.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenConstants.java
deleted file mode 100644
index 416a426..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenConstants.java
+++ /dev/null
@@ -1,104 +0,0 @@
-// 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.crypto.tink.apps.paymentmethodtoken;
-
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.Enums.HashType;
-import java.nio.charset.StandardCharsets;
-
-/** Various constants. */
-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";
-  // 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 EllipticCurves.CurveType P256_CURVE_TYPE = EllipticCurves.CurveType.NIST_P256;
-  public static final EllipticCurves.PointFormatType UNCOMPRESSED_POINT_FORMAT =
-      EllipticCurves.PointFormatType.UNCOMPRESSED;
-  public static final String PROTOCOL_VERSION_EC_V1 = "ECv1";
-  public static final String PROTOCOL_VERSION_EC_V2 = "ECv2";
-  public static final String PROTOCOL_VERSION_EC_V2_SIGNING_ONLY = "ECv2SigningOnly";
-  public static final HashType ECDSA_HASH_SHA256 = HashType.SHA256;
-
-  public static final String JSON_ENCRYPTED_MESSAGE_KEY = "encryptedMessage";
-  public static final String JSON_EPHEMERAL_PUBLIC_KEY = "ephemeralPublicKey";
-  public static final String JSON_INTERMEDIATE_SIGNING_KEY = "intermediateSigningKey";
-  public static final String JSON_KEY_EXPIRATION_KEY = "keyExpiration";
-  public static final String JSON_KEY_VALUE_KEY = "keyValue";
-  public static final String JSON_MESSAGE_EXPIRATION_KEY = "messageExpiration";
-  public static final String JSON_PROTOCOL_VERSION_KEY = "protocolVersion";
-  public static final String JSON_SIGNATURES_KEY = "signatures";
-  public static final String JSON_SIGNATURE_KEY = "signature";
-  public static final String JSON_SIGNED_KEY_KEY = "signedKey";
-  public static final String JSON_SIGNED_MESSAGE_KEY = "signedMessage";
-  public static final String JSON_TAG_KEY = "tag";
-
-  /** Represents configuration regarding each protocol version. */
-  enum ProtocolVersionConfig {
-    EC_V1(
-        /* protocolVersion= */ PROTOCOL_VERSION_EC_V1,
-        /* aesCtrKeySize= */ 128 / 8,
-        /* hmacSha256KeySize= */ 128 / 8,
-        /* isEncryptionRequired= */ true,
-        /* supportsIntermediateSigningKeys= */ false),
-    EC_V2(
-        /* protocolVersion= */ PROTOCOL_VERSION_EC_V2,
-        /* aesCtrKeySize= */ 256 / 8,
-        /* hmacSha256KeySize= */ 256 / 8,
-        /* isEncryptionRequired= */ true,
-        /* supportsIntermediateSigningKeys= */ true),
-    EC_V2_SIGNING_ONLY(
-        /* protocolVersion= */ PROTOCOL_VERSION_EC_V2_SIGNING_ONLY,
-        /* aesCtrKeySize= */ 256 / 8,
-        /* hmacSha256KeySize= */ 256 / 8,
-        /* isEncryptionRequired= */ false,
-        /* supportsIntermediateSigningKeys= */ true);
-
-    public final String protocolVersion;
-    public final int aesCtrKeySize;
-    public final int hmacSha256KeySize;
-    public final boolean isEncryptionRequired;
-    public final boolean supportsIntermediateSigningKeys;
-
-    ProtocolVersionConfig(
-        String protocolVersion,
-        int aesCtrKeySize,
-        int hmacSha256KeySize,
-        boolean isEncryptionRequired,
-        boolean supportsIntermediateSigningKeys) {
-      this.protocolVersion = protocolVersion;
-      this.aesCtrKeySize = aesCtrKeySize;
-      this.hmacSha256KeySize = hmacSha256KeySize;
-      this.isEncryptionRequired = isEncryptionRequired;
-      this.supportsIntermediateSigningKeys = supportsIntermediateSigningKeys;
-    }
-
-    public static ProtocolVersionConfig forProtocolVersion(String protocolVersion) {
-      for (ProtocolVersionConfig protocolVersionConfig : ProtocolVersionConfig.values()) {
-        if (protocolVersionConfig.protocolVersion.equals(protocolVersion)) {
-          return protocolVersionConfig;
-        }
-      }
-      throw new IllegalArgumentException("Unknown protocol version: " + protocolVersion);
-    }
-  }
-
-  private PaymentMethodTokenConstants() {}
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridDecrypt.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridDecrypt.java
deleted file mode 100644
index 6d2637a..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridDecrypt.java
+++ /dev/null
@@ -1,123 +0,0 @@
-// 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.crypto.tink.apps.paymentmethodtoken;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.crypto.tink.HybridDecrypt;
-import com.google.crypto.tink.apps.paymentmethodtoken.PaymentMethodTokenConstants.ProtocolVersionConfig;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.Bytes;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.Hkdf;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonParser;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.util.Arrays;
-
-/**
- * A {@link HybridDecrypt} implementation for the hybrid encryption used in <a
- * href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment Method
- * Token</a>.
- */
-class PaymentMethodTokenHybridDecrypt implements HybridDecrypt {
-  private final PaymentMethodTokenRecipientKem recipientKem;
-  private final ProtocolVersionConfig protocolVersionConfig;
-
-  PaymentMethodTokenHybridDecrypt(
-      final ECPrivateKey recipientPrivateKey, ProtocolVersionConfig protocolVersionConfig)
-      throws GeneralSecurityException {
-    this(
-        new PaymentMethodTokenRecipientKem() {
-          @Override
-          public byte[] computeSharedSecret(final byte[] ephemeralPublicKey)
-              throws GeneralSecurityException {
-            ECPublicKey publicKey =
-                EllipticCurves.getEcPublicKey(
-                    recipientPrivateKey.getParams(),
-                    PaymentMethodTokenConstants.UNCOMPRESSED_POINT_FORMAT,
-                    ephemeralPublicKey);
-            return EllipticCurves.computeSharedSecret(recipientPrivateKey, publicKey);
-          }
-        },
-        protocolVersionConfig);
-  }
-
-  PaymentMethodTokenHybridDecrypt(
-      final PaymentMethodTokenRecipientKem recipientKem,
-      ProtocolVersionConfig protocolVersionConfig) {
-    this.recipientKem = recipientKem;
-    this.protocolVersionConfig = protocolVersionConfig;
-  }
-
-  @Override
-  public byte[] decrypt(final byte[] ciphertext, final byte[] contextInfo)
-      throws GeneralSecurityException {
-    try {
-      JsonObject json = JsonParser.parseString(new String(ciphertext, UTF_8)).getAsJsonObject();
-      validate(json);
-      byte[] demKey = kem(json, contextInfo);
-      return dem(json, demKey);
-    } catch (JsonParseException | IllegalStateException e) {
-      throw new GeneralSecurityException("cannot decrypt; failed to parse JSON", e);
-    }
-  }
-
-  private byte[] kem(JsonObject json, final byte[] contextInfo) throws GeneralSecurityException {
-    int demKeySize = protocolVersionConfig.aesCtrKeySize + protocolVersionConfig.hmacSha256KeySize;
-    byte[] ephemeralPublicKey =
-        Base64.decode(
-            json.get(PaymentMethodTokenConstants.JSON_EPHEMERAL_PUBLIC_KEY).getAsString());
-    byte[] sharedSecret = recipientKem.computeSharedSecret(ephemeralPublicKey);
-    return Hkdf.computeEciesHkdfSymmetricKey(
-        ephemeralPublicKey,
-        sharedSecret,
-        PaymentMethodTokenConstants.HMAC_SHA256_ALGO,
-        PaymentMethodTokenConstants.HKDF_EMPTY_SALT,
-        contextInfo,
-        demKeySize);
-  }
-
-  private byte[] dem(JsonObject json, final byte[] demKey) throws GeneralSecurityException {
-    byte[] hmacSha256Key =
-        Arrays.copyOfRange(demKey, protocolVersionConfig.aesCtrKeySize, demKey.length);
-    byte[] encryptedMessage =
-        Base64.decode(
-            json.get(PaymentMethodTokenConstants.JSON_ENCRYPTED_MESSAGE_KEY).getAsString());
-    byte[] computedTag = PaymentMethodTokenUtil.hmacSha256(hmacSha256Key, encryptedMessage);
-    byte[] expectedTag =
-        Base64.decode(json.get(PaymentMethodTokenConstants.JSON_TAG_KEY).getAsString());
-    if (!Bytes.equal(expectedTag, computedTag)) {
-      throw new GeneralSecurityException("cannot decrypt; invalid MAC");
-    }
-    byte[] aesCtrKey = Arrays.copyOf(demKey, protocolVersionConfig.aesCtrKeySize);
-    return PaymentMethodTokenUtil.aesCtr(aesCtrKey, encryptedMessage);
-  }
-
-  private void validate(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.size() != 3) {
-      throw new GeneralSecurityException(
-          "The payload must contain exactly encryptedMessage, tag and ephemeralPublicKey");
-    }
-  }
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridEncrypt.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridEncrypt.java
deleted file mode 100644
index ff3a418..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridEncrypt.java
+++ /dev/null
@@ -1,91 +0,0 @@
-// 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.crypto.tink.apps.paymentmethodtoken;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.apps.paymentmethodtoken.PaymentMethodTokenConstants.ProtocolVersionConfig;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EciesHkdfSenderKem;
-import com.google.gson.JsonObject;
-import com.google.gson.internal.Streams;
-import com.google.gson.stream.JsonWriter;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPublicKey;
-import java.util.Arrays;
-
-/**
- * A {@link HybridEncrypt} implementation for the hybrid encryption used in <a
- * href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment Method
- * Token</a>.
- */
-class PaymentMethodTokenHybridEncrypt implements HybridEncrypt {
-  private final EciesHkdfSenderKem senderKem;
-  private final ProtocolVersionConfig protocolVersionConfig;
-
-  public PaymentMethodTokenHybridEncrypt(
-      final ECPublicKey recipientPublicKey, final ProtocolVersionConfig protocolVersionConfig) {
-    this.senderKem = new EciesHkdfSenderKem(recipientPublicKey);
-    this.protocolVersionConfig = protocolVersionConfig;
-  }
-
-  static String jsonEncodeCiphertext(byte[] ciphertext, byte[] tag, byte[] ephemeralPublicKey)
-      throws GeneralSecurityException {
-    JsonObject result = new JsonObject();
-    result.addProperty(
-        PaymentMethodTokenConstants.JSON_ENCRYPTED_MESSAGE_KEY, Base64.encode(ciphertext));
-    result.addProperty(
-        PaymentMethodTokenConstants.JSON_EPHEMERAL_PUBLIC_KEY, Base64.encode(ephemeralPublicKey));
-    result.addProperty(PaymentMethodTokenConstants.JSON_TAG_KEY, Base64.encode(tag));
-    StringWriter stringWriter = new StringWriter();
-    JsonWriter jsonWriter = new JsonWriter(stringWriter);
-    jsonWriter.setHtmlSafe(true);
-    try {
-      Streams.write(result, jsonWriter);
-      return stringWriter.toString();
-    } catch (IOException e) {
-      throw new GeneralSecurityException("cannot encrypt; JSON error", e);
-    }
-  }
-
-  @Override
-  public byte[] encrypt(final byte[] plaintext, final byte[] contextInfo)
-      throws GeneralSecurityException {
-    int symmetricKeySize =
-        protocolVersionConfig.aesCtrKeySize + protocolVersionConfig.hmacSha256KeySize;
-    EciesHkdfSenderKem.KemKey kemKey =
-        senderKem.generateKey(
-            PaymentMethodTokenConstants.HMAC_SHA256_ALGO,
-            PaymentMethodTokenConstants.HKDF_EMPTY_SALT,
-            contextInfo,
-            symmetricKeySize,
-            PaymentMethodTokenConstants.UNCOMPRESSED_POINT_FORMAT);
-    byte[] aesCtrKey = Arrays.copyOf(kemKey.getSymmetricKey(), protocolVersionConfig.aesCtrKeySize);
-    byte[] ciphertext = PaymentMethodTokenUtil.aesCtr(aesCtrKey, plaintext);
-    byte[] hmacSha256Key =
-        Arrays.copyOfRange(
-            kemKey.getSymmetricKey(), protocolVersionConfig.aesCtrKeySize, symmetricKeySize);
-    byte[] tag = PaymentMethodTokenUtil.hmacSha256(hmacSha256Key, ciphertext);
-    byte[] ephemeralPublicKey = kemKey.getKemBytes();
-
-    String jsonEncodedCiphertext = jsonEncodeCiphertext(ciphertext, tag, ephemeralPublicKey);
-    return jsonEncodedCiphertext.getBytes(UTF_8);
-  }
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipient.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipient.java
deleted file mode 100644
index 5f6cfe7..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipient.java
+++ /dev/null
@@ -1,629 +0,0 @@
-// 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.crypto.tink.apps.paymentmethodtoken;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.crypto.tink.HybridDecrypt;
-import com.google.crypto.tink.apps.paymentmethodtoken.PaymentMethodTokenConstants.ProtocolVersionConfig;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EcdsaVerifyJce;
-import com.google.crypto.tink.subtle.EllipticCurves.EcdsaEncoding;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonParser;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import org.joda.time.Instant;
-
-/**
- * An implementation of the recipient side of <a
- * href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment Method
- * Token</a>.
- *
- * <h3>Warning</h3>
- *
- * <p>This implementation only supports versions {@code ECv1}, {@code ECv2} and {@code
- * ECv2SigningOnly}.
- *
- * <h3>Typical usage</h3>
- *
- * <pre>{@code
- * PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder()
- *     .fetchSenderVerifyingKeysWith(
- *         GooglePaymentsPublicKeysManager.INSTANCE_PRODUCTION)
- *     .recipientId(recipientId)
- *     // Multiple recipient private keys can be added to support graceful key rotations
- *     .addRecipientPrivateKey(recipientPrivateKey1)
- *     .addRecipientPrivateKey(recipientPrivateKey2)
- *     .build();
- * String ciphertext = ...;
- * String plaintext = recipient.unseal(ciphertext);
- * }</pre>
- *
- * <h3>Custom decryption</h3>
- *
- * <p>Recipients that store private keys in HSM can provide implementations of {@link
- * PaymentMethodTokenRecipientKem} and configure Tink to use their custom code with {@link
- * PaymentMethodTokenRecipient.Builder#addRecipientKem}.
- *
- * <pre>{@code
- * PaymentMethodTokenRecipient recipient = new PaymentMethodTokenRecipient.Builder()
- *     .fetchSenderVerifyingKeysWith(
- *         GooglePaymentsPublicKeysManager.INSTANCE_PRODUCTION)
- *     .recipientId(recipientId)
- *     .addRecipientKem(new MyPaymentMethodTokenRecipientKem())
- *     .build();
- * String ciphertext = ...;
- * String plaintext = recipient.unseal(ciphertext);
- * }</pre>
- *
- * @see <a href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment
- *     Method Token standard</a>
- * @since 1.0.0
- */
-public final class PaymentMethodTokenRecipient {
-  private final String protocolVersion;
-  private final List<SenderVerifyingKeysProvider> senderVerifyingKeysProviders;
-  private final List<HybridDecrypt> hybridDecrypters = new ArrayList<>();
-  private final String senderId;
-  private final String recipientId;
-
-  PaymentMethodTokenRecipient(
-      String protocolVersion,
-      List<SenderVerifyingKeysProvider> senderVerifyingKeysProviders,
-      String senderId,
-      List<ECPrivateKey> recipientPrivateKeys,
-      List<PaymentMethodTokenRecipientKem> recipientKems,
-      String recipientId)
-      throws GeneralSecurityException {
-    if (!protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)
-        && !protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-        && !protocolVersion.equals(
-            PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)) {
-      throw new IllegalArgumentException("invalid version: " + protocolVersion);
-    }
-    this.protocolVersion = protocolVersion;
-    if (senderVerifyingKeysProviders == null || senderVerifyingKeysProviders.isEmpty()) {
-      throw new IllegalArgumentException(
-          "must set at least one way to get sender's verifying key using"
-              + " Builder.fetchSenderVerifyingKeysWith or Builder.senderVerifyingKeys");
-    }
-    this.senderVerifyingKeysProviders = senderVerifyingKeysProviders;
-    this.senderId = senderId;
-
-    ProtocolVersionConfig protocolVersionConfig =
-        ProtocolVersionConfig.forProtocolVersion(protocolVersion);
-    if (protocolVersionConfig.isEncryptionRequired) {
-      if (recipientPrivateKeys.isEmpty() && recipientKems.isEmpty()) {
-        throw new IllegalArgumentException(
-            "must add at least one recipient's decrypting key using Builder.addRecipientPrivateKey "
-                + "or Builder.addRecipientKem");
-      }
-      for (ECPrivateKey privateKey : recipientPrivateKeys) {
-        hybridDecrypters.add(
-            new PaymentMethodTokenHybridDecrypt(privateKey, protocolVersionConfig));
-      }
-      for (PaymentMethodTokenRecipientKem kem : recipientKems) {
-        hybridDecrypters.add(new PaymentMethodTokenHybridDecrypt(kem, protocolVersionConfig));
-      }
-    } else {
-      if (!recipientPrivateKeys.isEmpty() || !recipientKems.isEmpty()) {
-        throw new IllegalArgumentException(
-            "must not set private decrypting key using Builder.addRecipientPrivateKey "
-                + "or Builder.addRecipientDecrypter");
-      }
-    }
-
-    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.senderVerifyingKeysProviders,
-        builder.senderId,
-        builder.recipientPrivateKeys,
-        builder.recipientKems,
-        builder.recipientId);
-  }
-
-  /**
-   * Builder for {@link PaymentMethodTokenRecipient}.
-   *
-   * @since 1.0.0
-   */
-  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<SenderVerifyingKeysProvider> senderVerifyingKeysProviders =
-        new ArrayList<SenderVerifyingKeysProvider>();
-    private final List<ECPrivateKey> recipientPrivateKeys = new ArrayList<ECPrivateKey>();
-    private final List<PaymentMethodTokenRecipientKem> recipientKems = new ArrayList<>();
-
-    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;
-    }
-
-    /**
-     * Fetches verifying public keys of the sender using {@link GooglePaymentsPublicKeysManager}.
-     *
-     * <p>This is the preferred method of specifying the verifying public keys of the sender.
-     */
-    public Builder fetchSenderVerifyingKeysWith(
-        final GooglePaymentsPublicKeysManager googlePaymentsPublicKeysManager)
-        throws GeneralSecurityException {
-      this.senderVerifyingKeysProviders.add(
-          new SenderVerifyingKeysProvider() {
-            @Override
-            public List<ECPublicKey> get(String protocolVersion) throws GeneralSecurityException {
-              try {
-                return parseTrustedSigningKeysJson(
-                    protocolVersion, googlePaymentsPublicKeysManager.getTrustedSigningKeysJson());
-              } catch (IOException e) {
-                throw new GeneralSecurityException("Failed to fetch keys!", e);
-              }
-            }
-          });
-      return this;
-    }
-
-    /**
-     * Sets the trusted verifying public keys of the sender.
-     *
-     * <p><b>IMPORTANT</b>: Instead of using this method to set the verifying public keys of the
-     * sender, prefer calling {@link #fetchSenderVerifyingKeysWith} passing it an instance of {@link
-     * GooglePaymentsPublicKeysManager}. It will take care of fetching fresh keys and caching in
-     * memory. Only use this method if you can't use {@link #fetchSenderVerifyingKeysWith} and be
-     * aware you will need to handle Google key rotations yourself.
-     *
-     * <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(final String trustedSigningKeysJson)
-        throws GeneralSecurityException {
-      this.senderVerifyingKeysProviders.add(
-          new SenderVerifyingKeysProvider() {
-            @Override
-            public List<ECPublicKey> get(String protocolVersion) throws GeneralSecurityException {
-              return parseTrustedSigningKeysJson(protocolVersion, trustedSigningKeysJson);
-            }
-          });
-      return this;
-    }
-
-    /**
-     * Adds a verifying public key of the sender.
-     *
-     * <p><b>IMPORTANT</b>: Instead of using this method to set the verifying public keys of the
-     * sender, prefer calling {@link #fetchSenderVerifyingKeysWith} passing it an instance of {@link
-     * GooglePaymentsPublicKeysManager}. It will take care of fetching fresh keys and caching in
-     * memory. Only use this method if you can't use {@link #fetchSenderVerifyingKeysWith} and be
-     * aware you will need to handle Google key rotations yourself.
-     *
-     * <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(final String val) throws GeneralSecurityException {
-      this.senderVerifyingKeysProviders.add(
-          new SenderVerifyingKeysProvider() {
-            @Override
-            public List<ECPublicKey> get(String protocolVersion) throws GeneralSecurityException {
-              return Collections.singletonList(PaymentMethodTokenUtil.x509EcPublicKey(val));
-            }
-          });
-      return this;
-    }
-
-    /**
-     * Adds a verifying public key of the sender.
-     *
-     * <p><b>IMPORTANT</b>: Instead of using this method to set the verifying public keys of the
-     * sender, prefer calling {@link #fetchSenderVerifyingKeysWith} passing it an instance of {@link
-     * GooglePaymentsPublicKeysManager}. It will take care of fetching fresh keys and caching in
-     * memory. Only use this method if you can't use {@link #fetchSenderVerifyingKeysWith} and be
-     * aware you will need to handle Google key rotations yourself.
-     */
-    public Builder addSenderVerifyingKey(final ECPublicKey val) throws GeneralSecurityException {
-      this.senderVerifyingKeysProviders.add(
-          new SenderVerifyingKeysProvider() {
-            @Override
-            public List<ECPublicKey> get(String protocolVersion) throws GeneralSecurityException {
-              return Collections.singletonList(val);
-            }
-          });
-      return this;
-    }
-
-    /**
-     * Adds the decryption private key of the recipient.
-     *
-     * <p>It must be base64 encoded PKCS8 private key.
-     */
-    public Builder addRecipientPrivateKey(String val) throws GeneralSecurityException {
-      return addRecipientPrivateKey(PaymentMethodTokenUtil.pkcs8EcPrivateKey(val));
-    }
-
-    /** Adds the decryption private key of the recipient. */
-    public Builder addRecipientPrivateKey(ECPrivateKey val) throws GeneralSecurityException {
-      recipientPrivateKeys.add(val);
-      return this;
-    }
-
-    /**
-     * Adds a custom {@link PaymentMethodTokenRecipientKem}.
-     *
-     * <p>This is useful for clients that store keys in an HSM and need a more control on how the
-     * key is used. If you are not using an HSM, you probably should just use {@link
-     * #addRecipientPrivateKey}.
-     *
-     * @since 1.1.0
-     */
-    public Builder addRecipientKem(PaymentMethodTokenRecipientKem kem) {
-      recipientKems.add(kem);
-      return this;
-    }
-
-    public PaymentMethodTokenRecipient build() throws GeneralSecurityException {
-      return new PaymentMethodTokenRecipient(this);
-    }
-  }
-
-  /**
-   * Unseal the given {@code sealedMessage} by performing the necessary signature verification and
-   * decryption (if required) steps based on the protocolVersion.
-   */
-  public String unseal(final String sealedMessage) throws GeneralSecurityException {
-    try {
-      if (protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)) {
-        return unsealECV1(sealedMessage);
-      } else if (protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)) {
-        return unsealECV2(sealedMessage);
-      } else if (protocolVersion.equals(
-          PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)) {
-        return unsealECV2SigningOnly(sealedMessage);
-      }
-      throw new IllegalArgumentException("unsupported version: " + protocolVersion);
-    } catch (JsonParseException | IllegalStateException e) {
-      throw new GeneralSecurityException("cannot unseal; invalid JSON message", e);
-    }
-  }
-
-  private String unsealECV1(String sealedMessage) throws GeneralSecurityException {
-    JsonObject jsonMsg = JsonParser.parseString(sealedMessage).getAsJsonObject();
-    validateECV1(jsonMsg);
-    String signedMessage = verifyECV1(jsonMsg);
-    String decryptedMessage = decrypt(signedMessage);
-    validateMessage(decryptedMessage);
-    return decryptedMessage;
-  }
-
-  private String unsealECV2(String sealedMessage) throws GeneralSecurityException {
-    JsonObject jsonMsg = JsonParser.parseString(sealedMessage).getAsJsonObject();
-    validateECV2(jsonMsg);
-    String signedMessage = verifyECV2(jsonMsg);
-    String decryptedMessage = decrypt(signedMessage);
-    validateMessage(decryptedMessage);
-    return decryptedMessage;
-  }
-
-  private String unsealECV2SigningOnly(String sealedMessage) throws GeneralSecurityException {
-    JsonObject jsonMsg = JsonParser.parseString(sealedMessage).getAsJsonObject();
-    validateECV2(jsonMsg);
-    String message = verifyECV2(jsonMsg);
-    validateMessage(message);
-    return message;
-  }
-
-  private String verifyECV1(final JsonObject jsonMsg) throws GeneralSecurityException {
-    byte[] signature =
-        Base64.decode(jsonMsg.get(PaymentMethodTokenConstants.JSON_SIGNATURE_KEY).getAsString());
-    String signedMessage =
-        jsonMsg.get(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY).getAsString();
-    byte[] signedBytes = getSignedBytes(protocolVersion, signedMessage);
-    verify(
-        protocolVersion,
-        senderVerifyingKeysProviders,
-        Collections.singletonList(signature),
-        signedBytes);
-    return signedMessage;
-  }
-
-  private String verifyECV2(final JsonObject jsonMsg) throws GeneralSecurityException {
-    byte[] signature =
-        Base64.decode(jsonMsg.get(PaymentMethodTokenConstants.JSON_SIGNATURE_KEY).getAsString());
-    String signedMessage =
-        jsonMsg.get(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY).getAsString();
-    byte[] signedBytes = getSignedBytes(protocolVersion, signedMessage);
-    verify(
-        protocolVersion,
-        verifyIntermediateSigningKey(jsonMsg),
-        Collections.singletonList(signature),
-        signedBytes);
-    return signedMessage;
-  }
-
-  private byte[] getSignedBytes(String protocolVersion, String signedMessage)
-      throws GeneralSecurityException {
-    return PaymentMethodTokenUtil.toLengthValue(
-        // The order of the parameters matters.
-        senderId, recipientId, protocolVersion, signedMessage);
-  }
-
-  private void validateMessage(String decryptedMessage) throws GeneralSecurityException {
-    JsonObject decodedMessage;
-
-    try {
-      decodedMessage = JsonParser.parseString(decryptedMessage).getAsJsonObject();
-    } catch (JsonParseException | IllegalStateException e) {
-      // Message wasn't a valid JSON, so nothing to validate.
-      return;
-    }
-
-    // If message expiration is present, checking it.
-    if (decodedMessage.has(PaymentMethodTokenConstants.JSON_MESSAGE_EXPIRATION_KEY)) {
-      long expirationInMillis =
-          Long.parseLong(
-              decodedMessage
-                  .get(PaymentMethodTokenConstants.JSON_MESSAGE_EXPIRATION_KEY)
-                  .getAsString());
-      if (expirationInMillis <= Instant.now().getMillis()) {
-        throw new GeneralSecurityException("expired payload");
-      }
-    }
-  }
-
-  private static void verify(
-      final String protocolVersion,
-      final List<SenderVerifyingKeysProvider> senderVerifyingKeysProviders,
-      final List<byte[]> signatures,
-      final byte[] signedBytes)
-      throws GeneralSecurityException {
-    boolean verified = false;
-    for (SenderVerifyingKeysProvider verifyingKeysProvider : senderVerifyingKeysProviders) {
-      for (ECPublicKey publicKey : verifyingKeysProvider.get(protocolVersion)) {
-        EcdsaVerifyJce verifier =
-            new EcdsaVerifyJce(
-                publicKey, PaymentMethodTokenConstants.ECDSA_HASH_SHA256, EcdsaEncoding.DER);
-        for (byte[] signature : signatures) {
-          try {
-            verifier.verify(signature, signedBytes);
-            // 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(UTF_8), PaymentMethodTokenConstants.GOOGLE_CONTEXT_INFO_ECV1);
-        return new String(cleartext, UTF_8);
-      } catch (GeneralSecurityException e) {
-        // ignored, try again
-      }
-    }
-    throw new GeneralSecurityException("cannot decrypt");
-  }
-
-  private void validateECV1(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.size() != 3) {
-      throw new GeneralSecurityException(
-          "ECv1 message must contain exactly protocolVersion, signature and signedMessage");
-    }
-    String version =
-        jsonMsg.get(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY).getAsString();
-    if (!version.equals(protocolVersion)) {
-      throw new GeneralSecurityException("invalid version: " + version);
-    }
-  }
-
-  private void validateECV2(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.has(PaymentMethodTokenConstants.JSON_INTERMEDIATE_SIGNING_KEY)
-        || jsonMsg.size() != 4) {
-      throw new GeneralSecurityException(
-          protocolVersion
-              + " message must contain exactly protocolVersion, intermediateSigningKey, "
-              + "signature and signedMessage");
-    }
-    String version =
-        jsonMsg.get(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY).getAsString();
-    if (!version.equals(protocolVersion)) {
-      throw new GeneralSecurityException("invalid version: " + version);
-    }
-  }
-
-  /**
-   * Verifies the intermediate key and returns a singleton list containing of {@code
-   * SenderVerifyingKeysProvider} that provides the verified key.
-   */
-  private List<SenderVerifyingKeysProvider> verifyIntermediateSigningKey(JsonObject jsonMsg)
-      throws GeneralSecurityException {
-    JsonObject intermediateSigningKey =
-        jsonMsg.get(PaymentMethodTokenConstants.JSON_INTERMEDIATE_SIGNING_KEY).getAsJsonObject();
-    validateIntermediateSigningKey(intermediateSigningKey);
-    ArrayList<byte[]> signatures = new ArrayList<>();
-    JsonArray signaturesJson =
-        intermediateSigningKey
-            .get(PaymentMethodTokenConstants.JSON_SIGNATURES_KEY)
-            .getAsJsonArray();
-    for (int i = 0; i < signaturesJson.size(); i++) {
-      signatures.add(Base64.decode(signaturesJson.get(i).getAsString()));
-    }
-    String signedKeyAsString =
-        intermediateSigningKey.get(PaymentMethodTokenConstants.JSON_SIGNED_KEY_KEY).getAsString();
-    byte[] signedBytes =
-        PaymentMethodTokenUtil.toLengthValue(
-            // The order of the parameters matters.
-            senderId, protocolVersion, signedKeyAsString);
-    verify(protocolVersion, senderVerifyingKeysProviders, signatures, signedBytes);
-    JsonObject signedKey = JsonParser.parseString(signedKeyAsString).getAsJsonObject();
-    validateSignedKey(signedKey);
-    final String key = signedKey.get(PaymentMethodTokenConstants.JSON_KEY_VALUE_KEY).getAsString();
-    SenderVerifyingKeysProvider provider =
-        new SenderVerifyingKeysProvider() {
-          @Override
-          public List<ECPublicKey> get(String protocolVersion) throws GeneralSecurityException {
-            if (PaymentMethodTokenRecipient.this.protocolVersion.equals(protocolVersion)) {
-              return Collections.singletonList(PaymentMethodTokenUtil.x509EcPublicKey(key));
-            } else {
-              return Collections.emptyList();
-            }
-          }
-        };
-    return Collections.singletonList(provider);
-  }
-
-  private JsonObject validateIntermediateSigningKey(final JsonObject intermediateSigningKey)
-      throws GeneralSecurityException {
-    if (!intermediateSigningKey.has(PaymentMethodTokenConstants.JSON_SIGNATURES_KEY)
-        || !intermediateSigningKey.has(PaymentMethodTokenConstants.JSON_SIGNED_KEY_KEY)
-        || intermediateSigningKey.size() != 2) {
-      throw new GeneralSecurityException(
-          "intermediateSigningKey must contain exactly signedKey and signatures");
-    }
-    return intermediateSigningKey;
-  }
-
-  private void validateSignedKey(final JsonObject signedKey) throws GeneralSecurityException {
-    // Note: allowing further keys to be added so we can extend the protocol if needed in the future
-    if (!signedKey.has(PaymentMethodTokenConstants.JSON_KEY_VALUE_KEY)
-        || !signedKey.has(PaymentMethodTokenConstants.JSON_KEY_EXPIRATION_KEY)) {
-      throw new GeneralSecurityException(
-          "intermediateSigningKey.signedKey must contain keyValue and keyExpiration");
-    }
-
-    // If message expiration is present, checking it.
-    long expirationInMillis =
-        Long.parseLong(
-            signedKey.get(PaymentMethodTokenConstants.JSON_KEY_EXPIRATION_KEY).getAsString());
-    if (expirationInMillis <= Instant.now().getMillis()) {
-      throw new GeneralSecurityException("expired intermediateSigningKey");
-    }
-  }
-
-  private static List<ECPublicKey> parseTrustedSigningKeysJson(
-      String protocolVersion, String trustedSigningKeysJson) throws GeneralSecurityException {
-    List<ECPublicKey> senderVerifyingKeys = new ArrayList<>();
-    try {
-      JsonArray keys =
-          JsonParser.parseString(trustedSigningKeysJson)
-              .getAsJsonObject()
-              .get("keys")
-              .getAsJsonArray();
-      for (int i = 0; i < keys.size(); i++) {
-        JsonObject key = keys.get(i).getAsJsonObject();
-        if (protocolVersion.equals(
-            key.get(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY).getAsString())) {
-
-          if (key.has(PaymentMethodTokenConstants.JSON_KEY_EXPIRATION_KEY)) {
-            // If message expiration is present, checking it.
-            long expirationInMillis =
-                Long.parseLong(
-                    key.get(PaymentMethodTokenConstants.JSON_KEY_EXPIRATION_KEY).getAsString());
-            if (expirationInMillis <= Instant.now().getMillis()) {
-              // Ignore expired keys
-              continue;
-            }
-          } else if (!protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)) {
-            // keyExpiration is required in all versions except ECv1, so if it is missing we should
-            // skip using this key.
-            // In ECv1 the expiration is optional because it is assumed that the caller is
-            // respecting the HTTP cache headers and not using the trustedSigningKeysJson that are
-            // expired according to the headers.
-            continue;
-          }
-
-          senderVerifyingKeys.add(
-              PaymentMethodTokenUtil.x509EcPublicKey(key.get("keyValue").getAsString()));
-        }
-      }
-    } catch (JsonParseException | IllegalStateException e) {
-      throw new GeneralSecurityException("failed to extract trusted signing public keys", e);
-    }
-    if (senderVerifyingKeys.isEmpty()) {
-      throw new GeneralSecurityException("no trusted keys are available for this protocol version");
-    }
-    return senderVerifyingKeys;
-  }
-
-  private interface SenderVerifyingKeysProvider {
-    List<ECPublicKey> get(String protocolVersion) throws GeneralSecurityException;
-  }
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientKem.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientKem.java
deleted file mode 100644
index 47d322b..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientKem.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright 2018 Google LLC
-//
-// 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.crypto.tink.apps.paymentmethodtoken;
-
-import java.security.GeneralSecurityException;
-
-/**
- * Interface for recipient's key encapsulation mechanism (KEM).
- *
- * <p>Google Pay's tokens are encrypted using ECIES which is a hybrid encryption mode consisting of
- * two steps: key encapsulation mechanisam (KEM) using Elliptic Curve Diffie Hellman (ECDH) and HKDF
- * and data encapsulation mechanism (DEM) using AES-CTR-HMAC.
- *
- * <p>During encryption, the KEM step takes the recipient's public key and produces a DEM key and an
- * ephemeral public key. The DEM key is then used to encrypt the credit card data, and the ephemeral
- * public key is sent as the <b>ephemeralPublicKey</b> field of the payload.
- *
- * <p>To decrypt, the recipient must use their private key to compute an ECDH shared secret from the
- * ephemeral public key, and from that derive the DEM key using HKDF. If the recipient keeps the
- * private key in a HSM, they cannot load the private key in Tink, but they can implement this
- * interface and configure Tink to use their custom KEM implementation with {@link
- * PaymentMethodTokenRecipient.Builder#addRecipientKem}.
- *
- * @see <a href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment
- *     Method Token standard</a>
- * @since 1.1.0
- */
-public interface PaymentMethodTokenRecipientKem {
-  /**
-   * Computes a shared secret from the {@code ephemeralPublicKey}, using ECDH.
-   *
-   * <p>{@code ephemeralPublicKey} is a point on the elliptic curve defined in the <a
-   * href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment Method
-   * Token standard</a>, encoded in uncompressed point format. In version ECv1 and ECv2 of the
-   * standard, the elliptic curve is NIST P-256.
-   *
-   * <p>Note that you only needs to compute the shared secret, but you don't have to derive the DEM
-   * key with HKDF -- that process is handled by Tink.
-   */
-  byte[] computeSharedSecret(final byte[] ephemeralPublicKey) throws GeneralSecurityException;
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientKeyGen.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientKeyGen.java
deleted file mode 100644
index 2713854..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientKeyGen.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// 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.crypto.tink.apps.paymentmethodtoken;
-
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.interfaces.ECPublicKey;
-
-/**
- * A util that generates key pairs for the recipient side of <a
- * href="https://developers.google.com/android-pay/integration/payment-token-cryptography">Google
- * Payment Method Token</a>.
- *
- * <h3>Usage</h3>
- *
- * <pre>
- * bazel build apps/paymentmethodtoken/...
- * ./bazel-bin/apps/paymentmethodtoken/recipientkeygen
- * </pre>
- *
- * <p>Running that command will generate a fresh key pair. The private/public key can be found in
- * private_key.bin/public_key.bin. The content of private_key.bin can be passed to {@link
- * PaymentMethodTokenRecipient.Builder#addRecipientPrivateKey} and the content of public_key.bin can
- * be passed to {@link PaymentMethodTokenSender.Builder#rawUncompressedRecipientPublicKey}.
- */
-public final class PaymentMethodTokenRecipientKeyGen {
-  private static final String PRIVATE_KEY_FILE = "private_key.bin";
-
-  private static final String PUBLIC_KEY_FILE = "public_key.bin";
-
-  private static void generateKey() throws GeneralSecurityException, IOException {
-    KeyPair keyPair = EllipticCurves.generateKeyPair(PaymentMethodTokenConstants.P256_CURVE_TYPE);
-    writeBase64(PRIVATE_KEY_FILE, keyPair.getPrivate().getEncoded());
-
-    ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
-    writeBase64(
-        PUBLIC_KEY_FILE,
-        EllipticCurves.pointEncode(
-            PaymentMethodTokenConstants.P256_CURVE_TYPE,
-            PaymentMethodTokenConstants.UNCOMPRESSED_POINT_FORMAT,
-            publicKey.getW()));
-  }
-
-  private static void writeBase64(String pathname, byte[] content) throws IOException {
-    File out = new File(pathname);
-    if (out.exists()) {
-      System.out.println("Please make sure that " + pathname + " does not exist.");
-      System.exit(-1);
-    }
-    FileOutputStream stream = new FileOutputStream(out);
-    stream.write(Base64.encode(content, Base64.DEFAULT | Base64.NO_WRAP));
-    stream.close();
-  }
-
-  public static void main(String[] args) throws GeneralSecurityException, IOException {
-    System.out.println("Generating key....");
-    generateKey();
-    System.out.println("done.");
-  }
-
-  private PaymentMethodTokenRecipientKeyGen() {}
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenSender.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenSender.java
deleted file mode 100644
index 25d0fe2..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenSender.java
+++ /dev/null
@@ -1,335 +0,0 @@
-// 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.crypto.tink.apps.paymentmethodtoken;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.PublicKeySign;
-import com.google.crypto.tink.apps.paymentmethodtoken.PaymentMethodTokenConstants.ProtocolVersionConfig;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EcdsaSignJce;
-import com.google.crypto.tink.subtle.EllipticCurves.EcdsaEncoding;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonParser;
-import com.google.gson.internal.Streams;
-import com.google.gson.stream.JsonWriter;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-
-/**
- * An implementation of the sender side of <a
- * href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment Method
- * Token</a>.
- *
- * <h3>Warning</h3>
- *
- * <p>This implementation supports only versions {@code ECv1} and {@code ECv2}.
- *
- * <h3>Usage</h3>
- *
- * <pre>{@code
- * PaymentMethodTokenSender sender = new PaymentMethodTokenSender.Builder()
- *    .senderId(senderId)
- *    .senderSigningKey(senderPrivateKey)
- *    .recipientId(recipientId)
- *    .recipientPublicKey(recipientPublicKey)
- *    .build();
- * String plaintext = "blah";
- * String ciphertext = sender.seal(plaintext);
- * }</pre>
- *
- * @see <a href="https://developers.google.com/pay/api/payment-data-cryptography">Google Payment
- *     Method Token standard</a>
- * @since 1.0.0
- */
-public final class PaymentMethodTokenSender {
-  private final String protocolVersion;
-  private final ProtocolVersionConfig protocolVersionConfig;
-  private final PublicKeySign signer;
-  private final String senderIntermediateCert;
-  private final String senderId;
-  private final String recipientId;
-
-  private HybridEncrypt hybridEncrypter;
-
-  PaymentMethodTokenSender(Builder builder) throws GeneralSecurityException {
-    switch (builder.protocolVersion) {
-      case PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1:
-        validateV1(builder);
-        break;
-      case PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2:
-        validateV2(builder);
-        break;
-      case PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY:
-        validateV2SigningOnly(builder);
-        break;
-      default:
-        throw new IllegalArgumentException("invalid version: " + builder.protocolVersion);
-    }
-
-    this.protocolVersion = builder.protocolVersion;
-    this.protocolVersionConfig = ProtocolVersionConfig.forProtocolVersion(protocolVersion);
-    this.signer =
-        new EcdsaSignJce(
-            builder.senderIntermediateSigningKey != null
-                ? builder.senderIntermediateSigningKey
-                : builder.senderSigningKey,
-            PaymentMethodTokenConstants.ECDSA_HASH_SHA256,
-            EcdsaEncoding.DER);
-    this.senderId = builder.senderId;
-    if (protocolVersionConfig.isEncryptionRequired) {
-      this.hybridEncrypter =
-          new PaymentMethodTokenHybridEncrypt(builder.recipientPublicKey, protocolVersionConfig);
-    }
-    if (builder.recipientId == null) {
-      throw new IllegalArgumentException("must set recipient Id using Builder.recipientId");
-    }
-    this.recipientId = builder.recipientId;
-    this.senderIntermediateCert = builder.senderIntermediateCert;
-  }
-
-  /**
-   * Builder for {@link PaymentMethodTokenSender}.
-   *
-   * @since 1.0.0
-   */
-  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 ECPrivateKey senderIntermediateSigningKey = null;
-    private String senderIntermediateCert = 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 intermediate signing key of the sender.
-     *
-     * <p>It must be base64 encoded PKCS8 private key.
-     *
-     * @since 1.1.0
-     */
-    public Builder senderIntermediateSigningKey(String val) throws GeneralSecurityException {
-      return senderIntermediateSigningKey(PaymentMethodTokenUtil.pkcs8EcPrivateKey(val));
-    }
-
-    /**
-     * Sets the intermediate signing key of the sender.
-     *
-     * @since 1.1.0
-     */
-    public Builder senderIntermediateSigningKey(ECPrivateKey val) throws GeneralSecurityException {
-      senderIntermediateSigningKey = val;
-      return this;
-    }
-
-    /**
-     * JSON containing sender intermediate signing key and a signature of it by the sender signing
-     * key.
-     *
-     * <p>This can be generated by {@link SenderIntermediateCertFactory}.
-     *
-     * @since 1.1.0
-     */
-    public Builder senderIntermediateCert(String val) throws GeneralSecurityException {
-      this.senderIntermediateCert = 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;
-    }
-
-    public Builder recipientPublicKey(ECPublicKey val) throws GeneralSecurityException {
-      recipientPublicKey = 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 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)
-        || protocolVersion.equals(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-        || protocolVersion.equals(
-            PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)) {
-      return sealV1OrV2(message);
-    }
-    throw new GeneralSecurityException("Unsupported version: " + protocolVersion);
-  }
-
-  private String sealV1OrV2(final String message) throws GeneralSecurityException {
-    String signedMessage =
-        protocolVersionConfig.isEncryptionRequired
-            ? new String(
-                hybridEncrypter.encrypt(
-                    message.getBytes(UTF_8), PaymentMethodTokenConstants.GOOGLE_CONTEXT_INFO_ECV1),
-                UTF_8)
-            : message;
-    return signV1OrV2(signedMessage);
-  }
-
-  static String jsonEncodeSignedMessage(
-      String message, String protocolVersion, byte[] signature, String senderIntermediateCert)
-      throws GeneralSecurityException {
-    try {
-      JsonObject result = new JsonObject();
-      result.addProperty(PaymentMethodTokenConstants.JSON_SIGNATURE_KEY, Base64.encode(signature));
-      if (senderIntermediateCert != null) {
-        result.add(
-            PaymentMethodTokenConstants.JSON_INTERMEDIATE_SIGNING_KEY,
-            JsonParser.parseString(senderIntermediateCert).getAsJsonObject());
-      }
-      result.addProperty(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, protocolVersion);
-      result.addProperty(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY, message);
-      StringWriter stringWriter = new StringWriter();
-      JsonWriter jsonWriter = new JsonWriter(stringWriter);
-      jsonWriter.setHtmlSafe(true);
-      Streams.write(result, jsonWriter);
-      return stringWriter.toString();
-    } catch (JsonParseException | IllegalStateException | IOException e) {
-      throw new GeneralSecurityException("cannot seal; JSON error", e);
-    }
-  }
-
-  private String signV1OrV2(String message) throws GeneralSecurityException {
-    byte[] toSignBytes =
-        PaymentMethodTokenUtil.toLengthValue(
-            // The order of the parameters matters.
-            senderId, recipientId, protocolVersion, message);
-    byte[] signature = signer.sign(toSignBytes);
-    return jsonEncodeSignedMessage(message, protocolVersion, signature, senderIntermediateCert);
-  }
-
-  private static void validateV1(Builder builder) {
-    // ECv1 signed payloads directly.
-    if (builder.senderSigningKey == null) {
-      throw new IllegalArgumentException(
-          "must set sender's signing key using Builder.senderSigningKey");
-    }
-    if (builder.senderIntermediateSigningKey != null) {
-      throw new IllegalArgumentException(
-          "must not set sender's intermediate signing key using "
-              + "Builder.senderIntermediateSigningKey");
-    }
-    if (builder.senderIntermediateCert != null) {
-      throw new IllegalArgumentException(
-          "must not set signed sender's intermediate signing key using "
-              + "Builder.senderIntermediateCert");
-    }
-    if (builder.recipientPublicKey == null) {
-      throw new IllegalArgumentException(
-          "must set recipient's public key using Builder.recipientPublicKey");
-    }
-  }
-
-  private static void validateV2(Builder builder) {
-    validateIntermediateSigningKeys(builder);
-    if (builder.recipientPublicKey == null) {
-      throw new IllegalArgumentException(
-          "must set recipient's public key using Builder.recipientPublicKey");
-    }
-  }
-
-  private static void validateV2SigningOnly(Builder builder) {
-    validateIntermediateSigningKeys(builder);
-    if (builder.recipientPublicKey != null) {
-      throw new IllegalArgumentException(
-          "must not set recipient's public key using Builder.recipientPublicKey");
-    }
-  }
-
-  private static void validateIntermediateSigningKeys(Builder builder) {
-    // ECv2 and newer protocols use an intermediate signing key.
-    if (builder.senderSigningKey != null) {
-      throw new IllegalArgumentException(
-          "must not set sender's signing key using Builder.senderSigningKey");
-    }
-    if (builder.senderIntermediateSigningKey == null) {
-      throw new IllegalArgumentException(
-          "must set sender's intermediate signing key using "
-              + "Builder.senderIntermediateSigningKey");
-    }
-    if (builder.senderIntermediateCert == null) {
-      throw new IllegalArgumentException(
-          "must set signed sender's intermediate signing key using "
-              + "Builder.senderIntermediateCert");
-    }
-  }
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenUtil.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenUtil.java
deleted file mode 100644
index 185038d..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenUtil.java
+++ /dev/null
@@ -1,79 +0,0 @@
-// 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.crypto.tink.apps.paymentmethodtoken;
-
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.Bytes;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.EngineFactory;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import javax.crypto.Cipher;
-import javax.crypto.Mac;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/** Various helpers. */
-final class PaymentMethodTokenUtil {
-  static ECPublicKey rawUncompressedEcPublicKey(String rawUncompressedPublicKey)
-      throws GeneralSecurityException {
-    return EllipticCurves.getEcPublicKey(
-        PaymentMethodTokenConstants.P256_CURVE_TYPE,
-        PaymentMethodTokenConstants.UNCOMPRESSED_POINT_FORMAT,
-        Base64.decode(rawUncompressedPublicKey));
-  }
-
-  static ECPublicKey x509EcPublicKey(String x509PublicKey) throws GeneralSecurityException {
-    return EllipticCurves.getEcPublicKey(Base64.decode(x509PublicKey));
-  }
-
-  static ECPrivateKey pkcs8EcPrivateKey(String pkcs8PrivateKey) throws GeneralSecurityException {
-    return EllipticCurves.getEcPrivateKey(Base64.decode(pkcs8PrivateKey));
-  }
-
-  static byte[] toLengthValue(String... chunks) throws GeneralSecurityException {
-    byte[] out = new byte[0];
-    for (String chunk : chunks) {
-      byte[] bytes = chunk.getBytes(StandardCharsets.UTF_8);
-      out = Bytes.concat(out, Bytes.intToByteArray(4, bytes.length));
-      out = Bytes.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);
-  }
-
-  private PaymentMethodTokenUtil() {}
-}
diff --git a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/SenderIntermediateCertFactory.java b/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/SenderIntermediateCertFactory.java
deleted file mode 100644
index 0e3bfc7..0000000
--- a/apps/paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken/SenderIntermediateCertFactory.java
+++ /dev/null
@@ -1,218 +0,0 @@
-// Copyright 2018 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.crypto.tink.apps.paymentmethodtoken;
-
-import com.google.crypto.tink.PublicKeySign;
-import com.google.crypto.tink.apps.paymentmethodtoken.PaymentMethodTokenConstants.ProtocolVersionConfig;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EcdsaSignJce;
-import com.google.crypto.tink.subtle.EllipticCurves.EcdsaEncoding;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.internal.Streams;
-import com.google.gson.stream.JsonWriter;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPrivateKey;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Creates a signed certificate with the intermediate signing keys used by the sender in certain
- * protocol versions.
- *
- * @since 1.1.0
- */
-public class SenderIntermediateCertFactory {
-  private final List<PublicKeySign> signers;
-  private final String intermediateSigningKey;
-  private final String protocolVersion;
-  private final String senderId;
-  private final long expiration;
-
-  private SenderIntermediateCertFactory(
-      String protocolVersion,
-      String senderId,
-      List<ECPrivateKey> senderSigningKeys,
-      String intermediateSigningKey,
-      long expiration)
-      throws GeneralSecurityException {
-    if (!ProtocolVersionConfig.forProtocolVersion(protocolVersion)
-        .supportsIntermediateSigningKeys) {
-      throw new IllegalArgumentException("invalid version: " + protocolVersion);
-    }
-    if (senderSigningKeys.isEmpty()) {
-      throw new IllegalArgumentException(
-          "must add at least one sender's signing key using Builder.addSenderSigningKey");
-    }
-    if (expiration == 0) {
-      throw new IllegalArgumentException("must set expiration using Builder.expiration");
-    }
-    if (expiration < 0) {
-      throw new IllegalArgumentException("invalid negative expiration");
-    }
-    this.protocolVersion = protocolVersion;
-    this.senderId = senderId;
-    this.signers = new ArrayList<>();
-    for (ECPrivateKey senderSigningKey : senderSigningKeys) {
-      this.signers.add(
-          new EcdsaSignJce(
-              senderSigningKey, PaymentMethodTokenConstants.ECDSA_HASH_SHA256, EcdsaEncoding.DER));
-    }
-    this.intermediateSigningKey = intermediateSigningKey;
-    this.expiration = expiration;
-  }
-
-  /**
-   * Builder for {@link SenderIntermediateCertFactory}.
-   *
-   * @since 1.1.0
-   */
-  public static class Builder {
-    private final List<ECPrivateKey> senderSigningKeys = new ArrayList<>();
-    private String intermediateSigningKey;
-    private String protocolVersion = PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2;
-    private String senderId = PaymentMethodTokenConstants.GOOGLE_SENDER_ID;
-    private long expiration;
-
-    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 expiration in millis since epoch. */
-    public Builder expiration(long millisSinceEpoch) {
-      expiration = millisSinceEpoch;
-      return this;
-    }
-
-    /**
-     * Adds a signing key of the sender.
-     *
-     * <p>It must be base64 encoded PKCS8 private key.
-     */
-    public Builder addSenderSigningKey(String val) throws GeneralSecurityException {
-      return addSenderSigningKey(PaymentMethodTokenUtil.pkcs8EcPrivateKey(val));
-    }
-
-    /**
-     * Adds a signing key of the sender.
-     *
-     * <p>It must be base64 encoded PKCS8 private key.
-     */
-    public Builder addSenderSigningKey(ECPrivateKey val) throws GeneralSecurityException {
-      this.senderSigningKeys.add(val);
-      return this;
-    }
-
-    /**
-     * Sets the intermediate signing key being signed.
-     *
-     * <p>The public key specified here is a base64 (no wrapping, padded) version of the key encoded
-     * in ASN.1 type SubjectPublicKeyInfo defined in the X.509 standard.
-     */
-    public Builder senderIntermediateSigningKey(String val) throws GeneralSecurityException {
-      // Parsing to validate the format
-      PaymentMethodTokenUtil.x509EcPublicKey(val);
-      intermediateSigningKey = val;
-      return this;
-    }
-
-    public SenderIntermediateCertFactory build() throws GeneralSecurityException {
-      return new SenderIntermediateCertFactory(
-          protocolVersion, senderId, senderSigningKeys, intermediateSigningKey, expiration);
-    }
-  }
-
-  static String jsonEncodeSignedKey(String intermediateSigningKey, long expiration) {
-    try {
-      JsonObject jsonObj = new JsonObject();
-      jsonObj.addProperty(PaymentMethodTokenConstants.JSON_KEY_VALUE_KEY, intermediateSigningKey);
-      jsonObj.addProperty(
-          PaymentMethodTokenConstants.JSON_KEY_EXPIRATION_KEY, Long.toString(expiration));
-      StringWriter stringWriter = new StringWriter();
-      JsonWriter jsonWriter = new JsonWriter(stringWriter);
-      jsonWriter.setHtmlSafe(true);
-      Streams.write(jsonObj, jsonWriter);
-      return stringWriter.toString();
-    } catch (JsonParseException | IllegalStateException | IOException e) {
-      throw new AssertionError("Failed to perform JSON encoding", e);
-    }
-  }
-
-  static String jsonEncodeCertificate(String signedKey, ArrayList<String> signatures) {
-    try {
-      JsonArray jsonSignatures = new JsonArray();
-      for (String signature : signatures) {
-        jsonSignatures.add(signature);
-      }
-      JsonObject result = new JsonObject();
-      result.addProperty(PaymentMethodTokenConstants.JSON_SIGNED_KEY_KEY, signedKey);
-      result.add(PaymentMethodTokenConstants.JSON_SIGNATURES_KEY, jsonSignatures);
-      StringWriter stringWriter = new StringWriter();
-      JsonWriter jsonWriter = new JsonWriter(stringWriter);
-      jsonWriter.setHtmlSafe(true);
-      Streams.write(result, jsonWriter);
-      return stringWriter.toString();
-    } catch (JsonParseException | IllegalStateException | IOException e) {
-      throw new AssertionError("Failed to perform JSON encoding", e);
-    }
-  }
-
-  /**
-   * Creates the certificate.
-   *
-   * <p>This will return a serialized JSONObject in the following format:
-   *
-   * <pre>
-   *   {
-   *     // {
-   *     //   // A string that identifies this cert
-   *     //   "keyValue": "ZXBoZW1lcmFsUHVibGljS2V5"
-   *     //   // string (UTC milliseconds since epoch)
-   *     //   "expiration": "1520836260646",
-   *     // }
-   *     "signedKey": "... serialized JSON shown in comment above ...",
-   *     "signatures": ["signature1", "signature2", ...],
-   *   }
-   * </pre>
-   */
-  public String create() throws GeneralSecurityException {
-    String signedKey = jsonEncodeSignedKey(intermediateSigningKey, expiration);
-    byte[] toSignBytes =
-        PaymentMethodTokenUtil.toLengthValue(
-            // The order of the parameters matters.
-            senderId, protocolVersion, signedKey);
-    ArrayList<String> signatures = new ArrayList<>();
-    for (PublicKeySign signer : signers) {
-      byte[] signature = signer.sign(toSignBytes);
-      signatures.add(Base64.encode(signature));
-    }
-    return jsonEncodeCertificate(signedKey, signatures);
-  }
-}
diff --git a/apps/paymentmethodtoken/src/test/BUILD.bazel b/apps/paymentmethodtoken/src/test/BUILD.bazel
deleted file mode 100644
index c6f52d3..0000000
--- a/apps/paymentmethodtoken/src/test/BUILD.bazel
+++ /dev/null
@@ -1,43 +0,0 @@
-load("@tink_java//tools:gen_java_test_rules.bzl", "gen_java_test_rules")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-# Tests
-
-java_library(
-    name = "generator_test",
-    testonly = 1,
-    srcs = glob([
-        "**/*.java",
-    ]),
-    deps = [
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:google_payments_public_keys_manager",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_constants",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_hybrid_decrypt",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_hybrid_encrypt",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_recipient",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_recipient_kem",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_sender",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:payment_method_token_util",
-        "//paymentmethodtoken/src/main/java/com/google/crypto/tink/apps/paymentmethodtoken:sender_intermediate_cert_factory",
-        "@maven//:com_google_code_gson_gson",
-        "@maven//:com_google_http_client_google_http_client",
-        "@maven//:joda_time_joda_time",
-        "@maven//:junit_junit",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_decrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:random",
-    ],
-)
-
-gen_java_test_rules(
-    test_files = glob(["**/*Test.java"],
-    ),
-    deps = [
-        ":generator_test",
-    ],
-)
diff --git a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/GooglePaymentsPublicKeyManagerTest.java b/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/GooglePaymentsPublicKeyManagerTest.java
deleted file mode 100644
index 59cf9ad..0000000
--- a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/GooglePaymentsPublicKeyManagerTest.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// 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.crypto.tink.apps.paymentmethodtoken;
-
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.fail;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests for {@link GooglePaymentsPublicKeysManager}. */
-@RunWith(JUnit4.class)
-public class GooglePaymentsPublicKeyManagerTest {
-  @Test
-  public void builderShouldReturnSingletonsWhenMatching() {
-    assertSame(
-        GooglePaymentsPublicKeysManager.INSTANCE_PRODUCTION,
-        new GooglePaymentsPublicKeysManager.Builder().build());
-    assertSame(
-        GooglePaymentsPublicKeysManager.INSTANCE_TEST,
-        new GooglePaymentsPublicKeysManager.Builder()
-            .setKeysUrl(GooglePaymentsPublicKeysManager.KEYS_URL_TEST)
-            .build());
-  }
-
-  @Test
-  public void builderShouldReturnDifferentInstanceWhenNotMatchingSingletons() {
-    assertNotSame(
-        GooglePaymentsPublicKeysManager.INSTANCE_PRODUCTION,
-        new GooglePaymentsPublicKeysManager.Builder().setKeysUrl("https://abc").build());
-    assertNotSame(
-        GooglePaymentsPublicKeysManager.INSTANCE_TEST,
-        new GooglePaymentsPublicKeysManager.Builder().setKeysUrl("https://abc").build());
-  }
-
-  @Test
-  public void builderShouldThrowIllegalArgumentExceptionWhenUrlIsNotHttps() {
-    try {
-      new GooglePaymentsPublicKeysManager.Builder().setKeysUrl("http://abc").build();
-      fail("Expected IllegalArgumentException");
-    } catch (IllegalArgumentException ex) {
-      // expected.
-    }
-  }
-}
diff --git a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodJsonEncodingTest.java b/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodJsonEncodingTest.java
deleted file mode 100644
index 70b8ed4..0000000
--- a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodJsonEncodingTest.java
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright 2021 Google LLC
-//
-// 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.crypto.tink.apps.paymentmethodtoken;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.assertEquals;
-
-import java.util.ArrayList;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests for the exact Json-Encoding produced.
- *
- * These tests test implementation details. Do not depend on the this. For example, the particular
- * ordering of the elements or the particular character escaping used may change in the future.
- * */
-@RunWith(JUnit4.class)
-public final class PaymentMethodJsonEncodingTest {
-
-  @Test
-  public void testExactOutputOfJsonEncodeCiphertext() throws Exception {
-    byte[] ciphertext = "CiPhErTeXt".getBytes(UTF_8);
-    byte[] tag = "taaag".getBytes(UTF_8);
-    byte[] ephemeralPublicKey = "ephemeral Public Key".getBytes(UTF_8);
-
-    String jsonEncodedCiphertext =
-        PaymentMethodTokenHybridEncrypt.jsonEncodeCiphertext(ciphertext, tag, ephemeralPublicKey);
-
-    // JSONObject uses a HashMap, where the ordering is not defined. The ordering is however
-    // deterministic. And for jsonEncodeCiphertext, the order happens to be first "encryptedMessage"
-    // then "ephemeralPublicKey", and finally "tag". Also, JSONObject uses HTML-safe encoding.
-    assertEquals(
-        "{\"encryptedMessage\":\"Q2lQaEVyVGVYdA\\u003d\\u003d\",\"ephemeralPublicKey\":"
-            + "\"ZXBoZW1lcmFsIFB1YmxpYyBLZXk\\u003d\",\"tag\":\"dGFhYWc\\u003d\"}",
-        jsonEncodedCiphertext);
-  }
-
-  @Test
-  public void testExactOutputOfJsonEncodeSignedMessage() throws Exception {
-    String senderIntermediateCert =
-        "{\"signedKey\":\"{\\\"keyValue\\\":\\\"abcde\\\\u003d\\\\u003d\\\",\\\"keyExpiration\\\""
-            + ":\\\"1615299372858\\\"}\",\"signatures\":[\"fghijkl\\u003d\"]}";
-    String version = "ECv1";
-    String message =
-        "{\"encryptedMessage\":\"Q2lQaEVyVGVYdA\\u003d\\u003d\",\"ephemeralPublicKey\":\"ZXBoZW1l"
-            + "cmFsIFB1YmxpYyBLZXk\\u003d\",\"tag\":\"dGFhYWc\\u003d\"}";
-    byte[] signature = "the signature".getBytes(UTF_8);
-
-    String jsonEncodedSignedMessage =
-        PaymentMethodTokenSender.jsonEncodeSignedMessage(
-            message, version, signature, senderIntermediateCert);
-
-    String expected =
-        "{\"signature\":\"dGhlIHNpZ25hdHVyZQ\\u003d\\u003d\",\"intermediateSigningKey\":{\"signe"
-            + "dKey\":\"{\\\"keyValue\\\":\\\"abcde\\\\u003d\\\\u003d\\\",\\\"keyExpiration\\\":"
-            + "\\\"1615299372858\\\"}\",\"signatures\":[\"fghijkl\\u003d\"]},\"protocolVersion\""
-            + ":\"ECv1\",\"signedMessage\":\"{\\\"encryptedMessage\\\":\\\"Q2lQaEVyVGVYdA\\\\u00"
-            + "3d\\\\u003d\\\",\\\"ephemeralPublicKey\\\":\\\"ZXBoZW1lcmFsIFB1YmxpYyBLZXk\\\\u00"
-            + "3d\\\",\\\"tag\\\":\\\"dGFhYWc\\\\u003d\\\"}\"}";
-    assertEquals(expected, jsonEncodedSignedMessage);
-
-    String expected2 =
-        "{\"signature\":\"dGhlIHNpZ25hdHVyZQ\\u003d\\u003d\",\"protocolVersion\":\"ECv1\",\"sign"
-            + "edMessage\":\"{\\\"encryptedMessage\\\":\\\"Q2lQaEVyVGVYdA\\\\u003d\\\\u003d\\\","
-            + "\\\"ephemeralPublicKey\\\":\\\"ZXBoZW1lcmFsIFB1YmxpYyBLZXk\\\\u003d\\\",\\\"tag\\"
-            + "\":\\\"dGFhYWc\\\\u003d\\\"}\"}";
-
-    String jsonEncodedSignedMessage2 =
-        PaymentMethodTokenSender.jsonEncodeSignedMessage(message, version, signature, null);
-    assertEquals(expected2, jsonEncodedSignedMessage2);
-  }
-
-  @Test
-  public void testExactOutputOfJsonEncodedSignedKey() throws Exception {
-    String intermediateSigningKey =
-        "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSA"
-            + "M43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw==";
-    long expiration = 1520836260646L;
-    assertEquals(
-        "{\"keyValue\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yRydzqdg1"
-            + "TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw\\u003d\\u003d\",\"keyExpiration\""
-            + ":\"1520836260646\"}",
-        SenderIntermediateCertFactory.jsonEncodeSignedKey(intermediateSigningKey, expiration));
-  }
-
-  @Test
-  public void testExactOutputOfJsonEncodeCertificate() throws Exception {
-    String intermediateSigningKey =
-        "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSA"
-            + "M43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw==";
-    long expiration = 1520836260646L;
-    String signedKey =
-        SenderIntermediateCertFactory.jsonEncodeSignedKey(intermediateSigningKey, expiration);
-    ArrayList<String> signatures = new ArrayList<>();
-    signatures.add("iTzFvzFRxyCw==");
-    signatures.add("abcde090/+==");
-    signatures.add("xyz");
-    String expected =
-        "{\"signedKey\":\"{\\\"keyValue\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE"
-            + "/1+3HBVSbdv+j7NaArdgMyoSAM43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRx"
-            + "yCw\\\\u003d\\\\u003d\\\",\\\"keyExpiration\\\":\\\"1520836260646\\\"}\",\"signatur"
-            + "es\":[\"iTzFvzFRxyCw\\u003d\\u003d\",\"abcde090/+\\u003d\\u003d\",\"xyz\"]}";
-    assertEquals(
-        expected, SenderIntermediateCertFactory.jsonEncodeCertificate(signedKey, signatures));
-  }
-
-  @Test
-  public void testExactOutputOfWeirdJsonEncodeCertificate() throws Exception {
-    String intermediateSigningKey =
-        "\"\\==";
-    long expiration = -123;
-    String signedKey =
-        SenderIntermediateCertFactory.jsonEncodeSignedKey(intermediateSigningKey, expiration);
-    ArrayList<String> signatures = new ArrayList<>();
-    signatures.add("");
-    signatures.add("\\\"/+==");
-    String expected =
-        "{\"signedKey\":\"{\\\"keyValue\\\":\\\"\\\\\\\"\\\\\\\\\\\\u003d\\\\u003d"
-            + "\\\",\\\"keyExpiration\\\":\\\"-123\\\"}\",\"signatures\":[\"\",\"\\\\\\\"/+\\u003d"
-            + "\\u003d\"]}";
-    assertEquals(
-        expected, SenderIntermediateCertFactory.jsonEncodeCertificate(signedKey, signatures));
-  }
-}
diff --git a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridDecryptTest.java b/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridDecryptTest.java
deleted file mode 100644
index 387c3aa..0000000
--- a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridDecryptTest.java
+++ /dev/null
@@ -1,119 +0,0 @@
-// 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.crypto.tink.apps.paymentmethodtoken;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-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.apps.paymentmethodtoken.PaymentMethodTokenConstants.ProtocolVersionConfig;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.Random;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-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.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 = EllipticCurves.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, ProtocolVersionConfig.EC_V1);
-    HybridDecrypt hybridDecrypt =
-        new PaymentMethodTokenHybridDecrypt(recipientPrivateKey, ProtocolVersionConfig.EC_V1);
-    testModifyDecrypt(hybridEncrypt, hybridDecrypt);
-  }
-
-  public void testModifyDecrypt(HybridEncrypt hybridEncrypt, HybridDecrypt hybridDecrypt)
-      throws Exception {
-    byte[] plaintext = Random.randBytes(111);
-    byte[] context = "context info".getBytes(UTF_8);
-
-    byte[] ciphertext = hybridEncrypt.encrypt(plaintext, context);
-    byte[] decrypted = hybridDecrypt.decrypt(ciphertext, context);
-    assertArrayEquals(plaintext, decrypted);
-
-    JsonObject json = JsonParser.parseString(new String(ciphertext, UTF_8)).getAsJsonObject();
-
-    // Modify public key.
-    byte[] kem =
-        Base64.decode(
-            json.get(PaymentMethodTokenConstants.JSON_EPHEMERAL_PUBLIC_KEY).getAsString());
-    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.addProperty(
-            PaymentMethodTokenConstants.JSON_EPHEMERAL_PUBLIC_KEY,
-            Base64.encode(modifiedPublicKey));
-        try {
-          hybridDecrypt.decrypt(json.toString().getBytes(UTF_8), context);
-          fail("Invalid ciphertext, should have thrown exception");
-        } catch (GeneralSecurityException expected) {
-          // Expected
-        }
-      }
-    }
-
-    // Modify payload.
-    byte[] payload =
-        Base64.decode(
-            json.get(PaymentMethodTokenConstants.JSON_ENCRYPTED_MESSAGE_KEY).getAsString());
-    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.addProperty(
-            PaymentMethodTokenConstants.JSON_ENCRYPTED_MESSAGE_KEY, Base64.encode(modifiedPayload));
-        try {
-          hybridDecrypt.decrypt(json.toString().getBytes(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/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridEncryptTest.java b/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridEncryptTest.java
deleted file mode 100644
index 3fe9459..0000000
--- a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenHybridEncryptTest.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// 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.crypto.tink.apps.paymentmethodtoken;
-
-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.apps.paymentmethodtoken.PaymentMethodTokenConstants.ProtocolVersionConfig;
-import com.google.crypto.tink.subtle.EllipticCurves;
-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 = EllipticCurves.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, ProtocolVersionConfig.EC_V1);
-    HybridDecrypt hybridDecrypt =
-        new PaymentMethodTokenHybridDecrypt(recipientPrivateKey, ProtocolVersionConfig.EC_V1);
-    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/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientTest.java b/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientTest.java
deleted file mode 100644
index 4bce9f5..0000000
--- a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenRecipientTest.java
+++ /dev/null
@@ -1,1403 +0,0 @@
-// 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.crypto.tink.apps.paymentmethodtoken;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-import com.google.api.client.testing.http.MockHttpTransport;
-import com.google.api.client.testing.http.MockLowLevelHttpResponse;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import org.joda.time.Duration;
-import org.joda.time.Instant;
-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 {
-
-  /**
-   * Sample merchant public key.
-   *
-   * <p>Corresponds to public key of {@link #MERCHANT_PRIVATE_KEY_PKCS8_BASE64}
-   *
-   * <p>Created with:
-   *
-   * <pre>
-   * openssl ec -in merchant-key.pem -pubout -text -noout 2> /dev/null | grep "pub:" -A5 \
-   *     | xxd -r -p | base64
-   * </pre>
-   */
-  private static final String MERCHANT_PUBLIC_KEY_BASE64 =
-      "BOdoXP+9Aq473SnGwg3JU1aiNpsd9vH2ognq4PtDtlLGa3Kj8TPf+jaQNPyDSkh3JUhiS0KyrrlWhAgNZKHYF2Y=";
-
-  /**
-   * Sample merchant private key.
-   *
-   * <p>Corresponds to the private key of {@link #MERCHANT_PUBLIC_KEY_BASE64}
-   *
-   * <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";
-
-  /** An alternative merchant private key used during the tests. */
-  private static final String ALTERNATE_MERCHANT_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgOUIzccyJ3rTx6SVm"
-          + "XrWdtwUP0NU26nvc8KIYw2GmYZKhRANCAAR5AjmTNAE93hQEQE+PryLlgr6Q7FXyN"
-          + "XoZRk+1Fikhq61mFhQ9s14MOwGBxd5O6Jwn/sdUrWxkYk3idtNEN1Rz";
-
-  /** Sample Google provided JSON with its public signing keys. */
-  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"
-          + "      \"keyValue\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM"
-          + "43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw==\",\n"
-          + "      \"keyExpiration\": \""
-          + Instant.now().plus(Duration.standardDays(1)).getMillis()
-          + "\",\n"
-          + "      \"protocolVersion\": \"ECv2\"\n"
-          + "    },\n"
-          + "    {\n"
-          + "      \"keyValue\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENXvYqxD5WayKYhuXQevdGdLA8i"
-          + "fV4LsRS2uKvFo8wwyiwgQHB9DiKzG6T/P1Fu9Bl7zWy/se5Dy4wk1mJoPuxg==\",\n"
-          + "      \"keyExpiration\": \""
-          + Instant.now().plus(Duration.standardDays(1)).getMillis()
-          + "\",\n"
-          + "      \"protocolVersion\": \"ECv2SigningOnly\"\n"
-          + "    }\n"
-          + "  ]\n"
-          + "}";
-
-  /** Index within {@link #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON} of the ECv1 Google signing key. */
-  private static final int INDEX_OF_GOOGLE_SIGNING_EC_V1 = 0;
-
-  /** Index within {@link #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON} of the ECv2 Google signing key. */
-  private static final int INDEX_OF_GOOGLE_SIGNING_EC_V2 = 1;
-
-  /**
-   * Index within {@link #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON} of the ECv2SigningOnly Google signing
-   * key.
-   */
-  private static final int INDEX_OF_GOOGLE_SIGNING_EC_V2_SIGNING_ONLY = 2;
-
-  /**
-   * Sample Google private signing key for the ECv1 protocolVersion.
-   *
-   * <p>Corresponds to the ECv1 private key of the key in {@link
-   * #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V1_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZj/Dldxz8fvKVF5O"
-          + "TeAtK6tY3G1McmvhMppe6ayW6GahRANCAAQ9icfBLy56BYB7BC2XGLOYsXKfAdzF"
-          + "FPU8rTtwMDr8LixetUjVLNkJTHxTxLQuMytPqKt39Vbtt7czPq3+yu1F";
-
-  /**
-   * Sample Google private signing key for the ECv2 protocolVersion.
-   *
-   * <p>Corresponds to ECv2 private key of the key in {@link #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKvEdSS8f0mjTCNKev"
-          + "aKXIzfNC5b4A104gJWI9TsLIMqhRANCAAT/X7ccFVJt2/6Ps1oCt2AzKhIAz"
-          + "jfJHJ3Op2DVPGh1LMD3oOPgxzUSIqujHG6dq9Ui93Eacl4VWJPMW/MVHHIL";
-
-  /**
-   * Sample Google intermediate public signing key for the ECv2 protocolVersion.
-   *
-   * <p>Base64 version of the public key encoded in ASN.1 type SubjectPublicKeyInfo defined in the
-   * X.509 standard.
-   *
-   * <p>The intermediate public key will be signed by {@link
-   * #GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64 =
-      "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yR"
-          + "ydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw==";
-
-  /**
-   * Sample Google intermediate private signing key for the ECv2 protocolVersion.
-   *
-   * <p>Corresponds to private key of the key in {@link
-   * #GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKvEdSS8f0mjTCNKev"
-          + "aKXIzfNC5b4A104gJWI9TsLIMqhRANCAAT/X7ccFVJt2/6Ps1oCt2AzKhIAz"
-          + "jfJHJ3Op2DVPGh1LMD3oOPgxzUSIqujHG6dq9Ui93Eacl4VWJPMW/MVHHIL";
-
-  /**
-   * Sample Google private signing key for the ECv2SigningOnly protocolVersion.
-   *
-   * <p>Corresponds to ECv2SigningOnly private key of the key in {@link
-   * #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgRi9hSdY+knJ08odnY"
-          + "tZFMRi7ZYeMoasAijLhD4GiQ1yhRANCAAQ1e9irEPlZrIpiG5dB690Z0sDy"
-          + "J9XguxFLa4q8WjzDDKLCBAcH0OIrMbpP8/UW70GXvNbL+x7kPLjCTWYmg+7G";
-
-  /**
-   * Sample Google intermediate public signing key for the ECv2SigningOnly protocolVersion.
-   *
-   * <p>Base64 version of the public key encoded in ASN.1 type SubjectPublicKeyInfo defined in the
-   * X.509 standard.
-   *
-   * <p>The intermediate public key will be signed by {@link
-   * #GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64}.
-   */
-  private static final String
-      GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64 =
-          "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8OaurwvbyYm8JWDgFPRTIDg0/"
-              + "kcQTFAQ4txi5IP0AyM1QiagwRhDUfjpqZkpw8xt/DXwyWYM0DdHqoeV"
-              + "TKqmYQ==";
-
-  /**
-   * Sample Google intermediate private signing key for the ECv2SigningOnly protocolVersion.
-   *
-   * <p>Corresponds to private key of the key in {@link
-   * #GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64}.
-   */
-  private static final String
-      GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64 =
-          "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+Jvpkq26tpZ0s"
-              + "TTZVh4teEI41SnJdmkBzM8VZ5ZirE2hRANCAATw5q6vC9vJibwlYOAU"
-              + "9FMgODT+RxBMUBDi3GLkg/QDIzVCJqDBGENR+OmpmSnDzG38NfDJZgz"
-              + "QN0eqh5VMqqZh";
-
-  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} using the ECv1
-   * protocolVersion.
-   */
-  private static final String CIPHERTEXT_EC_V1 =
-      "{"
-          + "\"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==";
-
-  private static final class MyPaymentMethodTokenRecipientKem
-      implements PaymentMethodTokenRecipientKem {
-    private final ECPrivateKey privateKey;
-
-    public MyPaymentMethodTokenRecipientKem(String recipientPrivateKey)
-        throws GeneralSecurityException {
-      privateKey = PaymentMethodTokenUtil.pkcs8EcPrivateKey(recipientPrivateKey);
-    }
-
-    @Override
-    public byte[] computeSharedSecret(final byte[] ephemeralPublicKey)
-        throws GeneralSecurityException {
-      ECPublicKey publicKey =
-          EllipticCurves.getEcPublicKey(
-              privateKey.getParams(),
-              PaymentMethodTokenConstants.UNCOMPRESSED_POINT_FORMAT,
-              ephemeralPublicKey);
-      return EllipticCurves.computeSharedSecret(privateKey, publicKey);
-    }
-  }
-
-  @Test
-  public void testShouldDecryptECV1() 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_EC_V1));
-  }
-
-  @Test
-  public void testShouldDecryptECV1WithNonStrictJsonEncoding() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-  String ciphertextEcV1WithNonStrictJsonEncoding =
-      "{"
-          + "# comment \n"   // python-style comment terminated with new line
-          + "protocolVersion:'ECv1',"   // protocolVersion has no quotes, ECv1 has single quotes
-          + "/* a comment */"   // c-style comment
-          + "\"signedMessage\"="  // use = instead of :
-          + "// another comment \n"   // c-style comment terminated with new line
-          + ("\"{"
-              + "\\\"tag\\\":\\\"ZVwlJt7dU8Plk0+r8rPF8DmPTvDiOA1UAoNjDV+SqDE\\\\u003d\\\","
-              + "\\\"ephemeralPublicKey\\\":\\\"BPhVspn70Zj2Kkgu9t8+ApEuUWsI/zos5whGCQBlgOkuYagOis7"
-              + "qsrcbQrcprjvTZO3XOU+Qbcc28FSgsRtcgQE\\\\u003d\\\","
-              + "\\\"encryptedMessage\\\":\\\"12jUObueVTdy\\\"}\";")  // ; instead of ,
-          + "\"signature\":\"MEQCIDxBoUCoFRGReLdZ/cABlSSRIKoOEFoU3e27c14vMZtfAiBtX3pGMEpnw6mSAbnagC"
-          + "CgHlCk3NcFwWYEyxIE6KGZVA\\u003d\\u003d\"}";
-
-    assertEquals(PLAINTEXT, recipient.unseal(ciphertextEcV1WithNonStrictJsonEncoding));
-  }
-
-  @Test
-  public void testShouldDecryptECV1WhenUsingCustomKem() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientKem(
-                new MyPaymentMethodTokenRecipientKem(MERCHANT_PRIVATE_KEY_PKCS8_BASE64))
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(CIPHERTEXT_EC_V1));
-  }
-
-  @Test
-  public void testShouldDecryptECV1WhenFetchingSenderVerifyingKeys() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .fetchSenderVerifyingKeysWith(
-                new GooglePaymentsPublicKeysManager.Builder()
-                    .setHttpTransport(
-                        new MockHttpTransport.Builder()
-                            .setLowLevelHttpResponse(
-                                new MockLowLevelHttpResponse()
-                                    .setContent(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON))
-                            .build())
-                    .build())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(CIPHERTEXT_EC_V1));
-  }
-
-  @Test
-  public void testShouldTryAllKeysToDecryptECV1() 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_EC_V1));
-  }
-
-  @Test
-  public void testShouldTryAllCustomKemsToDecryptECV1() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientKem(
-                new MyPaymentMethodTokenRecipientKem(ALTERNATE_MERCHANT_PRIVATE_KEY_PKCS8_BASE64))
-            .addRecipientKem(
-                new MyPaymentMethodTokenRecipientKem(MERCHANT_PRIVATE_KEY_PKCS8_BASE64))
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(CIPHERTEXT_EC_V1));
-  }
-
-  @Test
-  public void testShouldFailIfDecryptingWithDifferentKeyECV1() 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_EC_V1);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot decrypt", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfDecryptingWithDifferentCustomKemECV1() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientKem(
-                new MyPaymentMethodTokenRecipientKem(ALTERNATE_MERCHANT_PRIVATE_KEY_PKCS8_BASE64))
-            .build();
-
-    try {
-      recipient.unseal(CIPHERTEXT_EC_V1);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot decrypt", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfVerifyingWithDifferentKeyECV1() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    trustedKeysJson
-        .get("keys")
-        .getAsJsonArray()
-        .get(INDEX_OF_GOOGLE_SIGNING_EC_V1)
-        .getAsJsonObject()
-        .addProperty("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_EC_V1);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldTryAllKeysToVerifySignatureECV1() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject correctKey = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject();
-    JsonObject wrongKey = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject().deepCopy();
-    wrongKey.addProperty("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY);
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(wrongKey);
-    newKeys.add(correctKey);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(CIPHERTEXT_EC_V1));
-  }
-
-  @Test
-  public void testShouldFailIfSignedECV1WithKeyForWrongProtocolVersion() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject correctKeyButWrongProtocol =
-        keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject();
-    correctKeyButWrongProtocol.addProperty(
-        PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, "ECv2");
-    JsonObject wrongKeyButRightProtocol = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject();
-    wrongKeyButRightProtocol.addProperty("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY);
-    wrongKeyButRightProtocol.addProperty(
-        PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY,
-        PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1);
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(correctKeyButWrongProtocol);
-    newKeys.add(wrongKeyButRightProtocol);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(CIPHERTEXT_EC_V1);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfNoSigningKeysForProtocolVersion() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key1 = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject();
-    key1.addProperty(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, "ECv2");
-    JsonObject key2 = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject();
-
-    key2.addProperty("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY);
-    key2.addProperty(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, "ECv3");
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key1);
-    newKeys.add(key2);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(CIPHERTEXT_EC_V1);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("no trusted keys are available for this protocol version", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfSignedMessageWasChangedInECV1() 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 = JsonParser.parseString(CIPHERTEXT_EC_V1).getAsJsonObject();
-    payload.addProperty(
-        PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY,
-        payload.get(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY).getAsString() + " ");
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfWrongRecipientInECV1() 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_EC_V1);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfECV1SetsWrongProtocolVersion() 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 = JsonParser.parseString(CIPHERTEXT_EC_V1).getAsJsonObject();
-    String invalidVersion = "ECv2";
-    payload.addProperty(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 = JsonParser.parseString(CIPHERTEXT_EC_V1).getAsJsonObject();
-    payload.addProperty(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, 1);
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      // expected
-    }
-  }
-
-  @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 = JsonParser.parseString(CIPHERTEXT_EC_V1).getAsJsonObject();
-    payload.addProperty(PaymentMethodTokenConstants.JSON_PROTOCOL_VERSION_KEY, 1.1);
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      // expected
-    }
-  }
-
-  @Test
-  public void testShouldSucceedIfMessageIsNotExpired() throws Exception {
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .senderSigningKey(GOOGLE_SIGNING_EC_V1_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();
-
-    JsonObject plaintext = new JsonObject();
-    plaintext.addProperty(
-        "messageExpiration",
-        // One day in the future
-        String.valueOf(Instant.now().plus(Duration.standardDays(1)).getMillis()));
-    plaintext.addProperty("someKey", "someValue");
-    String ciphertext = sender.seal(plaintext.toString());
-    JsonObject decrypted = JsonParser.parseString(recipient.unseal(ciphertext)).getAsJsonObject();
-
-    assertEquals("someValue", decrypted.get("someKey").getAsString());
-  }
-
-  @Test
-  public void testShouldFailIfMessageIsExpired() throws Exception {
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .senderSigningKey(GOOGLE_SIGNING_EC_V1_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();
-
-    JsonObject expired = new JsonObject();
-    expired.addProperty(
-        "messageExpiration",
-        // One day in the past
-        String.valueOf(Instant.now().minus(Duration.standardDays(1)).getMillis()));
-
-    String ciphertext = sender.seal(expired.toString());
-    try {
-      recipient.unseal(ciphertext);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("expired payload", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfTrustedKeyIsExpiredInECV1() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key1 = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject();
-    key1.addProperty(
-        "keyExpiration", // One day in the past
-        String.valueOf(Instant.now().minus(Duration.standardDays(1)).getMillis()));
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key1);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(CIPHERTEXT_EC_V1);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("no trusted keys are available for this protocol version", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldSucceedIfKeyExpirationIsMissingInTrustedKeyIsExpiredForECV1()
-      throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key1 = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V1).getAsJsonObject();
-    key1.remove("keyExpiration");
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key1);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(CIPHERTEXT_EC_V1));
-  }
-
-  @Test
-  public void testUnsealECV2() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(sealECV2(PLAINTEXT)));
-  }
-
-  @Test
-  public void testUnsealECV2WithCustomKem() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientKem(
-                new MyPaymentMethodTokenRecipientKem(MERCHANT_PRIVATE_KEY_PKCS8_BASE64))
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(sealECV2(PLAINTEXT)));
-  }
-
-  @Test
-  public void testShouldFailIfSignedMessageWasChangedInECV2() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-    JsonObject payload = JsonParser.parseString(sealECV2(PLAINTEXT)).getAsJsonObject();
-    payload.addProperty(
-        PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY,
-        payload.get(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY).getAsString() + " ");
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfECV2UseWrongSenderId() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .senderId("not-" + PaymentMethodTokenConstants.GOOGLE_SENDER_ID)
-            .build();
-
-    try {
-      recipient.unseal(sealECV2(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfVerifyingWithDifferentKeyECV2() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key1 = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2).getAsJsonObject();
-    key1.addProperty("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY);
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key1);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(sealECV2(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfTrustedKeyIsExpiredInECV2() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key1 = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2).getAsJsonObject();
-    key1.addProperty(
-        "keyExpiration", // One day in the past
-        String.valueOf(Instant.now().minus(Duration.standardDays(1)).getMillis()));
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key1);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(sealECV2(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("no trusted keys are available for this protocol version", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfKeyExpirationIsMissingInTrustedKeyECV2() throws Exception {
-    // Key expiration is required for V2
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key1 = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2).getAsJsonObject();
-    key1.remove("keyExpiration");
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key1);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(sealECV2(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("no trusted keys are available for this protocol version", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldTryAllKeysToVerifySignatureECV2() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject correctKey = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2).getAsJsonObject();
-    JsonObject wrongKey = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2).getAsJsonObject().deepCopy();
-    wrongKey.addProperty("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY);
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(wrongKey);
-    newKeys.add(correctKey);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(sealECV2(PLAINTEXT)));
-  }
-
-  @Test
-  public void testShouldFailIfSignedKeyWasChangedInECV2() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    JsonObject payload = JsonParser.parseString(sealECV2(PLAINTEXT)).getAsJsonObject();
-    JsonObject intermediateSigningKey = payload.get("intermediateSigningKey").getAsJsonObject();
-    intermediateSigningKey.addProperty(
-        "signedKey", intermediateSigningKey.get("signedKey").getAsString() + " ");
-    payload.add("intermediateSigningKey", intermediateSigningKey);
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSignatureForSignedKeyIsIncorrectInECV2() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-    JsonObject payload = JsonParser.parseString(sealECV2(PLAINTEXT)).getAsJsonObject();
-    JsonObject intermediateSigningKey = payload.get("intermediateSigningKey").getAsJsonObject();
-    JsonArray signatures = intermediateSigningKey.get("signatures").getAsJsonArray();
-    String correctSignature = signatures.get(0).getAsString();
-    byte[] wrongSignatureBytes = Base64.decode(correctSignature);
-    wrongSignatureBytes[0] = (byte) ~wrongSignatureBytes[0];
-    JsonArray newSignatures = new JsonArray();
-    newSignatures.add(Base64.encode(wrongSignatureBytes));
-    intermediateSigningKey.add("signatures", newSignatures);
-    payload.add("intermediateSigningKey", intermediateSigningKey);
-
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldTryVerifyingAllSignaturesForSignedKeyInECV2() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-    JsonObject payload = JsonParser.parseString(sealECV2(PLAINTEXT)).getAsJsonObject();
-    JsonObject intermediateSigningKey = payload.get("intermediateSigningKey").getAsJsonObject();
-    JsonArray signatures = intermediateSigningKey.get("signatures").getAsJsonArray();
-    String correctSignature = signatures.get(0).getAsString();
-    byte[] wrongSignatureBytes = Base64.decode(correctSignature);
-    wrongSignatureBytes[0] = (byte) ~wrongSignatureBytes[0];
-    JsonArray newSignatures = new JsonArray();
-    newSignatures.add(Base64.encode(wrongSignatureBytes));
-    newSignatures.add(correctSignature);
-    intermediateSigningKey.add("signatures", newSignatures);
-    payload.add("intermediateSigningKey", intermediateSigningKey);
-
-    assertEquals(PLAINTEXT, recipient.unseal(sealECV2(PLAINTEXT)));
-  }
-
-  @Test
-  public void testShouldThrowIfECV2UseWrongRecipientId() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId("not" + RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(sealECV2(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldAcceptNonExpiredECV2Message() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    JsonObject payload = new JsonObject();
-    payload.addProperty(
-        "messageExpiration",
-        // One day in the future
-        String.valueOf(Instant.now().plus(Duration.standardDays(1)).getMillis()));
-    String plaintext = payload.toString();
-    assertEquals(plaintext, recipient.unseal(sealECV2(plaintext)));
-  }
-
-  @Test
-  public void testShouldFailIfECV2MessageIsExpired() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    JsonObject payload = new JsonObject();
-    payload.addProperty(
-        "messageExpiration",
-        // One day in the past
-        String.valueOf(Instant.now().minus(Duration.standardDays(1)).getMillis()));
-
-    String ciphertext = sealECV2(payload.toString());
-    try {
-      recipient.unseal(ciphertext);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("expired payload", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfIntermediateSigningKeyIsExpiredInECV2() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderIntermediateSigningKey(
-                GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-            .senderIntermediateCert(
-                new SenderIntermediateCertFactory.Builder()
-                    .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-                    .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-                    .senderIntermediateSigningKey(
-                        GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-                    // Expiration date in the past.
-                    .expiration(Instant.now().minus(Duration.standardDays(1)).getMillis())
-                    .build()
-                    .create())
-            .recipientId(RECIPIENT_ID)
-            .rawUncompressedRecipientPublicKey(MERCHANT_PUBLIC_KEY_BASE64)
-            .build();
-
-    try {
-      recipient.unseal(sender.seal(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("expired intermediateSigningKey", e.getMessage());
-    }
-  }
-
-  private static String sealECV2(String plaintext) throws GeneralSecurityException {
-    return new PaymentMethodTokenSender.Builder()
-        .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-        .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-        .senderIntermediateCert(
-            new SenderIntermediateCertFactory.Builder()
-                .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-                .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-                .senderIntermediateSigningKey(
-                    GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-                .expiration(Instant.now().plus(Duration.standardDays(1)).getMillis())
-                .build()
-                .create())
-        .recipientId(RECIPIENT_ID)
-        .rawUncompressedRecipientPublicKey(MERCHANT_PUBLIC_KEY_BASE64)
-        .build()
-        .seal(plaintext);
-  }
-
-  @Test
-  public void testVerifyECV2SigningOnly() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(signECV2SigningOnly(PLAINTEXT)));
-  }
-
-  @Test
-  public void testShouldFailIfSignedMessageWasChangedInECV2SigningOnly() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-    JsonObject payload = JsonParser.parseString(signECV2SigningOnly(PLAINTEXT)).getAsJsonObject();
-    payload.addProperty(
-        PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY,
-        payload.get(PaymentMethodTokenConstants.JSON_SIGNED_MESSAGE_KEY).getAsString() + " ");
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfECV2SigningOnlyUseWrongSenderId() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .senderId("not-" + PaymentMethodTokenConstants.GOOGLE_SENDER_ID)
-            .build();
-
-    try {
-      recipient.unseal(signECV2SigningOnly(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfVerifyingWithDifferentKeyECV2SigningOnly() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2_SIGNING_ONLY).getAsJsonObject();
-    key.addProperty("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY);
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    try {
-      recipient.unseal(signECV2SigningOnly(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfTrustedKeyIsExpiredInECV2SigningOnly() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2_SIGNING_ONLY).getAsJsonObject();
-    key.addProperty(
-        "keyExpiration", // One day in the past
-        String.valueOf(Instant.now().minus(Duration.standardDays(1)).getMillis()));
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    try {
-      recipient.unseal(signECV2SigningOnly(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("no trusted keys are available for this protocol version", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfKeyExpirationIsMissingInTrustedKeyECV2SigningOnly() throws Exception {
-    // Key expiration is required for ECv2SigningOnly
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject key = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2_SIGNING_ONLY).getAsJsonObject();
-    key.remove("keyExpiration");
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(key);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    try {
-      recipient.unseal(signECV2SigningOnly(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("no trusted keys are available for this protocol version", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldTryAllKeysToVerifySignatureECV2SigningOnly() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    JsonArray keys = trustedKeysJson.get("keys").getAsJsonArray();
-    JsonObject correctKey = keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2_SIGNING_ONLY).getAsJsonObject();
-    JsonObject wrongKey =
-        keys.get(INDEX_OF_GOOGLE_SIGNING_EC_V2_SIGNING_ONLY).getAsJsonObject().deepCopy();
-    wrongKey.addProperty("keyValue", ALTERNATE_PUBLIC_SIGNING_KEY);
-    JsonArray newKeys = new JsonArray();
-    newKeys.add(wrongKey);
-    newKeys.add(correctKey);
-    trustedKeysJson.add("keys", newKeys);
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(trustedKeysJson.toString())
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    assertEquals(PLAINTEXT, recipient.unseal(signECV2SigningOnly(PLAINTEXT)));
-  }
-
-  @Test
-  public void testShouldFailIfSignedKeyWasChangedInECV2SigningOnly() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    JsonObject payload = JsonParser.parseString(signECV2SigningOnly(PLAINTEXT)).getAsJsonObject();
-    JsonObject intermediateSigningKey = payload.get("intermediateSigningKey").getAsJsonObject();
-    intermediateSigningKey.addProperty(
-        "signedKey", intermediateSigningKey.get("signedKey").getAsString() + " ");
-    payload.add("intermediateSigningKey", intermediateSigningKey);
-
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSignatureForSignedKeyIsIncorrectInECV2SigningOnly()
-      throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-    JsonObject payload = JsonParser.parseString(signECV2SigningOnly(PLAINTEXT)).getAsJsonObject();
-    JsonArray signatures =
-        payload.get("intermediateSigningKey").getAsJsonObject().get("signatures").getAsJsonArray();
-    String correctSignature = signatures.get(0).getAsString();
-    byte[] wrongSignatureBytes = Base64.decode(correctSignature);
-    wrongSignatureBytes[0] = (byte) ~wrongSignatureBytes[0];
-    JsonArray newSignatures = new JsonArray();
-    newSignatures.add(Base64.encode(wrongSignatureBytes));
-    JsonObject intermediateSigningKey = payload.get("intermediateSigningKey").getAsJsonObject();
-    intermediateSigningKey.add("signatures", newSignatures);
-    payload.add("intermediateSigningKey", intermediateSigningKey);
-
-    try {
-      recipient.unseal(payload.toString());
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldTryVerifyingAllSignaturesForSignedKeyInECV2SigningOnly() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-    JsonObject payload = JsonParser.parseString(signECV2SigningOnly(PLAINTEXT)).getAsJsonObject();
-    JsonArray signatures =
-        payload.get("intermediateSigningKey").getAsJsonObject().get("signatures").getAsJsonArray();
-    String correctSignature = signatures.get(0).getAsString();
-    byte[] wrongSignatureBytes = Base64.decode(correctSignature);
-    wrongSignatureBytes[0] = (byte) ~wrongSignatureBytes[0];
-    JsonArray newSignatures = new JsonArray();
-    newSignatures.add(Base64.encode(wrongSignatureBytes));
-    newSignatures.add(correctSignature);
-    JsonObject intermediateSigningKey = payload.get("intermediateSigningKey").getAsJsonObject();
-    intermediateSigningKey.add("signatures", newSignatures);
-    payload.add("intermediateSigningKey", intermediateSigningKey);
-
-    assertEquals(PLAINTEXT, recipient.unseal(signECV2SigningOnly(PLAINTEXT)));
-  }
-
-  @Test
-  public void testShouldThrowIfECV2SigningOnlyUseWrongRecipientId() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId("not" + RECIPIENT_ID)
-            .build();
-
-    try {
-      recipient.unseal(signECV2SigningOnly(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("cannot verify signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldAcceptNonExpiredECV2SigningOnlyMessage() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    JsonObject payload = new JsonObject();
-    payload.addProperty(
-        "messageExpiration",
-        // One day in the future
-        String.valueOf(Instant.now().plus(Duration.standardDays(1)).getMillis()));
-    String plaintext = payload.toString();
-    assertEquals(plaintext, recipient.unseal(signECV2SigningOnly(plaintext)));
-  }
-
-  @Test
-  public void testShouldFailIfECV2SigningOnlyMessageIsExpired() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    JsonObject payload = new JsonObject();
-    payload.addProperty(
-        "messageExpiration",
-        // One day in the past
-        String.valueOf(Instant.now().minus(Duration.standardDays(1)).getMillis()));
-    String ciphertext = signECV2SigningOnly(payload.toString());
-    try {
-      recipient.unseal(ciphertext);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("expired payload", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfIntermediateSigningKeyIsExpiredInECV2SigningOnly() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderIntermediateSigningKey(
-                GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-            .senderIntermediateCert(
-                new SenderIntermediateCertFactory.Builder()
-                    .protocolVersion(
-                        PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-                    .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64)
-                    .senderIntermediateSigningKey(
-                        GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-                    // Expiration date in the past.
-                    .expiration(Instant.now().minus(Duration.standardDays(1)).getMillis())
-                    .build()
-                    .create())
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    try {
-      recipient.unseal(sender.seal(PLAINTEXT));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("expired intermediateSigningKey", e.getMessage());
-    }
-  }
-
-  private static String signECV2SigningOnly(String plaintext) throws GeneralSecurityException {
-    return new PaymentMethodTokenSender.Builder()
-        .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-        .senderIntermediateSigningKey(
-            GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-        .senderIntermediateCert(
-            new SenderIntermediateCertFactory.Builder()
-                .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-                .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64)
-                .senderIntermediateSigningKey(
-                    GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-                .expiration(Instant.now().plus(Duration.standardDays(1)).getMillis())
-                .build()
-                .create())
-        .recipientId(RECIPIENT_ID)
-        .build()
-        .seal(plaintext);
-  }
-}
diff --git a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenSenderTest.java b/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenSenderTest.java
deleted file mode 100644
index 5ca98a0..0000000
--- a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/PaymentMethodTokenSenderTest.java
+++ /dev/null
@@ -1,577 +0,0 @@
-// 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.crypto.tink.apps.paymentmethodtoken;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-import com.google.crypto.tink.subtle.EllipticCurves;
-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 org.joda.time.Duration;
-import org.joda.time.Instant;
-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";
-
-  /** Sample Google provided JSON with its public signing keys. */
-  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"
-          + "      \"keyValue\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM"
-          + "43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw==\",\n"
-          + "      \"keyExpiration\": \""
-          + Instant.now().plus(Duration.standardDays(1)).getMillis()
-          + "\",\n"
-          + "      \"protocolVersion\": \"ECv2\"\n"
-          + "    },\n"
-          + "    {\n"
-          + "      \"keyValue\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENXvYqxD5WayKYhuXQevdGdLA8i"
-          + "fV4LsRS2uKvFo8wwyiwgQHB9DiKzG6T/P1Fu9Bl7zWy/se5Dy4wk1mJoPuxg==\",\n"
-          + "      \"keyExpiration\": \""
-          + Instant.now().plus(Duration.standardDays(1)).getMillis()
-          + "\",\n"
-          + "      \"protocolVersion\": \"ECv2SigningOnly\"\n"
-          + "    }\n"
-          + "  ]\n"
-          + "}";
-
-  /**
-   * Sample Google private signing key for the ECv1 protocolVersion.
-   *
-   * <p>Corresponds to the ECv1 private key of the key in {@link
-   * #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V1_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZj/Dldxz8fvKVF5O"
-          + "TeAtK6tY3G1McmvhMppe6ayW6GahRANCAAQ9icfBLy56BYB7BC2XGLOYsXKfAdzF"
-          + "FPU8rTtwMDr8LixetUjVLNkJTHxTxLQuMytPqKt39Vbtt7czPq3+yu1F";
-
-  /**
-   * Sample Google private signing key for the ECv2 protocolVersion.
-   *
-   * <p>Corresponds to ECv2 private key of the key in {@link #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKvEdSS8f0mjTCNKev"
-          + "aKXIzfNC5b4A104gJWI9TsLIMqhRANCAAT/X7ccFVJt2/6Ps1oCt2AzKhIAz"
-          + "jfJHJ3Op2DVPGh1LMD3oOPgxzUSIqujHG6dq9Ui93Eacl4VWJPMW/MVHHIL";
-
-  /**
-   * Sample Google intermediate public signing key for the ECv2 protocolVersion.
-   *
-   * <p>Base64 version of the public key encoded in ASN.1 type SubjectPublicKeyInfo defined in the
-   * X.509 standard.
-   *
-   * <p>The intermediate public key will be signed by {@link
-   * #GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64 =
-      "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yR"
-          + "ydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw==";
-
-  /**
-   * Sample Google intermediate private signing key for the ECv2 protocolVersion.
-   *
-   * <p>Corresponds to private key of the key in {@link
-   * #GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKvEdSS8f0mjTCNKev"
-          + "aKXIzfNC5b4A104gJWI9TsLIMqhRANCAAT/X7ccFVJt2/6Ps1oCt2AzKhIAz"
-          + "jfJHJ3Op2DVPGh1LMD3oOPgxzUSIqujHG6dq9Ui93Eacl4VWJPMW/MVHHIL";
-
-  /**
-   * Sample Google private signing key for the ECV2SigningOnly protocolVersion.
-   *
-   * <p>Corresponds to ECV2SigningOnly private key of the key in {@link
-   * #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON}.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgRi9hSdY+knJ08odnY"
-          + "tZFMRi7ZYeMoasAijLhD4GiQ1yhRANCAAQ1e9irEPlZrIpiG5dB690Z0sDy"
-          + "J9XguxFLa4q8WjzDDKLCBAcH0OIrMbpP8/UW70GXvNbL+x7kPLjCTWYmg+7G";
-
-  /**
-   * Sample Google intermediate public signing key for the ECV2SigningOnly protocolVersion.
-   *
-   * <p>Base64 version of the public key encoded in ASN.1 type SubjectPublicKeyInfo defined in the
-   * X.509 standard.
-   *
-   * <p>The intermediate public key will be signed by {@link
-   * #GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64}.
-   */
-  private static final String
-      GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64 =
-          "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8OaurwvbyYm8JWDgFPRTIDg0/"
-              + "kcQTFAQ4txi5IP0AyM1QiagwRhDUfjpqZkpw8xt/DXwyWYM0DdHqoeV"
-              + "TKqmYQ==";
-
-  /**
-   * Sample Google intermediate private signing key for the ECV2SigningOnly protocolVersion.
-   *
-   * <p>Corresponds to private key of the key in {@link
-   * #GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64}.
-   */
-  private static final String
-      GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64 =
-          "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+Jvpkq26tpZ0s"
-              + "TTZVh4teEI41SnJdmkBzM8VZ5ZirE2hRANCAATw5q6vC9vJibwlYOAU"
-              + "9FMgODT+RxBMUBDi3GLkg/QDIzVCJqDBGENR+OmpmSnDzG38NfDJZgz"
-              + "QN0eqh5VMqqZh";
-
-  private static final String RECIPIENT_ID = "someRecipient";
-
-  @Test
-  public void testECV1WithPrecomputedKeys() throws Exception {
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .senderSigningKey(GOOGLE_SIGNING_EC_V1_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 testECV1WithTestdataSeal() throws Exception {
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .addRecipientPrivateKey(MERCHANT_PRIVATE_KEY_PKCS8_BASE64)
-            .build();
-
-    // Seal where "=" has been escaped by \u003d.
-    // - tag is "bkd9lJMV/8Na34/9ZtZmDmgrZcxJ71ALhT/KpraqzR8=", = gets escapced by \u003d, so
-    //   signedMessage contains "tag": "bkd9lJMV/8Na34/9ZtZmDmgrZcxJ71ALhT/KpraqzR8\u003d"
-    // - this gets escaped in the 2nd encoding with \ -> \\ and " -> \". So the final seal
-    //   contains \"tag\":\"bkd9lJMV/8Na34/9ZtZmDmgrZcxJ71ALhT/KpraqzR8\\u003d\"
-    // Note that to encode this string in Java, we have to escape \ -> \\ and " -> \" again.
-    String testdataSeal =
-        "{\"signedMessage\":\"{\\\"tag\\\":\\\"bkd9lJMV/8Na34/9ZtZmDmgrZcxJ71ALhT/KpraqzR8\\\\u003d"
-            + "\\\",\\\"ephemeralPublicKey\\\":\\\"BLyk1Iwcx+yRQbCSsZgAwb8s/ChNDTlcCovgL5/37qDdia3x"
-            + "PM5GUb+HmbW9bF/c12p0ySxtU/MDcNkJZ0nWCVs\\\\u003d\\\",\\\"encryptedMessage\\\":\\\"ue"
-            + "63Gg\\\\u003d\\\\u003d\\\"}\",\"protocolVersion\":\"ECv1\",\"signature\":\"MEQCIFFdW"
-            + "ve35+jh7CT9QC9W6Leqx32P41oNxG2NDm6PaY1fAiAKHvklWHDPiLKYPb0zyXRGIl6GYvfQ3LAl1z5sR3CbS"
-            + "w\\u003d\\u003d\"}";
-
-    // Seal where "=" has not been escaped, but " and \ have to be escaped as before.
-    String testdataSeal2 =
-        "{\"signedMessage\":\"{\\\"encryptedMessage\\\":\\\"Ih3e7g==\\\",\\\"tag\\\":\\\"GlZ332kL5u"
-            + "ZICWrCsSSQ6KrFFqQyKI84SH2Wh6UTv8c=\\\",\\\"ephemeralPublicKey\\\":\\\"BBb/VaSEYJphhs"
-            + "ma1X24QrjdFr/DHo7IDu8owi6NR0tW9p5F9jn6wxu5by5Rs/bYkVdr8HNT9+MEpBOnL7Si/dQ=\\\"}\",\""
-            + "protocolVersion\":\"ECv1\",\"signature\":\"MEYCIQD6rOV4l9pm/MDr2jPkqnU2GSHbbnUaLYQow"
-            + "/0Y+S1axAIhAKUUO7N9eoBNuXySDDWDYrdu7r0IHxeeFtkubDxhplYU\"}";
-
-    // Seal where "=" has only been escaped in the outer encoding:
-    // - tag is "Jv2KH2lCVyNgGcQMbWSMf+N8RQCgTfkRq3J3f9qrBKE=", so
-    //   signedMessage contains "tag": "Jv2KH2lCVyNgGcQMbWSMf+N8RQCgTfkRq3J3f9qrBKE="
-    // - this gets escaped in the 2nd encoding with \ -> \\, " -> \" and = -> \u003d. So the final
-    //   seal contains \"tag\":\"Jv2KH2lCVyNgGcQMbWSMf+N8RQCgTfkRq3J3f9qrBKE\u003d\"
-    // Note that to encode this string in Java, we have to escape \ -> \\ and " -> \" again.
-    String testdataSeal3 =
-        "{\"signedMessage\":\"{\\\"encryptedMessage\\\":\\\"eTBsng\\u003d\\u003d\\\",\\\"tag\\\":\\"
-            + "\"Jv2KH2lCVyNgGcQMbWSMf+N8RQCgTfkRq3J3f9qrBKE\\u003d\\\",\\\"ephemeralPublicKey\\\":"
-            + "\\\"BGPS6h2ddaOA+H71e28xZ2OQZBJuVrCK3qUXc6G6ykHTr8ab9pmS7B87n9jg8qwYAWpFcRNgC2fxnrPL"
-            + "+p2Gk5U\\u003d\\\"}\",\"protocolVersion\":\"ECv1\",\"signature\":\"MEUCIQC+8NTo1xbem"
-            + "5lPhXuEwcYc0W03jdj3Q1YNI4XvoVAbuAIgT5WXy1wZ2GNXoaLauNGo0iHeOjsZT3wYUziZPHkSAlk\\u003"
-            + "d\"}";
-
-    // Weird token where for no reason the characters { -> \u007b and s -> \u0073 have been escaped.
-    String testdataSeal4 =
-        "{\"\\u0073ignedMe\\u0073\\u0073age\":\"\\u007b\\\"encryptedMe\\\\u0073\\\\u0073age\\\":\\"
-            + "\"tfAPcA\\\\u003d\\\\u003d\\\",\\\"tag\\\":\\\"bB72KLdziA4Oca77w7g7a4fbKpgFXVrtUr56W"
-            + "NpIL6M\\\\u003d\\\",\\\"ephemeralPublicKey\\\":\\\"BKYBrgWkOP7gqCcIqRjT1t0HjMdbEIMeP"
-            + "82VtfC7IaCNbHgL9vojnQ/Yxy8Kutn+MCvG3gRdTxGN+Pwl+1QjM6w\\\\u003d\\\"}\",\"protocolVer"
-            + "\\u0073ion\":\"ECv1\",\"\\u0073ignature\":\"MEUCIHDW7JS51iy1LeyiNri7tNqe0HYdPwmDoi/M"
-            + "j2RD+3f1AiEA9FBSpqDhQzEvn849IrxXWQlIkuJdUfLE1NxCBQ8mCj0\\u003d\"}";
-
-    String plaintext = "blah";
-    assertEquals(plaintext, recipient.unseal(testdataSeal));
-    assertEquals(plaintext, recipient.unseal(testdataSeal2));
-    assertEquals(plaintext, recipient.unseal(testdataSeal3));
-    assertEquals(plaintext, recipient.unseal(testdataSeal4));
-  }
-
-  @Test
-  public void testECV2WithPrecomputedKeys() throws Exception {
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderIntermediateSigningKey(
-                GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-            .senderIntermediateCert(
-                new SenderIntermediateCertFactory.Builder()
-                    .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-                    .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-                    .senderIntermediateSigningKey(
-                        GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-                    .expiration(Instant.now().plus(Duration.standardDays(1)).getMillis())
-                    .build()
-                    .create())
-            .recipientId(RECIPIENT_ID)
-            .rawUncompressedRecipientPublicKey(MERCHANT_PUBLIC_KEY_BASE64)
-            .build();
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .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 testECV2SigningOnlyWithPrecomputedKeys() throws Exception {
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderIntermediateSigningKey(
-                GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-            .senderIntermediateCert(
-                new SenderIntermediateCertFactory.Builder()
-                    .protocolVersion(
-                        PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-                    .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64)
-                    .senderIntermediateSigningKey(
-                        GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-                    .expiration(Instant.now().plus(Duration.standardDays(1)).getMillis())
-                    .build()
-                    .create())
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    PaymentMethodTokenRecipient recipient =
-        new PaymentMethodTokenRecipient.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-            .senderVerifyingKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .recipientId(RECIPIENT_ID)
-            .build();
-
-    String plaintext = "blah";
-    assertEquals(plaintext, recipient.unseal(sender.seal(plaintext)));
-  }
-
-  @Test
-  public void testShouldThrowWithUnsupportedProtocolVersion() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder().protocolVersion("ECv99").build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals("invalid version: ECv99", expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSignedIntermediateSigningKeyIsSetForECV1() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)
-          .senderSigningKey(GOOGLE_SIGNING_EC_V1_PRIVATE_KEY_PKCS8_BASE64)
-          .senderIntermediateCert(newSignedIntermediateSigningKey())
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must not set signed sender's intermediate signing key using "
-              + "Builder.senderIntermediateCert",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfIntermediateSigningKeyIsSetForECV1() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)
-          .senderSigningKey(GOOGLE_SIGNING_EC_V1_PRIVATE_KEY_PKCS8_BASE64)
-          .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must not set sender's intermediate signing key using "
-              + "Builder.senderIntermediateSigningKey",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSenderSigningKeyIsSetForECV2() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-          .senderSigningKey(GOOGLE_SIGNING_EC_V1_PRIVATE_KEY_PKCS8_BASE64)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must not set sender's signing key using Builder.senderSigningKey",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSignedIntermediateSigningKeyIsNotSetForECV2() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-          // no calls to senderIntermediateCert
-          .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must set signed sender's intermediate signing key using "
-              + "Builder.senderIntermediateCert",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfIntermediateSigningKeyIsNotSetForECV2() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-          // no calls to senderIntermediateSigningKey
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must set sender's intermediate signing key using Builder.senderIntermediateSigningKey",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSenderSigningKeyIsNotSetForECV1() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)
-          // no calls to senderSigningKey
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must set sender's signing key using Builder.senderSigningKey", expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSenderSigningKeyIsSetForECV2SigningOnly() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-          .senderSigningKey(GOOGLE_SIGNING_EC_V1_PRIVATE_KEY_PKCS8_BASE64)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must not set sender's signing key using Builder.senderSigningKey",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfSignedIntermediateSigningKeyIsNotSetForECV2SigningOnly()
-      throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-          // no calls to senderIntermediateCert
-          .senderIntermediateSigningKey(
-              GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must set signed sender's intermediate signing key using "
-              + "Builder.senderIntermediateCert",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfRecipientPublicKeyIsSetForECV2SigningOnly() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-          // no calls to senderIntermediateCert
-          .senderIntermediateSigningKey(
-              GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PRIVATE_KEY_PKCS8_BASE64)
-          .rawUncompressedRecipientPublicKey(MERCHANT_PUBLIC_KEY_BASE64)
-          .senderIntermediateCert(
-              new SenderIntermediateCertFactory.Builder()
-                  .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-                  .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_PRIVATE_KEY_PKCS8_BASE64)
-                  .senderIntermediateSigningKey(
-                      GOOGLE_SIGNING_EC_V2_SIGNING_ONLY_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-                  .expiration(Instant.now().plus(Duration.standardDays(1)).getMillis())
-                  .build()
-                  .create())
-          .recipientId(RECIPIENT_ID)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must not set recipient's public key using Builder.recipientPublicKey",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldThrowIfIntermediateSigningKeyIsNotSetForECV2SigningOnly() throws Exception {
-    try {
-      new PaymentMethodTokenSender.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-          // no calls to senderIntermediateSigningKey
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must set sender's intermediate signing key using Builder.senderIntermediateSigningKey",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testSendReceive() throws Exception {
-    ECParameterSpec spec = EllipticCurves.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)));
-  }
-
-  @Test
-  public void testWithKeysGeneratedByRecipientKeyGen() throws Exception {
-    ECParameterSpec spec = EllipticCurves.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();
-
-    // The keys here are generated by PaymentMethodTokenRecipientKeyGen.
-    String recipientPrivateKey =
-        "MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCAP5/1502pXYdMion22yiWK"
-            + "GoTBJN/wAAfdjBU6puyEMw==";
-    String recipientPublicKey =
-        "BJ995jnw2Ppn4BMP/ZKtlTOOIBQC+/L3PDcFRjowZuCkRqUZ/kGWE8c+zimZNHOZPzLB"
-            + "NVGJ3V8M/fM4g4o02Mc=";
-
-    PaymentMethodTokenSender sender =
-        new PaymentMethodTokenSender.Builder()
-            .senderId(senderId)
-            .senderSigningKey(senderPrivateKey)
-            .recipientId(recipientId)
-            .rawUncompressedRecipientPublicKey(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)));
-  }
-
-  private String newSignedIntermediateSigningKey() throws GeneralSecurityException {
-    return new SenderIntermediateCertFactory.Builder()
-        .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-        .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-        .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-        .expiration(Instant.now().plus(Duration.standardDays(1)).getMillis())
-        .build()
-        .create();
-  }
-}
diff --git a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/SenderIntermediateCertFactoryTest.java b/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/SenderIntermediateCertFactoryTest.java
deleted file mode 100644
index 566ebde..0000000
--- a/apps/paymentmethodtoken/src/test/java/com/google/crypto/tink/apps/paymentmethodtoken/SenderIntermediateCertFactoryTest.java
+++ /dev/null
@@ -1,161 +0,0 @@
-// Copyright 2018 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.crypto.tink.apps.paymentmethodtoken;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import org.joda.time.Duration;
-import org.joda.time.Instant;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests for {@link SenderIntermediateCertFactory}. */
-@RunWith(JUnit4.class)
-public class SenderIntermediateCertFactoryTest {
-
-  /**
-   * Sample Google private signing key for the ECv2 protocolVersion.
-   *
-   * <p>Base64 version of the private key encoded in PKCS8 format.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKvEdSS8f0mjTCNKev"
-          + "aKXIzfNC5b4A104gJWI9TsLIMqhRANCAAT/X7ccFVJt2/6Ps1oCt2AzKhIAz"
-          + "jfJHJ3Op2DVPGh1LMD3oOPgxzUSIqujHG6dq9Ui93Eacl4VWJPMW/MVHHIL";
-
-  /**
-   * Sample Google intermediate public signing key for the ECv2 protocolVersion.
-   *
-   * <p>Base64 version of the public key encoded in ASN.1 type SubjectPublicKeyInfo defined in the
-   * X.509 standard.
-   */
-  private static final String GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64 =
-      "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yR"
-          + "ydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw==";
-
-  @Test
-  public void shouldProduceSenderIntermediateCertJson() throws Exception {
-    String encoded =
-        new SenderIntermediateCertFactory.Builder()
-            .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-            .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-            .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-            .expiration(123456)
-            .build()
-            .create();
-
-    JsonObject decodedSignedIntermediateSigningKey =
-        JsonParser.parseString(encoded).getAsJsonObject();
-    assertTrue(decodedSignedIntermediateSigningKey.get("signedKey").isJsonPrimitive());
-    assertTrue(decodedSignedIntermediateSigningKey.get("signatures").isJsonArray());
-    assertEquals(2, decodedSignedIntermediateSigningKey.size());
-
-    JsonObject signedKey =
-        JsonParser.parseString(decodedSignedIntermediateSigningKey.get("signedKey").getAsString())
-            .getAsJsonObject();
-    assertTrue(signedKey.get("keyValue").getAsJsonPrimitive().isString());
-    assertTrue(signedKey.get("keyExpiration").getAsJsonPrimitive().isString());
-    assertEquals(2, signedKey.size());
-    assertEquals(
-        GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64,
-        signedKey.get("keyValue").getAsString());
-    assertEquals("123456", signedKey.get("keyExpiration").getAsString());
-    assertEquals(1, decodedSignedIntermediateSigningKey.get("signatures").getAsJsonArray().size());
-    assertFalse(
-        decodedSignedIntermediateSigningKey
-            .get("signatures")
-            .getAsJsonArray()
-            .get(0)
-            .getAsString()
-            .isEmpty());
-  }
-
-  @Test
-  public void shouldThrowIfExpirationNotSet() throws Exception {
-    try {
-      new SenderIntermediateCertFactory.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-          .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-          .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-          // no expiration
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals("must set expiration using Builder.expiration", expected.getMessage());
-    }
-  }
-
-  @Test
-  public void shouldThrowIfExpirationIsNegative() throws Exception {
-    try {
-      new SenderIntermediateCertFactory.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-          .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-          .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-          .expiration(-1)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals("invalid negative expiration", expected.getMessage());
-    }
-  }
-
-  @Test
-  public void shouldThrowIfNoSenderSigningKeyAdded() throws Exception {
-    try {
-      new SenderIntermediateCertFactory.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2)
-          .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-          // no call to addSenderSigningKey
-          .expiration(Instant.now().plus(Duration.standardDays(1)).getMillis())
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals(
-          "must add at least one sender's signing key using Builder.addSenderSigningKey",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void shouldSupportECV2SigningOnly() throws Exception {
-    new SenderIntermediateCertFactory.Builder()
-        .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V2_SIGNING_ONLY)
-        .senderIntermediateSigningKey(GOOGLE_SIGNING_EC_V2_INTERMEDIATE_PUBLIC_KEY_X509_BASE64)
-        .addSenderSigningKey(GOOGLE_SIGNING_EC_V2_PRIVATE_KEY_PKCS8_BASE64)
-        .expiration(123456)
-        .build();
-  }
-
-  @Test
-  public void shouldThrowIfInvalidProtocolVersionSet() throws Exception {
-    try {
-      new SenderIntermediateCertFactory.Builder()
-          .protocolVersion(PaymentMethodTokenConstants.PROTOCOL_VERSION_EC_V1)
-          .build();
-      fail("Should have thrown!");
-    } catch (IllegalArgumentException expected) {
-      assertEquals("invalid version: ECv1", expected.getMessage());
-    }
-  }
-}
diff --git a/apps/rewardedads/BUILD.bazel b/apps/rewardedads/BUILD.bazel
deleted file mode 100644
index a8f1330..0000000
--- a/apps/rewardedads/BUILD.bazel
+++ /dev/null
@@ -1,15 +0,0 @@
-load("@tink_java//tools:gen_maven_jar_rules.bzl", "gen_maven_jar_rules")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-gen_maven_jar_rules(
-    name = "maven",
-    doctitle = "Tink Cryptography API for Google Mobile Rewarded Video Ads SSV",
-    manifest_lines = [
-        "Automatic-Module-Name: com.google.crypto.tink.apps.rewardedads",
-    ],
-    root_packages = ["com.google.crypto.tink.apps.rewardedads"],
-    deps = ["//rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads:rewarded_ads_verifier"],
-)
diff --git a/apps/rewardedads/README.md b/apps/rewardedads/README.md
deleted file mode 100644
index b92e374..0000000
--- a/apps/rewardedads/README.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# An implementation of Google AdMob Rewarded Ads
-
-This app implements the verifier side of Server-Side Verification of Google
-AdMob Rewarded Ads.
-
-## Latest Release
-
-The most recent release is
-[1.7.0](https://github.com/google/tink/releases/tag/v1.7.0), released
-2022-08-09. API docs can be found
-[here](https://google.github.io/tink/javadoc/apps-rewardedads/1.7.0).
-
-The Maven group ID is `com.google.crypto.tink`, and the artifact ID is
-`apps-rewardedads`.
-
-To add a dependency using Maven:
-
-```xml
-<dependency>
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>apps-rewardedads</artifactId>
-  <version>1.7.0</version>
-</dependency>
-```
-
-## Snapshots
-
-Snapshots of this app built from the master branch are available through Maven
-using version `HEAD-SNAPSHOT`. API docs can be found
-[here](https://google.github.io/tink/javadoc/apps-rewardedads/HEAD-SNAPSHOT).
-
-To add a dependency using Maven:
-
-```xml
-<repositories>
-<repository>
-  <id>sonatype-snapshots</id>
-  <name>sonatype-snapshots</name>
-  <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
-  <snapshots>
-    <enabled>true</enabled>
-    <updatePolicy>always</updatePolicy>
-  </snapshots>
-  <releases>
-    <updatePolicy>always</updatePolicy>
-  </releases>
-</repository>
-</repositories>
-
-<dependency>
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>apps-rewardedads</artifactId>
-  <version>HEAD-SNAPSHOT</version>
-</dependency>
-```
diff --git a/apps/rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads/BUILD.bazel b/apps/rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads/BUILD.bazel
deleted file mode 100644
index a44132c..0000000
--- a/apps/rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads/BUILD.bazel
+++ /dev/null
@@ -1,17 +0,0 @@
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-java_library(
-    name = "rewarded_ads_verifier",
-    srcs = ["RewardedAdsVerifier.java"],
-    deps = [
-        "@maven//:com_google_code_gson_gson",
-        "@maven//:com_google_http_client_google_http_client",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:ecdsa_verify_jce",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:enums",
-        "@tink_java//src/main/java/com/google/crypto/tink/util:keys_downloader",
-    ],
-)
diff --git a/apps/rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads/RewardedAdsVerifier.java b/apps/rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads/RewardedAdsVerifier.java
deleted file mode 100644
index 85d26df..0000000
--- a/apps/rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads/RewardedAdsVerifier.java
+++ /dev/null
@@ -1,325 +0,0 @@
-// 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.crypto.tink.apps.rewardedads;
-
-import com.google.api.client.http.javanet.NetHttpTransport;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EcdsaVerifyJce;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.EllipticCurves.EcdsaEncoding;
-import com.google.crypto.tink.subtle.Enums.HashType;
-import com.google.crypto.tink.util.KeysDownloader;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonParser;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.nio.charset.Charset;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPublicKey;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-/**
- * An implementation of the verifier side of Server-Side Verification of Google AdMob Rewarded Ads.
- *
- * <p>Typical usage:
- *
- * <pre>{@code
- * RewardedAdsVerifier verifier = new RewardedAdsVerifier.Builder()
- *     .fetchVerifyingPublicKeysWith(
- *         RewardedAdsVerifier.KEYS_DOWNLOADER_INSTANCE_PROD)
- *     .build();
- * String rewardUrl = ...;
- * verifier.verify(rewardUrl);
- * }</pre>
- *
- * <p>This usage ensures that you always have the latest public keys, even when the keys were
- * recently rotated. It will fetch and cache the latest public keys from {#PUBLIC_KEYS_URL_PROD}.
- * When the cache expires, it will re-fetch the public keys. When initializing your server, we also
- * recommend that you call {@link KeysDownloader#refreshInBackground()} of {@link
- * RewardedAdsVerifier.KEYS_DOWNLOADER_INSTANCE_PROD} to proactively fetch the public keys.
- *
- * <p>If you've already downloaded the public keys and have other means to manage key rotation, you
- * can use {@link RewardedAdsVerifier.Builder#setVerifyingPublicKeys} to set the public keys. The
- * Builder also allows you to customize other properties.
- */
-public final class RewardedAdsVerifier {
-  private static final Charset UTF_8 = Charset.forName("UTF-8");
-
-  /** Default HTTP transport used by this class. */
-  private static final NetHttpTransport DEFAULT_HTTP_TRANSPORT =
-      new NetHttpTransport.Builder().build();
-
-  private static final Executor DEFAULT_BACKGROUND_EXECUTOR = Executors.newCachedThreadPool();
-  private final List<VerifyingPublicKeysProvider> verifyingPublicKeysProviders;
-
-  public static final String SIGNATURE_PARAM_NAME = "signature=";
-  public static final String KEY_ID_PARAM_NAME = "key_id=";
-
-  /** URL to fetch keys for environment production. */
-  public static final String PUBLIC_KEYS_URL_PROD =
-      "https://www.gstatic.com/admob/reward/verifier-keys.json";
-
-  /** URL to fetch keys for environment test. */
-  public static final String PUBLIC_KEYS_URL_TEST =
-      "https://www.gstatic.com/admob/reward/verifier-keys-test.json";
-
-  /**
-   * Instance configured to talk to fetch keys from production environment (from {@link
-   * #PUBLIC_KEYS_URL_PROD}).
-   */
-  public static final KeysDownloader KEYS_DOWNLOADER_INSTANCE_PROD =
-      new KeysDownloader(DEFAULT_BACKGROUND_EXECUTOR, DEFAULT_HTTP_TRANSPORT, PUBLIC_KEYS_URL_PROD);
-  /**
-   * Instance configured to talk to fetch keys from test environment (from {@link
-   * #PUBLIC_KEYS_URL_TEST}).
-   */
-  public static final KeysDownloader KEYS_DOWNLOADER_INSTANCE_TEST =
-      new KeysDownloader(DEFAULT_BACKGROUND_EXECUTOR, DEFAULT_HTTP_TRANSPORT, PUBLIC_KEYS_URL_TEST);
-
-  RewardedAdsVerifier(List<VerifyingPublicKeysProvider> verifyingPublicKeysProviders)
-      throws GeneralSecurityException {
-    if (verifyingPublicKeysProviders == null || verifyingPublicKeysProviders.isEmpty()) {
-      throw new IllegalArgumentException(
-          "must set at least one way to get verifying key using"
-              + " Builder.fetchVerifyingPublicKeysWith or Builder.setVerifyingPublicKeys");
-    }
-    this.verifyingPublicKeysProviders = verifyingPublicKeysProviders;
-  }
-
-  private RewardedAdsVerifier(Builder builder) throws GeneralSecurityException {
-    this(builder.verifyingPublicKeysProviders);
-  }
-
-  /**
-   * Verifies that {@code rewardUrl} has a valid signature.
-   *
-   * <p>This method requires that the name of the last two query parameters of {@code rewardUrl} are
-   * {@link #SIGNATURE_PARAM_NAME} and {@link #KEY_ID_PARAM_NAME} in that order.
-   */
-  public void verify(String rewardUrl) throws GeneralSecurityException {
-    URI uri;
-    try {
-      uri = new URI(rewardUrl);
-    } catch (URISyntaxException ex) {
-      throw new GeneralSecurityException(ex);
-    }
-    String queryString = uri.getQuery();
-    int i = queryString.indexOf(SIGNATURE_PARAM_NAME);
-    if (i <= 0 || queryString.charAt(i - 1) != '&') {
-      throw new GeneralSecurityException(
-          "signature and key id must be the last two query parameters");
-    }
-    byte[] tbsData =
-        queryString.substring(0, i - 1 /* i - 1 instead of i because of & */).getBytes(UTF_8);
-
-    String sigAndKeyId = queryString.substring(i);
-    i = sigAndKeyId.indexOf(KEY_ID_PARAM_NAME);
-    if (i == -1 || sigAndKeyId.charAt(i - 1) != '&') {
-      throw new GeneralSecurityException(
-          "signature and key id must be the last two query parameters");
-    }
-    String sig =
-        sigAndKeyId.substring(
-            SIGNATURE_PARAM_NAME.length(), i - 1 /* i - 1 instead of i because of & */);
-
-    // We don't have to check that keyId is the last parameter, because the long conversion would
-    // fail anyway if there's any trailing data.
-    try {
-      long keyId = Long.parseLong(sigAndKeyId.substring(i + KEY_ID_PARAM_NAME.length()));
-      verify(tbsData, keyId, Base64.urlSafeDecode(sig));
-    } catch (NumberFormatException ex) {
-      throw new GeneralSecurityException("key_id must be a long");
-    }
-  }
-
-  private void verify(final byte[] tbs, long keyId, final byte[] signature)
-      throws GeneralSecurityException {
-    boolean foundKeyId = false;
-    for (VerifyingPublicKeysProvider provider : verifyingPublicKeysProviders) {
-      Map<Long, ECPublicKey> publicKeys = provider.get();
-      if (publicKeys.containsKey(keyId)) {
-        foundKeyId = true;
-        ECPublicKey publicKey = publicKeys.get(keyId);
-        EcdsaVerifyJce verifier = new EcdsaVerifyJce(publicKey, HashType.SHA256, EcdsaEncoding.DER);
-        verifier.verify(signature, tbs);
-      }
-    }
-    if (!foundKeyId) {
-      throw new GeneralSecurityException("cannot find verifying key with key id: " + keyId);
-    }
-  }
-
-  /** Builder for RewardedAdsVerifier. */
-  public static class Builder {
-    private final List<VerifyingPublicKeysProvider> verifyingPublicKeysProviders =
-        new ArrayList<>();
-
-    public Builder() {}
-
-    /**
-     * Fetches verifying public keys of the sender using {@link KeysDownloader}.
-     *
-     * <p>This is the preferred method of specifying the verifying public keys.
-     */
-    public Builder fetchVerifyingPublicKeysWith(final KeysDownloader downloader)
-        throws GeneralSecurityException {
-      this.verifyingPublicKeysProviders.add(
-          new VerifyingPublicKeysProvider() {
-            @Override
-            public Map<Long, ECPublicKey> get() throws GeneralSecurityException {
-              try {
-                return parsePublicKeysJson(downloader.download());
-              } catch (IOException e) {
-                throw new GeneralSecurityException("Failed to fetch keys!", e);
-              }
-            }
-          });
-      return this;
-    }
-
-    /**
-     * Sets the trusted verifying public keys of the sender.
-     *
-     * <p><b>IMPORTANT</b>: Instead of using this method to set the verifying public keys of the
-     * sender, prefer calling {@link #fetchVerifyingPublicKeysWith} passing it an instance of {@link
-     * KeysDownloader}. It will take care of fetching fresh keys and caching in memory. Only use
-     * this method if you can't use {@link #fetchVerifyingPublicKeysWith} and be aware you will need
-     * to handle key rotations yourself.
-     *
-     * <p>The given string is a JSON object formatted like the following:
-     *
-     * <pre>
-     * {
-     *   "keys": [
-     *     {
-     *       keyId: 1916455855,
-     *       pem: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUaWMKcBHWdhUE+DncSIHhFCLLEln\nUs0LB9oanZ4K/FNICIM8ltS4nzc9yjmhgVQOlmSS6unqvN9t8sqajRTPcw==\n-----END PUBLIC KEY-----"
-     *       base64: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUaWMKcBHWdhUE+DncSIHhFCLLElnUs0LB9oanZ4K/FNICIM8ltS4nzc9yjmhgVQOlmSS6unqvN9t8sqajRTPcw=="
-     *     },
-     *     {
-     *       keyId: 3901585526,
-     *       pem: "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEtxg2BsK/fllIeADtLspezS6YfHFWXZ8tiJncm8LDBa/NxEC84akdWbWDCUrMMGIV27/3/e7UuKSEonjGvaDUsw==\n-----END PUBLIC KEY-----"
-     *       base64: "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEtxg2BsK/fllIeADtLspezS6YfHFWXZ8tiJncm8LDBa/NxEC84akdWbWDCUrMMGIV27/3/e7UuKSEonjGvaDUsw=="
-     *     },
-     *   ],
-     * }
-     * </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 setVerifyingPublicKeys(final String publicKeysJson)
-        throws GeneralSecurityException {
-      this.verifyingPublicKeysProviders.add(
-          new VerifyingPublicKeysProvider() {
-            @Override
-            public Map<Long, ECPublicKey> get() throws GeneralSecurityException {
-              return parsePublicKeysJson(publicKeysJson);
-            }
-          });
-      return this;
-    }
-
-    /**
-     * Adds a verifying public key of the sender.
-     *
-     * <p><b>IMPORTANT</b>: Instead of using this method to set the verifying public keys of the
-     * sender, prefer calling {@link #fetchVerifyingPublicKeysWith} passing it an instance of {@link
-     * KeysDownloader}. It will take care of fetching fresh keys and caching in memory. Only use
-     * this method if you can't use {@link #fetchVerifyingPublicKeysWith} and be aware you will need
-     * to handle Google key rotations yourself.
-     *
-     * <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 addVerifyingPublicKey(final long keyId, final String val)
-        throws GeneralSecurityException {
-      this.verifyingPublicKeysProviders.add(
-          new VerifyingPublicKeysProvider() {
-            @Override
-            public Map<Long, ECPublicKey> get() throws GeneralSecurityException {
-              return Collections.singletonMap(
-                  keyId, EllipticCurves.getEcPublicKey(Base64.decode(val)));
-            }
-          });
-      return this;
-    }
-
-    /**
-     * Adds a verifying public key of the sender.
-     *
-     * <p><b>IMPORTANT</b>: Instead of using this method to set the verifying public keys of the
-     * sender, prefer calling {@link #fetchVerifyingPublicKeysWith} passing it an instance of {@link
-     * KeysDownloader}. It will take care of fetching fresh keys and caching in memory. Only use
-     * this method if you can't use {@link #fetchVerifyingPublicKeysWith} and be aware you will need
-     * to handle Google key rotations yourself.
-     */
-    public Builder addVerifyingPublicKey(final long keyId, final ECPublicKey val)
-        throws GeneralSecurityException {
-      this.verifyingPublicKeysProviders.add(
-          new VerifyingPublicKeysProvider() {
-            @Override
-            public Map<Long, ECPublicKey> get() throws GeneralSecurityException {
-              return Collections.singletonMap(keyId, val);
-            }
-          });
-      return this;
-    }
-
-    public RewardedAdsVerifier build() throws GeneralSecurityException {
-      return new RewardedAdsVerifier(this);
-    }
-  }
-
-  private static Map<Long, ECPublicKey> parsePublicKeysJson(String publicKeysJson)
-      throws GeneralSecurityException {
-    Map<Long, ECPublicKey> publicKeys = new HashMap<>();
-    try {
-      JsonArray keys =
-          JsonParser.parseString(publicKeysJson).getAsJsonObject().get("keys").getAsJsonArray();
-      for (int i = 0; i < keys.size(); i++) {
-        JsonObject key = keys.get(i).getAsJsonObject();
-        publicKeys.put(
-            key.get("keyId").getAsLong(),
-            EllipticCurves.getEcPublicKey(Base64.decode(key.get("base64").getAsString())));
-      }
-    } catch (JsonParseException | IllegalStateException e) {
-      throw new GeneralSecurityException("failed to extract trusted signing public keys", e);
-    }
-    if (publicKeys.isEmpty()) {
-      throw new GeneralSecurityException("no trusted keys are available for this protocol version");
-    }
-    return publicKeys;
-  }
-
-  private interface VerifyingPublicKeysProvider {
-    Map<Long, ECPublicKey> get() throws GeneralSecurityException;
-  }
-}
diff --git a/apps/rewardedads/src/test/BUILD.bazel b/apps/rewardedads/src/test/BUILD.bazel
deleted file mode 100644
index 244de24..0000000
--- a/apps/rewardedads/src/test/BUILD.bazel
+++ /dev/null
@@ -1,35 +0,0 @@
-load("@tink_java//tools:gen_java_test_rules.bzl", "gen_java_test_rules")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-# Tests
-
-java_library(
-    name = "generator_test",
-    testonly = 1,
-    srcs = glob([
-        "**/*.java",
-    ]),
-    deps = [
-        "//rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads:rewarded_ads_verifier",
-        "@maven//:com_google_code_gson_gson",
-        "@maven//:com_google_http_client_google_http_client",
-        "@maven//:junit_junit",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:ecdsa_sign_jce",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:enums",
-        "@tink_java//src/main/java/com/google/crypto/tink/util:keys_downloader",
-    ],
-)
-
-gen_java_test_rules(
-    test_files = glob([
-        "**/*Test.java",
-    ]),
-    deps = [
-        ":generator_test",
-    ],
-)
diff --git a/apps/rewardedads/src/test/java/com/google/crypto/tink/apps/rewardedads/RewardedAdsVerifierTest.java b/apps/rewardedads/src/test/java/com/google/crypto/tink/apps/rewardedads/RewardedAdsVerifierTest.java
deleted file mode 100644
index f0e636e..0000000
--- a/apps/rewardedads/src/test/java/com/google/crypto/tink/apps/rewardedads/RewardedAdsVerifierTest.java
+++ /dev/null
@@ -1,371 +0,0 @@
-// 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.crypto.tink.apps.rewardedads;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import com.google.api.client.testing.http.MockHttpTransport;
-import com.google.api.client.testing.http.MockLowLevelHttpResponse;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EcdsaSignJce;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.EllipticCurves.EcdsaEncoding;
-import com.google.crypto.tink.subtle.Enums.HashType;
-import com.google.crypto.tink.util.KeysDownloader;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import java.net.URI;
-import java.nio.charset.Charset;
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Unit tests for {@code RewardedAdsVerifier}. */
-@RunWith(JUnit4.class)
-public class RewardedAdsVerifierTest {
-  private static final Charset UTF_8 = Charset.forName("UTF-8");
-
-  /** Sample Google provided JSON with its public signing keys. */
-  private static final String GOOGLE_VERIFYING_PUBLIC_KEYS_JSON =
-      "{\"keys\":[{"
-          + "\"keyId\":1234,"
-          + "\"base64\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPYnHwS8uegWAewQt"
-          + "lxizmLFynwHcxRT1PK07cDA6/C4sXrVI1SzZCUx8U8S0LjMrT6ird/VW7be3Mz6t/srtRQ==\""
-          + "}]}";
-
-  /**
-   * Sample Google private signing key.
-   *
-   * <p>Corresponds to private key of the key in {@link #GOOGLE_VERIFYING_PUBLIC_KEYS_JSON}.
-   */
-  private static final String GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64 =
-      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZj/Dldxz8fvKVF5O"
-          + "TeAtK6tY3G1McmvhMppe6ayW6GahRANCAAQ9icfBLy56BYB7BC2XGLOYsXKfAdzF"
-          + "FPU8rTtwMDr8LixetUjVLNkJTHxTxLQuMytPqKt39Vbtt7czPq3+yu1F";
-
-  // must match the value in GOOGLE_VERIFYING_PUBLIC_KEYS_JSON
-  private static final long KEY_ID = 1234;
-  private static final String REWARD_HOST_AND_PATH = "https://publisher.com/blah?";
-  private static final String REWARD_QUERY = "foo1=bar1&foo2=bar2";
-  private static final String REWARD_URL = REWARD_HOST_AND_PATH + REWARD_QUERY;
-
-  private static final String ALTERNATE_PUBLIC_SIGNING_KEY =
-      "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEU8E6JppGKFG40r5dDU1idHRN52NuwsemFzXZh1oUqh3bGUPgPioH+RoW"
-          + "nmVSUQz1WfM2426w9f0GADuXzpUkcw==";
-
-  private static String signUrl(String rewardUrl, String privateKey, long keyId) throws Exception {
-    EcdsaSignJce signer =
-        new EcdsaSignJce(
-            EllipticCurves.getEcPrivateKey(Base64.decode(privateKey)),
-            HashType.SHA256,
-            EcdsaEncoding.DER);
-    String queryString = new URI(rewardUrl).getQuery();
-    return buildUrl(rewardUrl, signer.sign(queryString.getBytes(UTF_8)), keyId);
-  }
-
-  private static String buildUrl(String rewardUrl, byte[] sig, long keyId) {
-    return new StringBuilder(rewardUrl)
-        .append("&")
-        .append(RewardedAdsVerifier.SIGNATURE_PARAM_NAME)
-        .append(Base64.urlSafeEncode(sig))
-        .append("&")
-        .append(RewardedAdsVerifier.KEY_ID_PARAM_NAME)
-        .append(keyId)
-        .toString();
-  }
-
-  @Test
-  public void testShouldVerify() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    verifier.verify(signUrl(REWARD_URL, GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64, KEY_ID));
-  }
-
-  @Test
-  public void testShouldVerifyIfKeyIdIsLargerThanMaxInt() throws Exception {
-    long keyId = Integer.MAX_VALUE + 1;
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-
-    trustedKeysJson.getAsJsonArray("keys").get(0).getAsJsonObject().addProperty("keyId", keyId);
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(trustedKeysJson.toString())
-            .build();
-    verifier.verify(signUrl(REWARD_URL, GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64, keyId));
-  }
-
-  @Test
-  public void testShouldVerifyWithEncodedUrl() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-
-    String rewardUrl = "https://publisher.com/path?foo=hello%20world&bar=user%40gmail.com";
-    String decodedQueryString = "foo=hello world&bar=user@gmail.com";
-    EcdsaSignJce signer =
-        new EcdsaSignJce(
-            EllipticCurves.getEcPrivateKey(Base64.decode(GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64)),
-            HashType.SHA256,
-            EcdsaEncoding.DER);
-    byte[] signature = signer.sign(decodedQueryString.getBytes(UTF_8));
-    String signedUrl = buildUrl(rewardUrl, signature, KEY_ID);
-    verifier.verify(signedUrl);
-  }
-
-  @Test
-  public void testShouldDecryptV1WhenFetchingSenderVerifyingKeys() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .fetchVerifyingPublicKeysWith(
-                new KeysDownloader.Builder()
-                    .setHttpTransport(
-                        new MockHttpTransport.Builder()
-                            .setLowLevelHttpResponse(
-                                new MockLowLevelHttpResponse()
-                                    .setContent(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON))
-                            .build())
-                    .setUrl("https://someUrl" /* unused */)
-                    .build())
-            .build();
-
-    verifier.verify(signUrl(REWARD_URL, GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64, KEY_ID));
-  }
-
-  @Test
-  public void testShouldFailIfVerifyingWithDifferentKey() throws Exception {
-    JsonObject trustedKeysJson =
-        JsonParser.parseString(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON).getAsJsonObject();
-    trustedKeysJson
-        .getAsJsonArray("keys")
-        .get(0)
-        .getAsJsonObject()
-        .addProperty("base64", ALTERNATE_PUBLIC_SIGNING_KEY);
-
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(trustedKeysJson.toString())
-            .build();
-
-    try {
-      verifier.verify(signUrl(REWARD_URL, GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64, KEY_ID));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertEquals("Invalid signature", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailIfKeyIdNotFound() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-
-    try {
-      verifier.verify(signUrl(REWARD_URL, GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64, KEY_ID + 1));
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException e) {
-      assertTrue(e.getMessage().contains("cannot find verifying key with key id"));
-    }
-  }
-
-  @Test
-  public void testShouldFailIfSignedMessageWasChanged() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    byte[] validSignedUrl =
-        signUrl(REWARD_URL, GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64, KEY_ID).getBytes(UTF_8);
-    for (int i = REWARD_HOST_AND_PATH.length(); i < REWARD_URL.length(); i++) {
-      byte[] modifiedUrl = Arrays.copyOf(validSignedUrl, validSignedUrl.length);
-      modifiedUrl[i] = (byte) (modifiedUrl[i] ^ 0xff);
-      try {
-        verifier.verify(new String(modifiedUrl, UTF_8));
-        fail("Expected GeneralSecurityException");
-      } catch (GeneralSecurityException e) {
-        assertEquals("Invalid signature", e.getMessage());
-      }
-    }
-  }
-
-  @Test
-  public void testShouldFailIfSignatureWasChanged() throws Exception {
-    EcdsaSignJce signer =
-        new EcdsaSignJce(
-            EllipticCurves.getEcPrivateKey(Base64.decode(GOOGLE_SIGNING_PRIVATE_KEY_PKCS8_BASE64)),
-            HashType.SHA256,
-            EcdsaEncoding.DER);
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-
-    byte[] validSig = signer.sign(REWARD_URL.getBytes(UTF_8));
-    for (int i = 0; i < validSig.length; i++) {
-      byte[] modifiedSig = Arrays.copyOf(validSig, validSig.length);
-      modifiedSig[i] = (byte) (modifiedSig[i] ^ 0xff);
-      String modifiedUrl = buildUrl(REWARD_URL, modifiedSig, KEY_ID);
-      try {
-        verifier.verify(modifiedUrl);
-        fail("Expected GeneralSecurityException");
-      } catch (GeneralSecurityException e) {
-        // Expected.
-        System.out.println(e);
-      }
-    }
-  }
-
-  @Test
-  public void testShouldFailWithoutSignature() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    try {
-      verifier.verify(REWARD_URL);
-    } catch (GeneralSecurityException e) {
-      assertEquals("signature and key id must be the last two query parameters", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailWithoutKeyId() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    try {
-      verifier.verify(
-          new StringBuilder(REWARD_URL)
-              .append("&")
-              .append(RewardedAdsVerifier.SIGNATURE_PARAM_NAME)
-              .append("foo")
-              .toString());
-    } catch (GeneralSecurityException e) {
-      assertEquals("signature and key id must be the last two query parameters", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailWithSignatureAndKeyIdNotTheLastParameters() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    try {
-      verifier.verify(
-          new StringBuilder(REWARD_HOST_AND_PATH)
-              .append(RewardedAdsVerifier.SIGNATURE_PARAM_NAME)
-              .toString());
-    } catch (GeneralSecurityException e) {
-      assertEquals("signature and key id must be the last two query parameters", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailWithSignatureAndKeyIdNotTheLastParameters2() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    try {
-      verifier.verify(
-          new StringBuilder(REWARD_HOST_AND_PATH)
-              .append(RewardedAdsVerifier.SIGNATURE_PARAM_NAME)
-              .append("foo")
-              .append("&")
-              .append(RewardedAdsVerifier.KEY_ID_PARAM_NAME)
-              .append("123")
-              .append("bar=baz")
-              .toString());
-    } catch (GeneralSecurityException e) {
-      assertEquals("signature and key id must be the last two query parameters", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailWithSignatureAndKeyIdNotTheLastParameters3() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    try {
-      verifier.verify(
-          new StringBuilder(REWARD_URL)
-              .append("&")
-              .append(RewardedAdsVerifier.SIGNATURE_PARAM_NAME)
-              .append("foo")
-              .append("&")
-              .append(RewardedAdsVerifier.KEY_ID_PARAM_NAME)
-              .append("123")
-              .append("&bar=baz") // this would be interpreted as part of the key ID
-              .toString());
-    } catch (GeneralSecurityException e) {
-      assertEquals("key_id must be a long", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailWithSignatureAndKeyIdNoAmpersand() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    try {
-      verifier.verify(
-          new StringBuilder(REWARD_URL)
-              .append("&")
-              .append(RewardedAdsVerifier.SIGNATURE_PARAM_NAME)
-              .append("foo")
-              .append(RewardedAdsVerifier.KEY_ID_PARAM_NAME)
-              .append("123")
-              .toString());
-    } catch (GeneralSecurityException e) {
-      assertEquals("signature and key id must be the last two query parameters", e.getMessage());
-    }
-  }
-
-  @Test
-  public void testShouldFailWithKeyIdNotLong() throws Exception {
-    RewardedAdsVerifier verifier =
-        new RewardedAdsVerifier.Builder()
-            .setVerifyingPublicKeys(GOOGLE_VERIFYING_PUBLIC_KEYS_JSON)
-            .build();
-    try {
-      verifier.verify(
-          new StringBuilder(REWARD_URL)
-              .append("&")
-              .append(RewardedAdsVerifier.SIGNATURE_PARAM_NAME)
-              .append("foo")
-              .append("&")
-              .append(RewardedAdsVerifier.KEY_ID_PARAM_NAME)
-              .append("not_long")
-              .toString());
-    } catch (GeneralSecurityException e) {
-      assertEquals("key_id must be a long", e.getMessage());
-    }
-  }
-}
diff --git a/apps/webpush/BUILD.bazel b/apps/webpush/BUILD.bazel
deleted file mode 100644
index 7260048..0000000
--- a/apps/webpush/BUILD.bazel
+++ /dev/null
@@ -1,20 +0,0 @@
-load("@tink_java//tools:gen_maven_jar_rules.bzl", "gen_maven_jar_rules")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-gen_maven_jar_rules(
-    name = "maven",
-    doctitle = "Tink Cryptography API for Message Encryption for Web Push (RFC 8291)",
-    manifest_lines = [
-        "Automatic-Module-Name: com.google.crypto.tink.apps.webpush",
-    ],
-    root_packages = ["com.google.crypto.tink.apps.webpush"],
-    deps = [
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_constants",
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_hybrid_decrypt",
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_hybrid_encrypt",
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_util",
-    ],
-)
diff --git a/apps/webpush/README.md b/apps/webpush/README.md
deleted file mode 100644
index 1018f4d..0000000
--- a/apps/webpush/README.md
+++ /dev/null
@@ -1,63 +0,0 @@
-# Message Encryption for Web Push
-
-This Tink app is an implementation of [RFC 8291 - Message Encryption for Web
-Push](https://tools.ietf.org/html/rfc8291).
-
-The most recent release is
-[1.7.0](https://github.com/google/tink/releases/tag/v1.7.0), released
-2022-08-09. API docs can be found
-[here](https://google.github.io/tink/javadoc/apps-webpush/1.7.0).
-
-## Installation
-
-To add a dependency using Maven:
-
-```xml
-<dependency>
-  <groupId>com.google.crypto.tink</groupId>
-  <artifactId>apps-webpush</artifactId>
-  <version>1.7.0</version>
-</dependency>
-```
-
-To add a dependency using Gradle:
-
-```
-dependencies {
-  implementation 'com.google.crypto.tink:apps-webpush:1.7.0'
-}
-```
-
-## Encryption
-
-```java
-import com.google.crypto.tink.HybridEncrypt;
-import java.security.interfaces.ECPublicKey;
-
-ECPublicKey reicipientPublicKey = ...;
-byte[] authSecret = ...;
-HybridEncrypt hybridEncrypt = new WebPushHybridEncrypt.Builder()
-     .withAuthSecret(authSecret)
-     .withRecipientPublicKey(recipientPublicKey)
-     .build();
-byte[] plaintext = ...;
-byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo, must be null */);
-```
-
-## Decryption
-
-```java
-import com.google.crypto.tink.HybridDecrypt;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-
-ECPrivateKey recipientPrivateKey = ...;
-ECPublicKey  recipientPublicKey = ...;
-HybridDecrypt hybridDecrypt = new WebPushHybridDecrypt.Builder()
-     .withAuthSecret(authSecret)
-     .withRecipientPublicKey(recipientPublicKey)
-     .withRecipientPrivateKey(recipientPrivateKey)
-     .build();
-byte[] ciphertext = ...;
-byte[] plaintext = hybridDecrypt.decrypt(ciphertext, /* contextInfo, must be null */);
-```
diff --git a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/BUILD.bazel b/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/BUILD.bazel
deleted file mode 100644
index 5d2afbb..0000000
--- a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/BUILD.bazel
+++ /dev/null
@@ -1,44 +0,0 @@
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-java_library(
-    name = "web_push_hybrid_decrypt",
-    srcs = ["WebPushHybridDecrypt.java"],
-    deps = [
-        ":web_push_constants",
-        ":web_push_util",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_decrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
-    ],
-)
-
-java_library(
-    name = "web_push_util",
-    srcs = ["WebPushUtil.java"],
-    deps = [
-        ":web_push_constants",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:bytes",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:hkdf",
-    ],
-)
-
-java_library(
-    name = "web_push_constants",
-    srcs = ["WebPushConstants.java"],
-    deps = ["@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves"],
-)
-
-java_library(
-    name = "web_push_hybrid_encrypt",
-    srcs = ["WebPushHybridEncrypt.java"],
-    deps = [
-        ":web_push_constants",
-        ":web_push_util",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:random",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:subtle_util_cluster",
-    ],
-)
diff --git a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushConstants.java b/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushConstants.java
deleted file mode 100644
index c864021..0000000
--- a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushConstants.java
+++ /dev/null
@@ -1,74 +0,0 @@
-// 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.crypto.tink.apps.webpush;
-
-import com.google.crypto.tink.subtle.EllipticCurves;
-import java.nio.charset.Charset;
-
-/** Various constants. */
-final class WebPushConstants {
-  static final Charset UTF_8 = Charset.forName("UTF-8");
-  static final int AUTH_SECRET_SIZE = 16;
-  static final int IKM_SIZE = 32;
-  static final int CEK_KEY_SIZE = 16;
-  static final int NONCE_SIZE = 12;
-  static final byte[] IKM_INFO =
-      new byte[] {'W', 'e', 'b', 'P', 'u', 's', 'h', ':', ' ', 'i', 'n', 'f', 'o', (byte) 0};
-  static final byte[] CEK_INFO =
-      new byte[] {
-        'C', 'o', 'n', 't', 'e', 'n', 't', '-', 'E', 'n', 'c', 'o', 'd', 'i', 'n', 'g', ':', ' ',
-        'a', 'e', 's', '1', '2', '8', 'g', 'c', 'm', (byte) 0
-      };
-  static final byte[] NONCE_INFO =
-      new byte[] {
-        'C', 'o', 'n', 't', 'e', 'n', 't', '-', 'E', 'n', 'c', 'o', 'd', 'i', 'n', 'g', ':', ' ',
-        'n', 'o', 'n', 'c', 'e', (byte) 0
-      };
-
-  static final int SALT_SIZE = 16;
-  static final int RECORD_SIZE_LEN = 4;
-  static final int PUBLIC_KEY_SIZE_LEN = 1;
-  static final int PUBLIC_KEY_SIZE = 65;
-  //   * salt:                    16
-  //   * record size:              4
-  //   * public key size:          1
-  //   * uncompressed public key: 65
-  //   * total                  : 86
-  static final int CONTENT_CODING_HEADER_SIZE =
-      SALT_SIZE + RECORD_SIZE_LEN + PUBLIC_KEY_SIZE_LEN + PUBLIC_KEY_SIZE;
-
-  // the byte 0x2 separating the payload and the padding
-  static final byte PADDING_DELIMITER_BYTE = (byte) 2;
-  static final int PADDING_DELIMETER_SIZE = 1;
-  static final int DEFAULT_PADDING_SIZE = 0;
-  static final int TAG_SIZE = 16;
-  //   * content coding header:   86
-  //   * padding delimeter:        1
-  //   * AES-GCM tag size:        16
-  //   * Total:                  103
-  static final int CIPHERTEXT_OVERHEAD =
-      CONTENT_CODING_HEADER_SIZE + PADDING_DELIMETER_SIZE + DEFAULT_PADDING_SIZE + TAG_SIZE;
-
-  static final int MAX_CIPHERTEXT_SIZE = 4096;
-
-  static final String HMAC_SHA256 = "HMACSHA256";
-  static final EllipticCurves.PointFormatType UNCOMPRESSED_POINT_FORMAT =
-      EllipticCurves.PointFormatType.UNCOMPRESSED;
-  static final EllipticCurves.CurveType NIST_P256_CURVE_TYPE = EllipticCurves.CurveType.NIST_P256;
-
-  private WebPushConstants() {}
-}
diff --git a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushHybridDecrypt.java b/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushHybridDecrypt.java
deleted file mode 100644
index 2062c54..0000000
--- a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushHybridDecrypt.java
+++ /dev/null
@@ -1,278 +0,0 @@
-// 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.crypto.tink.apps.webpush;
-
-import com.google.crypto.tink.HybridDecrypt;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.EngineFactory;
-import java.nio.ByteBuffer;
-import java.security.GeneralSecurityException;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.security.spec.ECPoint;
-import java.util.Arrays;
-import javax.crypto.Cipher;
-import javax.crypto.spec.GCMParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * A {@link HybridDecrypt} implementation for the hybrid encryption used in <a
- * href="https://tools.ietf.org/html/rfc8291">RFC 8291 - Web Push Message Encryption</a>.
- *
- * <h3>Ciphertext format</h3>
- *
- * <p>When used with <a href="https://tools.ietf.org/html/rfc8291#section-4">AES128-GCM content
- * encoding</a>, which is the only content encoding supported in this implementation, the ciphertext
- * is formatted according to RFC 8188 section 2, and looks as follows
- *
- * <pre>
- * // NOLINTNEXTLINE
- * +-----------+----------------+------------------+---------------------------------------------------
- * | salt (16) | recordsize (4) | publickeylen (1) | publickey (publickeylen) | aes128-gcm-ciphertext |
- * +-----------+----------------+------------------+---------------------------------------------------
- * </pre>
- *
- * <p>RFC 8188 divides messages into records which are encrypted independently. Web Push messages
- * cannot be longer than 3993 bytes, and are always encrypted in a single record with default size
- * of 4096 bytes. {@code aes128-gcm-ciphertext} is the encryption of the message padded with a
- * single byte of value {@code 0x02} (which indicates that this is the last and only record).
- *
- * <h3>Usage</h3>
- *
- * <pre>{@code
- * import com.google.crypto.tink.HybridDecrypt;
- * import com.google.crypto.tink.HybridEncrypt;
- * import java.security.interfaces.ECPrivateKey;
- * import java.security.interfaces.ECPublicKey;
- *
- * // Encryption.
- * ECPublicKey reicipientPublicKey = ...;
- * byte[] authSecret = ...;
- * HybridEncrypt hybridEncrypt = new WebPushHybridEncrypt.Builder()
- *      .withAuthSecret(authSecret)
- *      .withRecipientPublicKey(recipientPublicKey)
- *      .build();
- * byte[] plaintext = ...;
- * byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null);
- *
- * // Decryption.
- * ECPrivateKey recipientPrivateKey = ...;
- * HybridDecrypt hybridDecrypt = new WebPushHybridDecrypt.Builder()
- *      .withAuthSecret(authSecret)
- *      .withRecipientPublicKey(recipientPublicKey)
- *      .withRecipientPrivateKey(recipientPrivateKey)
- *      .build();
- * byte[] plaintext = hybridDecrypt.decrypt(ciphertext, null);
- * }</pre>
- *
- * @since 1.1.0
- */
-public final class WebPushHybridDecrypt implements HybridDecrypt {
-  private final ECPrivateKey recipientPrivateKey;
-  private final byte[] recipientPublicKey;
-  private final byte[] authSecret;
-  private final int recordSize;
-
-  private WebPushHybridDecrypt(Builder builder) throws GeneralSecurityException {
-    if (builder.recipientPrivateKey == null) {
-      throw new IllegalArgumentException(
-          "must set recipient's private key with Builder.withRecipientPrivateKey");
-    }
-    this.recipientPrivateKey = builder.recipientPrivateKey;
-
-    if (builder.recipientPublicKey == null
-        || builder.recipientPublicKey.length != WebPushConstants.PUBLIC_KEY_SIZE) {
-      throw new IllegalArgumentException(
-          "recipient public key must have " + WebPushConstants.PUBLIC_KEY_SIZE + " bytes");
-    }
-    this.recipientPublicKey = builder.recipientPublicKey;
-
-    if (builder.authSecret == null) {
-      throw new IllegalArgumentException("must set auth secret with Builder.withAuthSecret");
-    }
-    if (builder.authSecret.length != WebPushConstants.AUTH_SECRET_SIZE) {
-      throw new IllegalArgumentException(
-          "auth secret must have " + WebPushConstants.AUTH_SECRET_SIZE + " bytes");
-    }
-    this.authSecret = builder.authSecret;
-
-    if (builder.recordSize < WebPushConstants.CIPHERTEXT_OVERHEAD
-        || builder.recordSize > WebPushConstants.MAX_CIPHERTEXT_SIZE) {
-      throw new IllegalArgumentException(
-          String.format(
-              "invalid record size (%s); must be a number between [%s, %s]",
-              builder.recordSize,
-              WebPushConstants.CIPHERTEXT_OVERHEAD,
-              WebPushConstants.MAX_CIPHERTEXT_SIZE));
-    }
-    this.recordSize = builder.recordSize;
-  }
-
-  /**
-   * Builder for {@link WebPushHybridDecrypt}.
-   *
-   * @since 1.1.0
-   */
-  public static final class Builder {
-    private ECPrivateKey recipientPrivateKey = null;
-    private byte[] recipientPublicKey = null;
-    private byte[] authSecret = null;
-    private int recordSize = WebPushConstants.MAX_CIPHERTEXT_SIZE;
-
-    public Builder() {}
-
-    /**
-     * Sets the record size.
-     *
-     * <p>If set, this value must match the record size set with {@link
-     * WebPushHybridEncrypt.Builder#withRecordSize}.
-     *
-     * <p>If not set, a record size of 4096 bytes is used. This value should work for most users.
-     */
-    public Builder withRecordSize(int val) {
-      recordSize = val;
-      return this;
-    }
-
-    /** Sets the authentication secret. */
-    public Builder withAuthSecret(final byte[] val) {
-      authSecret = val.clone();
-      return this;
-    }
-
-    /** Sets the public key of the recipient. */
-    public Builder withRecipientPublicKey(ECPublicKey val) throws GeneralSecurityException {
-      recipientPublicKey =
-          EllipticCurves.pointEncode(
-              WebPushConstants.NIST_P256_CURVE_TYPE,
-              WebPushConstants.UNCOMPRESSED_POINT_FORMAT,
-              val.getW());
-      return this;
-    }
-
-    /**
-     * Sets the public key of the recipient.
-     *
-     * <p>The public key must be formatted as an uncompressed point format, i.e., it has {@code 65}
-     * bytes and the first byte must be {@code 0x04}.
-     */
-    public Builder withRecipientPublicKey(final byte[] val) {
-      recipientPublicKey = val.clone();
-      return this;
-    }
-
-    /** Sets the private key of the recipient. */
-    public Builder withRecipientPrivateKey(ECPrivateKey val) throws GeneralSecurityException {
-      recipientPrivateKey = val;
-      return this;
-    }
-
-    /**
-     * Sets the private key of the recipient.
-     *
-     * <p>The private key is the serialized bytes of the BigInteger returned by
-     * {@link ECPrivateKey#getS()}.
-     */
-    public Builder withRecipientPrivateKey(final byte[] val) throws GeneralSecurityException {
-      recipientPrivateKey =
-          EllipticCurves.getEcPrivateKey(WebPushConstants.NIST_P256_CURVE_TYPE, val);
-      return this;
-    }
-
-    public WebPushHybridDecrypt build() throws GeneralSecurityException {
-      return new WebPushHybridDecrypt(this);
-    }
-  }
-
-  @Override
-  public byte[] decrypt(final byte[] ciphertext, final byte[] contextInfo /* unused */)
-      throws GeneralSecurityException {
-    if (contextInfo != null) {
-      throw new GeneralSecurityException("contextInfo must be null because it is unused");
-    }
-
-    if (ciphertext.length < WebPushConstants.CIPHERTEXT_OVERHEAD) {
-      throw new GeneralSecurityException("ciphertext too short");
-    }
-
-    // A push service is not required to support more than 4096 octets of
-    // payload body. See https://tools.ietf.org/html/rfc8291#section-4.0.
-    if (ciphertext.length > WebPushConstants.MAX_CIPHERTEXT_SIZE) {
-      throw new GeneralSecurityException("ciphertext too long");
-    }
-
-    // Unpacking.
-    ByteBuffer record = ByteBuffer.wrap(ciphertext);
-    byte[] salt = new byte[WebPushConstants.SALT_SIZE];
-    record.get(salt);
-
-    int recordSize = record.getInt();
-    if (recordSize != this.recordSize
-        || recordSize < ciphertext.length
-        || recordSize > WebPushConstants.MAX_CIPHERTEXT_SIZE) {
-      throw new GeneralSecurityException("invalid record size: " + recordSize);
-    }
-
-    int publicKeySize = (int) record.get();
-    if (publicKeySize != WebPushConstants.PUBLIC_KEY_SIZE) {
-      throw new GeneralSecurityException("invalid ephemeral public key size: " + publicKeySize);
-    }
-
-    byte[] asPublicKey = new byte[WebPushConstants.PUBLIC_KEY_SIZE];
-    record.get(asPublicKey);
-    ECPoint asPublicPoint =
-        EllipticCurves.pointDecode(
-            WebPushConstants.NIST_P256_CURVE_TYPE,
-            WebPushConstants.UNCOMPRESSED_POINT_FORMAT,
-            asPublicKey);
-
-    byte[] payload = new byte[ciphertext.length - WebPushConstants.CONTENT_CODING_HEADER_SIZE];
-    record.get(payload);
-
-    // See https://tools.ietf.org/html/rfc8291#section-3.4.
-    byte[] ecdhSecret = EllipticCurves.computeSharedSecret(recipientPrivateKey, asPublicPoint);
-    byte[] ikm = WebPushUtil.computeIkm(ecdhSecret, authSecret, recipientPublicKey, asPublicKey);
-    byte[] cek = WebPushUtil.computeCek(ikm, salt);
-    byte[] nonce = WebPushUtil.computeNonce(ikm, salt);
-
-    return decrypt(cek, nonce, payload);
-  }
-
-  private byte[] decrypt(final byte[] key, final byte[] nonce, final byte[] ciphertext)
-      throws GeneralSecurityException {
-    Cipher cipher = EngineFactory.CIPHER.getInstance("AES/GCM/NoPadding");
-    GCMParameterSpec params = new GCMParameterSpec(8 * WebPushConstants.TAG_SIZE, nonce);
-    cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), params);
-    byte[] plaintext = cipher.doFinal(ciphertext);
-    if (plaintext.length == 0) {
-      throw new GeneralSecurityException("decryption failed");
-    }
-    // Remove zero paddings.
-    int index = plaintext.length - 1;
-    while (index > 0) {
-      if (plaintext[index] != 0) {
-        break;
-      }
-      index--;
-    }
-
-    if (plaintext[index] != WebPushConstants.PADDING_DELIMITER_BYTE) {
-      throw new GeneralSecurityException("decryption failed");
-    }
-    return Arrays.copyOf(plaintext, index);
-  }
-}
diff --git a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushHybridEncrypt.java b/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushHybridEncrypt.java
deleted file mode 100644
index dfb7555..0000000
--- a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushHybridEncrypt.java
+++ /dev/null
@@ -1,259 +0,0 @@
-// 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.crypto.tink.apps.webpush;
-
-import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.EngineFactory;
-import com.google.crypto.tink.subtle.Random;
-import java.nio.ByteBuffer;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.security.spec.ECPoint;
-import javax.crypto.Cipher;
-import javax.crypto.spec.GCMParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * A {@link HybridEncrypt} implementation for the hybrid encryption used in <a
- * href="https://tools.ietf.org/html/rfc8291">RFC 8291 - Web Push Message Encryption</a>.
- *
- * <h3>Ciphertext format</h3>
- *
- * <p>When used with <a href="https://tools.ietf.org/html/rfc8291#section-4">AES128-GCM content
- * encoding</a>, which is the only content encoding supported in this implementation, the ciphertext
- * is formatted according to RFC 8188 section 2, and looks as follows
- *
- * <pre>
- * // NOLINTNEXTLINE
- * +-----------+----------------+------------------+---------------------------------------------------
- * | salt (16) | recordsize (4) | publickeylen (1) | publickey (publickeylen) | aes128-gcm-ciphertext |
- * +-----------+----------------+------------------+---------------------------------------------------
- * </pre>
- *
- * <p>RFC 8188 divides messages into records which are encrypted independently. Web Push messages
- * cannot be longer than 3993 bytes, and are always encrypted in a single record with default size
- * of 4096 bytes. {@code aes128-gcm-ciphertext} is the encryption of the message padded with a
- * single byte of value {@code 0x02} (which indicates that this is the last and only record).
- *
- * <h3>Usage</h3>
- *
- * <pre>{@code
- * import com.google.crypto.tink.HybridDecrypt;
- * import com.google.crypto.tink.HybridEncrypt;
- * import java.security.interfaces.ECPrivateKey;
- * import java.security.interfaces.ECPublicKey;
- *
- * // Encryption.
- * ECPublicKey reicipientPublicKey = ...;
- * byte[] authSecret = ...;
- * HybridEncrypt hybridEncrypt = new WebPushHybridEncrypt.Builder()
- *      .withAuthSecret(authSecret)
- *      .withRecipientPublicKey(recipientPublicKey)
- *      .build();
- * byte[] plaintext = ...;
- * byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null);
- *
- * // Decryption.
- * ECPrivateKey recipientPrivateKey = ...;
- * HybridDecrypt hybridDecrypt = new WebPushHybridDecrypt.Builder()
- *      .withAuthSecret(authSecret)
- *      .withRecipientPublicKey(recipientPublicKey)
- *      .withRecipientPrivateKey(recipientPrivateKey)
- *      .build();
- * byte[] plaintext = hybridDecrypt.decrypt(ciphertext, null);
- * }</pre>
- *
- * @since 1.1.0
- */
-public final class WebPushHybridEncrypt implements HybridEncrypt {
-  private final byte[] recipientPublicKey;
-  private final byte[] authSecret;
-  private final ECPoint recipientPublicPoint;
-  private final int recordSize;
-  private final int paddingSize;
-
-  private WebPushHybridEncrypt(Builder builder) throws GeneralSecurityException {
-    if (builder.recipientPublicKey == null || builder.recipientPublicPoint == null) {
-      throw new IllegalArgumentException(
-          "must set recipient's public key with Builder.withRecipientPublicKey");
-    }
-    this.recipientPublicKey = builder.recipientPublicKey;
-    this.recipientPublicPoint = builder.recipientPublicPoint;
-
-    if (builder.authSecret == null) {
-      throw new IllegalArgumentException("must set auth secret with Builder.withAuthSecret");
-    }
-    if (builder.authSecret.length != WebPushConstants.AUTH_SECRET_SIZE) {
-      throw new IllegalArgumentException(
-          "auth secret must have " + WebPushConstants.AUTH_SECRET_SIZE + " bytes");
-    }
-    this.authSecret = builder.authSecret;
-    this.recordSize = builder.recordSize;
-    this.paddingSize = builder.paddingSize;
-  }
-
-  /**
-   * Builder for {@link WebPushHybridEncrypt}.
-   *
-   * @since 1.1.0
-   */
-  public static final class Builder {
-    private byte[] recipientPublicKey = null;
-    private ECPoint recipientPublicPoint = null;
-    private byte[] authSecret = null;
-    private int recordSize = WebPushConstants.MAX_CIPHERTEXT_SIZE;
-    private int paddingSize = WebPushConstants.DEFAULT_PADDING_SIZE;
-
-    public Builder() {}
-
-    /**
-     * Sets the record size.
-     *
-     * <p>If set, this value must match the record size set with {@link
-     * WebPushHybridEncrypt.Builder#withRecordSize}.
-     *
-     * <p>If not set, a record size of 4096 bytes is used. This value should work for most users.
-     */
-    public Builder withRecordSize(int val) {
-      if (val < WebPushConstants.CIPHERTEXT_OVERHEAD
-          || val > WebPushConstants.MAX_CIPHERTEXT_SIZE) {
-        throw new IllegalArgumentException(
-            String.format(
-                "invalid record size (%s); must be a number between [%s, %s]",
-                val, WebPushConstants.CIPHERTEXT_OVERHEAD, WebPushConstants.MAX_CIPHERTEXT_SIZE));
-      }
-
-      recordSize = val;
-      return this;
-    }
-
-    /**
-     * Sets the padding size which is default to 0.
-     *
-     * <p>The padding size cannot be larger than
-     */
-    public Builder withPaddingSize(int val) {
-      if (val < 0
-          || val > WebPushConstants.MAX_CIPHERTEXT_SIZE - WebPushConstants.CIPHERTEXT_OVERHEAD) {
-        throw new IllegalArgumentException(
-            String.format(
-                "invalid padding size (%s); must be a number between [%s, %s]",
-                val,
-                0,
-                WebPushConstants.MAX_CIPHERTEXT_SIZE - WebPushConstants.CIPHERTEXT_OVERHEAD));
-      }
-
-      paddingSize = val;
-      return this;
-    }
-
-    /** Sets the authentication secret. */
-    public Builder withAuthSecret(final byte[] val) {
-      authSecret = val.clone();
-      return this;
-    }
-
-    /** Sets the public key of the recipient. */
-    public Builder withRecipientPublicKey(ECPublicKey val) throws GeneralSecurityException {
-      recipientPublicPoint = val.getW();
-      recipientPublicKey =
-          EllipticCurves.pointEncode(
-              WebPushConstants.NIST_P256_CURVE_TYPE,
-              WebPushConstants.UNCOMPRESSED_POINT_FORMAT,
-              val.getW());
-
-      return this;
-    }
-
-    /**
-     * Sets the public key of the recipient.
-     *
-     * <p>The public key must be formatted as an uncompressed point format, i.e., it has {@code 65}
-     * bytes and the first byte must be {@code 0x04}.
-     */
-    public Builder withRecipientPublicKey(final byte[] val) throws GeneralSecurityException {
-      recipientPublicKey = val.clone();
-      recipientPublicPoint =
-          EllipticCurves.pointDecode(
-              WebPushConstants.NIST_P256_CURVE_TYPE,
-              WebPushConstants.UNCOMPRESSED_POINT_FORMAT,
-              recipientPublicKey);
-      return this;
-    }
-
-    public WebPushHybridEncrypt build() throws GeneralSecurityException {
-      return new WebPushHybridEncrypt(this);
-    }
-  }
-
-  @Override
-  public byte[] encrypt(final byte[] plaintext, final byte[] contextInfo /* unused */)
-      throws GeneralSecurityException {
-    if (contextInfo != null) {
-      throw new GeneralSecurityException("contextInfo must be null because it is unused");
-    }
-
-    if (plaintext.length > recordSize - paddingSize - WebPushConstants.CIPHERTEXT_OVERHEAD) {
-      throw new GeneralSecurityException(
-          String.format(
-              "plaintext too long; with record size = %d and padding size = %d, plaintext cannot"
-                  + " be longer than %d",
-              recordSize,
-              paddingSize,
-              recordSize - paddingSize - WebPushConstants.CIPHERTEXT_OVERHEAD));
-    }
-
-    // See https://tools.ietf.org/html/rfc8291#section-3.4.
-    KeyPair keyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey ephemeralPrivateKey = (ECPrivateKey) keyPair.getPrivate();
-    ECPublicKey ephemeralPublicKey = (ECPublicKey) keyPair.getPublic();
-    byte[] ecdhSecret =
-        EllipticCurves.computeSharedSecret(ephemeralPrivateKey, recipientPublicPoint);
-    byte[] ephemeralPublicKeyBytes =
-        EllipticCurves.pointEncode(
-            WebPushConstants.NIST_P256_CURVE_TYPE,
-            WebPushConstants.UNCOMPRESSED_POINT_FORMAT,
-            ephemeralPublicKey.getW());
-    byte[] ikm =
-        WebPushUtil.computeIkm(ecdhSecret, authSecret, recipientPublicKey, ephemeralPublicKeyBytes);
-    byte[] salt = Random.randBytes(WebPushConstants.SALT_SIZE);
-    byte[] cek = WebPushUtil.computeCek(ikm, salt);
-    byte[] nonce = WebPushUtil.computeNonce(ikm, salt);
-    return ByteBuffer.allocate(
-            WebPushConstants.CIPHERTEXT_OVERHEAD + plaintext.length + paddingSize)
-        .put(salt)
-        .putInt(recordSize)
-        .put((byte) WebPushConstants.PUBLIC_KEY_SIZE)
-        .put(ephemeralPublicKeyBytes)
-        .put(encrypt(cek, nonce, plaintext))
-        .array();
-  }
-
-  private byte[] encrypt(final byte[] key, final byte[] nonce, final byte[] plaintext)
-      throws GeneralSecurityException {
-    Cipher cipher = EngineFactory.CIPHER.getInstance("AES/GCM/NoPadding");
-    GCMParameterSpec params = new GCMParameterSpec(8 * WebPushConstants.TAG_SIZE, nonce);
-    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), params);
-    byte[] paddedPlaintext = new byte[plaintext.length + 1 + paddingSize];
-    paddedPlaintext[plaintext.length] = WebPushConstants.PADDING_DELIMITER_BYTE;
-    System.arraycopy(plaintext, 0, paddedPlaintext, 0, plaintext.length);
-    return cipher.doFinal(paddedPlaintext);
-  }
-}
diff --git a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushUtil.java b/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushUtil.java
deleted file mode 100644
index c2fc72e..0000000
--- a/apps/webpush/src/main/java/com/google/crypto/tink/apps/webpush/WebPushUtil.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// 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.crypto.tink.apps.webpush;
-
-import com.google.crypto.tink.subtle.Bytes;
-import com.google.crypto.tink.subtle.Hkdf;
-import java.security.GeneralSecurityException;
-
-/** Various helpers. */
-final class WebPushUtil {
-  public static byte[] computeIkm(
-      final byte[] ecdhSecret,
-      final byte[] authSecret,
-      final byte[] uaPublic,
-      final byte[] asPublic)
-      throws GeneralSecurityException {
-    byte[] keyInfo = Bytes.concat(WebPushConstants.IKM_INFO, uaPublic, asPublic);
-    return Hkdf.computeHkdf(
-        WebPushConstants.HMAC_SHA256,
-        ecdhSecret /* ikm */,
-        authSecret /* salt */,
-        keyInfo,
-        WebPushConstants.IKM_SIZE);
-  }
-
-  public static byte[] computeCek(final byte[] ikm, final byte[] salt)
-      throws GeneralSecurityException {
-    return Hkdf.computeHkdf(
-        WebPushConstants.HMAC_SHA256,
-        ikm,
-        salt,
-        WebPushConstants.CEK_INFO,
-        WebPushConstants.CEK_KEY_SIZE);
-  }
-
-  public static byte[] computeNonce(final byte[] ikm, final byte[] salt)
-      throws GeneralSecurityException {
-    return Hkdf.computeHkdf(
-        WebPushConstants.HMAC_SHA256,
-        ikm,
-        salt,
-        WebPushConstants.NONCE_INFO,
-        WebPushConstants.NONCE_SIZE);
-  }
-
-  private WebPushUtil() {}
-}
diff --git a/apps/webpush/src/test/BUILD.bazel b/apps/webpush/src/test/BUILD.bazel
deleted file mode 100644
index 309b3ad..0000000
--- a/apps/webpush/src/test/BUILD.bazel
+++ /dev/null
@@ -1,38 +0,0 @@
-load("@tink_java//tools:gen_java_test_rules.bzl", "gen_java_test_rules")
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])
-
-# Tests
-
-java_library(
-    name = "generator_test",
-    testonly = 1,
-    srcs = glob([
-        "**/*.java",
-    ]),
-    deps = [
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_constants",
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_hybrid_decrypt",
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_hybrid_encrypt",
-        "//webpush/src/main/java/com/google/crypto/tink/apps/webpush:web_push_util",
-        "@maven//:junit_junit",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_decrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink:hybrid_encrypt",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:base64",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:elliptic_curves",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:hex",
-        "@tink_java//src/main/java/com/google/crypto/tink/subtle:random",
-        "@tink_java//src/main/java/com/google/crypto/tink/testing:test_util",
-    ],
-)
-
-gen_java_test_rules(
-    test_files = glob([
-        "**/*Test.java",
-    ]),
-    deps = [
-        ":generator_test",
-    ],
-)
diff --git a/apps/webpush/src/test/java/com/google/crypto/tink/apps/webpush/WebPushHybridDecryptTest.java b/apps/webpush/src/test/java/com/google/crypto/tink/apps/webpush/WebPushHybridDecryptTest.java
deleted file mode 100644
index f8a6987..0000000
--- a/apps/webpush/src/test/java/com/google/crypto/tink/apps/webpush/WebPushHybridDecryptTest.java
+++ /dev/null
@@ -1,243 +0,0 @@
-// 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.crypto.tink.apps.webpush;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-import com.google.crypto.tink.HybridDecrypt;
-import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.subtle.Base64;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.Random;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.util.Arrays;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Unit tests for {@code WebPushHybridDecrypt}. */
-@RunWith(JUnit4.class)
-public class WebPushHybridDecryptTest {
-  // Copied from https://tools.ietf.org/html/rfc8291#section-5.
-  private static final String PLAINTEXT = "V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24";
-  private static final String RECEIVER_PRIVATE_KEY = "q1dXpw3UpT5VOmu_cf_v6ih07Aems3njxI-JWgLcM94";
-  private static final String RECEIVER_PUBLIC_KEY =
-      "BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4";
-  private static final String AUTH_SECRET = "BTBZMqHH6r4Tts7J_aSIgg";
-  private static final String CIPHERTEXT =
-      "DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27ml"
-          + "mlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPT"
-          + "pK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN";
-  private static final int RECORD_SIZE = 4096;
-
-  @Test
-  public void testWithRfc8291TestVector() throws Exception {
-    byte[] plaintext = Base64.urlSafeDecode(PLAINTEXT);
-    byte[] recipientPrivateKey = Base64.urlSafeDecode(RECEIVER_PRIVATE_KEY);
-    byte[] recipientPublicKey = Base64.urlSafeDecode(RECEIVER_PUBLIC_KEY);
-    byte[] authSecret = Base64.urlSafeDecode(AUTH_SECRET);
-    byte[] ciphertext = Base64.urlSafeDecode(CIPHERTEXT);
-
-    HybridDecrypt hybridDecrypt =
-        new WebPushHybridDecrypt.Builder()
-            .withRecordSize(RECORD_SIZE)
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(recipientPublicKey)
-            .withRecipientPrivateKey(recipientPrivateKey)
-            .build();
-    assertArrayEquals(plaintext, hybridDecrypt.decrypt(ciphertext, null /* contextInfo */));
-  }
-
-  @Test
-  public void testEncryptDecryptWithInvalidRecordSizes() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-
-    // Test with out of range record sizes.
-    {
-      try {
-        new WebPushHybridDecrypt.Builder()
-            .withRecordSize(WebPushConstants.MAX_CIPHERTEXT_SIZE + 1)
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-        fail("Expected IllegalArgumentException");
-      } catch (IllegalArgumentException ex) {
-        // expected.
-      }
-
-      try {
-        new WebPushHybridDecrypt.Builder()
-            .withRecordSize(WebPushConstants.CIPHERTEXT_OVERHEAD - 1)
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-
-      } catch (IllegalArgumentException ex) {
-        // expected.
-      }
-    }
-
-    // Test with random mismatched record size.
-    {
-      for (int i = 0; i < 50; i++) {
-        int recordSize =
-            WebPushConstants.CIPHERTEXT_OVERHEAD
-                + Random.randInt(
-                    WebPushConstants.MAX_CIPHERTEXT_SIZE
-                        - WebPushConstants.CIPHERTEXT_OVERHEAD
-                        - 1);
-        HybridEncrypt hybridEncrypt =
-            new WebPushHybridEncrypt.Builder()
-                .withRecordSize(recordSize)
-                .withAuthSecret(authSecret)
-                .withRecipientPublicKey(uaPublicKey)
-                .build();
-        HybridDecrypt hybridDecrypt =
-            new WebPushHybridDecrypt.Builder()
-                .withRecordSize(recordSize + 1)
-                .withAuthSecret(authSecret)
-                .withRecipientPublicKey(uaPublicKey)
-                .withRecipientPrivateKey(uaPrivateKey)
-                .build();
-        byte[] plaintext = Random.randBytes(recordSize - WebPushConstants.CIPHERTEXT_OVERHEAD);
-        byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-
-        try {
-          hybridDecrypt.decrypt(ciphertext, null /* contextInfo */);
-          fail("Expected GeneralSecurityException");
-        } catch (GeneralSecurityException ex) {
-          // expected.
-        }
-      }
-    }
-  }
-
-  @Test
-  public void testNonNullContextInfo() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-
-    HybridEncrypt hybridEncrypt =
-        new WebPushHybridEncrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .build();
-    HybridDecrypt hybridDecrypt =
-        new WebPushHybridDecrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-    byte[] plaintext = Random.randBytes(20);
-    byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-    try {
-      byte[] contextInfo = new byte[0];
-      hybridDecrypt.decrypt(ciphertext, contextInfo);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException ex) {
-      // expected;
-    }
-  }
-
-  @Test
-  public void testModifyCiphertext() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-
-    HybridEncrypt hybridEncrypt =
-        new WebPushHybridEncrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .build();
-    HybridDecrypt hybridDecrypt =
-        new WebPushHybridDecrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-    byte[] plaintext = Random.randBytes(20);
-    byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-
-    // Flipping bits.
-    for (int b = 0; b < ciphertext.length; b++) {
-      for (int bit = 0; bit < 8; bit++) {
-        byte[] modified = Arrays.copyOf(ciphertext, ciphertext.length);
-        modified[b] ^= (byte) (1 << bit);
-        try {
-          byte[] unused = hybridDecrypt.decrypt(modified, null /* contextInfo */);
-          fail("Decrypting modified ciphertext should fail");
-        } catch (GeneralSecurityException ex) {
-          // This is expected.
-        }
-      }
-    }
-
-    // Truncate the message.
-    for (int length = 0; length < ciphertext.length; length++) {
-      byte[] modified = Arrays.copyOf(ciphertext, length);
-      try {
-        byte[] unused = hybridDecrypt.decrypt(modified, null /* contextInfo */);
-        fail("Decrypting modified ciphertext should fail");
-      } catch (GeneralSecurityException ex) {
-        // This is expected.
-      }
-    }
-  }
-
-  @Test
-  public void testEncryptDecrypt_withPadding_shouldWork() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-
-    int paddingSize = 20;
-    int plaintextSize = 20;
-    HybridEncrypt hybridEncrypt =
-        new WebPushHybridEncrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withPaddingSize(paddingSize)
-            .withRecipientPublicKey(uaPublicKey)
-            .build();
-    HybridDecrypt hybridDecrypt =
-        new WebPushHybridDecrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-    byte[] plaintext = Random.randBytes(plaintextSize);
-    byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-
-    assertEquals(
-        ciphertext.length, plaintext.length + paddingSize + WebPushConstants.CIPHERTEXT_OVERHEAD);
-    assertArrayEquals(plaintext, hybridDecrypt.decrypt(ciphertext, null /* contextInfo */));
-  }
-}
diff --git a/apps/webpush/src/test/java/com/google/crypto/tink/apps/webpush/WebPushHybridEncryptTest.java b/apps/webpush/src/test/java/com/google/crypto/tink/apps/webpush/WebPushHybridEncryptTest.java
deleted file mode 100644
index 7b84b84..0000000
--- a/apps/webpush/src/test/java/com/google/crypto/tink/apps/webpush/WebPushHybridEncryptTest.java
+++ /dev/null
@@ -1,244 +0,0 @@
-// 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.crypto.tink.apps.webpush;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-import com.google.crypto.tink.HybridDecrypt;
-import com.google.crypto.tink.HybridEncrypt;
-import com.google.crypto.tink.subtle.EllipticCurves;
-import com.google.crypto.tink.subtle.Hex;
-import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.testing.TestUtil;
-import java.nio.ByteBuffer;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-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 WebPushHybridEncrypt}. */
-@RunWith(JUnit4.class)
-public class WebPushHybridEncryptTest {
-  @Test
-  public void testEncryptDecrypt() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] uaPublicKeyBytes =
-        EllipticCurves.pointEncode(
-            WebPushConstants.NIST_P256_CURVE_TYPE,
-            WebPushConstants.UNCOMPRESSED_POINT_FORMAT,
-            uaPublicKey.getW());
-    byte[] authSecret = Random.randBytes(16);
-    HybridEncrypt hybridEncrypt =
-        new WebPushHybridEncrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKeyBytes)
-            .build();
-    HybridDecrypt hybridDecrypt =
-        new WebPushHybridDecrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKeyBytes)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-
-    Set<String> salts = new TreeSet<>();
-    Set<String> ephemeralPublicKeys = new TreeSet<>();
-    Set<String> payloads = new TreeSet<>();
-    int numTests = 100;
-    if (TestUtil.isTsan()) {
-      numTests = 5;
-    }
-    for (int j = 0; j < numTests; j++) {
-      byte[] plaintext = Random.randBytes(j);
-      byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-      assertEquals(ciphertext.length, plaintext.length + WebPushConstants.CIPHERTEXT_OVERHEAD);
-      assertArrayEquals(plaintext, hybridDecrypt.decrypt(ciphertext, null /* contextInfo */));
-
-      // Checks that the encryption is randomized.
-      ByteBuffer record = ByteBuffer.wrap(ciphertext);
-      byte[] salt = new byte[WebPushConstants.SALT_SIZE];
-      record.get(salt);
-      salts.add(Hex.encode(salt));
-
-      int unused1 = record.getInt();
-      int unused2 = (int) record.get();
-
-      byte[] ephemeralPublicKey = new byte[WebPushConstants.PUBLIC_KEY_SIZE];
-      record.get(ephemeralPublicKey);
-      ephemeralPublicKeys.add(Hex.encode(ephemeralPublicKey));
-
-      byte[] payload = new byte[ciphertext.length - WebPushConstants.CONTENT_CODING_HEADER_SIZE];
-      record.get(payload);
-      payloads.add(Hex.encode(payload));
-    }
-    assertEquals(numTests, salts.size());
-    assertEquals(numTests, ephemeralPublicKeys.size());
-    assertEquals(numTests, payloads.size());
-  }
-
-  @Test
-  public void testEncryptDecryptWithVaryingRecordSizes() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-
-    int numTests = 100;
-    if (TestUtil.isTsan()) {
-      numTests = 5;
-    }
-    // Test with random, valid record sizes.
-    for (int i = 0; i < numTests; i++) {
-      int recordSize =
-          WebPushConstants.CIPHERTEXT_OVERHEAD
-              + Random.randInt(
-                  WebPushConstants.MAX_CIPHERTEXT_SIZE - WebPushConstants.CIPHERTEXT_OVERHEAD);
-      HybridEncrypt hybridEncrypt =
-          new WebPushHybridEncrypt.Builder()
-              .withRecordSize(recordSize)
-              .withAuthSecret(authSecret)
-              .withRecipientPublicKey(uaPublicKey)
-              .build();
-      HybridDecrypt hybridDecrypt =
-          new WebPushHybridDecrypt.Builder()
-              .withRecordSize(recordSize)
-              .withAuthSecret(authSecret)
-              .withRecipientPublicKey(uaPublicKey)
-              .withRecipientPrivateKey(uaPrivateKey)
-              .build();
-
-      byte[] plaintext = Random.randBytes(recordSize - WebPushConstants.CIPHERTEXT_OVERHEAD);
-      byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-      assertEquals(ciphertext.length, plaintext.length + WebPushConstants.CIPHERTEXT_OVERHEAD);
-      assertArrayEquals(plaintext, hybridDecrypt.decrypt(ciphertext, null /* contextInfo */));
-    }
-  }
-
-  @Test
-  public void testEncryptDecrypt_largestPossibleRecordSize() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-    // Test with largest possible record size.
-    HybridEncrypt hybridEncrypt =
-        new WebPushHybridEncrypt.Builder()
-            .withRecordSize(WebPushConstants.MAX_CIPHERTEXT_SIZE)
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .build();
-    HybridDecrypt hybridDecrypt =
-        new WebPushHybridDecrypt.Builder()
-            .withRecordSize(WebPushConstants.MAX_CIPHERTEXT_SIZE)
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-    byte[] plaintext =
-        Random.randBytes(
-            WebPushConstants.MAX_CIPHERTEXT_SIZE - WebPushConstants.CIPHERTEXT_OVERHEAD);
-    byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-    assertEquals(ciphertext.length, plaintext.length + WebPushConstants.CIPHERTEXT_OVERHEAD);
-    assertArrayEquals(plaintext, hybridDecrypt.decrypt(ciphertext, null /* contextInfo */));
-  }
-
-  @Test
-  public void testEncryptDecrypt_smallestPossibleRecordSize() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPrivateKey uaPrivateKey = (ECPrivateKey) uaKeyPair.getPrivate();
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-    // Test with smallest possible record size.
-    HybridEncrypt hybridEncrypt =
-        new WebPushHybridEncrypt.Builder()
-            .withRecordSize(WebPushConstants.CIPHERTEXT_OVERHEAD)
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .build();
-    HybridDecrypt hybridDecrypt =
-        new WebPushHybridDecrypt.Builder()
-            .withRecordSize(WebPushConstants.CIPHERTEXT_OVERHEAD)
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .withRecipientPrivateKey(uaPrivateKey)
-            .build();
-    byte[] plaintext = new byte[0];
-    byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null /* contextInfo */);
-    assertEquals(ciphertext.length, plaintext.length + WebPushConstants.CIPHERTEXT_OVERHEAD);
-    assertArrayEquals(plaintext, hybridDecrypt.decrypt(ciphertext, null /* contextInfo */));
-  }
-
-  @Test
-  public void testEncryptDecrypt_outOfRangeRecordSize_throws() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-
-    try {
-      new WebPushHybridEncrypt.Builder()
-          .withRecordSize(WebPushConstants.MAX_CIPHERTEXT_SIZE + 1)
-          .withAuthSecret(authSecret)
-          .withRecipientPublicKey(uaPublicKey)
-          .build();
-      fail("Expected IllegalArgumentException");
-    } catch (IllegalArgumentException ex) {
-      // expected.
-    }
-
-    try {
-      new WebPushHybridEncrypt.Builder()
-          .withRecordSize(WebPushConstants.CIPHERTEXT_OVERHEAD - 1)
-          .withAuthSecret(authSecret)
-          .withRecipientPublicKey(uaPublicKey)
-          .build();
-      fail("Expected IllegalArgumentException");
-    } catch (IllegalArgumentException ex) {
-      // expected.
-    }
-  }
-
-  @Test
-  public void testNonNullContextInfo() throws Exception {
-    KeyPair uaKeyPair = EllipticCurves.generateKeyPair(WebPushConstants.NIST_P256_CURVE_TYPE);
-    ECPublicKey uaPublicKey = (ECPublicKey) uaKeyPair.getPublic();
-    byte[] authSecret = Random.randBytes(16);
-
-    HybridEncrypt hybridEncrypt =
-        new WebPushHybridEncrypt.Builder()
-            .withAuthSecret(authSecret)
-            .withRecipientPublicKey(uaPublicKey)
-            .build();
-    byte[] plaintext = Random.randBytes(20);
-    byte[] contextInfo = new byte[0];
-
-    try {
-      byte[] unusedCiphertext = hybridEncrypt.encrypt(plaintext, contextInfo);
-      fail("Expected GeneralSecurityException");
-    } catch (GeneralSecurityException ex) {
-      // expected;
-    }
-  }
-}
-
diff --git a/cc/.bazelrc b/cc/.bazelrc
index 94aad05..e2435c9 100644
--- a/cc/.bazelrc
+++ b/cc/.bazelrc
@@ -1,3 +1,7 @@
 # Fix for grpc build error on macOS.
 # See: https://github.com/bazelbuild/bazel/issues/4341
-build --copt -DGRPC_BAZEL_BUILD
+build --copt=-DGRPC_BAZEL_BUILD
+# Minumum C++ version. Override it building this project with
+# `bazel build --cxxopt='-std=c++<XY>' --host_cxxopt='c++<XY>' ...`
+# (Both -std and --host_cxxopt must be set to force the desired version.)
+build --cxxopt='-std=c++14' --host_cxxopt='-std=c++14'
diff --git a/cc/.bazelversion b/cc/.bazelversion
index ac14c3d..09b254e 100644
--- a/cc/.bazelversion
+++ b/cc/.bazelversion
@@ -1 +1 @@
-5.1.1
+6.0.0
diff --git a/cc/BUILD.bazel b/cc/BUILD.bazel
index 5064fdb..c88dd8e 100644
--- a/cc/BUILD.bazel
+++ b/cc/BUILD.bazel
@@ -23,8 +23,6 @@
         "aead_key_templates.h",
         "binary_keyset_reader.h",
         "binary_keyset_writer.h",
-        "catalogue.h",
-        "config.h",
         "deterministic_aead.h",
         "deterministic_aead_config.h",
         "deterministic_aead_factory.h",
@@ -77,6 +75,7 @@
         ":input_stream",
         ":json_keyset_reader",
         ":json_keyset_writer",
+        ":key",
         ":key_manager",
         ":keyset_handle",
         ":keyset_manager",
@@ -396,41 +395,6 @@
 )
 
 cc_library(
-    name = "catalogue",
-    hdrs = ["catalogue.h"],
-    include_prefix = "tink",
-    deps = [
-        ":key_manager",
-        "//util:statusor",
-        "@com_google_absl//absl/base:core_headers",
-    ],
-)
-
-cc_library(
-    name = "config",
-    srcs = ["core/config.cc"],
-    hdrs = ["config.h"],
-    include_prefix = "tink",
-    deps = [
-        ":catalogue",
-        ":key_manager",
-        ":registry",
-        "//aead:aead_config",
-        "//daead:deterministic_aead_config",
-        "//hybrid:hybrid_config",
-        "//mac:mac_config",
-        "//proto:config_cc_proto",
-        "//signature:signature_config",
-        "//streamingaead:streaming_aead_config",
-        "//util:errors",
-        "//util:status",
-        "//util:statusor",
-        "@com_google_absl//absl/status",
-        "@com_google_absl//absl/strings",
-    ],
-)
-
-cc_library(
     name = "crypto_format",
     srcs = ["core/crypto_format.cc"],
     hdrs = ["crypto_format.h"],
@@ -453,6 +417,7 @@
         "//proto:tink_cc_proto",
         "//util:errors",
         "//util:statusor",
+        "@com_google_absl//absl/base:core_headers",
         "@com_google_absl//absl/container:flat_hash_map",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
@@ -507,20 +472,54 @@
     visibility = ["//visibility:public"],
     deps = [
         ":aead",
+        ":configuration",
+        ":insecure_secret_key_access",
+        ":key",
+        ":key_gen_configuration",
         ":key_manager",
+        ":key_status",
         ":keyset_reader",
         ":keyset_writer",
         ":primitive_set",
         ":registry",
+        "//internal:configuration_impl",
+        "//internal:key_gen_configuration_impl",
         "//internal:key_info",
+        "//internal:key_status_util",
+        "//internal:mutable_serialization_registry",
+        "//internal:proto_key_serialization",
+        "//internal:util",
         "//proto:tink_cc_proto",
         "//util:errors",
         "//util:keyset_util",
         "@com_google_absl//absl/base:core_headers",
         "@com_google_absl//absl/container:flat_hash_map",
+        "@com_google_absl//absl/log:check",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_library(
+    name = "keyset_handle_builder",
+    srcs = ["core/keyset_handle_builder.cc"],
+    hdrs = ["keyset_handle_builder.h"],
+    include_prefix = "tink",
+    visibility = ["//visibility:public"],
+    deps = [
+        ":key",
+        ":key_status",
+        ":keyset_handle",
+        ":parameters",
+        "//internal:keyset_handle_builder_entry",
+        "//proto:tink_cc_proto",
+        "//subtle:random",
+        "@com_google_absl//absl/log:check",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/types:optional",
     ],
 )
 
@@ -565,9 +564,9 @@
     include_prefix = "tink",
     visibility = ["//visibility:public"],
     deps = [
+        ":key_gen_configuration",
         ":keyset_handle",
-        ":keyset_reader",
-        ":registry",
+        "//internal:key_gen_configuration_impl",
         "//proto:tink_cc_proto",
         "//util:enums",
         "//util:errors",
@@ -703,6 +702,19 @@
 )
 
 cc_library(
+    name = "partial_key_access_token",
+    hdrs = ["partial_key_access_token.h"],
+    include_prefix = "tink",
+)
+
+cc_library(
+    name = "partial_key_access",
+    hdrs = ["partial_key_access.h"],
+    include_prefix = "tink",
+    deps = [":partial_key_access_token"],
+)
+
+cc_library(
     name = "secret_key_access_token",
     hdrs = ["secret_key_access_token.h"],
     include_prefix = "tink",
@@ -712,9 +724,31 @@
     name = "insecure_secret_key_access",
     hdrs = ["insecure_secret_key_access.h"],
     include_prefix = "tink",
+    visibility = ["//visibility:public"],
     deps = [":secret_key_access_token"],
 )
 
+cc_library(
+    name = "restricted_data",
+    srcs = ["core/restricted_data.cc"],
+    hdrs = ["restricted_data.h"],
+    include_prefix = "tink",
+    deps = [
+        ":secret_key_access_token",
+        "//subtle:random",
+        "//util:secret_data",
+        "@boringssl//:crypto",
+        "@com_google_absl//absl/log:check",
+    ],
+)
+
+cc_library(
+    name = "key_status",
+    hdrs = ["key_status.h"],
+    include_prefix = "tink",
+    visibility = ["//visibility:public"],
+)
+
 # tests
 
 cc_test(
@@ -723,6 +757,7 @@
     srcs = ["core/version_test.cc"],
     deps = [
         ":version",
+        "//internal:util",
         "@com_google_googletest//:gtest_main",
     ],
 )
@@ -788,18 +823,6 @@
 )
 
 cc_test(
-    name = "config_test",
-    size = "small",
-    srcs = ["core/config_test.cc"],
-    deps = [
-        ":config",
-        ":mac",
-        "//proto:config_cc_proto",
-        "@com_google_googletest//:gtest_main",
-    ],
-)
-
-cc_test(
     name = "crypto_format_test",
     size = "small",
     srcs = ["core/crypto_format_test.cc"],
@@ -820,6 +843,8 @@
         ":core/key_manager_impl",
         ":json_keyset_reader",
         ":json_keyset_writer",
+        ":key_gen_configuration",
+        ":key_status",
         ":keyset_handle",
         ":primitive_set",
         ":primitive_wrapper",
@@ -827,11 +852,16 @@
         "//aead:aead_key_templates",
         "//aead:aead_wrapper",
         "//aead:aes_gcm_key_manager",
+        "//config:fips_140_2",
+        "//config:key_gen_fips_140_2",
         "//config:tink_config",
+        "//config/internal:global_registry",
+        "//internal:fips_utils",
+        "//internal:key_gen_configuration_impl",
+        "//proto:aes_gcm_siv_cc_proto",
         "//proto:tink_cc_proto",
         "//signature:ecdsa_sign_key_manager",
         "//signature:signature_key_templates",
-        "//util:protobuf_helper",
         "//util:status",
         "//util:test_keyset_handle",
         "//util:test_matchers",
@@ -843,6 +873,34 @@
 )
 
 cc_test(
+    name = "keyset_handle_builder_test",
+    srcs = ["core/keyset_handle_builder_test.cc"],
+    deps = [
+        ":insecure_secret_key_access",
+        ":key_status",
+        ":keyset_handle_builder",
+        ":partial_key_access",
+        "//config:tink_config",
+        "//internal:legacy_proto_key",
+        "//internal:legacy_proto_parameters",
+        "//internal:proto_key_serialization",
+        "//internal:proto_parameters_serialization",
+        "//mac:aes_cmac_key",
+        "//mac:aes_cmac_parameters",
+        "//mac:mac_key_templates",
+        "//proto:aes_cmac_cc_proto",
+        "//proto:tink_cc_proto",
+        "//subtle:random",
+        "//util:status",
+        "//util:test_matchers",
+        "//util:test_util",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
     name = "key_manager_test",
     size = "small",
     srcs = ["core/key_manager_test.cc"],
@@ -861,7 +919,6 @@
     size = "small",
     srcs = ["core/keyset_manager_test.cc"],
     deps = [
-        ":config",
         ":keyset_handle",
         ":keyset_manager",
         "//aead:aead_config",
@@ -893,9 +950,11 @@
     size = "small",
     srcs = ["core/primitive_set_test.cc"],
     deps = [
+        ":cleartext_keyset_handle",
         ":crypto_format",
         ":mac",
         ":primitive_set",
+        "//keyderivation:keyset_deriver",
         "//proto:tink_cc_proto",
         "//util:test_matchers",
         "//util:test_util",
@@ -999,3 +1058,82 @@
         "@com_google_googletest//:gtest_main",
     ],
 )
+
+cc_test(
+    name = "core/partial_key_access_token_test",
+    srcs = ["core/partial_key_access_token_test.cc"],
+    deps = [
+        ":partial_key_access",
+        ":partial_key_access_token",
+        "@com_google_absl//absl/base:core_headers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "core/restricted_data_test",
+    srcs = ["core/restricted_data_test.cc"],
+    deps = [
+        ":insecure_secret_key_access",
+        ":restricted_data",
+        "//subtle:random",
+        "//util:secret_data",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "proto_keyset_format",
+    srcs = ["proto_keyset_format.cc"],
+    hdrs = ["proto_keyset_format.h"],
+    include_prefix = "tink",
+    visibility = ["//visibility:public"],
+    deps = [
+        ":binary_keyset_reader",
+        ":binary_keyset_writer",
+        ":cleartext_keyset_handle",
+        ":keyset_handle",
+        ":secret_key_access_token",
+        "//util:secret_data",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_test(
+    name = "proto_keyset_format_test",
+    srcs = ["proto_keyset_format_test.cc"],
+    deps = [
+        ":insecure_secret_key_access",
+        ":keyset_handle_builder",
+        ":mac",
+        ":proto_keyset_format",
+        "//config:tink_config",
+        "//internal:legacy_proto_parameters",
+        "//internal:proto_parameters_serialization",
+        "//mac:mac_key_templates",
+        "//signature:signature_key_templates",
+        "//util:secret_data",
+        "//util:test_matchers",
+        "@com_google_absl//absl/strings",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
+    name = "configuration",
+    hdrs = ["configuration.h"],
+    include_prefix = "tink",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//internal:key_type_info_store",
+        "//internal:keyset_wrapper_store",
+    ],
+)
+
+cc_library(
+    name = "key_gen_configuration",
+    hdrs = ["key_gen_configuration.h"],
+    include_prefix = "tink",
+    visibility = ["//visibility:public"],
+    deps = ["//internal:key_type_info_store"],
+)
diff --git a/cc/BUILD.gn b/cc/BUILD.gn
index 2ef21b2..e13bee0 100644
--- a/cc/BUILD.gn
+++ b/cc/BUILD.gn
@@ -181,19 +181,6 @@
   public_configs = [ "//third_party/tink:tink_config" ]
 }
 
-# CC Library : catalogue
-source_set("catalogue") {
-  configs += [ "//build/config:no_rtti" ]
-  configs -= [ "//build/config:no_rtti" ]
-  sources = [ "catalogue.h" ]
-  public_deps = [
-    ":key_manager",
-    "//third_party/abseil-cpp/absl/base:core_headers",
-    "//third_party/tink/cc/util:statusor",
-  ]
-  public_configs = [ "//third_party/tink:tink_config" ]
-}
-
 # CC Library : crypto_format
 source_set("crypto_format") {
   configs += [ "//build/config:no_rtti" ]
@@ -221,6 +208,7 @@
   ]
   public_deps = [
     ":crypto_format",
+    "//third_party/abseil-cpp/absl/base:core_headers",
     "//third_party/abseil-cpp/absl/container:flat_hash_map",
     "//third_party/abseil-cpp/absl/memory:memory",
     "//third_party/abseil-cpp/absl/status:status",
@@ -268,17 +256,30 @@
   ]
   public_deps = [
     ":aead",
+    ":configuration",
+    ":insecure_secret_key_access",
+    ":key",
+    ":key_gen_configuration",
     ":key_manager",
+    ":key_status",
     ":keyset_reader",
     ":keyset_writer",
     ":primitive_set",
     ":registry",
     "//third_party/abseil-cpp/absl/base:core_headers",
     "//third_party/abseil-cpp/absl/container:flat_hash_map",
+    "//third_party/abseil-cpp/absl/log:check",
     "//third_party/abseil-cpp/absl/memory:memory",
     "//third_party/abseil-cpp/absl/status:status",
     "//third_party/abseil-cpp/absl/strings:strings",
+    "//third_party/abseil-cpp/absl/types:optional",
+    "//third_party/tink/cc/internal:configuration_impl",
+    "//third_party/tink/cc/internal:key_gen_configuration_impl",
     "//third_party/tink/cc/internal:key_info",
+    "//third_party/tink/cc/internal:key_status_util",
+    "//third_party/tink/cc/internal:mutable_serialization_registry",
+    "//third_party/tink/cc/internal:proto_key_serialization",
+    "//third_party/tink/cc/internal:util",
     "//third_party/tink/cc/proto:tink_proto",
     "//third_party/tink/cc/util:errors",
     "//third_party/tink/cc/util:keyset_util",
@@ -433,3 +434,104 @@
   ]
   public_configs = [ "//third_party/tink:tink_config" ]
 }
+
+# CC Library : parameters
+source_set("parameters") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "parameters.h" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : key
+source_set("key") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "key.h" ]
+  public_deps = [
+    ":parameters",
+    "//third_party/abseil-cpp/absl/types:optional",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : partial_key_access_token
+source_set("partial_key_access_token") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "partial_key_access_token.h" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : partial_key_access
+source_set("partial_key_access") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "partial_key_access.h" ]
+  public_deps = [ ":partial_key_access_token" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : secret_key_access_token
+source_set("secret_key_access_token") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "secret_key_access_token.h" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : insecure_secret_key_access
+source_set("insecure_secret_key_access") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "insecure_secret_key_access.h" ]
+  public_deps = [ ":secret_key_access_token" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : restricted_data
+source_set("restricted_data") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [
+    "core/restricted_data.cc",
+    "restricted_data.h",
+  ]
+  public_deps = [
+    ":secret_key_access_token",
+    "//third_party/abseil-cpp/absl/log:check",
+    "//third_party/boringssl:crypto",
+    "//third_party/tink/cc/subtle:random",
+    "//third_party/tink/cc/util:secret_data",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : key_status
+source_set("key_status") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "key_status.h" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : configuration
+source_set("configuration") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "configuration.h" ]
+  public_deps = [
+    "//third_party/tink/cc/internal:key_type_info_store",
+    "//third_party/tink/cc/internal:keyset_wrapper_store",
+  ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
+
+# CC Library : key_gen_configuration
+source_set("key_gen_configuration") {
+  configs += [ "//build/config:no_rtti" ]
+  configs -= [ "//build/config:no_rtti" ]
+  sources = [ "key_gen_configuration.h" ]
+  public_deps = [ "//third_party/tink/cc/internal:key_type_info_store" ]
+  public_configs = [ "//third_party/tink:tink_config" ]
+}
diff --git a/cc/CMakeLists.txt b/cc/CMakeLists.txt
index a7defba..1cd07e4 100644
--- a/cc/CMakeLists.txt
+++ b/cc/CMakeLists.txt
@@ -6,6 +6,7 @@
 add_subdirectory(mac)
 add_subdirectory(monitoring)
 add_subdirectory(jwt)
+add_subdirectory(keyderivation)
 add_subdirectory(prf)
 add_subdirectory(signature)
 add_subdirectory(streamingaead)
@@ -16,12 +17,6 @@
 
 # Configuration settings for the build.
 
-if (TINK_USE_ABSL_STATUS)
-    target_compile_definitions(tink_util_status PUBLIC TINK_USE_ABSL_STATUS)
-endif()
-if (TINK_USE_ABSL_STATUSOR)
-    target_compile_definitions(tink_util_statusor INTERFACE TINK_USE_ABSL_STATUSOR)
-endif()
 if(USE_ONLY_FIPS)
     target_compile_definitions(tink_internal_fips_utils PUBLIC TINK_USE_ONLY_FIPS)
 endif()
@@ -39,9 +34,7 @@
     aead_key_templates.h
     binary_keyset_reader.h
     binary_keyset_writer.h
-    catalogue.h
     cleartext_keyset_handle.h
-    config.h
     deterministic_aead.h
     deterministic_aead_config.h
     deterministic_aead_factory.h
@@ -92,6 +85,7 @@
     tink::core::input_stream
     tink::core::json_keyset_reader
     tink::core::json_keyset_writer
+    tink::core::key
     tink::core::key_manager
     tink::core::keyset_handle
     tink::core::keyset_manager
@@ -373,39 +367,6 @@
 )
 
 tink_cc_library(
-  NAME catalogue
-  SRCS
-    catalogue.h
-  DEPS
-    tink::core::key_manager
-    absl::core_headers
-    tink::util::statusor
-)
-
-tink_cc_library(
-  NAME config
-  SRCS
-    core/config.cc
-    config.h
-  DEPS
-    tink::core::catalogue
-    tink::core::key_manager
-    tink::core::registry
-    absl::status
-    absl::strings
-    tink::aead::aead_config
-    tink::daead::deterministic_aead_config
-    tink::hybrid::hybrid_config
-    tink::mac::mac_config
-    tink::signature::signature_config
-    tink::streamingaead::streaming_aead_config
-    tink::util::errors
-    tink::util::status
-    tink::util::statusor
-    tink::proto::config_cc_proto
-)
-
-tink_cc_library(
   NAME crypto_format
   SRCS
     core/crypto_format.cc
@@ -424,6 +385,7 @@
     primitive_set.h
   DEPS
     tink::core::crypto_format
+    absl::core_headers
     absl::flat_hash_map
     absl::memory
     absl::status
@@ -472,23 +434,55 @@
     keyset_handle.h
   DEPS
     tink::core::aead
+    tink::core::configuration
+    tink::core::insecure_secret_key_access
+    tink::core::key
+    tink::core::key_gen_configuration
     tink::core::key_manager
+    tink::core::key_status
     tink::core::keyset_reader
     tink::core::keyset_writer
     tink::core::primitive_set
     tink::core::registry
     absl::core_headers
     absl::flat_hash_map
+    absl::check
     absl::memory
     absl::status
     absl::strings
+    absl::optional
+    tink::internal::configuration_impl
+    tink::internal::key_gen_configuration_impl
     tink::internal::key_info
+    tink::internal::key_status_util
+    tink::internal::mutable_serialization_registry
+    tink::internal::proto_key_serialization
+    tink::internal::util
     tink::util::errors
     tink::util::keyset_util
     tink::proto::tink_cc_proto
 )
 
 tink_cc_library(
+  NAME keyset_handle_builder
+  SRCS
+    core/keyset_handle_builder.cc
+    keyset_handle_builder.h
+  DEPS
+    tink::core::key
+    tink::core::key_status
+    tink::core::keyset_handle
+    tink::core::parameters
+    absl::check
+    absl::status
+    absl::strings
+    absl::optional
+    tink::internal::keyset_handle_builder_entry
+    tink::subtle::random
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_library(
   NAME cleartext_keyset_handle
   SRCS
     core/cleartext_keyset_handle.cc
@@ -525,13 +519,13 @@
     core/keyset_manager.cc
     keyset_manager.h
   DEPS
+    tink::core::key_gen_configuration
     tink::core::keyset_handle
-    tink::core::keyset_reader
-    tink::core::registry
     absl::core_headers
     absl::memory
     absl::status
     absl::synchronization
+    tink::internal::key_gen_configuration_impl
     tink::util::enums
     tink::util::errors
     tink::util::status
@@ -658,6 +652,26 @@
 )
 
 tink_cc_library(
+  NAME partial_key_access_token
+  SRCS
+    partial_key_access_token.h
+)
+
+tink_cc_library(
+  NAME partial_key_access
+  SRCS
+    partial_key_access.h
+  DEPS
+    tink::core::partial_key_access_token
+)
+
+tink_cc_library(
+  NAME secret_key_access_token
+  SRCS
+    secret_key_access_token.h
+)
+
+tink_cc_library(
   NAME insecure_secret_key_access
   SRCS
     insecure_secret_key_access.h
@@ -666,9 +680,22 @@
 )
 
 tink_cc_library(
-  NAME secret_key_access_token
+  NAME restricted_data
   SRCS
-    secret_key_access_token.h
+    core/restricted_data.cc
+    restricted_data.h
+  DEPS
+    tink::core::secret_key_access_token
+    absl::check
+    crypto
+    tink::subtle::random
+    tink::util::secret_data
+)
+
+tink_cc_library(
+  NAME key_status
+  SRCS
+    key_status.h
 )
 
 # tests
@@ -680,6 +707,7 @@
   DEPS
     tink::core::version
     gmock
+    tink::internal::util
 )
 
 tink_cc_test(
@@ -739,17 +767,6 @@
 )
 
 tink_cc_test(
-  NAME config_test
-  SRCS
-    core/config_test.cc
-  DEPS
-    tink::core::config
-    tink::core::mac
-    gmock
-    tink::proto::config_cc_proto
-)
-
-tink_cc_test(
   NAME crypto_format_test
   SRCS
     core/crypto_format_test.cc
@@ -770,6 +787,8 @@
     tink::core::key_manager_impl
     tink::core::json_keyset_reader
     tink::core::json_keyset_writer
+    tink::core::key_gen_configuration
+    tink::core::key_status
     tink::core::keyset_handle
     tink::core::primitive_set
     tink::core::primitive_wrapper
@@ -779,14 +798,47 @@
     tink::aead::aead_key_templates
     tink::aead::aead_wrapper
     tink::aead::aes_gcm_key_manager
+    tink::config::fips_140_2
+    tink::config::key_gen_fips_140_2
     tink::config::tink_config
+    tink::config::internal::global_registry
+    tink::internal::fips_utils
+    tink::internal::key_gen_configuration_impl
     tink::signature::ecdsa_sign_key_manager
     tink::signature::signature_key_templates
-    tink::util::protobuf_helper
     tink::util::status
     tink::util::test_keyset_handle
     tink::util::test_matchers
     tink::util::test_util
+    tink::proto::aes_gcm_siv_cc_proto
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_test(
+  NAME keyset_handle_builder_test
+  SRCS
+    core/keyset_handle_builder_test.cc
+  DEPS
+    tink::core::insecure_secret_key_access
+    tink::core::key_status
+    tink::core::keyset_handle_builder
+    tink::core::partial_key_access
+    gmock
+    absl::status
+    absl::strings
+    tink::config::tink_config
+    tink::internal::legacy_proto_key
+    tink::internal::legacy_proto_parameters
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
+    tink::mac::aes_cmac_key
+    tink::mac::aes_cmac_parameters
+    tink::mac::mac_key_templates
+    tink::subtle::random
+    tink::util::status
+    tink::util::test_matchers
+    tink::util::test_util
+    tink::proto::aes_cmac_cc_proto
     tink::proto::tink_cc_proto
 )
 
@@ -808,7 +860,6 @@
   SRCS
     core/keyset_manager_test.cc
   DEPS
-    tink::core::config
     tink::core::keyset_handle
     tink::core::keyset_manager
     gmock
@@ -838,10 +889,12 @@
   SRCS
     core/primitive_set_test.cc
   DEPS
+    tink::core::cleartext_keyset_handle
     tink::core::crypto_format
     tink::core::mac
     tink::core::primitive_set
     gmock
+    tink::keyderivation::keyset_deriver
     tink::util::test_matchers
     tink::util::test_util
     tink::proto::tink_cc_proto
@@ -942,3 +995,78 @@
     tink::core::secret_key_access_testonly
     gmock
 )
+
+tink_cc_test(
+  NAME partial_key_access_token_test
+  SRCS
+    core/partial_key_access_token_test.cc
+  DEPS
+    tink::core::partial_key_access
+    tink::core::partial_key_access_token
+    gmock
+    absl::core_headers
+)
+
+tink_cc_test(
+  NAME restricted_data_test
+  SRCS
+    core/restricted_data_test.cc
+  DEPS
+    tink::core::insecure_secret_key_access
+    tink::core::restricted_data
+    gmock
+    tink::subtle::random
+    tink::util::secret_data
+)
+
+tink_cc_library(
+  NAME proto_keyset_format
+  SRCS
+    proto_keyset_format.cc
+    proto_keyset_format.h
+  DEPS
+    tink::core::binary_keyset_reader
+    tink::core::binary_keyset_writer
+    tink::core::cleartext_keyset_handle
+    tink::core::keyset_handle
+    tink::core::secret_key_access_token
+    absl::strings
+    tink::util::secret_data
+)
+
+tink_cc_test(
+  NAME proto_keyset_format_test
+  SRCS
+    proto_keyset_format_test.cc
+  DEPS
+    tink::core::insecure_secret_key_access
+    tink::core::keyset_handle_builder
+    tink::core::mac
+    tink::core::proto_keyset_format
+    gmock
+    absl::strings
+    tink::config::tink_config
+    tink::internal::legacy_proto_parameters
+    tink::internal::proto_parameters_serialization
+    tink::mac::mac_key_templates
+    tink::signature::signature_key_templates
+    tink::util::secret_data
+    tink::util::test_matchers
+)
+
+tink_cc_library(
+  NAME configuration
+  SRCS
+    configuration.h
+  DEPS
+    tink::internal::key_type_info_store
+    tink::internal::keyset_wrapper_store
+)
+
+tink_cc_library(
+  NAME key_gen_configuration
+  SRCS
+    key_gen_configuration.h
+  DEPS
+    tink::internal::key_type_info_store
+)
diff --git a/cc/MODULE.bazel b/cc/MODULE.bazel
new file mode 100644
index 0000000..ab23a48
--- /dev/null
+++ b/cc/MODULE.bazel
@@ -0,0 +1,32 @@
+# Copyright 2023 Google LLC
+#
+# 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.
+
+"""Tink C++ Bazel Module definition."""
+module(
+    name = "tink_cc",
+    version = "2.0.0",
+)
+
+bazel_dep(name = "rules_cc", version = "0.0.5")
+bazel_dep(name = "rules_proto", version = "5.3.0-21.7")
+bazel_dep(name = "platforms", version = "0.0.6")
+bazel_dep(name = "bazel_skylib", version = "1.3.0")
+bazel_dep(name = "googletest", version = "1.12.1", repo_name = "com_google_googletest")
+bazel_dep(name = "protobuf", version = "21.7", repo_name = "com_google_protobuf")
+bazel_dep(name = "boringssl", version = "0.0.0-20230215-5c22014")
+bazel_dep(name = "rapidjson", version = "1.1.0")
+bazel_dep(name = "abseil-cpp", version = "20230125.1", repo_name="com_google_absl")
+
+wycheproof_extension = use_extension("//:extensions.bzl", "wycheproof_extension")
+use_repo(wycheproof_extension, "wycheproof")
diff --git a/cc/WORKSPACE b/cc/WORKSPACE
index ca79c62..af5976a 100644
--- a/cc/WORKSPACE
+++ b/cc/WORKSPACE
@@ -13,9 +13,3 @@
 load("@tink_cc//:tink_cc_deps_init.bzl", "tink_cc_deps_init")
 
 tink_cc_deps_init()
-
-load("@bazel_toolchains//rules:rbe_repo.bzl", "rbe_autoconfig")
-
-# Creates a default toolchain config for RBE. Use this as is if you are
-# using the rbe_ubuntu16_04 container, otherwise refer to RBE docs.
-rbe_autoconfig(name = "rbe_default")
diff --git a/cc/WORKSPACE.bzlmod b/cc/WORKSPACE.bzlmod
new file mode 100644
index 0000000..057d6aa
--- /dev/null
+++ b/cc/WORKSPACE.bzlmod
@@ -0,0 +1 @@
+# This replaces the content of the WORKSPACE file when using --enable_bzlmod.
diff --git a/cc/aead.h b/cc/aead.h
index 803fc51..45017d6 100644
--- a/cc/aead.h
+++ b/cc/aead.h
@@ -51,7 +51,7 @@
       absl::string_view ciphertext,
       absl::string_view associated_data) const = 0;
 
-  virtual ~Aead() {}
+  virtual ~Aead() = default;
 };
 
 }  // namespace tink
diff --git a/cc/aead/BUILD.bazel b/cc/aead/BUILD.bazel
index 28b3e8b..10648c4 100644
--- a/cc/aead/BUILD.bazel
+++ b/cc/aead/BUILD.bazel
@@ -214,6 +214,7 @@
         "//subtle:random",
         "//util:constants",
         "//util:enums",
+        "//util:input_stream_util",
         "//util:secret_data",
         "//util:status",
         "//util:statusor",
@@ -281,6 +282,7 @@
     deps = [
         "//:aead",
         "//:registry",
+        "//aead/internal:aead_util",
         "//proto:tink_cc_proto",
         "//util:status",
         "//util:statusor",
@@ -302,6 +304,7 @@
         "//:core/template_util",
         "//:kms_client",
         "//:kms_clients",
+        "//aead/internal:aead_util",
         "//internal:fips_utils",
         "//proto:kms_envelope_cc_proto",
         "//proto:tink_cc_proto",
@@ -329,6 +332,95 @@
     ],
 )
 
+cc_library(
+    name = "failing_aead",
+    testonly = 1,
+    srcs = ["failing_aead.cc"],
+    hdrs = ["failing_aead.h"],
+    include_prefix = "tink/aead",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//:aead",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_library(
+    name = "aead_parameters",
+    hdrs = ["aead_parameters.h"],
+    include_prefix = "tink/aead",
+    deps = ["//:parameters"],
+)
+
+cc_library(
+    name = "aead_key",
+    hdrs = ["aead_key.h"],
+    include_prefix = "tink/aead",
+    deps = [
+        ":aead_parameters",
+        "//:key",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_library(
+    name = "aes_gcm_parameters",
+    srcs = ["aes_gcm_parameters.cc"],
+    hdrs = ["aes_gcm_parameters.h"],
+    include_prefix = "tink/aead",
+    deps = [
+        ":aead_parameters",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_library(
+    name = "aes_gcm_key",
+    srcs = ["aes_gcm_key.cc"],
+    hdrs = ["aes_gcm_key.h"],
+    include_prefix = "tink/aead",
+    deps = [
+        ":aead_key",
+        ":aes_gcm_parameters",
+        "//:partial_key_access_token",
+        "//:restricted_data",
+        "//subtle:subtle_util",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_library(
+    name = "aes_gcm_proto_serialization",
+    srcs = ["aes_gcm_proto_serialization.cc"],
+    hdrs = ["aes_gcm_proto_serialization.h"],
+    include_prefix = "tink/aead",
+    deps = [
+        ":aes_gcm_key",
+        ":aes_gcm_parameters",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//:secret_key_access_token",
+        "//internal:key_parser",
+        "//internal:key_serializer",
+        "//internal:mutable_serialization_registry",
+        "//internal:parameters_parser",
+        "//internal:parameters_serializer",
+        "//internal:proto_key_serialization",
+        "//internal:proto_parameters_serialization",
+        "//proto:aes_gcm_cc_proto",
+        "//proto:tink_cc_proto",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
 # tests
 
 cc_test(
@@ -373,11 +465,11 @@
         "//:primitive_set",
         "//:registry",
         "//config:tink_fips",
+        "//internal:fips_utils",
         "//proto:tink_cc_proto",
         "//util:status",
         "//util:statusor",
         "//util:test_matchers",
-        "//util:test_util",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
         "@com_google_googletest//:gtest_main",
@@ -516,7 +608,6 @@
     deps = [
         ":aes_ctr_hmac_aead_key_manager",
         "//:aead",
-        "//:mac",
         "//proto:aes_ctr_cc_proto",
         "//proto:aes_ctr_hmac_aead_cc_proto",
         "//proto:common_cc_proto",
@@ -526,6 +617,7 @@
         "//subtle:aead_test_util",
         "//subtle:aes_ctr_boringssl",
         "//util:enums",
+        "//util:istream_input_stream",
         "//util:secret_data",
         "//util:status",
         "//util:statusor",
@@ -589,7 +681,9 @@
         ":aead_key_templates",
         ":kms_envelope_aead",
         "//:aead",
+        "//:keyset_handle",
         "//:registry",
+        "//internal:ssl_util",
         "//mac:mac_key_templates",
         "//proto:aes_gcm_cc_proto",
         "//util:status",
@@ -599,6 +693,7 @@
         "@com_google_absl//absl/base:endian",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
     ],
 )
@@ -608,6 +703,7 @@
     size = "small",
     srcs = ["kms_envelope_aead_key_manager_test.cc"],
     deps = [
+        ":aead_config",
         ":aead_key_templates",
         ":aes_eax_key_manager",
         ":kms_envelope_aead",
@@ -616,9 +712,11 @@
         "//:kms_client",
         "//:kms_clients",
         "//:registry",
+        "//mac:mac_key_templates",
         "//proto:kms_envelope_cc_proto",
         "//proto:tink_cc_proto",
         "//subtle:aead_test_util",
+        "//util:fake_kms_client",
         "//util:status",
         "//util:statusor",
         "//util:test_matchers",
@@ -650,19 +748,6 @@
     ],
 )
 
-cc_library(
-    name = "failing_aead",
-    testonly = 1,
-    srcs = ["failing_aead.cc"],
-    hdrs = ["failing_aead.h"],
-    include_prefix = "tink/aead",
-    visibility = ["//visibility:public"],
-    deps = [
-        "//:aead",
-        "@com_google_absl//absl/strings",
-    ],
-)
-
 cc_test(
     name = "failing_aead_test",
     srcs = ["failing_aead_test.cc"],
@@ -673,3 +758,51 @@
         "@com_google_googletest//:gtest_main",
     ],
 )
+
+cc_test(
+    name = "aes_gcm_parameters_test",
+    srcs = ["aes_gcm_parameters_test.cc"],
+    deps = [
+        ":aes_gcm_parameters",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "aes_gcm_key_test",
+    srcs = ["aes_gcm_key_test.cc"],
+    deps = [
+        ":aes_gcm_key",
+        ":aes_gcm_parameters",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/types:optional",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "aes_gcm_proto_serialization_test",
+    size = "small",
+    srcs = ["aes_gcm_proto_serialization_test.cc"],
+    deps = [
+        ":aes_gcm_key",
+        ":aes_gcm_parameters",
+        ":aes_gcm_proto_serialization",
+        "//:insecure_secret_key_access",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//internal:mutable_serialization_registry",
+        "//internal:proto_key_serialization",
+        "//internal:proto_parameters_serialization",
+        "//proto:aes_gcm_cc_proto",
+        "//proto:tink_cc_proto",
+        "//subtle:random",
+        "//util:test_matchers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
diff --git a/cc/aead/BUILD.gn b/cc/aead/BUILD.gn
index f4dc781..6eae32e 100644
--- a/cc/aead/BUILD.gn
+++ b/cc/aead/BUILD.gn
@@ -204,6 +204,7 @@
     "//third_party/tink/cc/subtle:random",
     "//third_party/tink/cc/util:constants",
     "//third_party/tink/cc/util:enums",
+    "//third_party/tink/cc/util:input_stream_util",
     "//third_party/tink/cc/util:secret_data",
     "//third_party/tink/cc/util:status",
     "//third_party/tink/cc/util:statusor",
@@ -277,6 +278,7 @@
     "//third_party/abseil-cpp/absl/strings:strings",
     "//third_party/tink/cc:aead",
     "//third_party/tink/cc:registry",
+    "//third_party/tink/cc/aead/internal:aead_util",
     "//third_party/tink/cc/proto:tink_proto",
     "//third_party/tink/cc/util:status",
     "//third_party/tink/cc/util:statusor",
@@ -302,6 +304,7 @@
     "//third_party/tink/cc:core/template_util",
     "//third_party/tink/cc:kms_client",
     "//third_party/tink/cc:kms_clients",
+    "//third_party/tink/cc/aead/internal:aead_util",
     "//third_party/tink/cc/internal:fips_utils",
     "//third_party/tink/cc/proto:kms_envelope_proto",
     "//third_party/tink/cc/proto:tink_proto",
diff --git a/cc/aead/CMakeLists.txt b/cc/aead/CMakeLists.txt
index c4adc4e..41264b9 100644
--- a/cc/aead/CMakeLists.txt
+++ b/cc/aead/CMakeLists.txt
@@ -199,6 +199,7 @@
     tink::subtle::random
     tink::util::constants
     tink::util::enums
+    tink::util::input_stream_util
     tink::util::secret_data
     tink::util::status
     tink::util::statusor
@@ -266,6 +267,7 @@
     absl::strings
     tink::core::aead
     tink::core::registry
+    tink::aead::internal::aead_util
     tink::util::status
     tink::util::statusor
     tink::proto::tink_cc_proto
@@ -286,6 +288,7 @@
     tink::core::template_util
     tink::core::kms_client
     tink::core::kms_clients
+    tink::aead::internal::aead_util
     tink::internal::fips_utils
     tink::util::constants
     tink::util::status
@@ -304,6 +307,91 @@
     absl::strings
     tink::core::aead
     tink::util::statusor
+  TESTONLY
+)
+
+tink_cc_library(
+  NAME failing_aead
+  SRCS
+    failing_aead.cc
+    failing_aead.h
+  DEPS
+    absl::strings
+    tink::core::aead
+  TESTONLY
+)
+
+tink_cc_library(
+  NAME aead_parameters
+  SRCS
+    aead_parameters.h
+  DEPS
+    tink::core::parameters
+)
+
+tink_cc_library(
+  NAME aead_key
+  SRCS
+    aead_key.h
+  DEPS
+    tink::aead::aead_parameters
+    absl::strings
+    tink::core::key
+)
+
+tink_cc_library(
+  NAME aes_gcm_parameters
+  SRCS
+    aes_gcm_parameters.cc
+    aes_gcm_parameters.h
+  DEPS
+    tink::aead::aead_parameters
+    absl::strings
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_library(
+  NAME aes_gcm_key
+  SRCS
+    aes_gcm_key.cc
+    aes_gcm_key.h
+  DEPS
+    tink::aead::aead_key
+    tink::aead::aes_gcm_parameters
+    absl::strings
+    absl::optional
+    tink::core::partial_key_access_token
+    tink::core::restricted_data
+    tink::subtle::subtle_util
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_library(
+  NAME aes_gcm_proto_serialization
+  SRCS
+    aes_gcm_proto_serialization.cc
+    aes_gcm_proto_serialization.h
+  DEPS
+    tink::aead::aes_gcm_key
+    tink::aead::aes_gcm_parameters
+    absl::status
+    absl::optional
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::core::secret_key_access_token
+    tink::internal::key_parser
+    tink::internal::key_serializer
+    tink::internal::mutable_serialization_registry
+    tink::internal::parameters_parser
+    tink::internal::parameters_serializer
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
+    tink::util::status
+    tink::util::statusor
+    tink::proto::aes_gcm_cc_proto
+    tink::proto::tink_cc_proto
 )
 
 # tests
@@ -351,10 +439,10 @@
     tink::core::primitive_set
     tink::core::registry
     tink::config::tink_fips
+    tink::internal::fips_utils
     tink::util::status
     tink::util::statusor
     tink::util::test_matchers
-    tink::util::test_util
     tink::proto::tink_cc_proto
 )
 
@@ -487,11 +575,11 @@
     gmock
     absl::status
     tink::core::aead
-    tink::core::mac
     tink::subtle::subtle
     tink::subtle::aead_test_util
     tink::subtle::aes_ctr_boringssl
     tink::util::enums
+    tink::util::istream_input_stream
     tink::util::secret_data
     tink::util::status
     tink::util::statusor
@@ -558,8 +646,11 @@
     absl::endian
     absl::memory
     absl::status
+    absl::strings
     tink::core::aead
+    tink::core::keyset_handle
     tink::core::registry
+    tink::internal::ssl_util
     tink::mac::mac_key_templates
     tink::util::status
     tink::util::statusor
@@ -573,6 +664,7 @@
   SRCS
     kms_envelope_aead_key_manager_test.cc
   DEPS
+    tink::aead::aead_config
     tink::aead::aead_key_templates
     tink::aead::aes_eax_key_manager
     tink::aead::kms_envelope_aead
@@ -584,7 +676,9 @@
     tink::core::kms_client
     tink::core::kms_clients
     tink::core::registry
+    tink::mac::mac_key_templates
     tink::subtle::aead_test_util
+    tink::util::fake_kms_client
     tink::util::status
     tink::util::statusor
     tink::util::test_matchers
@@ -612,16 +706,6 @@
     tink::proto::tink_cc_proto
 )
 
-tink_cc_library(
-  NAME failing_aead
-  SRCS
-    failing_aead.cc
-    failing_aead.h
-  DEPS
-    absl::strings
-    tink::core::aead
-)
-
 tink_cc_test(
   NAME failing_aead_test
   SRCS
@@ -632,3 +716,50 @@
     absl::status
     tink::util::test_matchers
 )
+
+tink_cc_test(
+  NAME aes_gcm_parameters_test
+  SRCS
+    aes_gcm_parameters_test.cc
+  DEPS
+    tink::aead::aes_gcm_parameters
+    gmock
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_test(
+  NAME aes_gcm_key_test
+  SRCS
+    aes_gcm_key_test.cc
+  DEPS
+    tink::aead::aes_gcm_key
+    tink::aead::aes_gcm_parameters
+    gmock
+    absl::optional
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_test(
+  NAME aes_gcm_proto_serialization_test
+  SRCS
+    aes_gcm_proto_serialization_test.cc
+  DEPS
+    tink::aead::aes_gcm_key
+    tink::aead::aes_gcm_parameters
+    tink::aead::aes_gcm_proto_serialization
+    gmock
+    tink::core::insecure_secret_key_access
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::internal::mutable_serialization_registry
+    tink::internal::proto_key_serialization
+    tink::internal::proto_parameters_serialization
+    tink::subtle::random
+    tink::util::test_matchers
+    tink::proto::aes_gcm_cc_proto
+    tink::proto::tink_cc_proto
+)
diff --git a/cc/aead/aead_config.cc b/cc/aead/aead_config.cc
index d627064..b5a84ca 100644
--- a/cc/aead/aead_config.cc
+++ b/cc/aead/aead_config.cc
@@ -34,15 +34,6 @@
 
 namespace crypto {
 namespace tink {
-
-using ::google::crypto::tink::RegistryConfig;
-
-// static
-const RegistryConfig& AeadConfig::Latest() {
-  static const RegistryConfig* config = new RegistryConfig();
-  return *config;
-}
-
 // static
 util::Status AeadConfig::Register() {
   auto status = MacConfig::Register();
diff --git a/cc/aead/aead_config.h b/cc/aead/aead_config.h
index 6431d64..08cb196 100644
--- a/cc/aead/aead_config.h
+++ b/cc/aead/aead_config.h
@@ -34,14 +34,6 @@
 //
 class AeadConfig {
  public:
-  static constexpr char kCatalogueName[] = "TinkAead";
-  static constexpr char kPrimitiveName[] = "Aead";
-
-  // Returns config of Aead implementations supported
-  // in the current Tink release.
-  ABSL_DEPRECATED("This is not supported anymore.")
-  static const google::crypto::tink::RegistryConfig& Latest();
-
   // Registers Aead primitive wrapper and key managers for all Aead key types
   // from the current Tink release.
   static crypto::tink::util::Status Register();
diff --git a/cc/aead/aead_config_test.cc b/cc/aead/aead_config_test.cc
index 726e3d1..4a7945d 100644
--- a/cc/aead/aead_config_test.cc
+++ b/cc/aead/aead_config_test.cc
@@ -29,27 +29,24 @@
 #include "tink/aead/aead_key_templates.h"
 #include "tink/aead/aes_gcm_key_manager.h"
 #include "tink/config/tink_fips.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/keyset_handle.h"
 #include "tink/primitive_set.h"
 #include "tink/registry.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
-#include "tink/util/test_util.h"
 #include "proto/tink.pb.h"
 
 namespace crypto {
 namespace tink {
 namespace {
 
-using ::crypto::tink::test::DummyAead;
 using ::crypto::tink::test::IsOk;
 using ::crypto::tink::test::StatusIs;
-using ::google::crypto::tink::KeysetInfo;
-using ::google::crypto::tink::KeyStatusType;
+using ::crypto::tink::util::StatusOr;
 using ::google::crypto::tink::KeyTemplate;
-using ::google::crypto::tink::OutputPrefixType;
-using ::testing::Eq;
+using ::testing::IsNull;
 using ::testing::Not;
 using ::testing::Test;
 
@@ -80,35 +77,17 @@
 
   ASSERT_THAT(AeadConfig::Register(), IsOk());
 
-  KeysetInfo::KeyInfo key_info;
-  key_info.set_status(KeyStatusType::ENABLED);
-  key_info.set_key_id(1234);
-  key_info.set_output_prefix_type(OutputPrefixType::RAW);
-  auto primitive_set = absl::make_unique<PrimitiveSet<Aead>>();
-  ASSERT_THAT(primitive_set->set_primary(*primitive_set->AddPrimitive(
-                  absl::make_unique<DummyAead>("dummy"), key_info)),
-              IsOk());
-
-  util::StatusOr<std::unique_ptr<Aead>> primitive_result =
-      Registry::Wrap(std::move(primitive_set));
-
-  ASSERT_THAT(primitive_result, IsOk());
-  util::StatusOr<std::string> encryption_result =
-      (*primitive_result)->Encrypt("secret", "");
-  ASSERT_THAT(encryption_result, IsOk());
-
-  util::StatusOr<std::string> decryption_result =
-      DummyAead("dummy").Decrypt(*encryption_result, "");
-  ASSERT_THAT(decryption_result, IsOk());
-  EXPECT_THAT(*decryption_result, Eq("secret"));
-
-  decryption_result = DummyAead("dummy").Decrypt(*encryption_result, "wrong");
-  EXPECT_THAT(decryption_result, Not(IsOk()));
+  StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
+      KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Gcm());
+  ASSERT_THAT(keyset_handle.status(), IsOk());
+  StatusOr<std::unique_ptr<Aead>> aead = (*keyset_handle)->GetPrimitive<Aead>();
+  ASSERT_THAT(aead.status(), IsOk());
+  ASSERT_THAT(*aead, Not(IsNull()));
 }
 
 // FIPS-only mode tests
 TEST_F(AeadConfigTest, RegisterNonFipsTemplates) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Only supported in FIPS-only mode with BoringCrypto.";
   }
 
@@ -128,7 +107,7 @@
 }
 
 TEST_F(AeadConfigTest, RegisterFipsValidTemplates) {
-  if (!IsFipsModeEnabled() || !FIPS_mode()) {
+  if (!IsFipsModeEnabled() || !internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Only supported in FIPS-only mode with BoringCrypto.";
   }
 
@@ -148,7 +127,7 @@
 }
 
 TEST_F(AeadConfigTest, RegisterFailsIfBoringCryptoNotAvailable) {
-  if (!IsFipsModeEnabled() || FIPS_mode()) {
+  if (!IsFipsModeEnabled() || internal::IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Only supported in FIPS-only mode with BoringCrypto not available.";
   }
diff --git a/cc/aead/aead_key.h b/cc/aead/aead_key.h
new file mode 100644
index 0000000..869ddc8
--- /dev/null
+++ b/cc/aead/aead_key.h
@@ -0,0 +1,52 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_AEAD_AEAD_KEY_H_
+#define TINK_AEAD_AEAD_KEY_H_
+
+#include "absl/strings/string_view.h"
+#include "tink/aead/aead_parameters.h"
+#include "tink/key.h"
+
+namespace crypto {
+namespace tink {
+
+// Represents a function to encrypt and decrypt data using authenticated
+// encryption with associated data (AEAD).
+class AeadKey : public Key {
+ public:
+  // Returns the bytes prefixed to every ciphertext generated by this key.
+  //
+  // In order to make key rotation more efficient, Tink allows every AEAD key to
+  // have an associated ciphertext output prefix. When decrypting a ciphertext,
+  // only keys with a matching prefix have to be tried.
+  //
+  // Note that a priori, the output prefix may not be unique in a keyset
+  // (i.e., different keys in a keyset may have the same prefix or one prefix
+  // may be a prefix of another). To avoid this, built-in Tink keys use the
+  // convention that the prefix is either '0x00<big endian key id>' or
+  // '0x01<big endian key id>'.
+  virtual absl::string_view GetOutputPrefix() const = 0;
+
+  const AeadParameters& GetParameters() const override = 0;
+
+  bool operator==(const Key& other) const override = 0;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_AEAD_AEAD_KEY_H_
diff --git a/cc/aead/aead_parameters.h b/cc/aead/aead_parameters.h
new file mode 100644
index 0000000..dc5caad
--- /dev/null
+++ b/cc/aead/aead_parameters.h
@@ -0,0 +1,32 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_AEAD_AEAD_PARAMETERS_H_
+#define TINK_AEAD_AEAD_PARAMETERS_H_
+
+#include "tink/parameters.h"
+
+namespace crypto {
+namespace tink {
+
+// Describes an `AeadKey` (e.g., key attributes), excluding the randomly chosen
+// key material.
+class AeadParameters : public Parameters {};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_AEAD_AEAD_PARAMETERS_H_
diff --git a/cc/aead/aes_ctr_hmac_aead_key_manager.cc b/cc/aead/aes_ctr_hmac_aead_key_manager.cc
index ffe7867..10a0634 100644
--- a/cc/aead/aes_ctr_hmac_aead_key_manager.cc
+++ b/cc/aead/aes_ctr_hmac_aead_key_manager.cc
@@ -34,6 +34,7 @@
 #include "tink/subtle/ind_cpa_cipher.h"
 #include "tink/subtle/random.h"
 #include "tink/util/enums.h"
+#include "tink/util/input_stream_util.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
@@ -52,12 +53,14 @@
 constexpr int kMinTagSizeInBytes = 10;
 }
 
-using crypto::tink::util::Enums;
-using crypto::tink::util::Status;
-using crypto::tink::util::StatusOr;
-using google::crypto::tink::AesCtrHmacAeadKey;
-using google::crypto::tink::AesCtrHmacAeadKeyFormat;
-using google::crypto::tink::HashType;
+using ::crypto::tink::util::Enums;
+using ::crypto::tink::util::Status;
+using ::crypto::tink::util::StatusOr;
+using ::google::crypto::tink::AesCtrHmacAeadKey;
+using ::google::crypto::tink::AesCtrHmacAeadKeyFormat;
+using ::google::crypto::tink::AesCtrKey;
+using ::google::crypto::tink::HashType;
+using ::google::crypto::tink::HmacKey;
 
 StatusOr<AesCtrHmacAeadKey> AesCtrHmacAeadKeyManager::CreateKey(
     const AesCtrHmacAeadKeyFormat& aes_ctr_hmac_aead_key_format) const {
@@ -176,5 +179,53 @@
   return HmacKeyManager().ValidateKeyFormat(key_format.hmac_key_format());
 }
 
+// To ensure the resulting key can provide key commitment, the AES-CTR key must
+// be derived first, then the HMAC key. This avoids situation where it's
+// possible to brute force raw key material so that the 32th byte of the
+// keystream is a 0 Give party A a key with this raw key material, saying that
+// the size of the HMAC key is 32 bytes and the size of the AES key is 16 bytes.
+// Give party B a key with this raw key material, saying that the size of the
+// HMAC key is 31 bytes and the size of the AES key is 16 bytes. Since HMAC will
+// pad the key with zeroes, this leads to both parties using the same HMAC key,
+// but a different AES key (offset by 1 byte)
+StatusOr<AesCtrHmacAeadKey> AesCtrHmacAeadKeyManager::DeriveKey(
+    const AesCtrHmacAeadKeyFormat& key_format,
+    InputStream* input_stream) const {
+  Status status = ValidateKeyFormat(key_format);
+  if (!status.ok()) {
+    return status;
+  }
+  StatusOr<std::string> aes_ctr_randomness = ReadBytesFromStream(
+      key_format.aes_ctr_key_format().key_size(), input_stream);
+  if (!aes_ctr_randomness.ok()) {
+    if (absl::IsOutOfRange(aes_ctr_randomness.status())) {
+      return crypto::tink::util::Status(
+          absl::StatusCode::kInvalidArgument,
+          "Could not get enough pseudorandomness from input stream");
+    }
+    return aes_ctr_randomness.status();
+  }
+  StatusOr<HmacKey> hmac_key =
+      HmacKeyManager().DeriveKey(key_format.hmac_key_format(), input_stream);
+  if (!hmac_key.ok()) {
+    return hmac_key.status();
+  }
+
+  google::crypto::tink::AesCtrHmacAeadKey key;
+  key.set_version(get_version());
+  *key.mutable_hmac_key() = hmac_key.value();
+
+  AesCtrKey* aes_ctr_key = key.mutable_aes_ctr_key();
+  aes_ctr_key->set_version(get_version());
+  aes_ctr_key->set_key_value(aes_ctr_randomness.value());
+  *aes_ctr_key->mutable_params() = key_format.aes_ctr_key_format().params();
+
+  status = ValidateKey(key);
+  if (!status.ok()) {
+    return status;
+  }
+  return key;
+}
+
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/aead/aes_ctr_hmac_aead_key_manager.h b/cc/aead/aes_ctr_hmac_aead_key_manager.h
index a241a9c..e3b2f42 100644
--- a/cc/aead/aes_ctr_hmac_aead_key_manager.h
+++ b/cc/aead/aes_ctr_hmac_aead_key_manager.h
@@ -67,6 +67,10 @@
   CreateKey(const google::crypto::tink::AesCtrHmacAeadKeyFormat& key_format)
       const override;
 
+  crypto::tink::util::StatusOr<google::crypto::tink::AesCtrHmacAeadKey>
+  DeriveKey(const google::crypto::tink::AesCtrHmacAeadKeyFormat& key_format,
+            InputStream* input_stream) const override;
+
   internal::FipsCompatibility FipsStatus() const override {
     return internal::FipsCompatibility::kRequiresBoringCrypto;
   }
diff --git a/cc/aead/aes_ctr_hmac_aead_key_manager_test.cc b/cc/aead/aes_ctr_hmac_aead_key_manager_test.cc
index 6815ec3..b84711a 100644
--- a/cc/aead/aes_ctr_hmac_aead_key_manager_test.cc
+++ b/cc/aead/aes_ctr_hmac_aead_key_manager_test.cc
@@ -19,6 +19,7 @@
 #include <stdint.h>
 
 #include <memory>
+#include <sstream>
 #include <string>
 #include <utility>
 
@@ -26,13 +27,13 @@
 #include "gtest/gtest.h"
 #include "absl/status/status.h"
 #include "tink/aead.h"
-#include "tink/mac.h"
 #include "tink/subtle/aead_test_util.h"
 #include "tink/subtle/aes_ctr_boringssl.h"
 #include "tink/subtle/encrypt_then_authenticate.h"
 #include "tink/subtle/hmac_boringssl.h"
 #include "tink/subtle/ind_cpa_cipher.h"
 #include "tink/util/enums.h"
+#include "tink/util/istream_input_stream.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
@@ -48,11 +49,11 @@
 
 using ::crypto::tink::test::IsOk;
 using ::crypto::tink::test::StatusIs;
+using ::crypto::tink::util::IstreamInputStream;
 using ::crypto::tink::util::StatusOr;
 using ::google::crypto::tink::AesCtrHmacAeadKey;
 using ::google::crypto::tink::AesCtrHmacAeadKeyFormat;
 using ::google::crypto::tink::HashType;
-using ::google::crypto::tink::KeyData;
 using ::testing::Eq;
 using ::testing::Not;
 using ::testing::SizeIs;
@@ -146,14 +147,24 @@
   AesCtrHmacAeadKeyFormat key_format = CreateValidKeyFormat();
   for (int len = 0; len < 42; ++len) {
     key_format.mutable_aes_ctr_key_format()->set_key_size(len);
+    IstreamInputStream input_stream{absl::make_unique<std::stringstream>(
+      "0123456789abcde0123456789abcdefghijklmnopqrztuvwxyz0123456789abcde01"
+      "23456789abcdefghijklmnopqrztuvwxyz0123456789abcde0123456789abcdefghi"
+      "jklmnopqrztuvwxyz")};
     if (len == 16 || len == 32) {
       EXPECT_THAT(AesCtrHmacAeadKeyManager().ValidateKeyFormat(key_format),
                   IsOk())
           << "for length " << len;
+      EXPECT_THAT(
+          AesCtrHmacAeadKeyManager().DeriveKey(key_format, &input_stream),
+          IsOk());
     } else {
       EXPECT_THAT(AesCtrHmacAeadKeyManager().ValidateKeyFormat(key_format),
                   Not(IsOk()))
           << "for length " << len;
+      EXPECT_THAT(
+          AesCtrHmacAeadKeyManager().DeriveKey(key_format, &input_stream),
+          Not(IsOk()));
     }
   }
 }
@@ -162,14 +173,24 @@
   AesCtrHmacAeadKeyFormat key_format = CreateValidKeyFormat();
   for (int len = 0; len < 42; ++len) {
     key_format.mutable_hmac_key_format()->set_key_size(len);
+    IstreamInputStream input_stream{absl::make_unique<std::stringstream>(
+      "0123456789abcde0123456789abcdefghijklmnopqrztuvwxyz0123456789abcde01"
+      "23456789abcdefghijklmnopqrztuvwxyz0123456789abcde0123456789abcdefghi"
+      "jklmnopqrztuvwxyz")};
     if (len >= 16) {
       EXPECT_THAT(AesCtrHmacAeadKeyManager().ValidateKeyFormat(key_format),
                   IsOk())
           << "for length " << len;
+      EXPECT_THAT(
+          AesCtrHmacAeadKeyManager().DeriveKey(key_format, &input_stream),
+          IsOk());
     } else {
       EXPECT_THAT(AesCtrHmacAeadKeyManager().ValidateKeyFormat(key_format),
                   Not(IsOk()))
           << "for length " << len;
+      EXPECT_THAT(
+          AesCtrHmacAeadKeyManager().DeriveKey(key_format, &input_stream),
+          Not(IsOk()));
     }
   }
 }
@@ -222,6 +243,92 @@
               IsOk());
 }
 
+TEST(AesCtrHmacAeadKeyManagerTest, Derive16ByteKey) {
+  AesCtrHmacAeadKeyFormat key_format;
+  key_format.mutable_aes_ctr_key_format()->set_key_size(16);
+  key_format.mutable_aes_ctr_key_format()->mutable_params()->set_iv_size(16);
+  key_format.mutable_hmac_key_format()->set_key_size(16);
+  key_format.mutable_hmac_key_format()->mutable_params()->set_tag_size(16);
+  key_format.mutable_hmac_key_format()->mutable_params()->set_hash(
+      google::crypto::tink::SHA256);
+  key_format.mutable_hmac_key_format()->set_version(0);
+
+  IstreamInputStream input_stream{absl::make_unique<std::stringstream>(
+      "0123456789abcde_YELLOW_SUBMARINE_EXTRA")};
+
+  StatusOr<AesCtrHmacAeadKey> derived_key =
+      AesCtrHmacAeadKeyManager().DeriveKey(key_format, &input_stream);
+  ASSERT_THAT(derived_key, IsOk());
+  EXPECT_THAT(derived_key.value().aes_ctr_key().key_value(),
+              Eq("0123456789abcde_"));
+  EXPECT_THAT(derived_key.value().hmac_key().key_value(),
+              Eq("YELLOW_SUBMARINE"));
+  EXPECT_THAT(derived_key.value().hmac_key().params().hash(),
+              key_format.hmac_key_format().params().hash());
+  EXPECT_THAT(derived_key.value().hmac_key().params().tag_size(),
+              key_format.hmac_key_format().params().tag_size());
+  EXPECT_THAT(derived_key.value().aes_ctr_key().params().iv_size(),
+              Eq(key_format.aes_ctr_key_format().params().iv_size()));
+}
+
+TEST(AesCtrHmacAeadKeyManagerTest, Derive32ByteKey) {
+  AesCtrHmacAeadKeyFormat format;
+  format.mutable_aes_ctr_key_format()->set_key_size(32);
+  format.mutable_aes_ctr_key_format()->mutable_params()->set_iv_size(16);
+  format.mutable_hmac_key_format()->set_key_size(32);
+  format.mutable_hmac_key_format()->mutable_params()->set_tag_size(16);
+  format.mutable_hmac_key_format()->mutable_params()->set_hash(
+      google::crypto::tink::SHA256);
+  format.mutable_hmac_key_format()->set_version(0);
+
+  IstreamInputStream input_stream{absl::make_unique<std::stringstream>(
+      "0123456789abcde0123456789abcdef_YELLOW_SUBMARINE_YELLOW_SUBMARIN")};
+
+  StatusOr<AesCtrHmacAeadKey> derived_key =
+      AesCtrHmacAeadKeyManager().DeriveKey(format, &input_stream);
+  ASSERT_THAT(derived_key, IsOk());
+  EXPECT_THAT(derived_key.value().aes_ctr_key().key_value(),
+              Eq("0123456789abcde0123456789abcdef_"));
+  EXPECT_THAT(derived_key.value().hmac_key().key_value(),
+              Eq("YELLOW_SUBMARINE_YELLOW_SUBMARIN"));
+}
+
+TEST(AesCtrHmacAeadKeyManagerTest, DeriveKeyNotEnoughRandomnessForAesCtrKey) {
+  AesCtrHmacAeadKeyFormat format;
+  format.mutable_aes_ctr_key_format()->set_key_size(32);
+  format.mutable_aes_ctr_key_format()->mutable_params()->set_iv_size(16);
+  format.mutable_hmac_key_format()->set_key_size(32);
+  format.mutable_hmac_key_format()->mutable_params()->set_tag_size(16);
+  format.mutable_hmac_key_format()->mutable_params()->set_hash(
+      google::crypto::tink::SHA256);
+  format.mutable_hmac_key_format()->set_version(0);
+
+  IstreamInputStream input_stream{
+      absl::make_unique<std::stringstream>("0123456789")};
+
+  ASSERT_THAT(
+      AesCtrHmacAeadKeyManager().DeriveKey(format, &input_stream).status(),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesCtrHmacAeadKeyManagerTest, DeriveKeyNotEnoughRandomnessForHmacKey) {
+  AesCtrHmacAeadKeyFormat format;
+  format.mutable_aes_ctr_key_format()->set_key_size(16);
+  format.mutable_aes_ctr_key_format()->mutable_params()->set_iv_size(16);
+  format.mutable_hmac_key_format()->set_key_size(32);
+  format.mutable_hmac_key_format()->mutable_params()->set_tag_size(16);
+  format.mutable_hmac_key_format()->mutable_params()->set_hash(
+      google::crypto::tink::SHA256);
+  format.mutable_hmac_key_format()->set_version(0);
+
+  IstreamInputStream input_stream{
+      absl::make_unique<std::stringstream>("YELLOW_SUBMARINE")};
+
+  ASSERT_THAT(
+      AesCtrHmacAeadKeyManager().DeriveKey(format, &input_stream).status(),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
 }  // namespace
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/aead/aes_gcm_key.cc b/cc/aead/aes_gcm_key.cc
new file mode 100644
index 0000000..4e93443
--- /dev/null
+++ b/cc/aead/aes_gcm_key.cc
@@ -0,0 +1,107 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/aead/aes_gcm_key.h"
+
+#include <string>
+
+#include "absl/strings/escaping.h"
+#include "absl/types/optional.h"
+#include "tink/aead/aes_gcm_parameters.h"
+#include "tink/partial_key_access_token.h"
+#include "tink/restricted_data.h"
+#include "tink/subtle/subtle_util.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+util::StatusOr<std::string> ComputeOutputPrefix(
+    const AesGcmParameters& parameters, absl::optional<int> id_requirement) {
+  switch (parameters.GetVariant()) {
+    case AesGcmParameters::Variant::kNoPrefix:
+      return std::string("");  // Empty prefix.
+    case AesGcmParameters::Variant::kCrunchy:
+      if (!id_requirement.has_value()) {
+        return util::Status(
+            absl::StatusCode::kInvalidArgument,
+            "id requirement must have value with kCrunchy or kLegacy");
+      }
+      return absl::StrCat(absl::HexStringToBytes("00"),
+                          subtle::BigEndian32(*id_requirement));
+    case AesGcmParameters::Variant::kTink:
+      if (!id_requirement.has_value()) {
+        return util::Status(absl::StatusCode::kInvalidArgument,
+                            "id requirement must have value with kTink");
+      }
+      return absl::StrCat(absl::HexStringToBytes("01"),
+                          subtle::BigEndian32(*id_requirement));
+    default:
+      return util::Status(
+          absl::StatusCode::kInvalidArgument,
+          absl::StrCat("Invalid variant: ", parameters.GetVariant()));
+  }
+}
+
+}  // namespace
+
+util::StatusOr<AesGcmKey> AesGcmKey::Create(const AesGcmParameters& parameters,
+                                            const RestrictedData& key_bytes,
+                                            absl::optional<int> id_requirement,
+                                            PartialKeyAccessToken token) {
+  if (parameters.KeySizeInBytes() != key_bytes.size()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Key size does not match AES-GCM parameters");
+  }
+  if (parameters.HasIdRequirement() && !id_requirement.has_value()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create key without ID requirement with parameters with ID "
+        "requirement");
+  }
+  if (!parameters.HasIdRequirement() && id_requirement.has_value()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create key with ID requirement with parameters without ID "
+        "requirement");
+  }
+  util::StatusOr<std::string> output_prefix =
+      ComputeOutputPrefix(parameters, id_requirement);
+  if (!output_prefix.ok()) {
+    return output_prefix.status();
+  }
+  return AesGcmKey(parameters, key_bytes, id_requirement,
+                   *std::move(output_prefix));
+}
+
+bool AesGcmKey::operator==(const Key& other) const {
+  const AesGcmKey* that = dynamic_cast<const AesGcmKey*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  if (GetParameters() != that->GetParameters()) {
+    return false;
+  }
+  if (id_requirement_ != that->id_requirement_) {
+    return false;
+  }
+  return key_bytes_ == that->key_bytes_;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/aead/aes_gcm_key.h b/cc/aead/aes_gcm_key.h
new file mode 100644
index 0000000..8159ea7
--- /dev/null
+++ b/cc/aead/aes_gcm_key.h
@@ -0,0 +1,84 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_AEAD_AES_GCM_KEY_H_
+#define TINK_AEAD_AES_GCM_KEY_H_
+
+#include <string>
+#include <utility>
+
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "tink/aead/aead_key.h"
+#include "tink/aead/aes_gcm_parameters.h"
+#include "tink/partial_key_access_token.h"
+#include "tink/restricted_data.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+// Represents an AEAD that uses AES-GCM.
+class AesGcmKey : public AeadKey {
+ public:
+  // Copyable and movable.
+  AesGcmKey(const AesGcmKey& other) = default;
+  AesGcmKey& operator=(const AesGcmKey& other) = default;
+  AesGcmKey(AesGcmKey&& other) = default;
+  AesGcmKey& operator=(AesGcmKey&& other) = default;
+
+  // Creates a new AES-GCM key.  If the parameters specify a variant that uses
+  // a prefix, then the id is used to compute this prefix.
+  static util::StatusOr<AesGcmKey> Create(const AesGcmParameters& parameters,
+                                          const RestrictedData& key_bytes,
+                                          absl::optional<int> id_requirement,
+                                          PartialKeyAccessToken token);
+
+  // Returns the underlying AES key.
+  util::StatusOr<RestrictedData> GetKeyBytes(
+      PartialKeyAccessToken token) const {
+    return key_bytes_;
+  }
+
+  absl::string_view GetOutputPrefix() const override { return output_prefix_; }
+
+  const AesGcmParameters& GetParameters() const override { return parameters_; }
+
+  absl::optional<int> GetIdRequirement() const override {
+    return id_requirement_;
+  }
+
+  bool operator==(const Key& other) const override;
+
+ private:
+  AesGcmKey(const AesGcmParameters& parameters, const RestrictedData& key_bytes,
+            absl::optional<int> id_requirement,
+            std::string output_prefix)
+      : parameters_(parameters),
+        key_bytes_(key_bytes),
+        id_requirement_(id_requirement),
+        output_prefix_(std::move(output_prefix)) {}
+
+  AesGcmParameters parameters_;
+  RestrictedData key_bytes_;
+  absl::optional<int> id_requirement_;
+  std::string output_prefix_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_AEAD_AES_GCM_KEY_H_
diff --git a/cc/aead/aes_gcm_key_test.cc b/cc/aead/aes_gcm_key_test.cc
new file mode 100644
index 0000000..06a0a35
--- /dev/null
+++ b/cc/aead/aes_gcm_key_test.cc
@@ -0,0 +1,285 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/aead/aes_gcm_key.h"
+
+#include <string>
+#include <tuple>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/types/optional.h"
+#include "tink/aead/aes_gcm_parameters.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::testing::Combine;
+using ::testing::Eq;
+using ::testing::Range;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct TestCase {
+  AesGcmParameters::Variant variant;
+  absl::optional<int> id_requirement;
+  std::string output_prefix;
+};
+
+using AesGcmKeyTest = TestWithParam<std::tuple<int, int, TestCase>>;
+
+INSTANTIATE_TEST_SUITE_P(
+    AesGcmKeyTestSuite, AesGcmKeyTest,
+    Combine(Values(16, 24, 32), Range(12, 16),
+            Values(TestCase{AesGcmParameters::Variant::kTink, 0x02030400,
+                            std::string("\x01\x02\x03\x04\x00", 5)},
+                   TestCase{AesGcmParameters::Variant::kCrunchy, 0x01030005,
+                            std::string("\x00\x01\x03\x00\x05", 5)},
+                   TestCase{AesGcmParameters::Variant::kNoPrefix, absl::nullopt,
+                            ""})));
+
+TEST_P(AesGcmKeyTest, CreateSucceeds) {
+  int key_size;
+  int iv_and_tag_size;  // NOTE: There's no requirement for IV size == tag size.
+  TestCase test_case;
+  std::tie(key_size, iv_and_tag_size, test_case) = GetParam();
+
+  util::StatusOr<AesGcmParameters> params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(key_size)
+          .SetIvSizeInBytes(iv_and_tag_size)
+          .SetTagSizeInBytes(iv_and_tag_size)
+          .SetVariant(test_case.variant)
+          .Build();
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(key_size);
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  EXPECT_THAT(key->GetParameters(), Eq(*params));
+  EXPECT_THAT(key->GetIdRequirement(), Eq(test_case.id_requirement));
+  EXPECT_THAT(key->GetOutputPrefix(), Eq(test_case.output_prefix));
+}
+
+TEST(AesGcmKeyTest, CreateKeyWithMismatchedKeySizeFails) {
+  // Key size parameter is 32 bytes.
+  util::StatusOr<AesGcmParameters> params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(params, IsOk());
+
+  // Key material is 16 bytes (another valid key length).
+  RestrictedData mismatched_secret = RestrictedData(/*num_random_bytes=*/16);
+
+  EXPECT_THAT(AesGcmKey::Create(*params, mismatched_secret,
+                                /*id_requirement=*/123, GetPartialKeyAccess())
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmKeyTest, CreateKeyWithInvalidIdRequirementFails) {
+  util::StatusOr<AesGcmParameters> no_prefix_params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+          .Build();
+  ASSERT_THAT(no_prefix_params, IsOk());
+
+  util::StatusOr<AesGcmParameters> tink_params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(tink_params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+
+  EXPECT_THAT(AesGcmKey::Create(*no_prefix_params, secret,
+                                /*id_requirement=*/123, GetPartialKeyAccess())
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(
+      AesGcmKey::Create(*tink_params, secret,
+                        /*id_requirement=*/absl::nullopt, GetPartialKeyAccess())
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(AesGcmKeyTest, GetKeyBytes) {
+  int key_size;
+  int iv_and_tag_size;  // NOTE: There's no requirement for IV size == tag size.
+  TestCase test_case;
+  std::tie(key_size, iv_and_tag_size, test_case) = GetParam();
+
+  util::StatusOr<AesGcmParameters> params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(key_size)
+          .SetIvSizeInBytes(iv_and_tag_size)
+          .SetTagSizeInBytes(iv_and_tag_size)
+          .SetVariant(test_case.variant)
+          .Build();
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(key_size);
+
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  EXPECT_THAT(key->GetKeyBytes(GetPartialKeyAccess()), IsOkAndHolds(secret));
+}
+
+TEST_P(AesGcmKeyTest, KeyEquals) {
+  int key_size;
+  int iv_and_tag_size;  // NOTE: There's no requirement for IV size == tag size.
+  TestCase test_case;
+  std::tie(key_size, iv_and_tag_size, test_case) = GetParam();
+
+  util::StatusOr<AesGcmParameters> params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(key_size)
+          .SetIvSizeInBytes(iv_and_tag_size)
+          .SetTagSizeInBytes(iv_and_tag_size)
+          .SetVariant(test_case.variant)
+          .Build();
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(key_size);
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<AesGcmKey> other_key = AesGcmKey::Create(
+      *params, secret, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(other_key, IsOk());
+
+  EXPECT_TRUE(*key == *other_key);
+  EXPECT_TRUE(*other_key == *key);
+  EXPECT_FALSE(*key != *other_key);
+  EXPECT_FALSE(*other_key != *key);
+}
+
+TEST(AesGcmKeyTest, DifferentVariantNotEqual) {
+  util::StatusOr<AesGcmParameters> crunchy_params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kCrunchy)
+          .Build();
+  ASSERT_THAT(crunchy_params, IsOk());
+
+  util::StatusOr<AesGcmParameters> tink_params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(tink_params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<AesGcmKey> key =
+      AesGcmKey::Create(*crunchy_params, secret, /*id_requirement=*/0x01020304,
+                        GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<AesGcmKey> other_key =
+      AesGcmKey::Create(*tink_params, secret, /*id_requirement=*/0x01020304,
+                        GetPartialKeyAccess());
+  ASSERT_THAT(other_key, IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+TEST(AesGcmKeyTest, DifferentSecretDataNotEqual) {
+  util::StatusOr<AesGcmParameters> params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret1 = RestrictedData(/*num_random_bytes=*/32);
+  RestrictedData secret2 = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *params, secret1, /*id_requirement=*/0x01020304, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<AesGcmKey> other_key = AesGcmKey::Create(
+      *params, secret2, /*id_requirement=*/0x01020304, GetPartialKeyAccess());
+  ASSERT_THAT(other_key, IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+TEST(AesGcmKeyTest, DifferentIdRequirementNotEqual) {
+  util::StatusOr<AesGcmParameters> params =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *params, secret, /*id_requirement=*/0x01020304, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<AesGcmKey> other_key = AesGcmKey::Create(
+      *params, secret, /*id_requirement=*/0x02030405, GetPartialKeyAccess());
+  ASSERT_THAT(other_key, IsOk());
+
+  EXPECT_TRUE(*key != *other_key);
+  EXPECT_TRUE(*other_key != *key);
+  EXPECT_FALSE(*key == *other_key);
+  EXPECT_FALSE(*other_key == *key);
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/aead/aes_gcm_parameters.cc b/cc/aead/aes_gcm_parameters.cc
new file mode 100644
index 0000000..d78d888
--- /dev/null
+++ b/cc/aead/aes_gcm_parameters.cc
@@ -0,0 +1,103 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/aead/aes_gcm_parameters.h"
+
+#include <set>
+
+#include "absl/strings/str_cat.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+AesGcmParameters::Builder& AesGcmParameters::Builder::SetKeySizeInBytes(
+    int key_size) {
+  key_size_in_bytes_ = key_size;
+  return *this;
+}
+
+AesGcmParameters::Builder& AesGcmParameters::Builder::SetIvSizeInBytes(
+    int iv_size) {
+  iv_size_in_bytes_ = iv_size;
+  return *this;
+}
+
+AesGcmParameters::Builder& AesGcmParameters::Builder::SetTagSizeInBytes(
+    int tag_size) {
+  tag_size_in_bytes_ = tag_size;
+  return *this;
+}
+
+AesGcmParameters::Builder& AesGcmParameters::Builder::SetVariant(
+    Variant variant) {
+  variant_ = variant;
+  return *this;
+}
+
+util::StatusOr<AesGcmParameters> AesGcmParameters::Builder::Build() {
+  if (key_size_in_bytes_ != 16 && key_size_in_bytes_ != 24 &&
+      key_size_in_bytes_ != 32) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("Key size should be 16, 24, or 32 bytes, got ",
+                     key_size_in_bytes_, " bytes."));
+  }
+  if (iv_size_in_bytes_ <= 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        absl::StrCat("IV size should be positive, got ",
+                                     iv_size_in_bytes_, " bytes."));
+  }
+  if (tag_size_in_bytes_ < 12 || tag_size_in_bytes_ > 16) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("Tag size should be between 12 and 16 bytes, got ",
+                     tag_size_in_bytes_, " bytes."));
+  }
+  static const std::set<Variant>* supported_variants = new std::set<Variant>(
+      {Variant::kTink, Variant::kCrunchy, Variant::kNoPrefix});
+  if (supported_variants->find(variant_) == supported_variants->end()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create AES-GCM parameters with unknown variant.");
+  }
+  return AesGcmParameters(key_size_in_bytes_, iv_size_in_bytes_,
+                          tag_size_in_bytes_, variant_);
+}
+
+bool AesGcmParameters::operator==(const Parameters& other) const {
+  const AesGcmParameters* that = dynamic_cast<const AesGcmParameters*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  if (key_size_in_bytes_ != that->key_size_in_bytes_) {
+    return false;
+  }
+  if (iv_size_in_bytes_ != that->iv_size_in_bytes_) {
+    return false;
+  }
+  if (tag_size_in_bytes_ != that->tag_size_in_bytes_) {
+    return false;
+  }
+  if (variant_ != that->variant_) {
+    return false;
+  }
+  return true;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/aead/aes_gcm_parameters.h b/cc/aead/aes_gcm_parameters.h
new file mode 100644
index 0000000..de87b2c
--- /dev/null
+++ b/cc/aead/aes_gcm_parameters.h
@@ -0,0 +1,105 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_AEAD_AES_GCM_PARAMETERS_H_
+#define TINK_AEAD_AES_GCM_PARAMETERS_H_
+
+#include "tink/aead/aead_parameters.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+// Describes the parameters of an `AesGcmKey`.
+class AesGcmParameters : public AeadParameters {
+ public:
+  // Description of the output prefix prepended to the ciphertext.
+  enum class Variant : int {
+    // Prepends '0x01<big endian key id>' to the ciphertext.
+    kTink = 1,
+    // Prepends '0x00<big endian key id>' to the ciphertext.
+    kCrunchy = 2,
+    // Does not prepend any prefix (i.e., keys must have no ID requirement).
+    kNoPrefix = 3,
+    // Added to guard from failures that may be caused by future expansions.
+    kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements = 20,
+  };
+
+  // Creates AES-GCM parameters instances.
+  class Builder {
+   public:
+    // Copyable and movable.
+    Builder(const Builder& other) = default;
+    Builder& operator=(const Builder& other) = default;
+    Builder(Builder&& other) = default;
+    Builder& operator=(Builder&& other) = default;
+
+    // Creates initially empty parameters builder.
+    Builder() = default;
+
+    Builder& SetKeySizeInBytes(int key_size);
+    Builder& SetIvSizeInBytes(int iv_size);
+    Builder& SetTagSizeInBytes(int tag_size);
+    Builder& SetVariant(Variant variant);
+
+    // Creates AES-GCM parameters object from this builder.
+    util::StatusOr<AesGcmParameters> Build();
+
+   private:
+    int key_size_in_bytes_;
+    int iv_size_in_bytes_;
+    int tag_size_in_bytes_;
+    Variant variant_;
+  };
+
+  // Copyable and movable.
+  AesGcmParameters(const AesGcmParameters& other) = default;
+  AesGcmParameters& operator=(const AesGcmParameters& other) = default;
+  AesGcmParameters(AesGcmParameters&& other) = default;
+  AesGcmParameters& operator=(AesGcmParameters&& other) = default;
+
+  int KeySizeInBytes() const { return key_size_in_bytes_; }
+
+  int IvSizeInBytes() const { return iv_size_in_bytes_; }
+
+  int TagSizeInBytes() const { return tag_size_in_bytes_; }
+
+  Variant GetVariant() const { return variant_; }
+
+  bool HasIdRequirement() const override {
+    return variant_ != Variant::kNoPrefix;
+  }
+
+  bool operator==(const Parameters& other) const override;
+
+ private:
+  AesGcmParameters(int key_size_in_bytes, int iv_size_in_bytes,
+                   int tag_size_in_bytes, Variant variant)
+      : key_size_in_bytes_(key_size_in_bytes),
+        iv_size_in_bytes_(iv_size_in_bytes),
+        tag_size_in_bytes_(tag_size_in_bytes),
+        variant_(variant) {}
+
+  int key_size_in_bytes_;
+  int iv_size_in_bytes_;
+  int tag_size_in_bytes_;
+  Variant variant_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_AEAD_AES_GCM_PARAMETERS_H_
diff --git a/cc/aead/aes_gcm_parameters_test.cc b/cc/aead/aes_gcm_parameters_test.cc
new file mode 100644
index 0000000..7238bb9
--- /dev/null
+++ b/cc/aead/aes_gcm_parameters_test.cc
@@ -0,0 +1,386 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/aead/aes_gcm_parameters.h"
+
+#include <tuple>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+using ::testing::Combine;
+using ::testing::Eq;
+using ::testing::IsTrue;
+using ::testing::Range;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct BuildTestCase {
+  AesGcmParameters::Variant variant;
+  int key_size;
+  int iv_size;
+  int tag_size;
+  bool has_id_requirement;
+};
+
+using AesGcmParametersBuildTest = TestWithParam<BuildTestCase>;
+
+INSTANTIATE_TEST_SUITE_P(
+    AesGcmParametersBuildTestSuite, AesGcmParametersBuildTest,
+    Values(BuildTestCase{AesGcmParameters::Variant::kTink, /*key_size=*/16,
+                         /*iv_size=*/12, /*tag_size=*/12,
+                         /*has_id_requirement=*/true},
+           BuildTestCase{AesGcmParameters::Variant::kCrunchy, /*key_size=*/24,
+                         /*iv_size=*/14, /*tag_size=*/14,
+                         /*has_id_requirement=*/true},
+           BuildTestCase{AesGcmParameters::Variant::kNoPrefix,
+                         /*key_size=*/32, /*iv_size=*/16, /*tag_size=*/16,
+                         /*has_id_requirement=*/false}));
+
+TEST_P(AesGcmParametersBuildTest, Build) {
+  BuildTestCase test_case = GetParam();
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(test_case.key_size)
+          .SetIvSizeInBytes(test_case.iv_size)
+          .SetTagSizeInBytes(test_case.tag_size)
+          .SetVariant(test_case.variant)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  EXPECT_THAT(parameters->KeySizeInBytes(), Eq(test_case.key_size));
+  EXPECT_THAT(parameters->IvSizeInBytes(), Eq(test_case.iv_size));
+  EXPECT_THAT(parameters->TagSizeInBytes(), Eq(test_case.tag_size));
+  EXPECT_THAT(parameters->GetVariant(), Eq(test_case.variant));
+  EXPECT_THAT(parameters->HasIdRequirement(), Eq(test_case.has_id_requirement));
+}
+
+TEST(AesGcmParametersTest, BuildWithoutSettingVariantFails) {
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(16)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, BuildWithInvalidVariantFails) {
+  EXPECT_THAT(
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::
+                          kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements)
+          .Build()
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, BuildWithoutSettingKeySizeFails) {
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, BuildWithInvalidKeySizeFails) {
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(15)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(17)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(23)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(25)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(31)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(33)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, BuildWithoutSettingIvSizeFails) {
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(16)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, BuildWithInvalidIvSizeFails) {
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(16)
+                  .SetIvSizeInBytes(0)
+                  .SetTagSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, BuildWithoutSettingTagSizeFails) {
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(16)
+                  .SetIvSizeInBytes(16)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, BuildWithInvalidTagSizeFails) {
+  // Too small.
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(16)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(11)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+  // Too big.
+  EXPECT_THAT(AesGcmParameters::Builder()
+                  .SetKeySizeInBytes(16)
+                  .SetIvSizeInBytes(16)
+                  .SetTagSizeInBytes(17)
+                  .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+                  .Build()
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(AesGcmParametersTest, CopyConstructor) {
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(16)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  AesGcmParameters copy(*parameters);
+  EXPECT_THAT(copy.KeySizeInBytes(), Eq(16));
+  EXPECT_THAT(copy.IvSizeInBytes(), Eq(16));
+  EXPECT_THAT(copy.TagSizeInBytes(), Eq(16));
+  EXPECT_THAT(copy.GetVariant(), Eq(AesGcmParameters::Variant::kTink));
+  EXPECT_THAT(copy.HasIdRequirement(), IsTrue());
+}
+
+TEST(AesGcmParametersTest, CopyAssignment) {
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(16)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  AesGcmParameters copy = *parameters;
+  EXPECT_THAT(copy.KeySizeInBytes(), Eq(16));
+  EXPECT_THAT(copy.IvSizeInBytes(), Eq(16));
+  EXPECT_THAT(copy.TagSizeInBytes(), Eq(16));
+  EXPECT_THAT(copy.GetVariant(), Eq(AesGcmParameters::Variant::kTink));
+  EXPECT_THAT(copy.HasIdRequirement(), IsTrue());
+}
+
+using AesGcmParametersVariantTest =
+    TestWithParam<std::tuple<int, int, AesGcmParameters::Variant>>;
+
+INSTANTIATE_TEST_SUITE_P(AesGcmParametersVariantTestSuite,
+                         AesGcmParametersVariantTest,
+                         Combine(Values(16, 24, 32), Range(12, 16),
+                                 Values(AesGcmParameters::Variant::kTink,
+                                        AesGcmParameters::Variant::kCrunchy,
+                                        AesGcmParameters::Variant::kNoPrefix)));
+
+TEST_P(AesGcmParametersVariantTest, ParametersEquals) {
+  int key_size;
+  int iv_and_tag_size;
+  AesGcmParameters::Variant variant;
+  std::tie(key_size, iv_and_tag_size, variant) = GetParam();
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(key_size)
+          .SetIvSizeInBytes(iv_and_tag_size)
+          .SetTagSizeInBytes(iv_and_tag_size)
+          .SetVariant(variant)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<AesGcmParameters> other_parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(key_size)
+          .SetIvSizeInBytes(iv_and_tag_size)
+          .SetTagSizeInBytes(iv_and_tag_size)
+          .SetVariant(variant)
+          .Build();
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters == *other_parameters);
+  EXPECT_TRUE(*other_parameters == *parameters);
+  EXPECT_FALSE(*parameters != *other_parameters);
+  EXPECT_FALSE(*other_parameters != *parameters);
+}
+
+TEST(AesGcmParametersTest, KeySizeNotEqual) {
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<AesGcmParameters> other_parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(24)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+TEST(AesGcmParametersTest, IvSizeNotEqual) {
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<AesGcmParameters> other_parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(12)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+TEST(AesGcmParametersTest, TagSizeNotEqual) {
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<AesGcmParameters> other_parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(14)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+TEST(AesGcmParametersTest, VariantNotEqual) {
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kTink)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<AesGcmParameters> other_parameters =
+      AesGcmParameters::Builder()
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(16)
+          .SetTagSizeInBytes(16)
+          .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+          .Build();
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/aead/aes_gcm_proto_serialization.cc b/cc/aead/aes_gcm_proto_serialization.cc
new file mode 100644
index 0000000..57272ff
--- /dev/null
+++ b/cc/aead/aes_gcm_proto_serialization.cc
@@ -0,0 +1,273 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/aead/aes_gcm_proto_serialization.h"
+
+#include <string>
+
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#include "tink/aead/aes_gcm_key.h"
+#include "tink/aead/aes_gcm_parameters.h"
+#include "tink/internal/key_parser.h"
+#include "tink/internal/key_serializer.h"
+#include "tink/internal/mutable_serialization_registry.h"
+#include "tink/internal/parameters_parser.h"
+#include "tink/internal/parameters_serializer.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/secret_key_access_token.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+#include "proto/aes_gcm.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::google::crypto::tink::AesGcmKeyFormat;
+using ::google::crypto::tink::OutputPrefixType;
+
+using AesGcmProtoParametersParserImpl =
+    internal::ParametersParserImpl<internal::ProtoParametersSerialization,
+                                   AesGcmParameters>;
+using AesGcmProtoParametersSerializerImpl =
+    internal::ParametersSerializerImpl<AesGcmParameters,
+                                       internal::ProtoParametersSerialization>;
+using AesGcmProtoKeyParserImpl =
+    internal::KeyParserImpl<internal::ProtoKeySerialization, AesGcmKey>;
+using AesGcmProtoKeySerializerImpl =
+    internal::KeySerializerImpl<AesGcmKey, internal::ProtoKeySerialization>;
+
+const absl::string_view kTypeUrl =
+    "type.googleapis.com/google.crypto.tink.AesGcmKey";
+
+util::StatusOr<AesGcmParameters::Variant> ToVariant(
+    OutputPrefixType output_prefix_type) {
+  switch (output_prefix_type) {
+    case OutputPrefixType::LEGACY:
+       ABSL_FALLTHROUGH_INTENDED;  // Parse LEGACY output prefix as CRUNCHY.
+    case OutputPrefixType::CRUNCHY:
+      return AesGcmParameters::Variant::kCrunchy;
+    case OutputPrefixType::RAW:
+      return AesGcmParameters::Variant::kNoPrefix;
+    case OutputPrefixType::TINK:
+      return AesGcmParameters::Variant::kTink;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine AesGcmParameters::Variant");
+  }
+}
+
+util::StatusOr<OutputPrefixType> ToOutputPrefixType(
+    AesGcmParameters::Variant variant) {
+  switch (variant) {
+    case AesGcmParameters::Variant::kCrunchy:
+      return OutputPrefixType::CRUNCHY;
+    case AesGcmParameters::Variant::kNoPrefix:
+      return OutputPrefixType::RAW;
+    case AesGcmParameters::Variant::kTink:
+      return OutputPrefixType::TINK;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine output prefix type");
+  }
+}
+
+// Legacy Tink AES-GCM key proto format assumes 12-byte random IVs and 16-byte
+// tags.
+util::Status ValidateParamsForProto(const AesGcmParameters& params) {
+  if (params.IvSizeInBytes() != 12) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Tink currently restricts AES-GCM IV size to 12 bytes.");
+  }
+  if (params.TagSizeInBytes() != 16) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Tink currently restricts AES-GCM tag size to 16 bytes.");
+  }
+  return util::OkStatus();
+}
+
+util::StatusOr<AesGcmParameters> ParseParameters(
+    const internal::ProtoParametersSerialization& serialization) {
+  if (serialization.GetKeyTemplate().type_url() != kTypeUrl) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong type URL when parsing AesGcmParameters.");
+  }
+
+  AesGcmKeyFormat proto_key_format;
+  if (!proto_key_format.ParseFromString(
+          serialization.GetKeyTemplate().value())) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to parse AesGcmKeyFormat proto");
+  }
+  if (proto_key_format.version() != 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Only version 0 keys are accepted.");
+  }
+
+  util::StatusOr<AesGcmParameters::Variant> variant =
+      ToVariant(serialization.GetKeyTemplate().output_prefix_type());
+  if (!variant.ok()) return variant.status();
+
+  // Legacy Tink AES-GCM key proto format assumes 12-byte random IVs and 16-byte
+  // tags.
+  return AesGcmParameters::Builder()
+      .SetVariant(*variant)
+      .SetKeySizeInBytes(proto_key_format.key_size())
+      .SetIvSizeInBytes(12)
+      .SetTagSizeInBytes(16)
+      .Build();
+}
+
+util::StatusOr<internal::ProtoParametersSerialization> SerializeParameters(
+    const AesGcmParameters& parameters) {
+  util::Status valid_params = ValidateParamsForProto(parameters);
+  if (!valid_params.ok()) return valid_params;
+
+  util::StatusOr<OutputPrefixType> output_prefix_type =
+      ToOutputPrefixType(parameters.GetVariant());
+  if (!output_prefix_type.ok()) return output_prefix_type.status();
+
+  AesGcmKeyFormat proto_key_format;
+  proto_key_format.set_key_size(parameters.KeySizeInBytes());
+
+  return internal::ProtoParametersSerialization::Create(
+      kTypeUrl, *output_prefix_type, proto_key_format.SerializeAsString());
+}
+
+util::StatusOr<AesGcmKey> ParseKey(
+    const internal::ProtoKeySerialization& serialization,
+    absl::optional<SecretKeyAccessToken> token) {
+  if (serialization.TypeUrl() != kTypeUrl) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong type URL when parsing AesGcmKey.");
+  }
+  if (!token.has_value()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "SecretKeyAccess is required");
+  }
+  google::crypto::tink::AesGcmKey proto_key;
+  RestrictedData restricted_data = serialization.SerializedKeyProto();
+  // OSS proto library complains if input is not converted to a string.
+  if (!proto_key.ParseFromString(
+          std::string(restricted_data.GetSecret(*token)))) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to parse AesGcmKey proto");
+  }
+  if (proto_key.version() != 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Only version 0 keys are accepted.");
+  }
+
+  util::StatusOr<AesGcmParameters::Variant> variant =
+      ToVariant(serialization.GetOutputPrefixType());
+  if (!variant.ok()) return variant.status();
+
+  // Legacy AES-GCM key proto format assumes 12-byte random IVs and 16-byte
+  // tags.
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(*variant)
+          .SetKeySizeInBytes(proto_key.key_value().length())
+          .SetIvSizeInBytes(12)
+          .SetTagSizeInBytes(16)
+          .Build();
+  if (!parameters.ok()) return parameters.status();
+
+  return AesGcmKey::Create(
+      *parameters, RestrictedData(proto_key.key_value(), *token),
+      serialization.IdRequirement(), GetPartialKeyAccess());
+}
+
+util::StatusOr<internal::ProtoKeySerialization> SerializeKey(
+    const AesGcmKey& key, absl::optional<SecretKeyAccessToken> token) {
+  util::Status valid_params = ValidateParamsForProto(key.GetParameters());
+  if (!valid_params.ok()) return valid_params;
+
+  util::StatusOr<RestrictedData> restricted_input =
+      key.GetKeyBytes(GetPartialKeyAccess());
+  if (!restricted_input.ok()) return restricted_input.status();
+  if (!token.has_value()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "SecretKeyAccess is required");
+  }
+
+  google::crypto::tink::AesGcmKey proto_key;
+  proto_key.set_version(0);
+  // OSS proto library complains if input is not converted to a string.
+  proto_key.set_key_value(std::string(restricted_input->GetSecret(*token)));
+
+  util::StatusOr<OutputPrefixType> output_prefix_type =
+      ToOutputPrefixType(key.GetParameters().GetVariant());
+  if (!output_prefix_type.ok()) return output_prefix_type.status();
+
+  RestrictedData restricted_output =
+      RestrictedData(proto_key.SerializeAsString(), *token);
+  return internal::ProtoKeySerialization::Create(
+      kTypeUrl, restricted_output, google::crypto::tink::KeyData::SYMMETRIC,
+      *output_prefix_type, key.GetIdRequirement());
+}
+
+AesGcmProtoParametersParserImpl* AesGcmProtoParametersParser() {
+  static auto* parser =
+      new AesGcmProtoParametersParserImpl(kTypeUrl, ParseParameters);
+  return parser;
+}
+
+AesGcmProtoParametersSerializerImpl* AesGcmProtoParametersSerializer() {
+  static auto* serializer =
+      new AesGcmProtoParametersSerializerImpl(kTypeUrl, SerializeParameters);
+  return serializer;
+}
+
+AesGcmProtoKeyParserImpl* AesGcmProtoKeyParser() {
+  static auto* parser = new AesGcmProtoKeyParserImpl(kTypeUrl, ParseKey);
+  return parser;
+}
+
+AesGcmProtoKeySerializerImpl* AesGcmProtoKeySerializer() {
+  static auto* serializer = new AesGcmProtoKeySerializerImpl(SerializeKey);
+  return serializer;
+}
+
+}  // namespace
+
+util::Status RegisterAesGcmProtoSerialization() {
+  util::Status status =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .RegisterParametersParser(AesGcmProtoParametersParser());
+  if (!status.ok()) return status;
+
+  status = internal::MutableSerializationRegistry::GlobalInstance()
+               .RegisterParametersSerializer(AesGcmProtoParametersSerializer());
+  if (!status.ok()) return status;
+
+  status = internal::MutableSerializationRegistry::GlobalInstance()
+               .RegisterKeyParser(AesGcmProtoKeyParser());
+  if (!status.ok()) return status;
+
+  return internal::MutableSerializationRegistry::GlobalInstance()
+      .RegisterKeySerializer(AesGcmProtoKeySerializer());
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/aead/aes_gcm_proto_serialization.h b/cc/aead/aes_gcm_proto_serialization.h
new file mode 100644
index 0000000..68a49f4
--- /dev/null
+++ b/cc/aead/aes_gcm_proto_serialization.h
@@ -0,0 +1,31 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_AEAD_AES_GCM_PROTO_SERIALIZATION_H_
+#define TINK_AEAD_AES_GCM_PROTO_SERIALIZATION_H_
+
+#include "tink/util/status.h"
+
+namespace crypto {
+namespace tink {
+
+// Registers proto parsers and serializers for AES-GCM parameters and keys.
+crypto::tink::util::Status RegisterAesGcmProtoSerialization();
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_AEAD_AES_GCM_PROTO_SERIALIZATION_H_
diff --git a/cc/aead/aes_gcm_proto_serialization_test.cc b/cc/aead/aes_gcm_proto_serialization_test.cc
new file mode 100644
index 0000000..f19c820
--- /dev/null
+++ b/cc/aead/aes_gcm_proto_serialization_test.cc
@@ -0,0 +1,515 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "tink/aead/aes_gcm_proto_serialization.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/aead/aes_gcm_key.h"
+#include "tink/aead/aes_gcm_parameters.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/internal/mutable_serialization_registry.h"
+#include "tink/internal/proto_key_serialization.h"
+#include "tink/internal/proto_parameters_serialization.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/subtle/random.h"
+#include "tink/util/test_matchers.h"
+#include "proto/aes_gcm.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::subtle::Random;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::AesGcmKeyFormat;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::OutputPrefixType;
+using ::testing::Eq;
+using ::testing::IsTrue;
+using ::testing::NotNull;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct TestCase {
+  AesGcmParameters::Variant variant;
+  OutputPrefixType output_prefix_type;
+  int key_size;
+  int iv_size;
+  int tag_size;
+  absl::optional<int> id;
+  std::string output_prefix;
+};
+
+class AesGcmProtoSerializationTest : public TestWithParam<TestCase> {
+ protected:
+  void SetUp() override {
+    internal::MutableSerializationRegistry::GlobalInstance().Reset();
+  }
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    AesGcmProtoSerializationTestSuite, AesGcmProtoSerializationTest,
+    Values(TestCase{AesGcmParameters::Variant::kTink, OutputPrefixType::TINK,
+                    /*key_size=*/16, /*iv_size=*/12, /*tag_size=*/16,
+                    /*id=*/0x02030400,
+                    /*output_prefix=*/std::string("\x01\x02\x03\x04\x00", 5)},
+           TestCase{AesGcmParameters::Variant::kCrunchy,
+                    OutputPrefixType::CRUNCHY, /*key_size=*/16, /*iv_size=*/12,
+                    /*tag_size=*/16, /*id=*/0x01030005,
+                    /*output_prefix=*/std::string("\x00\x01\x03\x00\x05", 5)},
+           TestCase{AesGcmParameters::Variant::kNoPrefix, OutputPrefixType::RAW,
+                    /*key_size=*/32, /*iv_size=*/12, /*tag_size=*/16,
+                    /*id=*/absl::nullopt, /*output_prefix=*/""}));
+
+TEST_P(AesGcmProtoSerializationTest, ParseParameters) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  AesGcmKeyFormat key_format_proto;
+  key_format_proto.set_version(0);
+  key_format_proto.set_key_size(test_case.key_size);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey",
+          test_case.output_prefix_type, key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  ASSERT_THAT(params, IsOk());
+  EXPECT_THAT((*params)->HasIdRequirement(), test_case.id.has_value());
+
+  const AesGcmParameters* gcm_params =
+      dynamic_cast<const AesGcmParameters*>(params->get());
+  ASSERT_THAT(gcm_params, NotNull());
+  EXPECT_THAT(gcm_params->GetVariant(), Eq(test_case.variant));
+  EXPECT_THAT(gcm_params->KeySizeInBytes(), Eq(test_case.key_size));
+  EXPECT_THAT(gcm_params->IvSizeInBytes(), Eq(test_case.iv_size));
+  EXPECT_THAT(gcm_params->TagSizeInBytes(), Eq(test_case.tag_size));
+}
+
+TEST_F(AesGcmProtoSerializationTest, ParseParametersWithInvalidSerialization) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  AesGcmKeyFormat key_format_proto;
+  key_format_proto.set_version(0);
+  key_format_proto.set_key_size(16);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey",
+          OutputPrefixType::RAW, "invalid_serialization");
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  EXPECT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesGcmProtoSerializationTest, ParseParametersWithUnkownOutputPrefix) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  AesGcmKeyFormat key_format_proto;
+  key_format_proto.set_version(0);
+  key_format_proto.set_key_size(16);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey",
+          OutputPrefixType::UNKNOWN_PREFIX,
+          key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  EXPECT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesGcmProtoSerializationTest, ParseParametersWithInvalidVersion) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  AesGcmKeyFormat key_format_proto;
+  key_format_proto.set_version(1);
+  key_format_proto.set_key_size(16);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey",
+          OutputPrefixType::RAW,
+          key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> params =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  EXPECT_THAT(params.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(AesGcmProtoSerializationTest, SerializeParameters) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(test_case.variant)
+          .SetKeySizeInBytes(test_case.key_size)
+          .SetIvSizeInBytes(test_case.iv_size)
+          .SetTagSizeInBytes(test_case.tag_size)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<internal::ProtoParametersSerialization>(
+              *parameters);
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(),
+              Eq("type.googleapis.com/google.crypto.tink.AesGcmKey"));
+
+  const internal::ProtoParametersSerialization* proto_serialization =
+      dynamic_cast<const internal::ProtoParametersSerialization*>(
+          serialization->get());
+  ASSERT_THAT(proto_serialization, NotNull());
+  EXPECT_THAT(proto_serialization->GetKeyTemplate().type_url(),
+              Eq("type.googleapis.com/google.crypto.tink.AesGcmKey"));
+  EXPECT_THAT(proto_serialization->GetKeyTemplate().output_prefix_type(),
+              Eq(test_case.output_prefix_type));
+
+  AesGcmKeyFormat key_format;
+  ASSERT_THAT(
+      key_format.ParseFromString(proto_serialization->GetKeyTemplate().value()),
+      IsTrue());
+  EXPECT_THAT(key_format.key_size(), Eq(test_case.key_size));
+}
+
+TEST_F(AesGcmProtoSerializationTest, SerializeParametersWithDisallowedIvSize) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+          .SetKeySizeInBytes(16)
+          .SetIvSizeInBytes(14)
+          .SetTagSizeInBytes(16)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<internal::ProtoParametersSerialization>(
+              *parameters);
+  EXPECT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesGcmProtoSerializationTest, SerializeParametersWithDisallowedTagSize) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+          .SetKeySizeInBytes(16)
+          .SetIvSizeInBytes(12)
+          .SetTagSizeInBytes(14)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeParameters<internal::ProtoParametersSerialization>(
+              *parameters);
+  EXPECT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(AesGcmProtoSerializationTest, ParseKey) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(test_case.key_size);
+  google::crypto::tink::AesGcmKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(raw_key_bytes);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey", serialized_key,
+          KeyData::SYMMETRIC, test_case.output_prefix_type, test_case.id);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key, IsOk());
+  EXPECT_THAT((*key)->GetIdRequirement(), Eq(test_case.id));
+  EXPECT_THAT((*key)->GetParameters().HasIdRequirement(),
+              test_case.id.has_value());
+
+  util::StatusOr<AesGcmParameters> expected_parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(test_case.variant)
+          .SetKeySizeInBytes(test_case.key_size)
+          .SetIvSizeInBytes(test_case.iv_size)
+          .SetTagSizeInBytes(test_case.tag_size)
+          .Build();
+  ASSERT_THAT(expected_parameters, IsOk());
+
+  util::StatusOr<AesGcmKey> expected_key = AesGcmKey::Create(
+      *expected_parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      test_case.id, GetPartialKeyAccess());
+  ASSERT_THAT(expected_key, IsOk());
+
+  EXPECT_THAT(**key, Eq(*expected_key));
+}
+
+TEST_F(AesGcmProtoSerializationTest, ParseLegacyKeyAsCrunchy) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  google::crypto::tink::AesGcmKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(raw_key_bytes);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::LEGACY, /*id_requirement=*/123);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(key, IsOk());
+
+  const AesGcmKey* aes_gcm_key = dynamic_cast<const AesGcmKey*>(key->get());
+  ASSERT_THAT(aes_gcm_key, NotNull());
+  EXPECT_THAT(aes_gcm_key->GetParameters().GetVariant(),
+              Eq(AesGcmParameters::Variant::kCrunchy));
+}
+
+TEST_F(AesGcmProtoSerializationTest, ParseKeyWithInvalidSerialization) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  RestrictedData serialized_key =
+      RestrictedData("invalid_serialization", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/0x23456789);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  EXPECT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesGcmProtoSerializationTest, ParseKeyNoSecretKeyAccess) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(16);
+  google::crypto::tink::AesGcmKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(raw_key_bytes);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/0x23456789);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, /*token=*/absl::nullopt);
+  EXPECT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesGcmProtoSerializationTest, ParseKeyWithInvalidVersion) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(16);
+  google::crypto::tink::AesGcmKey key_proto;
+  key_proto.set_version(1);  // Invalid version number.
+  key_proto.set_key_value(raw_key_bytes);
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          "type.googleapis.com/google.crypto.tink.AesGcmKey", serialized_key,
+          KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/0x23456789);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  EXPECT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_P(AesGcmProtoSerializationTest, SerializeKey) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(test_case.variant)
+          .SetKeySizeInBytes(test_case.key_size)
+          .SetIvSizeInBytes(test_case.iv_size)
+          .SetTagSizeInBytes(test_case.tag_size)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(test_case.key_size);
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      test_case.id, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(),
+              Eq("type.googleapis.com/google.crypto.tink.AesGcmKey"));
+
+  const internal::ProtoKeySerialization* proto_serialization =
+      dynamic_cast<const internal::ProtoKeySerialization*>(
+          serialization->get());
+  ASSERT_THAT(proto_serialization, NotNull());
+  EXPECT_THAT(proto_serialization->TypeUrl(),
+              Eq("type.googleapis.com/google.crypto.tink.AesGcmKey"));
+  EXPECT_THAT(proto_serialization->KeyMaterialType(), Eq(KeyData::SYMMETRIC));
+  EXPECT_THAT(proto_serialization->GetOutputPrefixType(),
+              Eq(test_case.output_prefix_type));
+  EXPECT_THAT(proto_serialization->IdRequirement(), Eq(test_case.id));
+
+  google::crypto::tink::AesGcmKey proto_key;
+  // OSS proto library complains if input is not converted to a string.
+  ASSERT_THAT(proto_key.ParseFromString(std::string(
+                  proto_serialization->SerializedKeyProto().GetSecret(
+                      InsecureSecretKeyAccess::Get()))),
+              IsTrue());
+  EXPECT_THAT(proto_key.key_value().size(), Eq(test_case.key_size));
+}
+
+TEST_F(AesGcmProtoSerializationTest, SerializeKeyWithDisallowedIvSize) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(14)
+          .SetTagSizeInBytes(16)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      /*id_requirement=*/absl::nullopt, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, InsecureSecretKeyAccess::Get());
+  EXPECT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesGcmProtoSerializationTest, SerializeKeyWithDisallowedTagSize) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+          .SetKeySizeInBytes(32)
+          .SetIvSizeInBytes(12)
+          .SetTagSizeInBytes(14)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      /*id_requirement=*/absl::nullopt, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, InsecureSecretKeyAccess::Get());
+  EXPECT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(AesGcmProtoSerializationTest, SerializeKeyNoSecretKeyAccess) {
+  ASSERT_THAT(RegisterAesGcmProtoSerialization(), IsOk());
+
+  util::StatusOr<AesGcmParameters> parameters =
+      AesGcmParameters::Builder()
+          .SetVariant(AesGcmParameters::Variant::kNoPrefix)
+          .SetKeySizeInBytes(16)
+          .SetIvSizeInBytes(12)
+          .SetTagSizeInBytes(16)
+          .Build();
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(16);
+  util::StatusOr<AesGcmKey> key = AesGcmKey::Create(
+      *parameters,
+      RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()),
+      /*id_requirement=*/absl::nullopt, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(*key, absl::nullopt);
+  EXPECT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/aead/cord_aead.h b/cc/aead/cord_aead.h
index 46a60ce..e57ba5f 100644
--- a/cc/aead/cord_aead.h
+++ b/cc/aead/cord_aead.h
@@ -56,7 +56,7 @@
       absl::Cord ciphertext,
       absl::Cord associated_data) const = 0;
 
-  virtual ~CordAead() {}
+  virtual ~CordAead() = default;
 };
 
 }  // namespace tink
diff --git a/cc/aead/cord_aead_wrapper.cc b/cc/aead/cord_aead_wrapper.cc
index 0efb3a0..263b3fc 100644
--- a/cc/aead/cord_aead_wrapper.cc
+++ b/cc/aead/cord_aead_wrapper.cc
@@ -56,7 +56,7 @@
   crypto::tink::util::StatusOr<absl::Cord> Decrypt(
       absl::Cord ciphertext, absl::Cord associated_data) const override;
 
-  ~CordAeadSetWrapper() override {}
+  ~CordAeadSetWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<CordAead>> aead_set_;
diff --git a/cc/aead/failing_aead.cc b/cc/aead/failing_aead.cc
index 81835c5..9f6c0f8 100644
--- a/cc/aead/failing_aead.cc
+++ b/cc/aead/failing_aead.cc
@@ -15,8 +15,10 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/aead/failing_aead.h"
 
+#include <memory>
 #include <string>
 #include <utility>
+
 #include "absl/strings/string_view.h"
 
 namespace crypto {
diff --git a/cc/aead/failing_aead.h b/cc/aead/failing_aead.h
index 70b80a9..88d8c81 100644
--- a/cc/aead/failing_aead.h
+++ b/cc/aead/failing_aead.h
@@ -16,6 +16,7 @@
 #ifndef TINK_AEAD_FAILING_AEAD_H_
 #define TINK_AEAD_FAILING_AEAD_H_
 
+#include <memory>
 #include <string>
 
 #include "absl/strings/string_view.h"
diff --git a/cc/aead/internal/BUILD.bazel b/cc/aead/internal/BUILD.bazel
index 0f7c72f..225bffc 100644
--- a/cc/aead/internal/BUILD.bazel
+++ b/cc/aead/internal/BUILD.bazel
@@ -11,6 +11,7 @@
         "//util:errors",
         "//util:statusor",
         "@boringssl//:crypto",
+        "@com_google_absl//absl/container:flat_hash_set",
         "@com_google_absl//absl/status",
     ],
 )
@@ -61,13 +62,11 @@
         "//internal:ssl_unique_ptr",
         "//subtle:random",
         "//subtle:subtle_util",
-        "//util:errors",
         "//util:secret_data",
         "//util:status",
         "//util:statusor",
         "@boringssl//:crypto",
         "@com_google_absl//absl/status",
-        "@com_google_absl//absl/strings",
         "@com_google_absl//absl/strings:cord",
     ],
 )
@@ -154,7 +153,7 @@
     name = "cord_aes_gcm_boringssl_test",
     size = "small",
     srcs = ["cord_aes_gcm_boringssl_test.cc"],
-    data = ["@wycheproof//testvectors:aes_gcm"],
+    data = ["//testvectors:aes_gcm"],
     deps = [
         ":cord_aes_gcm_boringssl",
         "//subtle:aes_gcm_boringssl",
@@ -199,7 +198,7 @@
 cc_test(
     name = "zero_copy_aes_gcm_boringssl_test",
     srcs = ["zero_copy_aes_gcm_boringssl_test.cc"],
-    data = ["@wycheproof//testvectors:aes_gcm"],
+    data = ["//testvectors:aes_gcm"],
     deps = [
         ":wycheproof_aead",
         ":zero_copy_aead",
@@ -235,21 +234,20 @@
     name = "ssl_aead_test",
     srcs = ["ssl_aead_test.cc"],
     data = [
-        "@wycheproof//testvectors:aes_gcm",
-        "@wycheproof//testvectors:aes_gcm_siv",
-        "@wycheproof//testvectors:chacha20_poly1305",
+        "//testvectors:aes_gcm",
+        "//testvectors:aes_gcm_siv",
+        "//testvectors:chacha20_poly1305",
     ],
     deps = [
         ":ssl_aead",
         ":wycheproof_aead",
-        "//config:tink_fips",
+        "//internal:fips_utils",
         "//internal:ssl_util",
         "//subtle:subtle_util",
         "//util:secret_data",
         "//util:statusor",
         "//util:test_matchers",
         "@com_google_absl//absl/container:flat_hash_set",
-        "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
         "@com_google_absl//absl/types:span",
@@ -265,6 +263,7 @@
         ":ssl_aead",
         "//config:tink_fips",
         "//internal:ssl_util",
+        "//internal:util",
         "//subtle:subtle_util",
         "//util:secret_data",
         "//util:statusor",
diff --git a/cc/aead/internal/BUILD.gn b/cc/aead/internal/BUILD.gn
index ffabdb7..1202a08 100644
--- a/cc/aead/internal/BUILD.gn
+++ b/cc/aead/internal/BUILD.gn
@@ -14,6 +14,7 @@
     "aead_util.h",
   ]
   public_deps = [
+    "//third_party/abseil-cpp/absl/container:flat_hash_set",
     "//third_party/abseil-cpp/absl/status:status",
     "//third_party/boringssl:crypto",
     "//third_party/tink/cc/util:errors",
@@ -60,13 +61,11 @@
     ":aead_util",
     "//third_party/abseil-cpp/absl/status:status",
     "//third_party/abseil-cpp/absl/strings:cord",
-    "//third_party/abseil-cpp/absl/strings:strings",
     "//third_party/boringssl:crypto",
     "//third_party/tink/cc/aead:cord_aead",
     "//third_party/tink/cc/internal:ssl_unique_ptr",
     "//third_party/tink/cc/subtle:random",
     "//third_party/tink/cc/subtle:subtle_util",
-    "//third_party/tink/cc/util:errors",
     "//third_party/tink/cc/util:secret_data",
     "//third_party/tink/cc/util:status",
     "//third_party/tink/cc/util:statusor",
diff --git a/cc/aead/internal/CMakeLists.txt b/cc/aead/internal/CMakeLists.txt
index e7c1a07..217d2b4 100644
--- a/cc/aead/internal/CMakeLists.txt
+++ b/cc/aead/internal/CMakeLists.txt
@@ -6,6 +6,7 @@
     aead_util.cc
     aead_util.h
   DEPS
+    absl::flat_hash_set
     absl::status
     crypto
     tink::util::errors
@@ -21,6 +22,7 @@
     absl::strings
     tink::subtle::wycheproof_util
     tink::util::statusor
+  TESTONLY
 )
 
 tink_cc_library(
@@ -52,14 +54,12 @@
   DEPS
     tink::aead::internal::aead_util
     absl::status
-    absl::strings
     absl::cord
     crypto
     tink::aead::cord_aead
     tink::internal::ssl_unique_ptr
     tink::subtle::random
     tink::subtle::subtle_util
-    tink::util::errors
     tink::util::secret_data
     tink::util::status
     tink::util::statusor
@@ -74,6 +74,7 @@
     gmock
     absl::strings
     tink::util::statusor
+  TESTONLY
 )
 
 tink_cc_library(
@@ -159,11 +160,10 @@
     tink::aead::internal::wycheproof_aead
     gmock
     absl::flat_hash_set
-    absl::memory
     absl::status
     absl::strings
     absl::span
-    tink::config::tink_fips
+    tink::internal::fips_utils
     tink::internal::ssl_util
     tink::subtle::subtle_util
     tink::util::secret_data
@@ -241,6 +241,7 @@
     absl::span
     tink::config::tink_fips
     tink::internal::ssl_util
+    tink::internal::util
     tink::subtle::subtle_util
     tink::util::secret_data
     tink::util::statusor
diff --git a/cc/aead/internal/aead_from_zero_copy_test.cc b/cc/aead/internal/aead_from_zero_copy_test.cc
index 2fe4486..9693661 100644
--- a/cc/aead/internal/aead_from_zero_copy_test.cc
+++ b/cc/aead/internal/aead_from_zero_copy_test.cc
@@ -49,7 +49,7 @@
 
 TEST(AeadFromZeroCopyTest, EncryptSucceeds) {
   std::unique_ptr<MockZeroCopyAead> mock_zero_copy_aead =
-      absl::WrapUnique(new MockZeroCopyAead());
+      std::make_unique<MockZeroCopyAead>();
   EXPECT_CALL(*mock_zero_copy_aead, MaxEncryptionSize(kPlaintext.size()))
       .WillOnce(Return(kCiphertext.size()));
   EXPECT_CALL(*mock_zero_copy_aead, Encrypt(kPlaintext, kAssociatedData, _))
@@ -66,7 +66,7 @@
 
 TEST(AeadFromZeroCopyTest, EncryptFailsIfZeroCopyEncryptFails) {
   std::unique_ptr<MockZeroCopyAead> mock_zero_copy_aead =
-      absl::WrapUnique(new MockZeroCopyAead());
+      std::make_unique<MockZeroCopyAead>();
   EXPECT_CALL(*mock_zero_copy_aead, MaxEncryptionSize(kPlaintext.size()))
       .WillOnce(Return(kCiphertext.size()));
   EXPECT_CALL(*mock_zero_copy_aead, Encrypt(kPlaintext, kAssociatedData, _))
@@ -79,7 +79,7 @@
 
 TEST(AeadFromZeroCopyTest, DecryptSucceeds) {
   std::unique_ptr<MockZeroCopyAead> mock_zero_copy_aead =
-      absl::WrapUnique(new MockZeroCopyAead());
+      std::make_unique<MockZeroCopyAead>();
   EXPECT_CALL(*mock_zero_copy_aead, MaxDecryptionSize(kCiphertext.size()))
       .WillOnce(Return(kPlaintext.size()));
   EXPECT_CALL(*mock_zero_copy_aead, Decrypt(kCiphertext, kAssociatedData, _))
@@ -96,7 +96,7 @@
 
 TEST(AeadFromZeroCopyTest, EncryptFailsIfZeroCopyDecryptFails) {
   std::unique_ptr<MockZeroCopyAead> mock_zero_copy_aead =
-      absl::WrapUnique(new MockZeroCopyAead());
+      std::make_unique<MockZeroCopyAead>();
   EXPECT_CALL(*mock_zero_copy_aead, MaxDecryptionSize(kCiphertext.size()))
       .WillOnce(Return(kPlaintext.size()));
   EXPECT_CALL(*mock_zero_copy_aead, Decrypt(kCiphertext, kAssociatedData, _))
diff --git a/cc/aead/internal/aead_util.cc b/cc/aead/internal/aead_util.cc
index bfe0719..22c0bce 100644
--- a/cc/aead/internal/aead_util.cc
+++ b/cc/aead/internal/aead_util.cc
@@ -15,6 +15,8 @@
 ///////////////////////////////////////////////////////////////////////////////
 #include "tink/aead/internal/aead_util.h"
 
+#include <string>
+
 #include "absl/status/status.h"
 #include "openssl/evp.h"
 #include "tink/util/errors.h"
@@ -24,6 +26,18 @@
 namespace tink {
 namespace internal {
 
+bool IsSupportedKmsEnvelopeAeadDekKeyType(absl::string_view key_type) {
+  static const auto *kSupportedDekKeyTypes =
+      new absl::flat_hash_set<std::string>({
+          "type.googleapis.com/google.crypto.tink.AesGcmKey",
+          "type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key",
+          "type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey",
+          "type.googleapis.com/google.crypto.tink.AesEaxKey",
+          "type.googleapis.com/google.crypto.tink.AesGcmSivKey",
+      });
+  return kSupportedDekKeyTypes->contains(key_type);
+}
+
 util::StatusOr<const EVP_CIPHER *> GetAesGcmCipherForKeySize(
     uint32_t key_size_in_bytes) {
   switch (key_size_in_bytes) {
diff --git a/cc/aead/internal/aead_util.h b/cc/aead/internal/aead_util.h
index c8c84d2..094b22d 100644
--- a/cc/aead/internal/aead_util.h
+++ b/cc/aead/internal/aead_util.h
@@ -16,6 +16,9 @@
 #ifndef TINK_AEAD_INTERNAL_AEAD_UTIL_H_
 #define TINK_AEAD_INTERNAL_AEAD_UTIL_H_
 
+#include <string>
+
+#include "absl/container/flat_hash_set.h"
 #include "openssl/evp.h"
 #include "tink/util/statusor.h"
 
@@ -23,6 +26,8 @@
 namespace tink {
 namespace internal {
 
+bool IsSupportedKmsEnvelopeAeadDekKeyType(absl::string_view key_type);
+
 // Returns a pointer to an AES-GCM EVP_CIPHER for the given key size.
 util::StatusOr<const EVP_CIPHER *> GetAesGcmCipherForKeySize(
     uint32_t key_size_in_bytes);
diff --git a/cc/aead/internal/aead_util_test.cc b/cc/aead/internal/aead_util_test.cc
index ea3941a..83c717f 100644
--- a/cc/aead/internal/aead_util_test.cc
+++ b/cc/aead/internal/aead_util_test.cc
@@ -27,6 +27,8 @@
 
 using ::crypto::tink::test::IsOk;
 using ::crypto::tink::test::IsOkAndHolds;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
 using ::testing::Not;
 
 TEST(AeadUtilTest, GetAesGcmCipherForKeySize) {
@@ -42,6 +44,15 @@
   }
 }
 
+TEST(AeadUtilTest, SupportedKmsEnvelopeAeadDekKeyTypes) {
+  EXPECT_THAT(IsSupportedKmsEnvelopeAeadDekKeyType(
+                  "type.googleapis.com/google.crypto.tink.AesGcmKey"),
+              IsTrue());
+  EXPECT_THAT(IsSupportedKmsEnvelopeAeadDekKeyType(
+                  "type.googleapis.com/google.crypto.tink.KmsEnvelopeAeadKey"),
+              IsFalse());
+}
+
 #ifdef OPENSSL_IS_BORINGSSL
 
 TEST(AeadUtilTest, GetAesAeadForKeySize) {
diff --git a/cc/aead/internal/cord_aes_gcm_boringssl.cc b/cc/aead/internal/cord_aes_gcm_boringssl.cc
index 321e9bf..4356bee 100644
--- a/cc/aead/internal/cord_aes_gcm_boringssl.cc
+++ b/cc/aead/internal/cord_aes_gcm_boringssl.cc
@@ -17,22 +17,18 @@
 #include "tink/aead/internal/cord_aes_gcm_boringssl.h"
 
 #include <cstdint>
-#include <iterator>
 #include <memory>
 #include <string>
 #include <utility>
-#include <vector>
 
 #include "absl/status/status.h"
 #include "absl/strings/cord.h"
 #include "openssl/evp.h"
-#include "openssl/err.h"
 #include "tink/aead/cord_aead.h"
 #include "tink/aead/internal/aead_util.h"
 #include "tink/internal/ssl_unique_ptr.h"
 #include "tink/subtle/random.h"
 #include "tink/subtle/subtle_util.h"
-#include "tink/util/errors.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
@@ -47,8 +43,8 @@
 
 // Set the IV `iv` for the given `context`. if `encryption` is true, set the
 // context for encryption, and for decryption otherwise.
-util::Status SetIv(EVP_CIPHER_CTX* context, absl::string_view iv,
-                   bool encryption) {
+util::Status SetIvAndDirection(EVP_CIPHER_CTX* context, absl::string_view iv,
+                               bool encryption) {
   const int encryption_flag = encryption ? 1 : 0;
   // Set the IV size.
   if (EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_SET_IVLEN, iv.size(),
@@ -67,28 +63,100 @@
   return util::OkStatus();
 }
 
+#if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_NUMBER < 0x30000000L
+// Returns a new EVP_CIPHER_CTX for encryption (`encryption` == true) or
+// decryption (`encryption` == false). It tries to skip part of the
+// initialization copying `partial_context`.
+util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> NewContextFromPartial(
+    EVP_CIPHER_CTX* partial_context, absl::string_view iv, bool encryption) {
+  internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
+  if (context == nullptr) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "EVP_CIPHER_CTX_new failed");
+  }
+  // Try making a copy of `partial_context` to skip some pre-computations.
+  //
+  // NOTE: With BoringSSL and OpenSSL 1.1.1 EVP_CIPHER_CTX_copy makes a copy
+  // of the `cipher_data` field of `context` as well, which contains the key
+  // material and IV (see [1] and [2]).
+  //
+  // [1]https://github.com/google/boringssl/blob/4c8bcf0da2951cacd8ed8eaa7fd2df4b22fca23b/crypto/fipsmodule/cipher/cipher.c#L116
+  // [2]https://github.com/openssl/openssl/blob/830bf8e1e4749ad65c51b6a1d0d769ae689404ba/crypto/evp/evp_enc.c#L703
+  if (EVP_CIPHER_CTX_copy(context.get(), partial_context) <= 0) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "EVP_CIPHER_CTX_copy failed");
+  }
+  util::Status res =
+      SetIvAndDirection(context.get(), iv, /*encryption=*/encryption);
+  if (!res.ok()) {
+    return res;
+  }
+  return std::move(context);
+}
+#else
+// Returns a new EVP_CIPHER_CTX for encryption (`encryption` == true) or
+// decryption (`encryption` == false) with given `key` and `iv`.
+//
+// NOTE: Copying the context fails with OpenSSL 3.0, which doesn't provide a
+// `dupctx` function for aead ciphers (see [1], [2]).
+//
+// [1]https://github.com/openssl/openssl/blob/eb52450f5151e8e78743ab05de21a344823316f5/crypto/evp/evp_enc.c#L1427
+// [2]https://github.com/openssl/openssl/blob/cac250755efd0c40cc6127a0e4baceb8d226c7e3/providers/implementations/include/prov/ciphercommon_aead.h#L30
+util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> NewContext(
+    const util::SecretData& key, absl::string_view iv, bool encryption) {
+  internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
+  if (context == nullptr) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "EVP_CIPHER_CTX_new failed");
+  }
+  util::StatusOr<const EVP_CIPHER*> cipher =
+      internal::GetAesGcmCipherForKeySize(key.size());
+  if (!cipher.ok()) {
+    return cipher.status();
+  }
+  if (EVP_CipherInit_ex(context.get(), *cipher, /*impl=*/nullptr,
+                        reinterpret_cast<const uint8_t*>(key.data()),
+                        /*iv=*/nullptr, /*enc=*/1) <= 0) {
+    return util::Status(absl::StatusCode::kInternal,
+                        "Context initialization failed");
+  }
+  util::Status res =
+      SetIvAndDirection(context.get(), iv, /*encryption=*/encryption);
+  if (!res.ok()) {
+    return res;
+  }
+  return std::move(context);
+}
+#endif
+
 }  // namespace
 
-util::StatusOr<std::unique_ptr<CordAead> > CordAesGcmBoringSsl::New(
-    util::SecretData key_value) {
+util::StatusOr<std::unique_ptr<CordAead>> CordAesGcmBoringSsl::New(
+    const util::SecretData& key_value) {
   util::StatusOr<const EVP_CIPHER*> cipher =
       internal::GetAesGcmCipherForKeySize(key_value.size());
   if (!cipher.ok()) {
     return cipher.status();
   }
 
-  internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
-  // Initialize the cipher now to have some precomputations on the key. The
-  // direction (enc/dec) is not important since it will be overwritten later.
-  if (EVP_CipherInit_ex(context.get(), *cipher, /*engine=*/nullptr,
+  internal::SslUniquePtr<EVP_CIPHER_CTX> partial_context(EVP_CIPHER_CTX_new());
+  // Initialize a partial context for the cipher to allow OpenSSL/BoringSSL
+  // making some precomputations on the key. Encrypt and Decrypt will try making
+  // a copy of this context to avoid doing the same initializations again and to
+  // guarantee thread safety.
+  //
+  // NOTE: It doesn't matter at this point if we set the direction to encryption
+  // or decryption, it will be overwritten later any time we call
+  // EVP_CipherInit_ex.
+  if (EVP_CipherInit_ex(partial_context.get(), *cipher, /*engine=*/nullptr,
                         reinterpret_cast<const uint8_t*>(&key_value[0]),
                         /*iv=*/nullptr, /*enc=*/1) <= 0) {
     return util::Status(absl::StatusCode::kInternal,
                         "Context initialization failed");
   }
 
-  std::unique_ptr<CordAead> aead =
-      absl::WrapUnique(new CordAesGcmBoringSsl(std::move(context)));
+  std::unique_ptr<CordAead> aead = absl::WrapUnique(
+      new CordAesGcmBoringSsl(std::move(partial_context), key_value));
   return std::move(aead);
 }
 
@@ -96,18 +164,21 @@
     absl::Cord plaintext, absl::Cord associated_data) const {
   std::string iv = subtle::Random::GetRandomBytes(kIvSizeInBytes);
 
-  internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
-  EVP_CIPHER_CTX_copy(context.get(), context_.get());
-
-  util::Status res = SetIv(context.get(), iv, /*encryption=*/true);
-  if (!res.ok()) {
-    return res;
+#if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_NUMBER < 0x30000000L
+  util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> context =
+      NewContextFromPartial(partial_context_.get(), iv, /*encryption=*/true);
+#else
+  util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> context =
+      NewContext(key_, iv, /*encryption=*/true);
+#endif
+  if (!context.ok()) {
+    return context.status();
   }
 
   int len = 0;
   // Process AAD.
   for (auto ad_chunk : associated_data.Chunks()) {
-    if (!EVP_EncryptUpdate(context.get(), /*out=*/nullptr, &len,
+    if (!EVP_EncryptUpdate(context->get(), /*out=*/nullptr, &len,
                            reinterpret_cast<const uint8_t*>(ad_chunk.data()),
                            ad_chunk.size())) {
       return util::Status(absl::StatusCode::kInternal, "Encryption failed");
@@ -124,7 +195,7 @@
 
   for (auto plaintext_chunk : plaintext.Chunks()) {
     if (!EVP_EncryptUpdate(
-            context.get(),
+            context->get(),
             reinterpret_cast<uint8_t*>(&(buffer[ciphertext_buffer_offset])),
             &len, reinterpret_cast<const uint8_t*>(plaintext_chunk.data()),
             plaintext_chunk.size())) {
@@ -132,13 +203,14 @@
     }
     ciphertext_buffer_offset += plaintext_chunk.size();
   }
-  if (!EVP_EncryptFinal_ex(context.get(), nullptr, &len)) {
+  if (!EVP_EncryptFinal_ex(context->get(), nullptr, &len)) {
     return util::Status(absl::StatusCode::kInternal, "Encryption failed");
   }
 
   std::string tag;
   subtle::ResizeStringUninitialized(&tag, kTagSizeInBytes);
-  if (!EVP_CIPHER_CTX_ctrl(context.get(), EVP_CTRL_GCM_GET_TAG, kTagSizeInBytes,
+  if (!EVP_CIPHER_CTX_ctrl(context->get(), EVP_CTRL_GCM_GET_TAG,
+                           kTagSizeInBytes,
                            reinterpret_cast<uint8_t*>(&tag[0]))) {
     return util::Status(absl::StatusCode::kInternal, "Encryption failed");
   }
@@ -157,23 +229,26 @@
     return util::Status(absl::StatusCode::kInternal, "Ciphertext too short");
   }
 
-  // First bytes contain IV.
+  // First bytes contain the IV.
   std::string iv = std::string(ciphertext.Subcord(0, kIvSizeInBytes));
   absl::Cord raw_ciphertext = ciphertext.Subcord(
       kIvSizeInBytes, ciphertext.size() - kIvSizeInBytes - kTagSizeInBytes);
 
-  internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
-  EVP_CIPHER_CTX_copy(context.get(), context_.get());
-
-  util::Status res = SetIv(context.get(), iv, /*encryption=*/false);
-  if (!res.ok()) {
-    return res;
+#if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_NUMBER < 0x30000000L
+  util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> context =
+      NewContextFromPartial(partial_context_.get(), iv, /*encryption=*/false);
+#else
+  util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> context =
+      NewContext(key_, iv, /*encryption=*/false);
+#endif
+  if (!context.ok()) {
+    return context.status();
   }
 
   int len = 0;
   // Process associated data.
   for (auto ad_chunk : associated_data.Chunks()) {
-    if (!EVP_DecryptUpdate(context.get(), nullptr, &len,
+    if (!EVP_DecryptUpdate(context->get(), /*out=*/nullptr, &len,
                            reinterpret_cast<const uint8_t*>(ad_chunk.data()),
                            ad_chunk.size())) {
       return util::Status(absl::StatusCode::kInternal, "Decryption failed");
@@ -192,7 +267,7 @@
       });
 
   for (auto ct_chunk : raw_ciphertext.Chunks()) {
-    if (!EVP_DecryptUpdate(context.get(),
+    if (!EVP_DecryptUpdate(context->get(),
                            reinterpret_cast<uint8_t*>(
                                &plaintext_buffer[plaintext_buffer_offset]),
                            &len,
@@ -207,13 +282,13 @@
   std::string tag = std::string(
       ciphertext.Subcord(ciphertext.size() - kTagSizeInBytes, kTagSizeInBytes));
 
-  if (!EVP_CIPHER_CTX_ctrl(context.get(), EVP_CTRL_GCM_SET_TAG, kTagSizeInBytes,
-                           &tag[0])) {
+  if (!EVP_CIPHER_CTX_ctrl(context->get(), EVP_CTRL_GCM_SET_TAG,
+                           kTagSizeInBytes, &tag[0])) {
     return util::Status(absl::StatusCode::kInternal,
                         "Could not set authentication tag");
   }
   // Verify authentication tag.
-  if (!EVP_DecryptFinal_ex(context.get(), nullptr, &len)) {
+  if (!EVP_DecryptFinal_ex(context->get(), nullptr, &len)) {
     return util::Status(absl::StatusCode::kInternal, "Authentication failed");
   }
   return result;
diff --git a/cc/aead/internal/cord_aes_gcm_boringssl.h b/cc/aead/internal/cord_aes_gcm_boringssl.h
index 9dcd288..273b523 100644
--- a/cc/aead/internal/cord_aes_gcm_boringssl.h
+++ b/cc/aead/internal/cord_aes_gcm_boringssl.h
@@ -20,12 +20,10 @@
 #include <memory>
 #include <utility>
 
-#include "absl/strings/string_view.h"
 #include "openssl/evp.h"
 #include "tink/aead/cord_aead.h"
 #include "tink/internal/ssl_unique_ptr.h"
 #include "tink/util/secret_data.h"
-#include "tink/util/status.h"
 #include "tink/util/statusor.h"
 
 namespace crypto {
@@ -35,7 +33,7 @@
 class CordAesGcmBoringSsl : public CordAead {
  public:
   static crypto::tink::util::StatusOr<std::unique_ptr<CordAead>> New(
-      util::SecretData key_value);
+      const util::SecretData& key_value);
 
   crypto::tink::util::StatusOr<absl::Cord> Encrypt(
       absl::Cord plaintext, absl::Cord associated_data) const override;
@@ -44,10 +42,15 @@
       absl::Cord ciphertext, absl::Cord associated_data) const override;
 
  private:
-  explicit CordAesGcmBoringSsl(internal::SslUniquePtr<EVP_CIPHER_CTX> context)
-      : context_(std::move(context)) {}
+  explicit CordAesGcmBoringSsl(
+      internal::SslUniquePtr<EVP_CIPHER_CTX> partial_context,
+      const util::SecretData& key)
+      : partial_context_(std::move(partial_context)), key_(key) {}
 
-  internal::SslUniquePtr<EVP_CIPHER_CTX> context_;
+  // Partially-initialized EVP_CIPHER_CTX context that is copied for every
+  // Encrypt/Decrypt operation.
+  internal::SslUniquePtr<EVP_CIPHER_CTX> partial_context_;
+  util::SecretData key_;
 };
 
 }  // namespace internal
diff --git a/cc/aead/internal/ssl_aead.cc b/cc/aead/internal/ssl_aead.cc
index 4b62a8a..c8b2629 100644
--- a/cc/aead/internal/ssl_aead.cc
+++ b/cc/aead/internal/ssl_aead.cc
@@ -44,36 +44,10 @@
 
 ABSL_CONST_INIT const int kXchacha20Poly1305TagSizeInBytes = 16;
 ABSL_CONST_INIT const int kAesGcmTagSizeInBytes = 16;
+ABSL_CONST_INIT const int kAesGcmSivTagSizeInBytes = 16;
 
 namespace {
 
-// Sets `iv` to the given `context`, as well as the "direction"
-// (encrypt/decrypt) based on `encryption`.
-util::Status SetIvAndDirection(EVP_CIPHER_CTX *context, absl::string_view iv,
-                               bool encryption) {
-  if (context == nullptr) {
-    return util::Status(absl::StatusCode::kInternal,
-                        "Context must not be null");
-  }
-  const int encryption_flag = encryption ? 1 : 0;
-  // Set the size for IV first, then set the IV bytes.
-  if (EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_AEAD_SET_IVLEN, iv.size(),
-                          /*ptr=*/nullptr) <= 0) {
-    return util::Status(absl::StatusCode::kInternal, "Setting IV size failed");
-  }
-  if (EVP_CipherInit_ex(context, /*cipher=*/nullptr, /*impl=*/nullptr,
-                        /*key=*/nullptr,
-                        reinterpret_cast<const uint8_t *>(iv.data()),
-                        /*enc=*/encryption_flag) <= 0) {
-    return util::Status(
-        absl::StatusCode::kInternal,
-        absl::StrCat("Failed to initialize the context with IV of size ",
-                     iv.size(), " and for ",
-                     encryption ? "encryption" : "decryption"));
-  }
-  return util::OkStatus();
-}
-
 // Encrypts/Decrypts `data` and writes the result into `out`. The direction
 // (encrypt/decrypt) is given by `context`. `out` is assumed to be large enough
 // to hold the encrypted/decrypted content.
@@ -108,9 +82,9 @@
 
 class OpenSslOneShotAeadImpl : public SslOneShotAead {
  public:
-  OpenSslOneShotAeadImpl(internal::SslUniquePtr<EVP_CIPHER_CTX> context,
-                         size_t tag_size)
-      : context_(std::move(context)), tag_size_(tag_size) {}
+  explicit OpenSslOneShotAeadImpl(const util::SecretData &key,
+                                  const EVP_CIPHER *cipher, size_t tag_size)
+      : key_(key), cipher_(cipher), tag_size_(tag_size) {}
 
   util::StatusOr<int64_t> Encrypt(absl::string_view plaintext,
                                   absl::string_view associated_data,
@@ -140,25 +114,15 @@
                        associated_data.size()));
     }
 
-    // For thread safety we copy the context and only then set the IV. This
-    // allows to allocate an AesGcmBoringSsl cipher, and initialize the context
-    // to force precomputation on the key, and only then set a different IV
-    // for each call to `Encrypt`.
-    internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
-    // This makes a copy of the `cipher_data` field of the context too, which
-    // contains the key material and IV (see
-    // https://github.com/google/boringssl/blob/master/crypto/fipsmodule/cipher/cipher.c#L116).
-    EVP_CIPHER_CTX_copy(context.get(), context_.get());
-
-    util::Status res =
-        SetIvAndDirection(context.get(), iv, /*encryption=*/true);
-    if (!res.ok()) {
-      return res;
+    util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> context =
+        GetContext(iv, /*encryption=*/true);
+    if (!context.ok()) {
+      return context.status();
     }
 
     // Set the associated data.
     int len = 0;
-    if (EVP_EncryptUpdate(context.get(), /*out=*/nullptr, &len,
+    if (EVP_EncryptUpdate(context->get(), /*out=*/nullptr, &len,
                           reinterpret_cast<const uint8_t *>(ad.data()),
                           ad.size()) <= 0) {
       return util::Status(absl::StatusCode::kInternal,
@@ -166,18 +130,18 @@
     }
 
     util::StatusOr<int64_t> raw_ciphertext_bytes =
-        UpdateCipher(context.get(), plaintext_data, out);
+        UpdateCipher(context->get(), plaintext_data, out);
     if (!raw_ciphertext_bytes.ok()) {
       return raw_ciphertext_bytes.status();
     }
 
-    if (EVP_EncryptFinal_ex(context.get(), /*out=*/nullptr, &len) <= 0) {
+    if (EVP_EncryptFinal_ex(context->get(), /*out=*/nullptr, &len) <= 0) {
       return util::Status(absl::StatusCode::kInternal, "Finalization failed");
     }
 
     // Write the tag after the ciphertext.
     absl::Span<char> tag = out.subspan(*raw_ciphertext_bytes, tag_size_);
-    if (EVP_CIPHER_CTX_ctrl(context.get(), EVP_CTRL_AEAD_GET_TAG, tag_size_,
+    if (EVP_CIPHER_CTX_ctrl(context->get(), EVP_CTRL_AEAD_GET_TAG, tag_size_,
                             reinterpret_cast<uint8_t *>(tag.data())) <= 0) {
       return util::Status(absl::StatusCode::kInternal, "Failed to get the tag");
     }
@@ -218,18 +182,15 @@
                        associated_data.size()));
     }
 
-    internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
-    EVP_CIPHER_CTX_copy(context.get(), context_.get());
-
-    util::Status res =
-        SetIvAndDirection(context.get(), iv, /*encryption=*/false);
-    if (!res.ok()) {
-      return res;
+    util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> context =
+        GetContext(iv, /*encryption=*/false);
+    if (!context.ok()) {
+      return context.status();
     }
 
     int len = 0;
     // Add the associated data.
-    if (EVP_DecryptUpdate(context.get(), /*out=*/nullptr, &len,
+    if (EVP_DecryptUpdate(context->get(), /*out=*/nullptr, &len,
                           reinterpret_cast<const uint8_t *>(ad.data()),
                           ad.size()) <= 0) {
       return util::Status(absl::StatusCode::kInternal,
@@ -246,7 +207,7 @@
     auto tag = std::string(ciphertext.substr(raw_ciphertext_size, tag_size_));
 
     // Set the tag.
-    if (EVP_CIPHER_CTX_ctrl(context.get(), EVP_CTRL_AEAD_SET_TAG, tag_size_,
+    if (EVP_CIPHER_CTX_ctrl(context->get(), EVP_CTRL_AEAD_SET_TAG, tag_size_,
                             reinterpret_cast<uint8_t *>(&tag[0])) <= 0) {
       return util::Status(absl::StatusCode::kInternal,
                           "Could not set authentication tag");
@@ -267,12 +228,12 @@
         absl::MakeCleanup([out] { OPENSSL_cleanse(out.data(), out.size()); });
 
     util::StatusOr<int64_t> written_bytes =
-        UpdateCipher(context.get(), raw_ciphertext, out_buffer);
+        UpdateCipher(context->get(), raw_ciphertext, out_buffer);
     if (!written_bytes.ok()) {
       return written_bytes.status();
     }
 
-    if (!EVP_DecryptFinal_ex(context.get(), /*out=*/nullptr, &len)) {
+    if (!EVP_DecryptFinal_ex(context->get(), /*out=*/nullptr, &len)) {
       return util::Status(absl::StatusCode::kInternal, "Authentication failed");
     }
 
@@ -281,20 +242,70 @@
     return *written_bytes;
   }
 
+  int64_t CiphertextSize(int64_t plaintext_length) const override {
+    return plaintext_length + tag_size_;
+  }
+
+  int64_t PlaintextSize(int64_t ciphertext_length) const override {
+    if (ciphertext_length < tag_size_) {
+      return 0;
+    }
+    return ciphertext_length - tag_size_;
+  }
+
  private:
-  const internal::SslUniquePtr<EVP_CIPHER_CTX> context_;
+  // Returns a new EVP_CIPHER_CTX for encryption (`ecryption` == true) or
+  // decryption (`encryption` == false).
+  util::StatusOr<internal::SslUniquePtr<EVP_CIPHER_CTX>> GetContext(
+      absl::string_view iv, bool encryption) const {
+    internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
+    if (context == nullptr) {
+      return util::Status(absl::StatusCode::kInternal,
+                          "EVP_CIPHER_CTX_new failed");
+    }
+    const int encryption_flag = encryption ? 1 : 0;
+    if (EVP_CipherInit_ex(context.get(), cipher_, /*impl=*/nullptr,
+                          /*key=*/nullptr, /*iv=*/nullptr,
+                          encryption_flag) <= 0) {
+      return util::Status(
+          absl::StatusCode::kInternal,
+          absl::StrCat("Failed initializializing context for ",
+                       encryption ? "encryption" : "decryption"));
+    }
+    // Set the size for IV first, then set the IV bytes.
+    if (EVP_CIPHER_CTX_ctrl(context.get(), EVP_CTRL_AEAD_SET_IVLEN, iv.size(),
+                            /*ptr=*/nullptr) <= 0) {
+      return util::Status(
+          absl::StatusCode::kInternal,
+          absl::StrCat("Failed stting size of the IV to ", iv.size()));
+    }
+    if (EVP_CipherInit_ex(context.get(), /*cipher=*/nullptr, /*impl=*/nullptr,
+                          reinterpret_cast<const uint8_t *>(key_.data()),
+                          reinterpret_cast<const uint8_t *>(iv.data()),
+                          encryption_flag) <= 0) {
+      return util::Status(
+          absl::StatusCode::kInternal,
+          absl::StrCat("Failed to set key of size ", key_.size(),
+                       "and IV of size ", iv.size()));
+    }
+
+    return std::move(context);
+  }
+
+  const util::SecretData key_;
+  const EVP_CIPHER *cipher_;
   const size_t tag_size_;
 };
 
 #ifdef OPENSSL_IS_BORINGSSL
 
-// Implementation of the one-shot AEAD cypter. This is purposely internal to an
-// anonymous namespace to disallow direct use of this class other than through
-// the Create* functions below.
+// Implementation of the one-shot AEAD cypter. This is purposely internal to
+// an anonymous namespace to disallow direct use of this class other than
+// through the Create* functions below.
 class BoringSslOneShotAeadImpl : public SslOneShotAead {
  public:
-  BoringSslOneShotAeadImpl(internal::SslUniquePtr<EVP_AEAD_CTX> context,
-                           size_t tag_size)
+  explicit BoringSslOneShotAeadImpl(
+      internal::SslUniquePtr<EVP_AEAD_CTX> context, size_t tag_size)
       : context_(std::move(context)), tag_size_(tag_size) {}
 
   util::StatusOr<int64_t> Encrypt(absl::string_view plaintext,
@@ -388,70 +399,24 @@
     return out_len;
   }
 
+  int64_t CiphertextSize(int64_t plaintext_length) const override {
+    return plaintext_length + tag_size_;
+  }
+
+  int64_t PlaintextSize(int64_t ciphertext_length) const override {
+    if (ciphertext_length < tag_size_) {
+      return 0;
+    }
+    return ciphertext_length - tag_size_;
+  }
+
+ private:
   const internal::SslUniquePtr<EVP_AEAD_CTX> context_;
   const size_t tag_size_;
 };
 
 #endif
 
-#ifdef OPENSSL_IS_BORINGSSL
-// Always use the BoringSSL APIs when available.
-using SslOneShotAeadImpl = BoringSslOneShotAeadImpl;
-#else
-using SslOneShotAeadImpl = OpenSslOneShotAeadImpl;
-#endif
-
-// One shot implementing AES-GCM.
-class SslAesGcmOneShotAead : public SslOneShotAeadImpl {
- public:
-#ifdef OPENSSL_IS_BORINGSSL
-  explicit SslAesGcmOneShotAead(internal::SslUniquePtr<EVP_AEAD_CTX> context)
-      : BoringSslOneShotAeadImpl(std::move(context), kAesGcmTagSizeInBytes) {}
-#else
-  explicit SslAesGcmOneShotAead(internal::SslUniquePtr<EVP_CIPHER_CTX> context)
-      : SslOneShotAeadImpl(std::move(context), kAesGcmTagSizeInBytes) {}
-#endif
-
-  int64_t CiphertextSize(int64_t plaintext_length) const override {
-    return plaintext_length + kAesGcmTagSizeInBytes;
-  }
-
-  int64_t PlaintextSize(int64_t ciphertext_length) const override {
-    if (ciphertext_length < kAesGcmTagSizeInBytes) {
-      return 0;
-    }
-    return ciphertext_length - kAesGcmTagSizeInBytes;
-  }
-};
-
-// One shot implementing AES-GCM-SIV.
-using SslAesGcmSivOneShotAead = SslAesGcmOneShotAead;
-
-// One shot implementing XCHACHA-POLY-1305.
-class SslXchacha20Poly1305OneShotAead : public SslOneShotAeadImpl {
- public:
-#ifdef OPENSSL_IS_BORINGSSL
-  explicit SslXchacha20Poly1305OneShotAead(
-      internal::SslUniquePtr<EVP_AEAD_CTX> context)
-      : BoringSslOneShotAeadImpl(std::move(context), kAesGcmTagSizeInBytes) {}
-#else
-  explicit SslXchacha20Poly1305OneShotAead(
-      internal::SslUniquePtr<EVP_CIPHER_CTX> context)
-      : SslOneShotAeadImpl(std::move(context), kAesGcmTagSizeInBytes) {}
-#endif
-
-  int64_t CiphertextSize(int64_t plaintext_length) const override {
-    return plaintext_length + kXchacha20Poly1305TagSizeInBytes;
-  }
-
-  int64_t PlaintextSize(int64_t ciphertext_length) const override {
-    if (ciphertext_length < kXchacha20Poly1305TagSizeInBytes) {
-      return 0;
-    }
-    return ciphertext_length - kXchacha20Poly1305TagSizeInBytes;
-  }
-};
-
 }  // namespace
 
 util::StatusOr<std::unique_ptr<SslOneShotAead>> CreateAesGcmOneShotCrypter(
@@ -470,6 +435,8 @@
         absl::StatusCode::kInternal,
         absl::StrCat("EVP_AEAD_CTX_new failed: ", internal::GetSslErrors()));
   }
+  return {absl::make_unique<BoringSslOneShotAeadImpl>(std::move(context),
+                                                      kAesGcmTagSizeInBytes)};
 #else
   util::StatusOr<const EVP_CIPHER *> aead_cipher =
       GetAesGcmCipherForKeySize(key.size());
@@ -477,25 +444,9 @@
     return aead_cipher.status();
   }
 
-  internal::SslUniquePtr<EVP_CIPHER_CTX> context(EVP_CIPHER_CTX_new());
-
-  if (context == nullptr) {
-    return util::Status(absl::StatusCode::kInternal,
-                        "EVP_CIPHER_CTX_new failed");
-  }
-
-  // Initialize the context for the cipher having OpenSSL to make some
-  // precomputations on the key. It doesn't matter at this point if we set
-  // encryption or decryption, it will be overwritten later on anyways any
-  // time we call EVP_CipherInit_ex.
-  if (EVP_CipherInit_ex(context.get(), *aead_cipher, /*impl=*/nullptr,
-                        reinterpret_cast<const uint8_t *>(&key[0]),
-                        /*iv=*/nullptr, /*enc=*/1) <= 0) {
-    return util::Status(absl::StatusCode::kInternal,
-                        "Context initialization failed");
-  }
+  return absl::make_unique<OpenSslOneShotAeadImpl>(key, *aead_cipher,
+                                                   kAesGcmTagSizeInBytes);
 #endif
-  return {absl::make_unique<SslAesGcmOneShotAead>(std::move(context))};
 }
 
 util::StatusOr<std::unique_ptr<SslOneShotAead>> CreateAesGcmSivOneShotCrypter(
@@ -513,7 +464,8 @@
                         absl::StrCat("EVP_AEAD_CTX_new initialization Failed: ",
                                      internal::GetSslErrors()));
   }
-  return {absl::make_unique<SslAesGcmSivOneShotAead>(std::move(context))};
+  return {absl::make_unique<BoringSslOneShotAeadImpl>(
+      std::move(context), kAesGcmSivTagSizeInBytes)};
 #else
   return util::Status(absl::StatusCode::kUnimplemented,
                       "AES-GCM-SIV is unimplemented for OpenSSL");
@@ -538,8 +490,8 @@
                         absl::StrCat("EVP_AEAD_CTX_new initialization Failed: ",
                                      internal::GetSslErrors()));
   }
-  return {
-      absl::make_unique<SslXchacha20Poly1305OneShotAead>(std::move(context))};
+  return {absl::make_unique<BoringSslOneShotAeadImpl>(
+      std::move(context), kXchacha20Poly1305TagSizeInBytes)};
 #else
   return util::Status(absl::StatusCode::kUnimplemented,
                       "Xchacha20-Poly1305 is unimplemented for OpenSSL");
diff --git a/cc/aead/internal/ssl_aead.h b/cc/aead/internal/ssl_aead.h
index 6ff877a..3fdc158 100644
--- a/cc/aead/internal/ssl_aead.h
+++ b/cc/aead/internal/ssl_aead.h
@@ -28,9 +28,10 @@
 namespace tink {
 namespace internal {
 
+// Tag sizes.
 ABSL_CONST_INIT extern const int kXchacha20Poly1305TagSizeInBytes;
-// Tag size for both AES-GCM and AES-GCM-SIV.
 ABSL_CONST_INIT extern const int kAesGcmTagSizeInBytes;
+ABSL_CONST_INIT extern const int kAesGcmSivTagSizeInBytes;
 
 // Interface for one-shot AEAD crypters.
 class SslOneShotAead {
diff --git a/cc/aead/internal/ssl_aead_large_inputs_test.cc b/cc/aead/internal/ssl_aead_large_inputs_test.cc
index 1ca2400..9427b78 100644
--- a/cc/aead/internal/ssl_aead_large_inputs_test.cc
+++ b/cc/aead/internal/ssl_aead_large_inputs_test.cc
@@ -33,6 +33,7 @@
 #include "tink/aead/internal/ssl_aead.h"
 #include "tink/config/tink_fips.h"
 #include "tink/internal/ssl_util.h"
+#include "tink/internal/util.h"
 #include "tink/subtle/subtle_util.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/statusor.h"
@@ -87,6 +88,9 @@
 
 // Encrypt/decrypt with an input larger than a MAX int.
 TEST_P(SslOneShotAeadLargeInputsTest, EncryptDecryptLargeInput) {
+  if (IsWindows()) {
+    GTEST_SKIP() << "Skipping on Windows: this currently times out";
+  }
   const int64_t buff_size =
       static_cast<int64_t>(std::numeric_limits<int>::max()) + 1024;
   std::string large_input(buff_size, '0');
diff --git a/cc/aead/internal/ssl_aead_test.cc b/cc/aead/internal/ssl_aead_test.cc
index 375e9cd..23fd35b 100644
--- a/cc/aead/internal/ssl_aead_test.cc
+++ b/cc/aead/internal/ssl_aead_test.cc
@@ -34,7 +34,7 @@
 #include "absl/strings/string_view.h"
 #include "absl/types/span.h"
 #include "tink/aead/internal/wycheproof_aead.h"
-#include "tink/config/tink_fips.h"
+#include "tink/internal/fips_utils.h"
 #include "tink/internal/ssl_util.h"
 #include "tink/subtle/subtle_util.h"
 #include "tink/util/secret_data.h"
@@ -524,7 +524,7 @@
 }
 
 TEST(SslOneShotAeadTest, AesGcmTestFipsOnly) {
-  if (IsFipsModeEnabled() && !FIPS_mode()) {
+  if (IsFipsModeEnabled() && !IsFipsEnabledInSsl()) {
     GTEST_SKIP() << "Test should not run in FIPS mode when BoringCrypto is "
                     "unavailable.";
   }
@@ -539,7 +539,7 @@
 }
 
 TEST(SslOneShotAeadTest, AesGcmTestTestFipsFailWithoutBoringCrypto) {
-  if (!IsFipsModeEnabled() || FIPS_mode()) {
+  if (!IsFipsModeEnabled() || IsFipsEnabledInSsl()) {
     GTEST_SKIP()
         << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
   }
diff --git a/cc/aead/internal/zero_copy_aead_wrapper.cc b/cc/aead/internal/zero_copy_aead_wrapper.cc
index 3ed95cc..b1e8114 100644
--- a/cc/aead/internal/zero_copy_aead_wrapper.cc
+++ b/cc/aead/internal/zero_copy_aead_wrapper.cc
@@ -60,7 +60,7 @@
       absl::string_view ciphertext,
       absl::string_view associated_data) const override;
 
-  ~ZeroCopyAeadSetWrapper() override {}
+  ~ZeroCopyAeadSetWrapper() override = default;
 
  private:
   std::unique_ptr<PrimitiveSet<ZeroCopyAead>> aead_set_;
diff --git a/cc/aead/internal/zero_copy_aead_wrapper.h b/cc/aead/internal/zero_copy_aead_wrapper.h
index 1964a6d..beca0aa 100644
--- a/cc/aead/internal/zero_copy_aead_wrapper.h
+++ b/cc/aead/internal/zero_copy_aead_wrapper.h
@@ -17,6 +17,8 @@
 #ifndef TINK_AEAD_INTERNAL_ZERO_COPY_AEAD_WRAPPER_H_
 #define TINK_AEAD_INTERNAL_ZERO_COPY_AEAD_WRAPPER_H_
 
+#include <memory>
+
 #include "tink/aead.h"
 #include "tink/aead/internal/zero_copy_aead.h"
 #include "tink/primitive_set.h"
@@ -46,4 +48,4 @@
 }  // namespace tink
 }  // namespace crypto
 
-#endif  // TINK_AEAD_AEAD_WRAPPER_H_
+#endif  // TINK_AEAD_INTERNAL_ZERO_COPY_AEAD_WRAPPER_H_
diff --git a/cc/aead/internal/zero_copy_aead_wrapper_test.cc b/cc/aead/internal/zero_copy_aead_wrapper_test.cc
index 489b7de..b4e2218 100644
--- a/cc/aead/internal/zero_copy_aead_wrapper_test.cc
+++ b/cc/aead/internal/zero_copy_aead_wrapper_test.cc
@@ -16,6 +16,7 @@
 
 #include "tink/aead/internal/zero_copy_aead_wrapper.h"
 
+#include <cstring>
 #include <memory>
 #include <string>
 #include <utility>
diff --git a/cc/aead/kms_envelope_aead.cc b/cc/aead/kms_envelope_aead.cc
index 887c253..8f36b0d 100644
--- a/cc/aead/kms_envelope_aead.cc
+++ b/cc/aead/kms_envelope_aead.cc
@@ -27,6 +27,7 @@
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
 #include "tink/aead.h"
+#include "tink/aead/internal/aead_util.h"
 #include "tink/registry.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
@@ -60,6 +61,11 @@
 util::StatusOr<std::unique_ptr<Aead>> KmsEnvelopeAead::New(
     const google::crypto::tink::KeyTemplate& dek_template,
     std::unique_ptr<Aead> remote_aead) {
+  if (!internal::IsSupportedKmsEnvelopeAeadDekKeyType(
+          dek_template.type_url())) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "unsupported key type");
+  }
   if (remote_aead == nullptr) {
     return util::Status(absl::StatusCode::kInvalidArgument,
                         "remote_aead must be non-null");
diff --git a/cc/aead/kms_envelope_aead.h b/cc/aead/kms_envelope_aead.h
index 1fd9d89..acfe066 100644
--- a/cc/aead/kms_envelope_aead.h
+++ b/cc/aead/kms_envelope_aead.h
@@ -58,7 +58,7 @@
       absl::string_view ciphertext,
       absl::string_view associated_data) const override;
 
-  ~KmsEnvelopeAead() override {}
+  ~KmsEnvelopeAead() override = default;
 
  private:
   KmsEnvelopeAead(const google::crypto::tink::KeyTemplate& dek_template,
diff --git a/cc/aead/kms_envelope_aead_key_manager.h b/cc/aead/kms_envelope_aead_key_manager.h
index 04b9e31..69c99d1 100644
--- a/cc/aead/kms_envelope_aead_key_manager.h
+++ b/cc/aead/kms_envelope_aead_key_manager.h
@@ -25,6 +25,7 @@
 #include "absl/status/status.h"
 #include "absl/strings/str_cat.h"
 #include "tink/aead.h"
+#include "tink/aead/internal/aead_util.h"
 #include "tink/core/key_type_manager.h"
 #include "tink/core/template_util.h"
 #include "tink/internal/fips_utils.h"
@@ -75,6 +76,11 @@
       return crypto::tink::util::Status(absl::StatusCode::kInvalidArgument,
                                         "Missing kek_uri.");
     }
+    if (!internal::IsSupportedKmsEnvelopeAeadDekKeyType(
+            format.dek_template().type_url())) {
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "unsupported dek key type");
+    }
     return util::OkStatus();
   }
 
diff --git a/cc/aead/kms_envelope_aead_key_manager_test.cc b/cc/aead/kms_envelope_aead_key_manager_test.cc
index 7b32aa1..4cee9c1 100644
--- a/cc/aead/kms_envelope_aead_key_manager_test.cc
+++ b/cc/aead/kms_envelope_aead_key_manager_test.cc
@@ -26,13 +26,16 @@
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
 #include "tink/aead.h"
+#include "tink/aead/aead_config.h"
 #include "tink/aead/aead_key_templates.h"
 #include "tink/aead/aes_eax_key_manager.h"
 #include "tink/aead/kms_envelope_aead.h"
 #include "tink/kms_client.h"
 #include "tink/kms_clients.h"
+#include "tink/mac/mac_key_templates.h"
 #include "tink/registry.h"
 #include "tink/subtle/aead_test_util.h"
+#include "tink/util/fake_kms_client.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
@@ -46,8 +49,9 @@
 using ::crypto::tink::test::DummyAead;
 using ::crypto::tink::test::DummyKmsClient;
 using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
 using ::crypto::tink::test::StatusIs;
-using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::KeyTemplate;
 using ::google::crypto::tink::KmsEnvelopeAeadKey;
 using ::google::crypto::tink::KmsEnvelopeAeadKeyFormat;
 using ::testing::Eq;
@@ -118,7 +122,15 @@
 
 TEST(KmsEnvelopeAeadKeyManagerTest, ValidateKeyFormatNoTemplate) {
   KmsEnvelopeAeadKeyFormat key_format;
-  *key_format.mutable_dek_template() = AeadKeyTemplates::Aes128Eax();
+  key_format.set_kek_uri("Some uri");
+  EXPECT_THAT(KmsEnvelopeAeadKeyManager().ValidateKeyFormat(key_format),
+              Not(IsOk()));
+}
+
+TEST(KmsEnvelopeAeadKeyManagerTest, ValidateKeyFormatInvalidDekTemplate) {
+  KmsEnvelopeAeadKeyFormat key_format;
+  key_format.set_kek_uri("Some uri");
+  *key_format.mutable_dek_template() = MacKeyTemplates::HmacSha256();
   EXPECT_THAT(KmsEnvelopeAeadKeyManager().ValidateKeyFormat(key_format),
               Not(IsOk()));
 }
@@ -229,6 +241,54 @@
               IsOk());
 }
 
+class KmsEnvelopeAeadKeyManagerDekTemplatesTest
+    : public testing::TestWithParam<KeyTemplate> {
+  void SetUp() override { ASSERT_THAT(AeadConfig::Register(), IsOk()); }
+};
+
+TEST_P(KmsEnvelopeAeadKeyManagerDekTemplatesTest, EncryptDecryp) {
+  util::StatusOr<std::string> kek_uri_result =
+      test::FakeKmsClient::CreateFakeKeyUri();
+  ASSERT_THAT(kek_uri_result, IsOk());
+  std::string kek_uri = kek_uri_result.value();
+  util::Status register_fake_kms_client_status =
+      test::FakeKmsClient::RegisterNewClient(kek_uri, /*credentials_path=*/"");
+  ASSERT_THAT(register_fake_kms_client_status, IsOk());
+
+  KeyTemplate dek_template = GetParam();
+  KeyTemplate env_template =
+      AeadKeyTemplates::KmsEnvelopeAead(kek_uri, dek_template);
+  util::StatusOr<std::unique_ptr<KeysetHandle>> handle =
+      KeysetHandle::GenerateNew(env_template);
+  ASSERT_THAT(handle, IsOk());
+  util::StatusOr<std::unique_ptr<Aead>> envelope_aead =
+      (*handle)->GetPrimitive<Aead>();
+  ASSERT_THAT(envelope_aead, IsOk());
+
+  std::string plaintext = "plaintext";
+  std::string associated_data = "associated_data";
+  util::StatusOr<std::string> ciphertext =
+      (*envelope_aead)->Encrypt(plaintext, associated_data);
+  ASSERT_THAT(ciphertext, IsOk());
+  util::StatusOr<std::string> decrypted =
+      (*envelope_aead)->Decrypt(ciphertext.value(), associated_data);
+  EXPECT_THAT(decrypted, IsOkAndHolds(plaintext));
+
+  std::string invalid_associated_data = "invalid_associated_data";
+  util::StatusOr<std::string> decrypted_with_invalid_associated_data =
+      (*envelope_aead)->Decrypt(ciphertext.value(), invalid_associated_data);
+  EXPECT_THAT(decrypted_with_invalid_associated_data.status(), Not(IsOk()));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    KmsEnvelopeAeadKeyManagerDekTemplatesTest,
+    KmsEnvelopeAeadKeyManagerDekTemplatesTest,
+    testing::Values(AeadKeyTemplates::Aes128Gcm(),
+                    AeadKeyTemplates::Aes256Gcm(),
+                    AeadKeyTemplates::Aes128CtrHmacSha256(),
+                    AeadKeyTemplates::Aes128Eax(),
+                    AeadKeyTemplates::Aes128GcmNoPrefix()));
+
 }  // namespace
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/aead/kms_envelope_aead_test.cc b/cc/aead/kms_envelope_aead_test.cc
index 673a1b5..ef8395d 100644
--- a/cc/aead/kms_envelope_aead_test.cc
+++ b/cc/aead/kms_envelope_aead_test.cc
@@ -21,15 +21,18 @@
 #include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/base/internal/endian.h"
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
+#include "absl/strings/string_view.h"
 #include "tink/aead.h"
 #include "tink/aead/aead_config.h"
 #include "tink/aead/aead_key_templates.h"
+#include "tink/keyset_handle.h"
 #include "tink/mac/mac_key_templates.h"
 #include "tink/registry.h"
 #include "tink/util/status.h"
@@ -37,147 +40,249 @@
 #include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 #include "proto/aes_gcm.pb.h"
+#include "tink/internal/ssl_util.h"
 
 namespace crypto {
 namespace tink {
 namespace {
 
-using crypto::tink::test::DummyAead;
-using crypto::tink::test::IsOk;
-using crypto::tink::test::StatusIs;
-using testing::HasSubstr;
+using ::crypto::tink::Aead;
+using ::crypto::tink::test::DummyAead;
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::KeyTemplate;
+using ::testing::Eq;
+using ::testing::HasSubstr;
+using ::testing::Not;
+using ::testing::SizeIs;
+using ::testing::Test;
 
+constexpr int kEncryptedDekPrefixSize = 4;
+constexpr absl::string_view kRemoteAeadName = "kms-backed-aead";
 
-TEST(KmsEnvelopeAeadTest, BasicEncryptDecrypt) {
-  EXPECT_THAT(AeadConfig::Register(), IsOk());
+class KmsEnvelopeAeadTest : public Test {
+ protected:
+  void TearDown() override { Registry::Reset(); }
+};
 
-  auto dek_template = AeadKeyTemplates::Aes128Eax();
-  std::string remote_aead_name = "kms-backed-aead";
-  auto remote_aead = absl::make_unique<DummyAead>(remote_aead_name);
+TEST_F(KmsEnvelopeAeadTest, EncryptDecryptSucceed) {
+  ASSERT_THAT(AeadConfig::Register(), IsOk());
 
-  auto aead_result = KmsEnvelopeAead::New(dek_template, std::move(remote_aead));
-  EXPECT_THAT(aead_result, IsOk());
-  auto aead = std::move(aead_result.value());
+  // Use an AES-128-GCM primitive as the remote one.
+  util::StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
+      KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Gcm());
+  ASSERT_THAT(keyset_handle, IsOk());
+  KeyTemplate dek_template = AeadKeyTemplates::Aes128Eax();
+  util::StatusOr<std::unique_ptr<Aead>> remote_aead =
+      (*keyset_handle)->GetPrimitive<Aead>();
+
+  util::StatusOr<std::unique_ptr<Aead>> envelope_aead =
+      KmsEnvelopeAead::New(dek_template, *std::move(remote_aead));
+  ASSERT_THAT(envelope_aead, IsOk());
+
   std::string message = "Some data to encrypt.";
-  std::string aad = "Some data to authenticate.";
-  auto encrypt_result = aead->Encrypt(message, aad);
-  EXPECT_THAT(encrypt_result, IsOk());
-  auto decrypt_result = aead->Decrypt(encrypt_result.value(), aad);
-  EXPECT_THAT(decrypt_result, IsOk());
-  EXPECT_EQ(decrypt_result.value(), message);
+  std::string aad = "Some associated data.";
+  util::StatusOr<std::string> encrypt_result =
+      (*envelope_aead)->Encrypt(message, aad);
+  ASSERT_THAT(encrypt_result, IsOk());
+  util::StatusOr<std::string> decrypt_result =
+      (*envelope_aead)->Decrypt(encrypt_result.value(), aad);
+  EXPECT_THAT(decrypt_result, IsOkAndHolds(message));
 }
 
-TEST(KmsEnvelopeAeadTest, NullAead) {
-  auto dek_template = AeadKeyTemplates::Aes128Eax();
-  auto aead_result = KmsEnvelopeAead::New(dek_template, nullptr);
-  EXPECT_THAT(aead_result.status(), StatusIs(absl::StatusCode::kInvalidArgument,
-                                             HasSubstr("non-null")));
-}
-
-TEST(KmsEnvelopeAeadTest, MissingDekKeyManager) {
-  Registry::Reset();
-  auto dek_template = AeadKeyTemplates::Aes128Eax();
-  std::string remote_aead_name = "kms-backed-aead";
-  auto remote_aead = absl::make_unique<DummyAead>(remote_aead_name);
-  auto aead_result = KmsEnvelopeAead::New(dek_template, std::move(remote_aead));
-  EXPECT_THAT(aead_result.status(),
-              StatusIs(absl::StatusCode::kNotFound, HasSubstr("AesEaxKey")));
-}
-
-TEST(KmsEnvelopeAeadTest, WrongDekPrimitive) {
-  EXPECT_THAT(AeadConfig::Register(), IsOk());
-  auto dek_template = MacKeyTemplates::HmacSha256();
-  std::string remote_aead_name = "kms-backed-aead";
-  auto remote_aead = absl::make_unique<DummyAead>(remote_aead_name);
-  auto aead_result = KmsEnvelopeAead::New(dek_template, std::move(remote_aead));
-  EXPECT_THAT(aead_result.status(),
-              StatusIs(absl::StatusCode::kInvalidArgument,
-                       HasSubstr("not among supported primitives")));
-}
-
-TEST(KmsEnvelopeAeadTest, DecryptionErrors) {
-  EXPECT_THAT(AeadConfig::Register(), IsOk());
-
-  auto dek_template = AeadKeyTemplates::Aes128Gcm();
-  std::string remote_aead_name = "kms-backed-aead";
-  auto remote_aead = absl::make_unique<DummyAead>(remote_aead_name);
-
-  auto aead_result = KmsEnvelopeAead::New(dek_template, std::move(remote_aead));
-  EXPECT_THAT(aead_result, IsOk());
-  auto aead = std::move(aead_result.value());
-  std::string message = "Some data to encrypt.";
-  std::string aad = "Some data to authenticate.";
-  auto encrypt_result = aead->Encrypt(message, aad);
-  EXPECT_THAT(encrypt_result, IsOk());
-  auto ct = encrypt_result.value();
-
-  // Empty ciphertext.
-  auto decrypt_result = aead->Decrypt("", aad);
+TEST_F(KmsEnvelopeAeadTest, NewFailsIfReamoteAeadIsNull) {
+  KeyTemplate dek_template = AeadKeyTemplates::Aes128Eax();
   EXPECT_THAT(
-      decrypt_result.status(),
+      KmsEnvelopeAead::New(dek_template, /*remote_aead=*/nullptr).status(),
+      StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("non-null")));
+}
+
+TEST_F(KmsEnvelopeAeadTest, NewFailsIfDekKeyManagerIsNotRegistered) {
+  KeyTemplate dek_template = AeadKeyTemplates::Aes128Eax();
+  auto remote_aead = absl::make_unique<DummyAead>(kRemoteAeadName);
+  EXPECT_THAT(
+      KmsEnvelopeAead::New(dek_template, std::move(remote_aead)).status(),
+      StatusIs(absl::StatusCode::kNotFound, HasSubstr("AesEaxKey")));
+}
+
+TEST_F(KmsEnvelopeAeadTest, NewFailsIfUsingDekTemplateOfUnsupportedKeyType) {
+  ASSERT_THAT(AeadConfig::Register(), IsOk());
+  KeyTemplate dek_template = MacKeyTemplates::HmacSha256();
+  auto remote_aead = absl::make_unique<DummyAead>(kRemoteAeadName);
+  EXPECT_THAT(
+      KmsEnvelopeAead::New(dek_template, std::move(remote_aead)).status(),
+      StatusIs(absl::StatusCode::kInvalidArgument,
+               HasSubstr("unsupported key type")));
+}
+
+TEST_F(KmsEnvelopeAeadTest, DecryptFailsWithInvalidCiphertextOrAad) {
+  ASSERT_THAT(AeadConfig::Register(), IsOk());
+
+  KeyTemplate dek_template = AeadKeyTemplates::Aes128Gcm();
+  auto remote_aead = absl::make_unique<DummyAead>(kRemoteAeadName);
+  util::StatusOr<std::unique_ptr<Aead>> aead =
+      KmsEnvelopeAead::New(dek_template, std::move(remote_aead));
+  ASSERT_THAT(aead, IsOk());
+
+  std::string message = "Some data to encrypt.";
+  std::string aad = "Some associated data.";
+  util::StatusOr<std::string> encrypt_result = (*aead)->Encrypt(message, aad);
+  ASSERT_THAT(encrypt_result, IsOk());
+  auto ciphertext = absl::string_view(*encrypt_result);
+
+  // Ciphertext has size zero or smaller than 4 bytes.
+  EXPECT_THAT(
+      (*aead)->Decrypt(/*ciphertext=*/"", aad).status(),
+      StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("too short")));
+  EXPECT_THAT(
+      (*aead)->Decrypt(/*ciphertext=*/"sh", aad).status(),
       StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("too short")));
 
-  // Short ciphertext.
-  decrypt_result = aead->Decrypt("sh", aad);
+  // Ciphertext is smaller than the size of the key.
+  const int dek_encrypted_key_size = absl::big_endian::Load32(
+      reinterpret_cast<const uint8_t*>(ciphertext.data()));
+  // We leave only key size and key truncated by one.
   EXPECT_THAT(
-      decrypt_result.status(),
-      StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("too short")));
-
-  // Truncated ciphertext.
-  decrypt_result = aead->Decrypt(ct.substr(2), aad);
-  EXPECT_THAT(
-      decrypt_result.status(),
+      (*aead)
+          ->Decrypt(ciphertext.substr(0, 4 + dek_encrypted_key_size - 1), aad)
+          .status(),
       StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("invalid")));
 
-  // Corrupted ciphertext.
-  auto ct_copy = ct;
-  ct_copy[4] = 'a';  // corrupt serialized DEK.
-  decrypt_result = aead->Decrypt(ct_copy, aad);
+  std::string corrupted_ciphertext = *encrypt_result;
+  // Corrupt the serialized DEK.
+  corrupted_ciphertext[4] = 'a';
   EXPECT_THAT(
-      decrypt_result.status(),
+      (*aead)->Decrypt(corrupted_ciphertext, aad).status(),
       StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("invalid")));
 
   // Wrong associated data.
-  decrypt_result = aead->Decrypt(ct, "wrong aad");
-  EXPECT_THAT(decrypt_result.status(),
+  EXPECT_THAT((*aead)->Decrypt(ciphertext, "wrong aad").status(),
               StatusIs(absl::StatusCode::kInternal,
                        HasSubstr("Authentication failed")));
 }
 
-TEST(KmsEnvelopeAeadTest, KeyFormat) {
-  EXPECT_THAT(AeadConfig::Register(), IsOk());
+TEST_F(KmsEnvelopeAeadTest, DekMaintainsCorrectKeyFormat) {
+  ASSERT_THAT(AeadConfig::Register(), IsOk());
 
-  auto dek_template = AeadKeyTemplates::Aes128Gcm();
+  KeyTemplate dek_template = AeadKeyTemplates::Aes128Gcm();
+  auto kms_remote_aead = absl::make_unique<DummyAead>(kRemoteAeadName);
+  util::StatusOr<std::unique_ptr<Aead>> aead =
+      KmsEnvelopeAead::New(dek_template, std::move(kms_remote_aead));
+  ASSERT_THAT(aead, IsOk());
 
-  // Construct a remote AEAD which uses same key template for this test.
-  std::string remote_aead_name = "kms-backed-aead";
-  auto remote_aead = absl::make_unique<DummyAead>(remote_aead_name);
-
-  // Create envelope AEAD and encrypt some data.
-  auto aead_result = KmsEnvelopeAead::New(dek_template, std::move(remote_aead));
-  EXPECT_THAT(aead_result, IsOk());
-
-  auto aead = std::move(aead_result.value());
   std::string message = "Some data to encrypt.";
-  std::string aad = "Some data to authenticate.";
-  auto encrypt_result = aead->Encrypt(message, aad);
-  EXPECT_THAT(encrypt_result, IsOk());
-  auto ct = encrypt_result.value();
+  std::string aad = "Some associated data.";
+  util::StatusOr<std::string> ciphertext = (*aead)->Encrypt(message, aad);
+  ASSERT_THAT(ciphertext, IsOk());
 
-  // Recover DEK from ciphertext
-  auto enc_dek_size =
-      absl::big_endian::Load32(reinterpret_cast<const uint8_t*>(ct.data()));
-
-  remote_aead = absl::make_unique<DummyAead>(remote_aead_name);
-  auto dek_decrypt_result =
-      remote_aead->Decrypt(ct.substr(4, enc_dek_size), "");
+  // Recover DEK from ciphertext (see
+  // https://developers.google.com/tink/wire-format#envelope_encryption).
+  auto enc_dek_size = absl::big_endian::Load32(
+      reinterpret_cast<const uint8_t*>(ciphertext->data()));
+  DummyAead remote_aead = DummyAead(kRemoteAeadName);
+  absl::string_view encrypted_dek =
+      absl::string_view(*ciphertext)
+          .substr(kEncryptedDekPrefixSize, enc_dek_size);
+  util::StatusOr<std::string> dek_proto_bytes =
+      remote_aead.Decrypt(encrypted_dek,
+                          /*associated_data=*/"");
+  ASSERT_THAT(dek_proto_bytes, IsOk());
 
   // Check if we can deserialize a GCM key proto from the decrypted DEK.
   google::crypto::tink::AesGcmKey key;
-  EXPECT_THAT(key.ParseFromString(dek_decrypt_result.value()), true);
-  EXPECT_THAT(key.key_value().size(), testing::Eq(16));
+  EXPECT_TRUE(key.ParseFromString(dek_proto_bytes.value()));
+  EXPECT_THAT(key.key_value(), SizeIs(16));
 }
 
+TEST_F(KmsEnvelopeAeadTest, MultipleEncryptionsProduceDifferentDeks) {
+  ASSERT_THAT(AeadConfig::Register(), IsOk());
+
+  KeyTemplate dek_template = AeadKeyTemplates::Aes128Gcm();
+  auto kms_remote_aead = absl::make_unique<DummyAead>(kRemoteAeadName);
+  util::StatusOr<std::unique_ptr<Aead>> aead =
+      KmsEnvelopeAead::New(dek_template, std::move(kms_remote_aead));
+  ASSERT_THAT(aead, IsOk());
+
+  std::string message = "Some data to encrypt.";
+  std::string aad = "Some associated data.";
+
+  constexpr int kNumIterations = 2;
+  std::vector<google::crypto::tink::AesGcmKey> ciphertexts;
+  ciphertexts.reserve(kNumIterations);
+  for (int i = 0; i < kNumIterations; i++) {
+    util::StatusOr<std::string> ciphertext = (*aead)->Encrypt(message, aad);
+    ASSERT_THAT(ciphertext, IsOk());
+
+    auto enc_dek_size = absl::big_endian::Load32(
+        reinterpret_cast<const uint8_t*>(ciphertext->data()));
+    DummyAead remote_aead = DummyAead(kRemoteAeadName);
+    util::StatusOr<std::string> dek_proto_bytes = remote_aead.Decrypt(
+        ciphertext->substr(kEncryptedDekPrefixSize, enc_dek_size),
+        /*associated_data=*/"");
+    ASSERT_THAT(dek_proto_bytes, IsOk());
+
+    google::crypto::tink::AesGcmKey key;
+    ASSERT_TRUE(key.ParseFromString(dek_proto_bytes.value()));
+    ASSERT_THAT(key.key_value(), SizeIs(16));
+    ciphertexts.push_back(key);
+  }
+
+  for (int i = 0; i < ciphertexts.size() - 1; i++) {
+    for (int j = i + 1; j < ciphertexts.size(); j++) {
+      EXPECT_THAT(ciphertexts[i].SerializeAsString(),
+                  Not(Eq(ciphertexts[j].SerializeAsString())));
+    }
+  }
+}
+
+class KmsEnvelopeAeadDekTemplatesTest
+    : public testing::TestWithParam<KeyTemplate> {
+  void SetUp() override { ASSERT_THAT(AeadConfig::Register(), IsOk()); }
+};
+
+TEST_P(KmsEnvelopeAeadDekTemplatesTest, EncryptDecrypt) {
+  // Use an AES-128-GCM primitive as the remote AEAD.
+  util::StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
+      KeysetHandle::GenerateNew(AeadKeyTemplates::Aes128Gcm());
+  ASSERT_THAT(keyset_handle, IsOk());
+  util::StatusOr<std::unique_ptr<Aead>> remote_aead =
+      (*keyset_handle)->GetPrimitive<Aead>();
+
+  KeyTemplate dek_template = GetParam();
+  util::StatusOr<std::unique_ptr<Aead>> envelope_aead =
+      KmsEnvelopeAead::New(dek_template, *std::move(remote_aead));
+  ASSERT_THAT(envelope_aead, IsOk());
+
+  std::string plaintext = "plaintext";
+  std::string associated_data = "associated_data";
+  util::StatusOr<std::string> ciphertext =
+      (*envelope_aead)->Encrypt(plaintext, associated_data);
+  ASSERT_THAT(ciphertext, IsOk());
+  util::StatusOr<std::string> decrypted =
+      (*envelope_aead)->Decrypt(ciphertext.value(), associated_data);
+  EXPECT_THAT(decrypted, IsOkAndHolds(plaintext));
+}
+
+std::vector<KeyTemplate> GetTestTemplates() {
+  std::vector<KeyTemplate> templates = {
+    AeadKeyTemplates::Aes128Gcm(),
+    AeadKeyTemplates::Aes256Gcm(),
+    AeadKeyTemplates::Aes128CtrHmacSha256(),
+    AeadKeyTemplates::Aes128Eax(),
+    AeadKeyTemplates::Aes128GcmNoPrefix()
+  };
+  if (internal::IsBoringSsl()) {
+    templates.push_back(AeadKeyTemplates::XChaCha20Poly1305());
+    templates.push_back(AeadKeyTemplates::Aes256GcmSiv());
+  }
+  return templates;
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    KmsEnvelopeAeadDekTemplatesTest, KmsEnvelopeAeadDekTemplatesTest,
+    testing::ValuesIn(GetTestTemplates()));
+
 }  // namespace
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/binary_keyset_reader.h b/cc/binary_keyset_reader.h
index 01d4220..7b718c1 100644
--- a/cc/binary_keyset_reader.h
+++ b/cc/binary_keyset_reader.h
@@ -18,6 +18,7 @@
 #define TINK_BINARY_KEYSET_READER_H_
 
 #include <istream>
+#include <memory>
 #include <string>
 
 #include "absl/strings/string_view.h"
diff --git a/cc/binary_keyset_writer.h b/cc/binary_keyset_writer.h
index 802ba41..5e5a0dc 100644
--- a/cc/binary_keyset_writer.h
+++ b/cc/binary_keyset_writer.h
@@ -17,6 +17,7 @@
 #ifndef TINK_BINARY_KEYSET_WRITER_H_
 #define TINK_BINARY_KEYSET_WRITER_H_
 
+#include <memory>
 #include <ostream>
 #include <utility>
 
@@ -38,7 +39,7 @@
       std::unique_ptr<std::ostream> destination_stream);
 
   crypto::tink::util::Status
-  Write(const google::crypto::tink::Keyset& keyset) override;;
+  Write(const google::crypto::tink::Keyset& keyset) override;
 
   crypto::tink::util::Status
   Write(const google::crypto::tink::EncryptedKeyset& encrypted_keyset) override;
diff --git a/cc/catalogue.h b/cc/catalogue.h
deleted file mode 100644
index 0452c4c..0000000
--- a/cc/catalogue.h
+++ /dev/null
@@ -1,46 +0,0 @@
-// 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.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#ifndef TINK_CATALOGUE_H_
-#define TINK_CATALOGUE_H_
-
-#include <string>
-
-#include "absl/base/macros.h"
-#include "tink/key_manager.h"
-#include "tink/util/statusor.h"
-
-namespace crypto {
-namespace tink {
-
-// This class is deprecated. We don't support catalogues anymore.
-template <class P>
-class ABSL_DEPRECATED("Catalogues are not supported anymore.") Catalogue {
- public:
-  // Returns a key manager for the given 'type_url', 'primitive_name',
-  // and version at least 'min_version' (if any found).
-  // Caller owns the returned manager.
-  virtual crypto::tink::util::StatusOr<std::unique_ptr<KeyManager<P>>>
-  GetKeyManager(const std::string& type_url, const std::string& primitive_name,
-                uint32_t min_version) const = 0;
-
-  virtual ~Catalogue() {}
-};
-
-}  // namespace tink
-}  // namespace crypto
-
-#endif  // TINK_CATALOGUE_H_
diff --git a/cc/config.h b/cc/config.h
deleted file mode 100644
index 099a448..0000000
--- a/cc/config.h
+++ /dev/null
@@ -1,116 +0,0 @@
-// 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.
-//
-///////////////////////////////////////////////////////////////////////////////
-
-#ifndef TINK_CONFIG_H_
-#define TINK_CONFIG_H_
-
-#include <string>
-
-#include "absl/status/status.h"
-#include "absl/strings/ascii.h"
-#include "tink/aead/aead_config.h"
-#include "tink/catalogue.h"
-#include "tink/daead/deterministic_aead_config.h"
-#include "tink/hybrid/hybrid_config.h"
-#include "tink/key_manager.h"
-#include "tink/mac/mac_config.h"
-#include "tink/registry.h"
-#include "tink/signature/signature_config.h"
-#include "tink/streamingaead/streaming_aead_config.h"
-#include "tink/util/errors.h"
-#include "tink/util/status.h"
-#include "proto/config.pb.h"
-
-namespace crypto {
-namespace tink {
-
-// Static methods for handling of Tink configurations.
-//
-// Configurations, i.e., collections of key types and their corresponding key
-// managers supported by a specific run-time environment enable control
-// of Tink setup via JSON-formatted config files that determine which key types
-// are supported, and provide a mechanism for deprecation of obsolete/outdated
-// cryptographic schemes (see tink/proto/config.proto for more info).
-//
-// Example usage:
-//
-// RegistryConfig registry_config = ...;
-// auto status = Config::Register(registry_config);
-//
-class Config {
- public:
-  // Returns a KeyTypeEntry for Tink key types with the specified parameters.
-  static std::unique_ptr<google::crypto::tink::KeyTypeEntry>
-  GetTinkKeyTypeEntry(const std::string& catalogue_name,
-                      const std::string& primitive_name,
-                      const std::string& key_proto_name,
-                      int key_manager_version, bool new_key_allowed);
-
-  // Registers a key manager according to the specification in 'entry'.
-  template <class P>
-  static crypto::tink::util::Status Register(
-      const google::crypto::tink::KeyTypeEntry& entry);
-
-  // Registers key managers and primitive wrappers according to the
-  // specification in 'config'.
-  static crypto::tink::util::Status Register(
-      const google::crypto::tink::RegistryConfig& config);
-
- private:
-  static crypto::tink::util::Status Validate(
-      const google::crypto::tink::KeyTypeEntry& entry);
-};
-
-///////////////////////////////////////////////////////////////////////////////
-// Implementation details of templated methods.
-
-// static
-template <class P>
-crypto::tink::util::Status Config::Register(
-    const google::crypto::tink::KeyTypeEntry& entry) {
-  util::Status status;
-  std::string primitive_name = absl::AsciiStrToLower(entry.primitive_name());
-
-  if (primitive_name == "mac") {
-    status = MacConfig::Register();
-  } else if (primitive_name == "aead") {
-    status = AeadConfig::Register();
-  } else if (primitive_name == "deterministicaead") {
-    status = DeterministicAeadConfig::Register();
-  } else if (primitive_name == "hybridencrypt" ||
-             primitive_name == "hybriddecrypt") {
-    status = HybridConfig::Register();
-  } else if (primitive_name == "publickeysign" ||
-             primitive_name == "publickeyverify") {
-    status = SignatureConfig::Register();
-  } else if (primitive_name == "streamingaead") {
-    status = StreamingAeadConfig::Register();
-  } else {
-    status = util::Status(
-        absl::StatusCode::kInvalidArgument,
-        absl::StrCat("Non-standard primitive '", entry.primitive_name(),
-                     "', call Registry::RegisterKeyManager "
-                     "and Registry::"
-                     "RegisterPrimitiveWrapper directly."));
-  }
-  if (!status.ok()) return status;
-  return util::OkStatus();
-}
-
-}  // namespace tink
-}  // namespace crypto
-
-#endif  // TINK_CONFIG_H_
diff --git a/cc/config/BUILD.bazel b/cc/config/BUILD.bazel
index 8ec12df..8d77a3e 100644
--- a/cc/config/BUILD.bazel
+++ b/cc/config/BUILD.bazel
@@ -11,7 +11,6 @@
     include_prefix = "tink/config",
     visibility = ["//visibility:public"],
     deps = [
-        "//:config",
         "//:key_manager",
         "//:registry",
         "//daead:deterministic_aead_config",
@@ -38,28 +37,6 @@
     build_setting_default = False,
 )
 
-bool_flag(
-    name = "tink_use_absl_status",
-    build_setting_default = False,
-)
-
-config_setting(
-    name = "absl_status_enabled",
-    flag_values = {"//config:tink_use_absl_status": "True"},
-    visibility = ["//visibility:public"],
-)
-
-bool_flag(
-    name = "tink_use_absl_statusor",
-    build_setting_default = False,
-)
-
-config_setting(
-    name = "absl_statusor_enabled",
-    flag_values = {"//config:tink_use_absl_statusor": "True"},
-    visibility = ["//visibility:public"],
-)
-
 cc_library(
     name = "tink_fips",
     srcs = ["tink_fips.cc"],
@@ -75,6 +52,60 @@
     ],
 )
 
+cc_library(
+    name = "fips_140_2",
+    srcs = ["fips_140_2.cc"],
+    hdrs = ["fips_140_2.h"],
+    include_prefix = "tink/config",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//:configuration",
+        "//aead:aead_wrapper",
+        "//aead:aes_ctr_hmac_aead_key_manager",
+        "//aead:aes_gcm_key_manager",
+        "//internal:configuration_impl",
+        "//internal:fips_utils",
+        "//mac:hmac_key_manager",
+        "//mac:mac_wrapper",
+        "//mac/internal:chunked_mac_wrapper",
+        "//prf:hmac_prf_key_manager",
+        "//prf:prf_set_wrapper",
+        "//signature:ecdsa_sign_key_manager",
+        "//signature:ecdsa_verify_key_manager",
+        "//signature:public_key_sign_wrapper",
+        "//signature:public_key_verify_wrapper",
+        "//signature:rsa_ssa_pkcs1_sign_key_manager",
+        "//signature:rsa_ssa_pkcs1_verify_key_manager",
+        "//signature:rsa_ssa_pss_sign_key_manager",
+        "//signature:rsa_ssa_pss_verify_key_manager",
+        "@com_google_absl//absl/log:check",
+    ],
+)
+
+cc_library(
+    name = "key_gen_fips_140_2",
+    srcs = ["key_gen_fips_140_2.cc"],
+    hdrs = ["key_gen_fips_140_2.h"],
+    include_prefix = "tink/config",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//:key_gen_configuration",
+        "//aead:aes_ctr_hmac_aead_key_manager",
+        "//aead:aes_gcm_key_manager",
+        "//internal:fips_utils",
+        "//internal:key_gen_configuration_impl",
+        "//mac:hmac_key_manager",
+        "//prf:hmac_prf_key_manager",
+        "//signature:ecdsa_sign_key_manager",
+        "//signature:ecdsa_verify_key_manager",
+        "//signature:rsa_ssa_pkcs1_sign_key_manager",
+        "//signature:rsa_ssa_pkcs1_verify_key_manager",
+        "//signature:rsa_ssa_pss_sign_key_manager",
+        "//signature:rsa_ssa_pss_verify_key_manager",
+        "@com_google_absl//absl/log:check",
+    ],
+)
+
 # tests
 
 cc_test(
@@ -84,7 +115,6 @@
     deps = [
         ":tink_config",
         "//:aead",
-        "//:config",
         "//:deterministic_aead",
         "//:hybrid_decrypt",
         "//:hybrid_encrypt",
@@ -127,3 +157,50 @@
         "@com_google_googletest//:gtest_main",
     ],
 )
+
+cc_test(
+    name = "fips_140_2_test",
+    srcs = ["fips_140_2_test.cc"],
+    deps = [
+        ":fips_140_2",
+        "//aead:aead_key_templates",
+        "//aead:aes_ctr_hmac_aead_key_manager",
+        "//aead:aes_gcm_key_manager",
+        "//internal:configuration_impl",
+        "//internal:fips_utils",
+        "//internal:key_type_info_store",
+        "//mac:aes_cmac_key_manager",
+        "//mac:hmac_key_manager",
+        "//prf:hmac_prf_key_manager",
+        "//proto:tink_cc_proto",
+        "//signature:ecdsa_verify_key_manager",
+        "//signature:rsa_ssa_pkcs1_verify_key_manager",
+        "//signature:rsa_ssa_pss_verify_key_manager",
+        "//util:test_keyset_handle",
+        "//util:test_matchers",
+        "//util:test_util",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "key_gen_fips_140_2_test",
+    srcs = ["key_gen_fips_140_2_test.cc"],
+    deps = [
+        ":key_gen_fips_140_2",
+        "//aead:aead_key_templates",
+        "//aead:aes_ctr_hmac_aead_key_manager",
+        "//aead:aes_gcm_key_manager",
+        "//internal:fips_utils",
+        "//internal:key_gen_configuration_impl",
+        "//mac:aes_cmac_key_manager",
+        "//mac:hmac_key_manager",
+        "//prf:hmac_prf_key_manager",
+        "//proto:tink_cc_proto",
+        "//signature:ecdsa_verify_key_manager",
+        "//signature:rsa_ssa_pkcs1_verify_key_manager",
+        "//signature:rsa_ssa_pss_verify_key_manager",
+        "//util:test_matchers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
diff --git a/cc/config/CMakeLists.txt b/cc/config/CMakeLists.txt
index d479a98..010958c 100644
--- a/cc/config/CMakeLists.txt
+++ b/cc/config/CMakeLists.txt
@@ -1,5 +1,7 @@
 tink_module(config)
 
+add_subdirectory(internal)
+
 tink_cc_library(
   NAME tink_config
   SRCS
@@ -7,7 +9,6 @@
     tink_config.h
   DEPS
     absl::core_headers
-    tink::core::config
     tink::core::key_manager
     tink::core::registry
     tink::daead::deterministic_aead_config
@@ -41,6 +42,56 @@
     tink::util::status
 )
 
+tink_cc_library(
+  NAME fips_140_2
+  SRCS
+    fips_140_2.cc
+    fips_140_2.h
+  DEPS
+    absl::check
+    tink::core::configuration
+    tink::aead::aead_wrapper
+    tink::aead::aes_ctr_hmac_aead_key_manager
+    tink::aead::aes_gcm_key_manager
+    tink::internal::configuration_impl
+    tink::internal::fips_utils
+    tink::mac::hmac_key_manager
+    tink::mac::mac_wrapper
+    tink::mac::internal::chunked_mac_wrapper
+    tink::prf::hmac_prf_key_manager
+    tink::prf::prf_set_wrapper
+    tink::signature::ecdsa_verify_key_manager
+    tink::signature::public_key_sign_wrapper
+    tink::signature::public_key_verify_wrapper
+    tink::signature::rsa_ssa_pkcs1_sign_key_manager
+    tink::signature::rsa_ssa_pkcs1_verify_key_manager
+    tink::signature::rsa_ssa_pss_sign_key_manager
+    tink::signature::rsa_ssa_pss_verify_key_manager
+    tink::signature::ecdsa_sign_key_manager
+)
+
+tink_cc_library(
+  NAME key_gen_fips_140_2
+  SRCS
+    key_gen_fips_140_2.cc
+    key_gen_fips_140_2.h
+  DEPS
+    absl::check
+    tink::core::key_gen_configuration
+    tink::aead::aes_ctr_hmac_aead_key_manager
+    tink::aead::aes_gcm_key_manager
+    tink::internal::fips_utils
+    tink::internal::key_gen_configuration_impl
+    tink::mac::hmac_key_manager
+    tink::prf::hmac_prf_key_manager
+    tink::signature::ecdsa_verify_key_manager
+    tink::signature::rsa_ssa_pkcs1_sign_key_manager
+    tink::signature::rsa_ssa_pkcs1_verify_key_manager
+    tink::signature::rsa_ssa_pss_sign_key_manager
+    tink::signature::rsa_ssa_pss_verify_key_manager
+    tink::signature::ecdsa_sign_key_manager
+)
+
 # tests
 
 tink_cc_test(
@@ -53,7 +104,6 @@
     absl::status
     tink::core::cc
     tink::core::aead
-    tink::core::config
     tink::core::deterministic_aead
     tink::core::hybrid_decrypt
     tink::core::hybrid_encrypt
@@ -89,3 +139,50 @@
     tink::util::status
     tink::util::test_matchers
 )
+
+tink_cc_test(
+  NAME fips_140_2_test
+  SRCS
+    fips_140_2_test.cc
+  DEPS
+    tink::config::fips_140_2
+    gmock
+    tink::aead::aead_key_templates
+    tink::aead::aes_ctr_hmac_aead_key_manager
+    tink::aead::aes_gcm_key_manager
+    tink::internal::configuration_impl
+    tink::internal::fips_utils
+    tink::internal::key_type_info_store
+    tink::mac::aes_cmac_key_manager
+    tink::mac::hmac_key_manager
+    tink::prf::hmac_prf_key_manager
+    tink::signature::ecdsa_verify_key_manager
+    tink::signature::rsa_ssa_pkcs1_verify_key_manager
+    tink::signature::rsa_ssa_pss_verify_key_manager
+    tink::util::test_keyset_handle
+    tink::util::test_matchers
+    tink::util::test_util
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_test(
+  NAME key_gen_fips_140_2_test
+  SRCS
+    key_gen_fips_140_2_test.cc
+  DEPS
+    tink::config::key_gen_fips_140_2
+    gmock
+    tink::aead::aead_key_templates
+    tink::aead::aes_ctr_hmac_aead_key_manager
+    tink::aead::aes_gcm_key_manager
+    tink::internal::fips_utils
+    tink::internal::key_gen_configuration_impl
+    tink::mac::aes_cmac_key_manager
+    tink::mac::hmac_key_manager
+    tink::prf::hmac_prf_key_manager
+    tink::signature::ecdsa_verify_key_manager
+    tink::signature::rsa_ssa_pkcs1_verify_key_manager
+    tink::signature::rsa_ssa_pss_verify_key_manager
+    tink::util::test_matchers
+    tink::proto::tink_cc_proto
+)
diff --git a/cc/config/fips_140_2.cc b/cc/config/fips_140_2.cc
new file mode 100644
index 0000000..0eafff5
--- /dev/null
+++ b/cc/config/fips_140_2.cc
@@ -0,0 +1,134 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "tink/config/fips_140_2.h"
+
+#include "absl/log/check.h"
+#include "tink/aead/aead_wrapper.h"
+#include "tink/aead/aes_ctr_hmac_aead_key_manager.h"
+#include "tink/aead/aes_gcm_key_manager.h"
+#include "tink/configuration.h"
+#include "tink/internal/configuration_impl.h"
+#include "tink/internal/fips_utils.h"
+#include "tink/mac/hmac_key_manager.h"
+#include "tink/mac/internal/chunked_mac_wrapper.h"
+#include "tink/mac/mac_wrapper.h"
+#include "tink/prf/hmac_prf_key_manager.h"
+#include "tink/prf/prf_set_wrapper.h"
+#include "tink/signature/ecdsa_verify_key_manager.h"
+#include "tink/signature/public_key_sign_wrapper.h"
+#include "tink/signature/public_key_verify_wrapper.h"
+#include "tink/signature/rsa_ssa_pkcs1_sign_key_manager.h"
+#include "tink/signature/rsa_ssa_pkcs1_verify_key_manager.h"
+#include "tink/signature/rsa_ssa_pss_sign_key_manager.h"
+#include "tink/signature/rsa_ssa_pss_verify_key_manager.h"
+#include "tink/signature/ecdsa_sign_key_manager.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+util::Status AddMac(Configuration& config) {
+  util::Status status = internal::ConfigurationImpl::AddPrimitiveWrapper(
+      absl::make_unique<MacWrapper>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+  status = internal::ConfigurationImpl::AddPrimitiveWrapper(
+      absl::make_unique<internal::ChunkedMacWrapper>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+
+  return internal::ConfigurationImpl::AddKeyTypeManager(
+      absl::make_unique<HmacKeyManager>(), config);
+}
+
+util::Status AddAead(Configuration& config) {
+  util::Status status = internal::ConfigurationImpl::AddPrimitiveWrapper(
+      absl::make_unique<AeadWrapper>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+
+  status = internal::ConfigurationImpl::AddKeyTypeManager(
+      absl::make_unique<AesCtrHmacAeadKeyManager>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+  return internal::ConfigurationImpl::AddKeyTypeManager(
+      absl::make_unique<AesGcmKeyManager>(), config);
+}
+
+util::Status AddPrf(Configuration& config) {
+  util::Status status = internal::ConfigurationImpl::AddPrimitiveWrapper(
+      absl::make_unique<PrfSetWrapper>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+
+  return internal::ConfigurationImpl::AddKeyTypeManager(
+      absl::make_unique<HmacPrfKeyManager>(), config);
+}
+
+util::Status AddSignature(Configuration& config) {
+  util::Status status = internal::ConfigurationImpl::AddPrimitiveWrapper(
+      absl::make_unique<PublicKeySignWrapper>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+  status = internal::ConfigurationImpl::AddPrimitiveWrapper(
+      absl::make_unique<PublicKeyVerifyWrapper>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+
+  status = internal::ConfigurationImpl::AddAsymmetricKeyManagers(
+      absl::make_unique<EcdsaSignKeyManager>(),
+      absl::make_unique<EcdsaVerifyKeyManager>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+  status = internal::ConfigurationImpl::AddAsymmetricKeyManagers(
+      absl::make_unique<RsaSsaPssSignKeyManager>(),
+      absl::make_unique<RsaSsaPssVerifyKeyManager>(), config);
+  if (!status.ok()) {
+    return status;
+  }
+  return internal::ConfigurationImpl::AddAsymmetricKeyManagers(
+      absl::make_unique<RsaSsaPkcs1SignKeyManager>(),
+      absl::make_unique<RsaSsaPkcs1VerifyKeyManager>(), config);
+}
+
+}  // namespace
+
+const Configuration& ConfigFips140_2() {
+  static const Configuration* instance = [] {
+    internal::SetFipsRestricted();
+
+    static Configuration* config = new Configuration();
+    CHECK_OK(AddMac(*config));
+    CHECK_OK(AddAead(*config));
+    CHECK_OK(AddPrf(*config));
+    CHECK_OK(AddSignature(*config));
+
+    return config;
+  }();
+  return *instance;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/config/fips_140_2.h b/cc/config/fips_140_2.h
new file mode 100644
index 0000000..24fa9b1
--- /dev/null
+++ b/cc/config/fips_140_2.h
@@ -0,0 +1,33 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef TINK_CONFIG_FIPS_140_2_H_
+#define TINK_CONFIG_FIPS_140_2_H_
+
+#include "tink/configuration.h"
+
+namespace crypto {
+namespace tink {
+
+// Configuration used to generate primitives using FIPS 140-2-compliant key
+// types. Importing this Configuration restricts Tink to FIPS globally and
+// requires BoringSSL to be built with the BoringCrypto module.
+const Configuration& ConfigFips140_2();
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_CONFIG_FIPS_140_2_H_
diff --git a/cc/config/fips_140_2_test.cc b/cc/config/fips_140_2_test.cc
new file mode 100644
index 0000000..7f134c9
--- /dev/null
+++ b/cc/config/fips_140_2_test.cc
@@ -0,0 +1,142 @@
+// Copyright 2023 Google LLC
+//
+// 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.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include "tink/config/fips_140_2.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "tink/aead/aead_key_templates.h"
+#include "tink/aead/aes_ctr_hmac_aead_key_manager.h"
+#include "tink/aead/aes_gcm_key_manager.h"
+#include "tink/internal/configuration_impl.h"
+#include "tink/internal/fips_utils.h"
+#include "tink/internal/key_type_info_store.h"
+#include "tink/mac/aes_cmac_key_manager.h"
+#include "tink/mac/hmac_key_manager.h"
+#include "tink/prf/hmac_prf_key_manager.h"
+#include "tink/signature/ecdsa_verify_key_manager.h"
+#include "tink/signature/rsa_ssa_pkcs1_verify_key_manager.h"
+#include "tink/signature/rsa_ssa_pss_verify_key_manager.h"
+#include "tink/util/test_keyset_handle.h"
+#include "tink/util/test_matchers.h"
+#include "tink/util/test_util.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::IsOkAndHolds;
+using ::crypto::tink::test::StatusIs;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::Keyset;
+using ::google::crypto::tink::KeyStatusType;
+using ::google::crypto::tink::KeyTemplate;
+using ::google::crypto::tink::OutputPrefixType;
+
+class Fips1402Test : public ::testing::Test {
+ protected:
+  void TearDown() override { internal::UnSetFipsRestricted(); }
+};
+
+TEST_F(Fips1402Test, ConfigFips1402) {
+  if (!internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP() << "Only test in FIPS mode";
+  }
+
+  util::StatusOr<const internal::KeyTypeInfoStore*> store =
+      internal::ConfigurationImpl::GetKeyTypeInfoStore(ConfigFips140_2());
+  ASSERT_THAT(store, IsOk());
+
+  EXPECT_THAT((*store)->Get(HmacKeyManager().get_key_type()), IsOk());
+  EXPECT_THAT((*store)->Get(AesCtrHmacAeadKeyManager().get_key_type()), IsOk());
+  EXPECT_THAT((*store)->Get(AesGcmKeyManager().get_key_type()), IsOk());
+  EXPECT_THAT((*store)->Get(HmacPrfKeyManager().get_key_type()), IsOk());
+  EXPECT_THAT((*store)->Get(EcdsaVerifyKeyManager().get_key_type()), IsOk());
+  EXPECT_THAT((*store)->Get(RsaSsaPssVerifyKeyManager().get_key_type()),
+              IsOk());
+  EXPECT_THAT((*store)->Get(RsaSsaPkcs1VerifyKeyManager().get_key_type()),
+              IsOk());
+}
+
+TEST_F(Fips1402Test, ConfigFips1402FailsInNonFipsMode) {
+  if (internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP() << "Only test in non-FIPS mode";
+  }
+
+  EXPECT_DEATH_IF_SUPPORTED(
+      ConfigFips140_2(), "BoringSSL not built with the BoringCrypto module.");
+}
+
+TEST_F(Fips1402Test, NonFipsTypeNotPresent) {
+  if (!internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP() << "Only test in FIPS mode";
+  }
+
+  util::StatusOr<const internal::KeyTypeInfoStore*> store =
+      internal::ConfigurationImpl::GetKeyTypeInfoStore(ConfigFips140_2());
+  ASSERT_THAT(store, IsOk());
+  EXPECT_THAT((*store)->Get(AesCmacKeyManager().get_key_type()).status(),
+              StatusIs(absl::StatusCode::kNotFound));
+}
+
+TEST_F(Fips1402Test, NewKeyDataAndGetPrimitive) {
+  if (!internal::IsFipsEnabledInSsl()) {
+    GTEST_SKIP() << "Only test in FIPS mode";
+  }
+
+  // TODO(b/265705174): Replace with KeysetHandle::GenerateNew once that takes a
+  // config parameter.
+  util::StatusOr<const internal::KeyTypeInfoStore*> store =
+      internal::ConfigurationImpl::GetKeyTypeInfoStore(ConfigFips140_2());
+  ASSERT_THAT(store, IsOk());
+  KeyTemplate templ = AeadKeyTemplates::Aes128Gcm();
+  util::StatusOr<internal::KeyTypeInfoStore::Info*> info =
+      (*store)->Get(templ.type_url());
+  ASSERT_THAT(info, IsOk());
+
+  util::StatusOr<std::unique_ptr<KeyData>> key_data =
+      (*info)->key_factory().NewKeyData(templ.value());
+  ASSERT_THAT(key_data, IsOk());
+
+  Keyset keyset;
+  uint32_t key_id = 0;
+  test::AddKeyData(**key_data, key_id, OutputPrefixType::TINK,
+                   KeyStatusType::ENABLED, &keyset);
+  keyset.set_primary_key_id(key_id);
+
+  std::unique_ptr<KeysetHandle> handle =
+      TestKeysetHandle::GetKeysetHandle(keyset);
+  util::StatusOr<std::unique_ptr<Aead>> aead =
+      handle->GetPrimitive<Aead>(ConfigFips140_2());
+  EXPECT_THAT(aead, IsOk());
+
+  std::string plaintext = "plaintext";
+  std::string ad = "ad";
+  util::StatusOr<std::string> ciphertext = (*aead)->Encrypt(plaintext, ad);
+  ASSERT_THAT(ciphertext, IsOk());
+
+  util::StatusOr<std::string> decrypted = (*aead)->Decrypt(*ciphertext, ad);
+  EXPECT_THAT(decrypted, IsOkAndHolds(plaintext));
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/config/internal/BUILD.bazel b/cc/config/internal/BUILD.bazel
new file mode 100644
index 0000000..6d94c3a
--- /dev/null
+++ b/cc/config/internal/BUILD.bazel
@@ -0,0 +1,29 @@
+package(def