A Google Tink KMS extension for
Thales CipherTrust Manager:
a KmsClient/Aead pair that keeps your key-encryption key (KEK) inside
CipherTrust Manager and forwards all wrap/unwrap operations to its REST API
(/api/v1/crypto, AES-GCM with associated data).
Structured after the official out-of-repo Tink extensions (tink-java-hcvault, tink-java-awskms). The Tink maintainers do not accept new KMS backends upstream, so this lives as a standalone extension, as they recommend.
ciphertrust://<host[:port]>/<key-name>
Example: ciphertrust://cm.example.com/my-kek, where my-kek is the name of
an AES key in CipherTrust Manager with the Encrypt and Decrypt usages
(usageMask: 12). Connections always use HTTPS. The key does not need to be
exportable — mark it unexportable so the KEK can never leave the appliance.
import ch.tillo.tink.ciphertrust.CipherTrustCredentials;
import ch.tillo.tink.ciphertrust.CipherTrustKmsClient;
import com.google.crypto.tink.Aead;
String keyUri = "ciphertrust://cm.example.com/my-kek";
Aead kek = CipherTrustKmsClient
.create(keyUri, CipherTrustCredentials.usernamePassword("tink-service", password))
.getAead(keyUri);Envelope encryption (KEK stays remote, data keys are local):
Aead envelope = KmsEnvelopeAead.create(PredefinedAeadParameters.AES256_GCM, kek);
byte[] ciphertext = envelope.encrypt(plaintext, associatedData);Wrapping a Tink keyset with the CipherTrust KEK (the "vault master key" pattern used by CLM/secret-store applications):
String wrapped = TinkJsonProtoKeysetFormat.serializeEncryptedKeyset(handle, kek, ad);
KeysetHandle restored = TinkJsonProtoKeysetFormat.parseEncryptedKeyset(wrapped, kek, ad);Applications that resolve KMS-backed keysets through Tink's global registry can use the registration helper instead:
CipherTrustKmsClient.register(Optional.of(keyUri), Optional.of("/etc/ciphertrust-creds.json"));Two grant types of POST /api/v1/auth/tokens are supported; the short-lived
JWT (300 s by default) is cached and renewed transparently, including one
reactive re-authentication if the server answers 401.
| Source | Password grant | Refresh-token grant |
|---|---|---|
| Programmatic | CipherTrustCredentials.usernamePassword(u, p) |
CipherTrustCredentials.ofRefreshToken(t) |
Environment (withDefaultCredentials()) |
CIPHERTRUST_USERNAME + CIPHERTRUST_PASSWORD |
CIPHERTRUST_REFRESH_TOKEN |
JSON file (withCredentials(path)) |
{"username": "...", "password": "..."} |
{"refresh_token": "..."} |
Use a dedicated CipherTrust service user that owns (or is granted) only the KEK — key usage does not require any admin group membership.
Aead.encrypt returns a small self-describing JSON envelope:
{"v":1,"id":"<opaque key id>","ver":<key version>,"iv":"<b64>","tag":"<b64>","ct":"<b64>"}The envelope pins the key version used at encryption time, so decryption
keeps working after the KEK is rotated (rotation only affects which version
new encryptions use). Decryption always addresses the key by the name the
Aead was configured with — the id in the envelope is informational only
and is never used to select the key — so a ciphertext produced under a
different CipherTrust key fails the GCM tag check instead of being silently
decrypted, even when the same credentials can access that other key.
Associated data is forwarded to CipherTrust's aad field and enforced by the
server-side GCM operation.
Every operation is bounded in time. The defaults (10 s connect, 30 s per HTTP
exchange, 3 attempts with exponential backoff starting at 300 ms) can be tuned
with CipherTrustTransport:
CipherTrustKmsClient client = CipherTrustKmsClient.create(keyUri, credentials)
.withTransport(CipherTrustTransport.of(
Duration.ofSeconds(5), // connect timeout
Duration.ofSeconds(15), // request timeout
3, // attempts (1 = no retries)
Duration.ofMillis(300))); // initial backoff, doubled per retryOnly transient transport failures are retried: I/O errors and HTTP 502/503/504 (a CipherTrust node restarting behind its front-end). Deterministic answers — bad credentials, unknown key, AAD mismatch, any other 4xx and 500 — fail immediately. All operations this library issues are safe to repeat, so retries never duplicate effects.
An unreachable CipherTrust Manager therefore surfaces as a
GeneralSecurityException within roughly attempts x (connect + request timeout) + backoff — never as an indefinite hang. This matters when the first
call sits on an application's boot path, e.g. unwrapping a vault keyset.
If the CipherTrust Manager presents a certificate from a private CA, supply a pre-configured client:
HttpClient httpClient = HttpClient.newBuilder().sslContext(myTrustingContext).build();
CipherTrustKmsClient client = CipherTrustKmsClient.create(keyUri, credentials)
.withHttpClient(httpClient);A single HttpClient (the one you supply, or a lazily-created default) is
shared across every Aead the client produces.
- Java 11+.
- Compiled against
com.google.crypto.tink:tink1.21.0; theKmsClient,Aead, andKmsClientssurfaces used here are stable since Tink 1.0.0, so the artifact also runs against newer cores on the host application's classpath. - Verified against CipherTrust Manager 2.22.0 (also exercised by the live-integration test suite below). The key-management and crypto REST services used here are included in the free Community Edition.
- Only runtime dependencies:
tinkitself andgson(which is already a transitive dependency oftink).
mvn verify
Unit tests run against an in-process fake that mimics CipherTrust Manager
behavior verified on a live 2.22 instance (strict Accept header checking,
token expiry/revocation, versioned keys, AAD enforcement).
Live integration tests are enabled automatically when the environment defines:
CIPHERTRUST_TEST_KEY_URI=ciphertrust://cm.example.com/my-test-kek
CIPHERTRUST_USERNAME=tink-service
CIPHERTRUST_PASSWORD=... # or CIPHERTRUST_REFRESH_TOKEN=...