KMS envelope encryption for Rust
Library provides envelope encryption with a key encryption key held by a KMS, and AEAD encryption without a KMS:
- KMS providers for:
- Google Cloud KMS;
- AWS KMS;
- Azure Key Vault and Managed HSM;
- HashiCorp Vault Transit;
- A local KMS with a key the application holds, for development, tests and single-host installs;
- AEAD algorithms ChaCha20-Poly1305, AES-128-GCM, AES-256-GCM and AES-256-GCM-SIV;
- Tink’s KMS envelope format for every provider, so Tink reads the values of GCP and AWS keys and the library reads Tink’s, see Formats;
- Crypto backends ring, aws-lc-rs (including its FIPS module) and the pure Rust RustCrypto crates. A value sealed by one backend opens with any other;
- Per-call envelopes, store-less DEK reuse, and an in-process cache of decrypted data encryption keys;
- Deterministic encryption with AES-SIV, for values that are looked up by equality;
- Typed fields,
Encrypted<T, F>, stored with serde, tokio-postgres or sqlx; - Optional KMS-side random generators for GCP and AWS;
- Tracing spans for every operation and every KMS call, with no plaintext, key material or AAD in them;
- No unsafe code.
To start using the library, see Getting started.
Envelope encryption
A value is sealed with an AEAD algorithm under a data encryption key (DEK). The DEK is then
encrypted by a key encryption key (KEK) that never leaves the KMS, and the encrypted DEK is stored
next to the cipher text. KMS documentation, and Azure Key Vault’s wrapKey and unwrapKey
operations, call this wrapping the DEK. To read the value, the KMS decrypts the DEK, and the DEK
opens the value locally.
So the KMS sees only DEKs, never the data, and a KMS call is needed only to encrypt or decrypt a DEK. Access to the data is controlled by access to the KEK: revoke the KMS permission, or destroy the key, and the stored values cannot be opened.
The additional authenticated data (AAD) is authenticated but not encrypted: a value opens only with the AAD it was sealed with. Use it to tie a value to where it is stored, such as a table and a row id, so that a value copied to another row fails to open.
Choosing a mode
The ways to use a KMS differ in how many DEKs exist, how often the KMS is called and what a stored value holds:
- Per-call envelope, the default of
KmsAeadEnvelope: a new DEK for every value, encrypted with one KMS call, and stored in the value itself. Every decryption decrypts its DEK with one more KMS call, unless the envelope has a DEK cache. Use it when values are written rarely, such as secrets and configuration, or when one DEK per value is a requirement. See Envelope encryption; - Store-less DEK reuse, a
KmsAeadEnvelopewith aDekReusePolicy: one DEK seals many values and is renewed by age or by count, so a write calls the KMS only to renew it. Values still carry their encrypted DEK, so nothing is stored apart from them, and reads decrypt through the DEK cache. Use it for high write rates where a KMS call per value costs too much. See DEK caching and reuse; - One fixed key,
encryptofAeadCipherorDeterministicCipherwith one key that the KMS encrypts once and the DEK cache decrypts: values carry a 4-byte key id in Tink’s wire format instead of an encrypted DEK, and BigQuery and Tink read them with a keyset of that key. Use it for a column that BigQuery reads, with no key rotation. See One fixed key; - Keyring, the
kms-aead-keyringcrate: a keyring record per scope holds the encrypted DEK versions, and values carry a 4-byte key id in Tink’s wire format instead of an encrypted DEK. The record is the place for central rotation and retirement of keys, and for values that BigQuery and Tink read across rotations. See kms-aead-keyring.
Both envelope modes seal a value of a Rust type, such as an application newtype, into an
Encrypted<T, Envelope> that carries its encrypted DEK, and serde, tokio-postgres and sqlx store
it as bytes, so a column of secrets needs no store of its own. See
Typed fields.
AeadCipher and DeterministicCipher work without a KMS at all, with keys the application
holds.
Licence
Apache Software License (ASL)
Getting started
Cargo.toml:
[dependencies]
kms-aead = { version = "0.30", features = ["gcp-kms-encryption", "aws-lc"] }
The crate has no default features. A build makes three choices, and they do not constrain each other:
- the KMS providers, any number of them, or none for AEAD without a KMS;
- the TLS root store and the TLS crypto provider of each provider that has a choice;
- the crypto backends that run the AEAD algorithms, one or more.
Features
KMS providers:
gcp-kms-encryption: Google Cloud KMS, with the native TLS roots of the system. Be aware it also turns on native-tls in reqwest, so in the same build the Azure provider uses native-tls whatever its TLS feature;gcp-kms-encryption-webpki: Google Cloud KMS with the bundled WebPKI roots;gcp-kms-encryption-base: Google Cloud KMS without a TLS root store or crypto provider. The application enables them with its owngcloud-sdkfeatures, such astls-rootsortls-webpki-roots, and the crate does not build without one;aws-kms-encryption: AWS KMS;azure-kms-encryption: Azure Key Vault and Managed HSM, with rustls and the aws-lc-rs crypto provider. Other TLS variants, choose one:azure-kms-encryption-tls-aws-lc-rs, the same asazure-kms-encryption;azure-kms-encryption-tls-no-provider, rustls with the crypto provider the application installs. The application must install a process-default rustlsCryptoProviderbefore creating the provider, otherwise rustls panics;azure-kms-encryption-native-tls, the platform TLS library;
hashicorp-vault-transit-kms-encryption: HashiCorp Vault Transit, with the system root certificates and the aws-lc-rs crypto provider. To choose them, enable one root store and one crypto provider instead:hashicorp-vault-transit-kms-encryption-tls-rootsorhashicorp-vault-transit-kms-encryption-tls-webpki;hashicorp-vault-transit-kms-encryption-tls-aws-lc-rsorhashicorp-vault-transit-kms-encryption-tls-ring.
The local KMS provider, LocalKmsProvider, is in every build and needs no feature.
Crypto backends, see Crypto backends:
ring: the ring library;aws-lc: the aws-lc-rs library;aws-lc-fips: the aws-lc-rs library on the AWS-LC FIPS module. Needs CMake and Go to build;rustcrypto: the pure Rust RustCrypto crates, no C compiler needed.
Other features:
deterministic: deterministic encryption with AES-SIV, see Deterministic encryption;legacy-formats: reads the layouts written by 0.29 and earlier. Deprecated, see Formats;serde:SerializeandDeserializeforEncrypted, as bytes, forKmsAeadErrorPublicGenericDetailsand forSecretValue;serde-json:Json, a typed field’s plaintext of any serde type, stored as its JSON;postgres:ToSqlandFromSqlof postgres-types forEncrypted, asBYTEA, for tokio-postgres;sqlx: sqlx’s PostgreSQL traits forEncrypted, asBYTEA, without a sqlx runtime or TLS stack.
See Typed fields.
Choosing a backend
The default features of the four cloud providers already build aws-lc-rs for TLS, so aws-lc is
the backend that adds no second crypto library to a provider build. ring and rustcrypto work
with every provider too, and a build may enable several backends at once. Each feature adds a type
to kms_aead::backends, Ring, AwsLc or RustCrypto, and every type that seals or encrypts
takes one as its backend type parameter:
fn example() -> kms_aead::KmsAeadResult<()> {
use kms_aead::backends::AwsLc;
use kms_aead::{AeadAlgorithm, AeadCipher};
let cipher = AeadCipher::<AwsLc>::new(AeadAlgorithm::Aes256Gcm)?;
let _ = cipher;
Ok(())
}
There is no default backend. Code generic over the backend bounds it with B: AeadBackend and
nothing else, see Crypto backends.
Feature rules
The crate refuses some feature combinations at compile time with compile_error!:
- a provider feature without a backend feature fails with “kms-aead needs a crypto backend for its KMS providers: enable “ring”, “aws-lc”, “aws-lc-fips” or “rustcrypto”“;
- HashiCorp Vault Transit needs exactly one of
-tls-rootsand-tls-webpki, and exactly one of-tls-aws-lc-rsand-tls-ring.hashicorp-vault-transit-kms-encryptionalready enables-tls-rootsand-tls-aws-lc-rs, so combine the other variants withhashicorp-vault-transit-kms-encryption-baseinstead; - Azure needs exactly one of
azure-kms-encryption-tls-aws-lc-rs(orazure-kms-encryption),azure-kms-encryption-tls-no-providerandazure-kms-encryption-native-tls.
The aws-lc-rs feature that aws-lc and aws-lc-fips enable is not a backend choice: enabled
alone, the aws-lc-rs build fails. Use aws-lc or aws-lc-fips.
A build without any backend feature compiles, and LocalKmsProvider is in it, but nothing that
seals or encrypts can be constructed, since no type implements AeadBackend.
First example
The local KMS needs no cloud account, so it is the quickest way to try the library. An ephemeral KEK lives as long as the process:
use kms_aead::backends::Ring;
use kms_aead::formats::Envelope;
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::{Encrypted, KmsAeadEnvelope};
use secret_vault_value::SecretValue;
#[tokio::main(flavor = "current_thread")]
async fn main() -> kms_aead::KmsAeadResult<()> {
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("example".to_string())?)?;
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::new(provider)?;
let aad = b"orders/1042";
let secret = SecretValue::from("4111 1111 1111 1111");
let encrypted: Encrypted<SecretValue, Envelope> = envelope.encrypt(aad, &secret).await?;
let decrypted: SecretValue = envelope.decrypt(aad, &encrypted).await?;
assert_eq!(decrypted, secret);
Ok(())
}
To move to a cloud KMS, replace the provider, see KMS providers. The rest of the code stays the same.
MSRV
The minimum supported Rust version is 1.99, for every feature.
KMS providers
A provider encrypts and decrypts data encryption keys with one key encryption key (KEK). Each one is
a type in kms_aead::providers, enabled by its feature:
| KMS | Type | Feature |
|---|---|---|
| Google Cloud KMS | GcpKmsProvider | gcp-kms-encryption |
| AWS KMS | AwsKmsProvider | aws-kms-encryption |
| Azure Key Vault and Managed HSM | AzureKmsProvider | azure-kms-encryption |
| HashiCorp Vault Transit | HcVaultTransitProvider | hashicorp-vault-transit-kms-encryption |
| A KEK the process holds | LocalKmsProvider | none, in every build |
Every provider implements KmsAeadEncryptionProvider, and KmsAeadEnvelope takes any of them:
use kms_aead::backends::AwsLc;
use kms_aead::providers::{GcpKmsKeyRef, GcpKmsProvider};
use kms_aead::KmsAeadEnvelope;
async fn example() -> kms_aead::KmsAeadResult<()> {
let kms_ref = GcpKmsKeyRef::new(
"my-project".to_string(),
"europe-north1".to_string(),
"my-key-ring".to_string(),
"my-key".to_string(),
);
let provider = GcpKmsProvider::new(&kms_ref).await?;
let envelope = KmsAeadEnvelope::<GcpKmsProvider, AwsLc>::new(provider)?;
let _ = envelope;
Ok(())
}
Each provider names its configured KEK on every decryption, so a DEK encrypted under another KEK
fails at the KMS and never opens a value. provider.kek() returns that KEK as a KekRef: the KMS
and the key name. It is also the provider and key field of the KMS spans
and the key of the DEK cache. Nothing else is checked: an encrypted DEK carries
no context that the KMS authenticates, see Security considerations.
KMS random generators
By default a DEK comes from the CSPRNG of the crypto backend. The GCP and AWS providers can take it from the KMS instead, at the cost of one more KMS request per new DEK:
- GCP:
GcpKmsProviderOptions::new().with_use_kms_random_gen(true), Cloud KMSGenerateRandomBytesat the HSM protection level; - AWS:
AwsKmsProviderOptions::new().with_use_kms_random_gen(true), AWS KMSGenerateRandom.
Azure, Vault Transit and the local KMS always use the backend’s CSPRNG. Nonces always come from the backend’s CSPRNG.
Your own provider
An application can implement KmsAeadEncryptionProvider for a KMS of its own, or as a wrapper
around another provider. The implementation:
- returns from
kek()aKekRefwithKekProvider::Otherand a key name that identifies the key across every KMS instance it may address. A wrapper returns the inner provider’skek(); - names that KEK to the KMS on every decryption;
- keeps key material, plaintext or encrypted, out of its errors’
DisplayandDebug.
generate_key_material has a default implementation that returns Ok(None), so the backend
generates the key.
Google Cloud KMS
Feature gcp-kms-encryption, or gcp-kms-encryption-webpki for the bundled WebPKI roots.
GcpKmsKeyRef names a CryptoKey by its project, location, key ring and key. The provider encrypts
under the primary version of the key, and each encrypted DEK records the version that encrypted it,
so DEKs encrypted before a rotation still decrypt afterwards.
use kms_aead::backends::AwsLc;
use kms_aead::providers::{GcpKmsKeyRef, GcpKmsProvider, GcpKmsProviderOptions};
use kms_aead::KmsAeadEnvelope;
use secret_vault_value::SecretValue;
async fn example() -> kms_aead::KmsAeadResult<()> {
let kms_ref = GcpKmsKeyRef::new(
"my-project".to_string(),
"europe-north1".to_string(),
"my-key-ring".to_string(),
"my-key".to_string(),
);
let provider = GcpKmsProvider::new(&kms_ref).await?;
let envelope = KmsAeadEnvelope::<GcpKmsProvider, AwsLc>::new(provider)?;
// Or with DEKs from the Cloud KMS generator at the HSM protection level
let envelope = KmsAeadEnvelope::<GcpKmsProvider, AwsLc>::new(
GcpKmsProvider::with_options(
&kms_ref,
GcpKmsProviderOptions::new().with_use_kms_random_gen(true),
)
.await?,
)?;
let cipher_text = envelope
.encrypt(b"orders/1042", &SecretValue::from("secret"))
.await?;
let _ = cipher_text;
Ok(())
}
Full example available here.
Authentication
GcpKmsProvider::new and with_options build a Cloud KMS client with the application default
credentials. They look for credentials in the following places, preferring the first location
found:
- A JSON file whose path is specified by the
GOOGLE_APPLICATION_CREDENTIALSenvironment variable; - A JSON file in a location known to the gcloud command-line tool using
gcloud auth application-default login; - On Google Compute Engine, Cloud Run, GKE, etc., the metadata server.
For local development don’t confuse gcloud auth login with gcloud auth application-default login,
since the first one authorises only the gcloud tool to access the Cloud Platform.
GcpKmsProvider::with_client takes a client the application built itself, for other
credentials.
The principal needs cloudkms.cryptoKeyVersions.useToEncrypt and useToDecrypt on the key, as
in the roles/cloudkms.cryptoKeyEncrypterDecrypter role, and cloudkms.locations.generateRandomBytes
on the location when the KMS generator is on.
Errors
A failed KMS call is a NetworkError whose code is the gRPC status code, such as
PermissionDenied or NotFound.
AWS KMS
Feature aws-kms-encryption.
AwsKmsKeyRef names the key by the AWS account and the key id, which is anything AWS KMS
accepts: a key id, a key ARN, an alias name such as alias/orders, or an alias ARN.
use kms_aead::backends::AwsLc;
use kms_aead::providers::{AwsKmsKeyRef, AwsKmsProvider, AwsKmsProviderOptions};
use kms_aead::KmsAeadEnvelope;
use secret_vault_value::SecretValue;
async fn example() -> kms_aead::KmsAeadResult<()> {
let kms_ref = AwsKmsKeyRef::new(
"123456789012".to_string(),
"arn:aws:kms:eu-west-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab".to_string(),
);
let provider = AwsKmsProvider::new(&kms_ref).await?;
let envelope = KmsAeadEnvelope::<AwsKmsProvider, AwsLc>::new(provider)?;
// Or with DEKs from AWS KMS GenerateRandom
let envelope = KmsAeadEnvelope::<AwsKmsProvider, AwsLc>::new(
AwsKmsProvider::with_options(
&kms_ref,
AwsKmsProviderOptions::new().with_use_kms_random_gen(true),
)
.await?,
)?;
let cipher_text = envelope
.encrypt(b"orders/1042", &SecretValue::from("secret"))
.await?;
let _ = cipher_text;
Ok(())
}
Full example available here.
Authentication and region
AwsKmsProvider::new loads the AWS configuration from the environment, as the AWS SDK does:
environment variables, the shared config and credentials files, the instance or container
role, etc. It sends no request, so missing credentials or an unreachable endpoint fail the first
encryption or decryption.
Without aws_region in the key reference, the provider takes the region of the AWS
configuration it loads. Set one with AwsKmsKeyRef::new(..).with_aws_region(region).
The principal needs kms:Encrypt and kms:Decrypt on the key, and kms:GenerateRandom when
the KMS generator is on.
Key ARN and aliases
A decryption names the configured key to AWS KMS, so a DEK encrypted under another key fails with
IncorrectKeyException, even when the principal may decrypt with that key.
Configure the key ARN rather than an alias: a DEK encrypted under an alias that later moves to
another key fails to decrypt with IncorrectKeyException.
The provider turns the key id into an ARN with the region and the account: an alias name into
an alias ARN, a key id into a key ARN, and an ARN stays as it is. That ARN always starts with
arn:aws:, so outside the aws partition, such as in GovCloud or China, give the full ARN.
Errors
Every error is an EncryptionError whose code is the AWS error code, such as
IncorrectKeyException or NotFoundException, and whose message is the one AWS KMS returned.
Its root cause is the SDK error.
Azure Key Vault
Feature azure-kms-encryption, or one of the TLS variants in
Getting started.
AzureKmsKeyRef names the key by the vault URL and the key name. Without a key version, the
latest version encrypts new DEKs. Each encrypted DEK records the version that encrypted it, and the
decryption uses that version, so DEKs encrypted before a rotation still decrypt afterwards.
The provider takes any Azure credential, for example one from azure_identity:
use kms_aead::backends::AwsLc;
use kms_aead::providers::{AzureKmsKeyRef, AzureKmsProvider};
use kms_aead::KmsAeadEnvelope;
use secret_vault_value::SecretValue;
async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let kms_ref = AzureKmsKeyRef::new(
"https://my-vault.vault.azure.net".to_string(),
"my-key".to_string(),
);
// Azure CLI or Azure Developer CLI credentials for local runs;
// use `azure_identity::ManagedIdentityCredential` in Azure deployments
let credential = azure_identity::DeveloperToolsCredential::new(None)?;
let provider = AzureKmsProvider::new(&kms_ref, credential)?;
let envelope = KmsAeadEnvelope::<AzureKmsProvider, AwsLc>::new(provider)?;
let cipher_text = envelope
.encrypt(b"orders/1042", &SecretValue::from("secret"))
.await?;
let _ = cipher_text;
Ok(())
}
Full example available here.
The provider sends no request until the first encryption or decryption. The principal needs the wrapKey and unwrapKey key operations, as in the “Key Vault Crypto User” role.
Options
AzureKmsProviderOptions has:
algorithm: the wrap algorithm,RsaOaep256by default for RSA keys,A256Kwfor Managed HSM AES keys.RsaOaep(SHA-1) is accepted for existing keys;Rsa1_5and unknown algorithms are refused. Encrypted DEKs do not record the algorithm, so it stays fixed for the life of the key;client_options: settings passed to the Azure SDK, such as a custom HTTP transport or retry policy.
Encrypted DEKs
Key Vault decrypts only with the key version that encrypted, so the provider’s encrypted DEK carries
it before Key Vault’s encrypted DEK, in the library’s own layout, see
Formats. No Tink client exists for Azure, so Tink does not read the
values of this provider, and the plaintext Key Vault encrypts is the raw DEK of 16, 32 or 64 bytes,
as in 0.29, rather than the DEK as Tink serialises it. Every wrap algorithm takes it, A256Kw too,
which takes only multiples of 8 bytes. Store the encrypted DEK as it is, see
Envelope encryption.
Errors
A failed request carries Key Vault’s error code, such as Forbidden or KeyNotFound, or
AZURE_STATUS_<status> when the response has none. A server error or a rate limit is a
NetworkError, any other status an EncryptionError. A decryption of an encrypted DEK in another
layout fails with AZURE_ENCRYPTED_DEK_FORMAT, before any request.
HashiCorp Vault Transit
Feature hashicorp-vault-transit-kms-encryption, or one root store and one crypto provider
variant, see Getting started.
The provider encrypts DEKs with a key of the Vault Transit secrets engine. HcVaultTransitKeyRef
names the key, and the mount is transit unless set with with_mount.
use kms_aead::backends::AwsLc;
use kms_aead::providers::{HcVaultTransitKeyRef, HcVaultTransitProvider};
use kms_aead::KmsAeadEnvelope;
use secret_vault_value::SecretValue;
async fn example() -> kms_aead::KmsAeadResult<()> {
// VAULT_ADDR, VAULT_TOKEN, VAULT_NAMESPACE, VAULT_CACERT and VAULT_CLIENT_TIMEOUT are read by
// the provider
let key_ref = HcVaultTransitKeyRef::new("orders".to_string());
let provider = HcVaultTransitProvider::new(&key_ref)?;
let envelope = KmsAeadEnvelope::<HcVaultTransitProvider, AwsLc>::new(provider)?;
let cipher_text = envelope
.encrypt(b"orders/1042", &SecretValue::from("secret"))
.await?;
let _ = cipher_text;
Ok(())
}
Full example available here.
Options
HcVaultTransitProvider::new reads its options from the variables the Vault CLI reads, with
HcVaultTransitProviderOptions::from_env: VAULT_ADDR (required), VAULT_TOKEN,
VAULT_NAMESPACE, VAULT_CACERT and VAULT_CLIENT_TIMEOUT. An empty variable counts as unset.
VAULT_CLIENT_TIMEOUT takes the formats the Vault CLI takes: whole seconds (30), whole days
(2d) or a Go duration (30s, 1h30m). Zero, which the Vault CLI reads as no time limit, is
refused, as is a value in any other format.
HcVaultTransitProvider::with_options takes them from the application:
use kms_aead::providers::{
HcVaultTransitKeyRef, HcVaultTransitProvider, HcVaultTransitProviderOptions,
HcVaultTransitToken,
};
use secret_vault_value::SecretValue;
use std::time::Duration;
fn example() -> kms_aead::KmsAeadResult<()> {
let options = HcVaultTransitProviderOptions::new("https://vault.example.com:8200".to_string())
.with_token(HcVaultTransitToken::try_from(SecretValue::from("hvs.example"))?)
.with_namespace("team-a".to_string())
.with_timeout(Duration::from_secs(10));
let provider = HcVaultTransitProvider::with_options(
&HcVaultTransitKeyRef::new("orders".to_string()).with_mount("transit-eu".to_string()),
options,
)?;
let _ = provider;
Ok(())
}
The options are:
address: the Vault server, such ashttps://vault.example.com:8200;token: sent with every request. Without one the requests carry no token, which is what a Vault Agent or Vault Proxy withuse_auto_auth_tokenexpects;namespace: the Vault Enterprise namespace of the mount;ca_certificate: a PEM file with CA certificates for a server whose certificate comes from a private CA. The file adds to the root store and does not replace it, unlikeVAULT_CACERTin the Vault CLI, so a server with a certificate from any public CA in the root store is trusted as well;timeout: the time limit for one request, from opening the connection to the last byte of the response, 60 seconds by default.
The provider sends no request when it is created, so a wrong token or an unreachable server
fail the first encryption or decryption. An http address to a host other than localhost or a
loopback address logs a WARN event at creation, since the token and the DEKs then cross the
network unencrypted.
The token needs update on <mount>/encrypt/<key> and <mount>/decrypt/<key>.
Key versions and rotation
The encrypted DEK is the Transit ciphertext, such as vault:v1:.... It names the key version, so
it still decrypts after the key is rotated. The KMS spans record that version as key_version.
The decrypt names the configured key in its URL, so Vault refuses a ciphertext of another key.
Errors
A response with an error status is VAULT_STATUS_<status>: a NetworkError for a server error
or a rate limit, an EncryptionError otherwise. VAULT_HTTP and VAULT_TIMEOUT are network
errors for a request that got no complete response, and VAULT_RESPONSE,
VAULT_RESPONSE_TOO_LARGE and VAULT_CIPHERTEXT are encryption errors for a response or a
encrypted DEK that cannot be read. Invalid options fail at creation with VAULT_INVALID_OPTIONS,
or VAULT_TLS when the TLS configuration cannot be built.
Local KMS
LocalKmsProvider encrypts DEKs with a key encryption key the process holds, with AES-256-GCM on
the backend it is built with. It is in every build, with no feature, for:
- development and tests without a cloud KMS;
- single-host installs where the operator protects the KEK.
kms-aead builds one only when the application calls one of its constructors; no provider falls
back to it. Each constructor logs one INFO event naming the KEK id, its versions and where
each version came from (ephemeral, file, env or secret), never the key.
KEK ids and versions
A LocalKekId names the KEK: 1 to 255 bytes of ASCII alphanumerics, -, _ and .. Every
encrypted DEK records it, and a decryption refuses a DEK encrypted under another id. The id is also
the key of the DEK cache, so one id must name one set of KEK material: two
providers with the same id and different keys share cache entries.
A provider holds one or more LocalKek versions of 32 bytes each. The highest version encrypts new
DEKs, and every version it holds decrypts. Each encrypted DEK records its version, so adding a new
version keeps the DEKs encrypted under older ones readable.
A KEK comes from one of:
LocalKek::from_secret: bytes the application already holds, such as a key an operator unseals with their own tool (systemd-creds, a TPM, sops, age);LocalKek::from_file: the standard base64 of the 32 bytes in a file, trailing whitespace allowed. The file must be a regular file of at most 256 bytes. No permission or ownership check runs: restricting who can read or replace the file is the operator’s job;LocalKek::from_env: the standard base64 of the 32 bytes in an environment variable. Be aware the variable stays in the process environment for the life of the process: the same user can read it through/proc/<pid>/environ, and every child process inherits it.
Errors name the file or the variable, never its content.
Tests and development
An ephemeral provider has one random version-1 KEK that lives as long as the provider, so the DEKs it encrypts cannot be decrypted once the process exits. It suits tests:
use kms_aead::backends::Ring;
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::KmsAeadEnvelope;
use secret_vault_value::SecretValue;
#[tokio::main(flavor = "current_thread")]
async fn main() -> kms_aead::KmsAeadResult<()> {
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("tests".to_string())?)?;
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::new(provider)?;
let encrypted = envelope
.encrypt(b"orders/1042", &SecretValue::from("secret"))
.await?;
let decrypted: SecretValue = envelope.decrypt(b"orders/1042", &encrypted).await?;
assert_eq!(decrypted, SecretValue::from("secret"));
Ok(())
}
For a development environment whose data outlives a run, keep a KEK in a variable or a file. Generate one with:
openssl rand -base64 32
use kms_aead::providers::{LocalKek, LocalKekId, LocalKekVersion, LocalKmsProvider};
use kms_aead::backends::Ring;
use std::num::NonZeroU32;
fn example() -> kms_aead::KmsAeadResult<()> {
let version = LocalKekVersion::from(NonZeroU32::MIN);
let provider = LocalKmsProvider::<Ring>::new(
LocalKekId::try_from("dev".to_string())?,
vec![LocalKek::from_env(version, "APP_DEV_KEK")?],
)?;
let _ = provider;
Ok(())
}
A VM or a single host
On a host, keep the KEK out of the environment. With systemd, encrypt it with systemd-creds,
bound to the host’s TPM where it has one, and let systemd pass it to the service as a credential:
openssl rand -base64 32 | systemd-creds encrypt --name=kek-v1 - /etc/credstore.encrypted/kek-v1
[Service]
LoadCredentialEncrypted=kek-v1:/etc/credstore.encrypted/kek-v1
systemd decrypts it into $CREDENTIALS_DIRECTORY, readable only by the service:
use kms_aead::providers::{LocalKek, LocalKekId, LocalKekVersion, LocalKmsProvider};
use kms_aead::backends::AwsLc;
use std::num::NonZeroU32;
use std::path::PathBuf;
fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let credentials = PathBuf::from(std::env::var("CREDENTIALS_DIRECTORY")?);
let version = LocalKekVersion::from(NonZeroU32::MIN);
let provider = LocalKmsProvider::<AwsLc>::new(
LocalKekId::try_from("orders-host-1".to_string())?,
vec![LocalKek::from_file(version, &credentials.join("kek-v1"))?],
)?;
let _ = provider;
Ok(())
}
To rotate, add kek-v2 the same way and list both versions. Version 2 encrypts new DEKs, and the
DEKs encrypted under version 1 still decrypt. Version 1 can be dropped once nothing encrypted under
it is left.
Limits
- No IAM, no audit log and no HSM. Protecting the KEK, at rest and in backups, is the operator’s job;
- Every DEK encryption is one AES-256-GCM encryption with a random 96-bit nonce, so a KEK version is safe for about 2^32 encryptions. With the per-call envelope every value costs one encryption: a host sealing 1,000 values per second spends 2^32 encryptions in 50 days. Use DEK reuse so that one encryption serves many values, and add a new KEK version before the budget is spent;
- Crypto-shredding means destroying every copy of the KEK, backups included;
- DEKs encrypted by a local KEK cannot be exported to BigQuery, which decrypts with Cloud KMS.
Errors
An encryption fails with RANDOM_GENERATOR when the backend’s CSPRNG fails. A decryption fails with
LOCAL_ENCRYPTED_DEK_FORMAT when the encrypted DEK is not in the local layout,
LOCAL_KEK_ID_MISMATCH when it was encrypted under another KEK id, LOCAL_KEK_UNKNOWN_VERSION
when the provider does not hold its version, and DECRYPT when it is not authentic under that
version.
Creating a provider fails with INVALID_LOCAL_KEK_ID for an invalid id, LOCAL_KEK_VERSIONS
when the versions are empty or list one version twice, and LOCAL_KEK_SOURCE or
LOCAL_KEK_LENGTH when a KEK cannot be read or is not 32 bytes.
Envelope encryption
KmsAeadEnvelope seals values under data encryption keys (DEKs) on the crypto backend it is built
with, and encrypts the DEKs with the KEK of its provider. A call that makes no KMS call is not
async, and every call opens one DEBUG span, see Observability.
use kms_aead::backends::Ring;
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::{AeadAlgorithm, KmsAeadEnvelope, KmsAeadEnvelopeOptions};
use secret_vault_value::SecretValue;
#[tokio::main(flavor = "current_thread")]
async fn main() -> kms_aead::KmsAeadResult<()> {
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("example".to_string())?)?;
// AES-256-GCM by default
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::new(provider)?;
// Or another algorithm
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("example".to_string())?)?;
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::with_options(
provider,
KmsAeadEnvelopeOptions::new().with_algorithm(AeadAlgorithm::ChaCha20Poly1305),
)?;
let _ = envelope;
Ok(())
}
The value does not record the algorithm, so the writer and the reader of a value must be configured with the same one. The backend can differ, see Crypto backends. The default, AES-256-GCM, is read by every Tink implementation too, see Formats. Values that 0.29 and earlier sealed under their default open only with ChaCha20-Poly1305 configured, see Migrating from 0.29.
Encrypting values
encrypt seals a value under a new DEK, encrypts the DEK with one KMS call, and returns both in
one Encrypted<T, Envelope>, where T is the type of the value. decrypt decrypts the DEK with
one KMS call, opens the value and returns a T. A value with no type of its own is a
SecretValue:
use kms_aead::backends::Ring;
use kms_aead::formats::Envelope;
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::{Encrypted, KmsAeadEnvelope};
use secret_vault_value::SecretValue;
#[tokio::main(flavor = "current_thread")]
async fn main() -> kms_aead::KmsAeadResult<()> {
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("example".to_string())?)?;
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::new(provider)?;
let aad = b"orders/1042";
let encrypted: Encrypted<SecretValue, Envelope> = envelope
.encrypt(aad, &SecretValue::from("4111 1111 1111 1111"))
.await?;
// The bytes to store, and the value read back from them
let stored: Vec<u8> = encrypted.into_value();
let read_back = Encrypted::<SecretValue, Envelope>::try_from(stored)?;
let decrypted: SecretValue = envelope.decrypt(aad, &read_back).await?;
assert_eq!(decrypted, SecretValue::from("4111 1111 1111 1111"));
Ok(())
}
value() and into_value() return the bytes to store, and to_hex_string() returns them as
lowercase hex. Stored bytes become an Encrypted again only through Encrypted::try_from, and
stored hex through Encrypted::try_from_hex_string, which check them against the layout of the
format.
The value is in Tink’s KMS envelope format for every provider, so Tink’s KmsEnvelopeAead reads
it with the same KMS key and algorithm, see Formats.
A new DEK comes from the backend’s CSPRNG, or from the KMS generator when the provider is configured to use one, see KMS random generators.
Typed fields
encrypt and decrypt take a value of any type that implements Plaintext, so a field can have
a type of its own. The value is encoded by its Plaintext implementation, and the result is an
Encrypted<T, Envelope> that names the type:
use kms_aead::backends::Ring;
use kms_aead::formats::Envelope;
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::{Encrypted, KmsAeadEnvelope, KmsAeadResult, Plaintext};
use secret_vault_value::SecretValue;
#[derive(Debug, Clone, PartialEq)]
struct Email(String);
// Sealed as the same UTF-8 bytes as a String
impl Plaintext for Email {
fn to_plaintext(&self) -> KmsAeadResult<SecretValue> {
self.0.to_plaintext()
}
fn from_plaintext(plaintext: SecretValue) -> KmsAeadResult<Self> {
String::from_plaintext(plaintext).map(Email)
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> KmsAeadResult<()> {
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("example".to_string())?)?;
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::new(provider)?;
let aad = b"users/1042/email";
let email = Email("alice@example.com".to_string());
let encrypted: Encrypted<Email, Envelope> = envelope.encrypt(aad, &email).await?;
// The bytes to store, and the field read back from them
let stored: Vec<u8> = encrypted.into_value();
let read_back = Encrypted::<Email, Envelope>::try_from(stored)?;
let decrypted: Email = envelope.decrypt(aad, &read_back).await?;
assert_eq!(decrypted, email);
Ok(())
}
Plaintextis implemented forString,Vec<u8>,SecretValue,bool,i32,i64,u32andu64, and with theserde-jsonfeature forJson<T>, any serde type stored as its compact JSON. An application type maps to one of them, asEmaildoes;- the calls run in the envelope’s mode, a DEK per value or store-less DEK reuse, and decrypt through the envelope’s DEK cache, whatever the type;
Encrypted::try_fromchecks the stored bytes against Tink’s KMS envelope layout, so a truncated or foreign value fails when it is read, before any KMS call, withUNKNOWN_CIPHER_TEXT_LAYOUT,INVALID_CIPHER_TEXT_FORMATor, withoutlegacy-formats,LEGACY_FORMAT. It checks the framing only: a changed value fails to decrypt withDECRYPT;decryptfails withINVALID_PLAINTEXTwhen the plaintext is not an encoding ofT.
Encrypted<T, Envelope> has no ==: two values of the same plaintext differ, so compare
value() to tell whether two values are the same stored bytes. Debug shows the length only. See
Formats for its layout.
Storing typed fields
Three features store Encrypted as bytes, and each one reads it back through TryFrom, so a
malformed stored value fails in the decode:
serde:SerializeandDeserialize, abytesvalue in Firestore and an array of numbers in serde_json;postgres:ToSqlandFromSqlof postgres-types, for tokio-postgres, asBYTEA, and a slice asBYTEA[]for= ANY($1);sqlx: sqlx’sType,Encode,DecodeandPgHasArrayTypefor PostgreSQL, asBYTEA. It depends onsqlx-coreandsqlx-postgresonly, so the application chooses sqlx’s runtime and TLS stack.
With sqlx:
use kms_aead::formats::Envelope;
use kms_aead::Encrypted;
use sqlx::PgPool;
#[derive(Clone)]
struct Email(String);
async fn store(pool: &PgPool, id: i64, email: &Encrypted<Email, Envelope>) -> sqlx::Result<()> {
sqlx::query("INSERT INTO users (id, email) VALUES ($1, $2)")
.bind(id)
.bind(email)
.execute(pool)
.await?;
Ok(())
}
async fn load(pool: &PgPool, id: i64) -> sqlx::Result<Encrypted<Email, Envelope>> {
sqlx::query_scalar("SELECT email FROM users WHERE id = $1")
.bind(id)
.fetch_one(pool)
.await
}
One fixed key
AeadCipher::encrypt seals a field under a key the application holds, in Tink’s layout of a key
whose output prefix is TINK, [0x01][key id: u32 BE] before the AEAD value: an
Encrypted<T, Randomized>. DeterministicCipher::encrypt does the same for an
Encrypted<T, Deterministic>. The key id is a KeyId, the application’s constant, and Tink
and BigQuery read the values with a keyset of the one key under that id:
use kms_aead::backends::Ring;
use kms_aead::cache::{DekCache, DekCacheOptions};
use kms_aead::formats::{Randomized, TinkKeyType, TinkKeyset};
use kms_aead::providers::{GcpKmsKeyRef, GcpKmsProvider};
use kms_aead::{
AeadAlgorithm, AeadCipher, DataEncryptionKey, Encrypted, EncryptedDataEncryptionKey, KeyId,
KmsAeadEncryptionProvider, KmsAeadResult, Plaintext,
};
use secret_vault_value::SecretValue;
#[derive(Debug, Clone, PartialEq)]
struct Email(String);
impl Plaintext for Email {
fn to_plaintext(&self) -> KmsAeadResult<SecretValue> {
self.0.to_plaintext()
}
fn from_plaintext(plaintext: SecretValue) -> KmsAeadResult<Self> {
String::from_plaintext(plaintext).map(Email)
}
}
// The id of the one key, in every value and in the keyset
const EMAIL_KEY: KeyId = KeyId::new(1);
async fn example() -> KmsAeadResult<()> {
let key_ref = GcpKmsKeyRef::new(
"my-project".to_string(),
"europe-north1".to_string(),
"my-key-ring".to_string(),
"my-key".to_string(),
);
let provider = GcpKmsProvider::new(&key_ref).await?;
let cipher = AeadCipher::<Ring>::new(AeadAlgorithm::Aes256Gcm)?;
let cache = DekCache::new(DekCacheOptions::new())?;
// Once, by an administrative job: the key encrypted by Cloud KMS, stored in the settings
let (_, encrypted_dek): (DataEncryptionKey, EncryptedDataEncryptionKey) =
cipher.generate_encrypted_dek(&provider).await?;
// On every write and read: the key decrypted through the cache, no KMS call while it is cached
let email = Email("alice@example.com".to_string());
let (encrypted, _) = cache
.with_dek(&provider, &encrypted_dek, |dek| {
cipher.encrypt(EMAIL_KEY, b"users/1042/email", &email, dek)
})
.await?;
let encrypted: Encrypted<Email, Randomized> = encrypted?;
let (decrypted, _) = cache
.with_dek(&provider, &encrypted_dek, |dek| {
cipher.decrypt(b"users/1042/email", &encrypted, dek)
})
.await?;
let decrypted: Email = decrypted?;
assert_eq!(decrypted, email);
// Once, for BigQuery: the keyset of the one key, encrypted by the same Cloud KMS key
let mut keyset = TinkKeyset::new(EMAIL_KEY);
let (added, _) = cache
.with_dek(&provider, &encrypted_dek, |dek| {
keyset.add_key(EMAIL_KEY, TinkKeyType::from(cipher.algorithm()), dek)
})
.await?;
added?;
let first_level_keyset: EncryptedDataEncryptionKey = provider
.encrypt_data_encryption_key(&DataEncryptionKey::from(keyset.to_bytes()))
.await?;
let _ = first_level_keyset;
Ok(())
}
BigQuery reads the column with KEYS.KEYSET_CHAIN over the encrypted keyset, passed as a BYTES
query parameter:
SELECT AEAD.DECRYPT_STRING(
KEYS.KEYSET_CHAIN(
'gcp-kms://projects/my-project/locations/europe-north1/keyRings/my-key-ring/cryptoKeys/my-key',
@first_level_keyset),
u.email,
CONCAT('users/', CAST(u.id AS STRING), '/email'))
FROM users u;
- BigQuery reads AES-256-GCM values with
AEAD.DECRYPT_STRINGandAEAD.DECRYPT_BYTES, and the AES-SIV values ofDeterministicCipherwithDETERMINISTIC_DECRYPT_STRINGandDETERMINISTIC_DECRYPT_BYTES.KEYS.KEYSET_CHAINdecrypts with Cloud KMS only; AeadCipher::decryptreads the values Tink and BigQuery write under the same key and id. The caller picks the key bykey_id(), which the prefix names without authenticating it;- a rotation needs a second key id, values of both ids in one column and a keyset of both keys. kms-aead-keyring keeps those versions.
Beacons
A beacon, or blind index, finds rows by a field’s value without decrypting the column: an
HMAC-SHA256 of the value under a beacon key, truncated, stored beside the encrypted field. A
query calculates the beacon of the value it looks for and matches it by equality. Beacon<T> holds
the bytes to store, and the beacon key is a 32-byte key encrypted by the KMS, as a DEK is:
use kms_aead::backends::Ring;
use kms_aead::formats::Envelope;
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::{
Beacon, BeaconAlgorithm, BeaconBits, BeaconMac, Encrypted, KeyId, KmsAeadEnvelope,
KmsAeadResult, Plaintext,
};
use secret_vault_value::SecretValue;
#[derive(Debug, Clone, PartialEq)]
struct Email(String);
// The application's id of its beacon key, which every beacon names
const EMAIL_BEACON_KEY: KeyId = KeyId::new(1);
impl Email {
// The beacon of an email matches regardless of case
fn new(email: &str) -> Self {
Email(email.to_lowercase())
}
}
impl Plaintext for Email {
fn to_plaintext(&self) -> KmsAeadResult<SecretValue> {
self.0.to_plaintext()
}
fn from_plaintext(plaintext: SecretValue) -> KmsAeadResult<Self> {
String::from_plaintext(plaintext).map(Email)
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> KmsAeadResult<()> {
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("example".to_string())?)?;
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::new(provider)?;
// Once, by an administrative job: the beacon key, stored beside the application's settings
let (_, encrypted_dek) = BeaconMac::<Ring>::new(BeaconAlgorithm::HmacSha256)
.generate_encrypted_dek(envelope.provider())
.await?;
// On every write: the field and its beacon
let email = Email::new("Alice@Example.com");
let encrypted: Encrypted<Email, Envelope> =
envelope.encrypt(b"users/1042/email", &email).await?;
let beacon: Beacon<Email> = envelope
.beacon(EMAIL_BEACON_KEY, b"users/email", BeaconBits::DEFAULT, &email, &encrypted_dek)
.await?;
// A lookup: the beacon of the value looked for, then a decryption of each matching row
let probe: Beacon<Email> = envelope
.beacon(
EMAIL_BEACON_KEY,
b"users/email",
BeaconBits::DEFAULT,
&Email::new("alice@example.com"),
&encrypted_dek,
)
.await?;
assert_eq!(probe, beacon);
let decrypted: Email = envelope.decrypt(b"users/1042/email", &encrypted).await?;
assert_eq!(decrypted, email);
Ok(())
}
- the context,
users/emailhere, names the field and never a row, so that equal values in one field give equal beacons. A beacon of one field never matches a beacon of another; BeaconBitssets how much of the HMAC a beacon keeps, 1 to 256 bits, 16 by default. Withndistinct values andbbits, aboutn / 2^bvalues share each beacon: a lookup returns that many rows, which the application decrypts and filters, and an equal beacon never proves an equal value. More bits leak more; fewer bits return more rows to filter;- normalisation is the application’s: the beacon of
Alice@Example.comand ofalice@example.comdiffer unless the type normalises the value before it is sealed, asEmail::newdoes; - the key id, a
KeyId, is the application’s number for the beacon key, written into every beacon so that a column can hold beacons of two keys at once; - the beacon key is decrypted through the envelope’s DEK cache, as a DEK is.
A beacon column leaks which rows are likely to hold equal values, and with them how often each value occurs, to anyone who reads the column. A leaked beacon key lets its holder test guesses against the whole column offline. Beacons fit fields that are looked up and have many distinct values, such as emails and external ids, and never low-cardinality fields such as a status or a country. BigQuery’s SQL has no HMAC-SHA256 with a KMS-encrypted DEK, so a query in BigQuery cannot calculate a beacon; it matches beacons the application calculated.
Rotating a beacon key in store-less mode is manual. Generate the new key and keep both, write
beacons under the new one, query with a beacon under each key until a re-index has rewritten
every stored beacon, then drop the old key. Beacon::key_id tells which key wrote a stored
beacon. kms-aead-keyring does this with key versions.
Beacon<T> is stored as Encrypted is, with the serde, postgres and sqlx features, and
read back through TryFrom, which checks its layout. See Formats.
Beacon or deterministic field
A deterministic field also finds rows by equality, by its cipher text:
| Beacon | Deterministic field | |
|---|---|---|
| Stored value | Randomised, so its AAD can name the row, and a value copied into another row fails to open | Equal for equal values, so its AAD names the field and never the row, and a value copied into another row opens there |
| Match | The truncated HMAC, with deliberate collisions | Always the exact value |
| Key | A beacon key, rotated separately from the data keys | The field’s own key |
| Cost | An extra column, and a decryption and filter of the candidates on every lookup | Nothing beyond the field |
| BigQuery | SQL cannot calculate a beacon; it matches beacons the application calculated | SQL decrypts the field and searches it by equality |
With the default 16 bits, there are 65,536 possible beacons. A column of 1,000,000 distinct emails gives about 1,000,000 / 65,536, or 15, emails per beacon, so a lookup of one email returns about 15 rows. The application decrypts them and keeps the one equal to the email it looks for.
AAD
The AAD is authenticated but not encrypted: a value opens only with the AAD it was sealed with,
and a wrong AAD fails with DECRYPT. Use it to bind a value to its place, such as
<table>/<row id>/<column>, so that a value copied into another row or column fails to open.
The AAD is not stored in the value, so the reader rebuilds it from the same data. It is never logged or put in an error, but it is not secret either: keep secrets in the value itself.
Working with DEKs yourself
The *_with_dek and *_with_encrypted_dek calls let the application keep one DEK for many
values and store the encrypted DEK apart from them, such as one DEK per tenant or per file. The
values are Encrypted<T, Bare>, with no encrypted DEK in them.
use kms_aead::backends::Ring;
use kms_aead::formats::Bare;
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::{Encrypted, KmsAeadEnvelope};
use secret_vault_value::SecretValue;
#[tokio::main(flavor = "current_thread")]
async fn main() -> kms_aead::KmsAeadResult<()> {
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("example".to_string())?)?;
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::new(provider)?;
// One KMS call: a new DEK and the same DEK encrypted
let (dek, encrypted_dek) = envelope.generate_encrypted_dek().await?;
// No KMS call: seal and open with the plaintext DEK
let first: Encrypted<SecretValue, Bare> =
envelope.encrypt_with_dek(b"files/1/chunk/0", &SecretValue::from("first"), &dek)?;
let second: Encrypted<SecretValue, Bare> =
envelope.encrypt_with_dek(b"files/1/chunk/1", &SecretValue::from("second"), &dek)?;
let decrypted: SecretValue = envelope.decrypt_with_dek(b"files/1/chunk/0", &first, &dek)?;
assert_eq!(decrypted, SecretValue::from("first"));
// Later, with only the encrypted DEK: the envelope decrypts it with the KMS
let decrypted: SecretValue = envelope
.decrypt_with_encrypted_dek(b"files/1/chunk/1", &second, &encrypted_dek)
.await?;
assert_eq!(decrypted, SecretValue::from("second"));
Ok(())
}
The calls are:
generate_encrypted_dek: a new DEK and the same DEK encrypted, one KMS call;encrypt_with_dekanddecrypt_with_dek: with a plaintext DEK, no KMS call;encrypt_with_encrypted_dekanddecrypt_with_encrypted_dek: with an encrypted DEK, decrypted through the envelope’s DEK cache, so one KMS call per call without a cache.
Be aware not to seal more than about 2^32 values under one DEK: every value takes a random 96-bit nonce, see Security considerations.
Storing encrypted DEKs
EncryptedDataEncryptionKey holds the bytes the provider returned, which also name the KMS key
version where the KMS needs one to decrypt after a rotation. Store value() or to_hex_string(),
and read them back with EncryptedDataEncryptionKey::from or try_from_hex_string():
use kms_aead::backends::Ring;
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::{EncryptedDataEncryptionKey, KmsAeadEnvelope, ValueStruct};
#[tokio::main(flavor = "current_thread")]
async fn main() -> kms_aead::KmsAeadResult<()> {
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("example".to_string())?)?;
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::new(provider)?;
let (_dek, encrypted_dek) = envelope.generate_encrypted_dek().await?;
let stored: Vec<u8> = encrypted_dek.value().clone();
let read_back_encrypted_dek = EncryptedDataEncryptionKey::from(stored);
let stored_hex: String = encrypted_dek.to_hex_string();
let read_back_hex_encrypted_dek = EncryptedDataEncryptionKey::try_from_hex_string(&stored_hex)?;
assert_eq!(read_back_encrypted_dek, read_back_hex_encrypted_dek);
Ok(())
}
The encrypted DEK is the one Tink stores in its KMS envelope, so Encrypted::join of a bare value
and its encrypted DEK is an Encrypted<T, Envelope> that Tink reads. See
Formats for each provider’s bytes.
DEK caching and reuse
Without a cache, every read of a value decrypts its DEK with one KMS call, and every
encrypt encrypts a new DEK with another. The library has two ways to make fewer KMS calls:
DekCache, a cache of decrypted DEKs, which serves repeated reads of values under the same DEK without the KMS;- store-less DEK reuse, with a
DekReusePolicy, where one DEK seals many values, so a write calls the KMS only when the DEK is renewed.
DEK cache
kms_aead::cache::DekCache holds plaintext DEKs a provider decrypted, keyed by the provider’s
KEK and the encrypted DEK’s bytes. A per-call envelope has no cache unless the options give it one:
use kms_aead::backends::Ring;
use kms_aead::cache::{DekCache, DekCacheOptions};
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::{KmsAeadEnvelope, KmsAeadEnvelopeOptions};
use std::num::NonZeroUsize;
use std::time::Duration;
fn main() -> kms_aead::KmsAeadResult<()> {
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("example".to_string())?)?;
let cache = DekCache::new(
DekCacheOptions::new()
.with_ttl(Duration::from_secs(5 * 60))
.with_max_entries(NonZeroUsize::new(10_000).expect("not zero")),
)?;
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::with_options(
provider,
KmsAeadEnvelopeOptions::new().with_dek_cache(cache.clone()),
)?;
let _ = envelope;
Ok(())
}
decrypt, the *_with_encrypted_dek calls and a store-less encrypt decrypt through the
envelope’s cache. Clone shares the same cache, so one cache may serve several envelopes and
several KEKs: an entry serves only a lookup that names the same KEK, and a key decrypted under
one KEK never reaches a caller with the provider of another.
The cache:
- serves an entry for its TTL after it was inserted, however often it is used, 15 minutes by default;
- holds at most
max_entriesentries, 1024 by default, and evicts the oldest entry past it; - makes one provider call for concurrent misses of the same key, and every caller receives its result, the key or the error;
- keeps no entry for a failed decryption, so during a KMS outage every call fails as fast as the KMS does;
- zeroises a key when its entry leaves the cache: once it is evicted, invalidated, cleared, or purged after it expired.
DekCache::disabled() holds nothing, and every lookup calls the provider. It is what a per-call
envelope has by default.
Sizing
Size max_entries above the number of distinct DEKs read per TTL. With per-call envelopes every
value has its own DEK, so a cache helps only values read more than once within the TTL. With
store-less writers the number of DEKs is the number of writers times the renewals per TTL, which
is small.
A longer TTL saves more KMS calls, and keeps DEKs in memory for longer, see Key lifetime.
Invalidation
A disabled KMS key or a revoked IAM grant reaches the cached keys only when their entries expire. To drop one at once, invalidate it with the envelope’s own KEK:
use kms_aead::backends::Ring;
use kms_aead::cache::{DekCache, DekCacheOptions};
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::{KmsAeadEnvelope, KmsAeadEnvelopeOptions};
use kms_aead::KmsAeadEncryptionProvider;
#[tokio::main(flavor = "current_thread")]
async fn main() -> kms_aead::KmsAeadResult<()> {
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("example".to_string())?)?;
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::with_options(provider,
KmsAeadEnvelopeOptions::new().with_dek_cache(DekCache::new(DekCacheOptions::new())?))?;
let (_dek, encrypted_dek) = envelope.generate_encrypted_dek().await?;
// One encrypted DEK
envelope
.dek_cache()
.invalidate(envelope.provider().kek(), &encrypted_dek);
// Every DEK
envelope.dek_cache().clear();
Ok(())
}
Pass envelope.provider().kek() to invalidate, with the KmsAeadEncryptionProvider trait in
scope: a KekRef built by hand that differs from it matches no entry.
Key lifetime
The cache runs no thread and no task of its own. Expired entries are dropped at the next call on
the cache, at purge_expired, or when the last clone of the cache is dropped. With no calls, the
bytes of an expired key stay in memory. An application whose process can sit idle while the cache
holds keys, and that needs the TTL to bound their lifetime in memory, calls purge_expired on a
timer of its own:
use kms_aead::cache::{DekCache, DekCacheOptions};
fn example() -> kms_aead::KmsAeadResult<()> {
let cache = DekCache::new(DekCacheOptions::new())?;
let purged = cache.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(60));
loop {
interval.tick().await;
purged.purge_expired();
}
});
Ok(())
}
A key reaches the caller only inside the closure of DekCache::with_dek, and stays intact until
the closure returns, whatever leaves the cache meanwhile. The closure must not keep a copy of the
key: a copy taken out of it outlives the cache’s control of its lifetime.
Store-less DEK reuse
A KmsAeadEnvelope with a DekReusePolicy encrypts many values under one DEK and renews it at
the first encryption past max_age, past max_uses or past max_bytes, whichever comes first. A renewal makes one
KMS encrypt call. Each value still carries its encrypted DEK in Tink’s KMS envelope, so nothing is
stored apart from the values, and values under earlier DEKs stay readable.
use kms_aead::backends::Ring;
use kms_aead::formats::Envelope;
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::{DekReusePolicy, Encrypted, KmsAeadEnvelope, KmsAeadEnvelopeOptions};
use secret_vault_value::SecretValue;
use std::num::NonZeroU64;
use std::time::Duration;
#[tokio::main(flavor = "current_thread")]
async fn main() -> kms_aead::KmsAeadResult<()> {
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("example".to_string())?)?;
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::with_options(
provider,
KmsAeadEnvelopeOptions::new().with_dek_reuse(
DekReusePolicy::new()
.with_max_age(Duration::from_secs(60 * 60))
.with_max_uses(NonZeroU64::new(1_000_000).expect("not zero")),
),
)?;
// One KMS encryption for both values
let first: Encrypted<SecretValue, Envelope> =
envelope.encrypt(b"events/1", &SecretValue::from("first")).await?;
let second: Encrypted<SecretValue, Envelope> =
envelope.encrypt(b"events/2", &SecretValue::from("second")).await?;
// Read with any envelope over the same KEK and algorithm
let decrypted_first: SecretValue = envelope.decrypt(b"events/1", &first).await?;
let decrypted_second: SecretValue = envelope.decrypt(b"events/2", &second).await?;
assert_eq!(decrypted_first, SecretValue::from("first"));
assert_eq!(decrypted_second, SecretValue::from("second"));
Ok(())
}
The defaults are a max_age of 24 hours and a max_uses of 2^24. max_age must be longer than
zero and max_uses at most 2^32, the bound for random 96-bit nonces under one key, otherwise the
envelope fails to build with INVALID_DEK_REUSE_POLICY.
Volume limit
max_bytes counts the plaintext bytes sealed under the DEK. Its default depends on the algorithm:
| Algorithm | Default max_bytes | Most allowed |
|---|---|---|
| AES-128-GCM, AES-256-GCM | 2^38 bytes (256 GiB) | 2^52 bytes |
| AES-256-GCM-SIV | no limit beyond max_uses | any |
| ChaCha20-Poly1305 | no limit beyond max_uses | any |
The figures come from these bounds:
- AES-GCM: RFC 8446, section 5.5, allows “up to 2^24.5 full-size records (about 24 million)” of 2^14 bytes, 2^38.5 bytes, “while keeping a safety margin of approximately 2^-57 for Authenticated Encryption (AE) security”. Luykx and Paterson, “Limits on Authenticated Encryption Use in TLS”, table 1, give the same 2^24.5 records for a confidentiality advantage of 2^-60. The default of 2^38 bytes stays below that. The most allowed, 2^52 bytes, is the 2^48 blocks RFC 8452, section 9, allows under one AES-GCM key for an advantage of 2^-32;
- AES-GCM-SIV: RFC 8452, section 9, allows 2^32 values of up to 8 GiB each under one key with
random nonces for an advantage of 2^-32, so
max_usesbounds it already; - ChaCha20-Poly1305: draft-irtf-cfrg-aead-limits, section 6.3, gives it no confidentiality limit beyond the security of ChaCha20 itself, and its integrity bound depends on the number of forgery attempts, which no writer can count.
A max_bytes set in the policy replaces the default, under any algorithm. Above the most
allowed, the envelope fails to build with INVALID_DEK_REUSE_POLICY.
A value that would take the count past max_bytes renews the DEK before it is sealed. A value
longer than max_bytes alone is sealed under a new DEK of its own, as the per-call envelope does,
and the current DEK stays in use for the next values.
The bounds count blocks of plaintext and AAD, and one more block per value, and max_bytes counts
plaintext bytes only. At the default max_uses and an AAD of 16 bytes or less, the difference is
at most 3 × 2^24 blocks beside the 2^34 blocks of the default.
The DEK lives in the cache
The envelope keeps only the encrypted DEK. Its plaintext lives in the envelope’s DekCache, which a
store-less envelope always has: DekCache::new(DekCacheOptions::new()) unless the options give it
another, and INVALID_DEK_REUSE_POLICY beside DekCache::disabled().
The first encryption after the DEK has left the cache renews it. So the DEK is renewed:
- at the cache’s TTL, whatever
max_age; - when the cache evicts it for capacity;
- after
invalidatewithenvelope.provider().kek(), or afterclear.
In a cache shared with readers, size max_entries above the number of distinct DEKs read per
TTL, or give the writer a cache of its own, otherwise reads evict the writer’s DEK and it renews
more often than the policy says.
A writer that falls quiet leaves its DEK in memory until the cache’s next call or
purge_expired, see Key lifetime. The values sealed while the cache holds the
DEK are readable by whoever reads the process’s memory in that window.
Crypto backends
The AEAD algorithms run on a crypto library that a feature compiles in. Each backend feature adds
a type to kms_aead::backends, and AeadCipher, DeterministicCipher, KmsAeadEnvelope and LocalKmsProvider take
that type as their backend type parameter B. A build may enable any number of backends, and
there is no default one.
| Feature | Type | Library |
|---|---|---|
ring | Ring | ring |
aws-lc | AwsLc | aws-lc-rs |
aws-lc-fips | AwsLc, on the FIPS module | aws-lc-rs |
rustcrypto | RustCrypto | the RustCrypto AEADs |
There is no default because Cargo unifies features: a default chosen by features would change for every crate in a build when one crate enables another backend.
Code generic over the backend
The backend types implement AeadBackend, a sealed trait in kms_aead::backends. Its items are the
constant NAME, the backend’s name in spans and logs: ring, aws-lc or rustcrypto, and
generate_random_bytes. Code that works with any backend bounds its type parameter with
AeadBackend and nothing else:
use kms_aead::backends::{AeadBackend, Ring};
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::{KmsAeadEnvelope, KmsAeadResult};
use secret_vault_value::SecretValue;
struct Orders<B> {
envelope: KmsAeadEnvelope<LocalKmsProvider<B>, B>,
}
impl<B: AeadBackend> Orders<B> {
fn new() -> KmsAeadResult<Self> {
let provider = LocalKmsProvider::ephemeral(LocalKekId::try_from("orders".to_string())?)?;
Ok(Self {
envelope: KmsAeadEnvelope::new(provider)?,
})
}
async fn round_trip(&self, card: &SecretValue) -> KmsAeadResult<SecretValue> {
let sealed = self.envelope.encrypt(b"orders/1042", card).await?;
self.envelope.decrypt(b"orders/1042", &sealed).await
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> KmsAeadResult<()> {
let orders = Orders::<Ring>::new()?;
let card = SecretValue::from("4111 1111 1111 1111");
assert_eq!(orders.round_trip(&card).await?, card);
Ok(())
}
B::generate_random_bytes(len) returns len bytes from the CSPRNG of the backend B, for
secrets the application generates itself.
Algorithms
| Algorithm | Key | Ring | AwsLc | RustCrypto |
|---|---|---|---|---|
ChaCha20Poly1305 (RFC 8439) | 32 bytes | yes | yes | yes |
Aes128Gcm | 16 bytes | yes | yes | yes |
Aes256Gcm, the envelope’s default | 32 bytes | yes | yes | yes |
Aes256GcmSiv (RFC 8452) | 32 bytes | no | yes | yes |
Every algorithm writes [nonce: 12][ciphertext][tag: 16], with a random nonce from the backend’s
CSPRNG. A backend that does not implement the algorithm fails at construction, never later, with
KmsAeadError::UnsupportedAlgorithm and the code UNSUPPORTED_ALGORITHM:
use kms_aead::backends::Ring;
use kms_aead::{errors::KmsAeadError, AeadAlgorithm, AeadCipher};
let result = AeadCipher::<Ring>::new(AeadAlgorithm::Aes256GcmSiv);
assert!(matches!(result, Err(KmsAeadError::UnsupportedAlgorithm(_))));
AeadAlgorithm reads and writes its names with FromStr and Display: chacha20-poly1305,
aes-128-gcm, aes-256-gcm and aes-256-gcm-siv.
AEAD without a KMS
AeadCipher seals and opens values with a key the application holds. encrypt_bare writes the
AEAD output alone, an Encrypted<T, Bare>:
use kms_aead::backends::Ring;
use kms_aead::formats::Bare;
use kms_aead::{AeadAlgorithm, AeadCipher, Encrypted};
use secret_vault_value::SecretValue;
fn main() -> kms_aead::KmsAeadResult<()> {
let cipher = AeadCipher::<Ring>::new(AeadAlgorithm::ChaCha20Poly1305)?;
let dek = cipher.generate_dek()?;
let aad = b"orders/1042";
let cipher_text: Encrypted<SecretValue, Bare> =
cipher.encrypt_bare(aad, &SecretValue::from("secret"), &dek)?;
let decrypted: SecretValue = cipher.decrypt_bare(aad, &cipher_text, &dek)?;
assert_eq!(decrypted, SecretValue::from("secret"));
Ok(())
}
Full example available here.
encrypt writes an Encrypted<T, Randomized>: the same output after Tink’s TINK prefix, which
names the key by a KeyId, see One fixed key.
generate_dek makes a DEK without a KMS. generate_encrypted_dek makes a DEK and encrypts it with
a provider in one call, for an application that stores the encrypted DEK itself. The other calls
of AeadCipher make no KMS call and open no span.
Cross-backend compatibility
Every backend writes the same bytes for the same algorithm, so a value sealed by one backend
opens with any other that implements its algorithm. A writer on Ring and a reader on AwsLc
work together, and moving an application from one backend to another needs no migration of its
data:
use kms_aead::backends::{Ring, RustCrypto};
use kms_aead::formats::Bare;
use kms_aead::{AeadAlgorithm, AeadCipher, Encrypted};
use secret_vault_value::SecretValue;
fn main() -> kms_aead::KmsAeadResult<()> {
let writer = AeadCipher::<Ring>::new(AeadAlgorithm::Aes256Gcm)?;
let reader = AeadCipher::<RustCrypto>::new(AeadAlgorithm::Aes256Gcm)?;
let dek = writer.generate_dek()?;
let cipher_text: Encrypted<SecretValue, Bare> =
writer.encrypt_bare(b"aad", &SecretValue::from("secret"), &dek)?;
let decrypted: SecretValue = reader.decrypt_bare(b"aad", &cipher_text, &dek)?;
assert_eq!(decrypted, SecretValue::from("secret"));
Ok(())
}
ring
ChaCha20-Poly1305, AES-128-GCM and AES-256-GCM. It has no AES-256-GCM-SIV. ring does not zeroise its expanded key schedules, so they stay in freed memory after each call.
aws-lc-rs and FIPS
aws-lc builds aws-lc-rs with every algorithm. It is the backend that adds no second crypto
library to a build with a cloud provider, since the providers already build aws-lc-rs for TLS.
aws-lc-fips runs the same backend on the AWS-LC FIPS module, and builds only the FIPS module,
so it does not need aws-lc. It needs CMake and Go to build. Be aware it moves every user of
aws-lc-rs in the build to the FIPS module, the rustls aws-lc-rs provider included. A build with
both features compiles both C libraries, and aws-lc-rs runs on the FIPS one. AES-256-GCM is the
algorithm for FIPS deployments. AES-SIV, the deterministic algorithm, is not FIPS-approved, see
Deterministic encryption.
aws-lc-rs copies the key into an AEAD context that AWS-LC allocates with its own allocator, which overwrites every allocation with zeros before freeing it. The expanded key schedule does not stay in freed memory after a call, as long as the application leaves AWS-LC’s allocator in place. The record keys AES-256-GCM-SIV derives for each call live on the stack, and AWS-LC does not clear them.
RustCrypto
Every algorithm, in pure Rust, with getrandom as the CSPRNG. It needs no C compiler. The
feature turns on the crates’ zeroize features, so their key objects, and the Poly1305 and
POLYVAL keys each call derives, are zeroised when dropped.
Deterministic encryption
Feature deterministic.
Randomised encryption gives a different cipher text every time, even for the same value. A deterministic algorithm gives equal cipher texts for equal values under one key and one AAD, so a sealed column can be compared, joined or looked up by equality without opening it.
The library provides AES-SIV (RFC 5297) with a 64-byte key, DeterministicAlgorithm::Aes256Siv.
Its name for FromStr and Display is aes-256-siv. DeterministicCipher takes a backend, as
AeadCipher does, and draws its keys from the backend’s CSPRNG. Every backend seals and opens with
the RustCrypto aes-siv crate, so a value sealed under one backend opens under any other.
AES-SIV is not a FIPS-approved mode. A FIPS deployment keeps deterministic fields outside its
validated boundary, also on aws-lc-fips, where the keys come from the FIPS module’s DRBG.
What it leaks
Be aware what equal cipher texts tell a reader of the stored data:
- Equality: which rows hold the same value;
- Frequency: how often each value occurs. On a column with few distinct values, such as a country, a status or a yes/no answer, the most frequent cipher text is the most frequent value, and the values can be read back from public statistics.
So do not use deterministic encryption:
- for a column with few distinct values or a skewed distribution;
- when equality itself is sensitive;
- when the value is never looked up by equality: randomised encryption leaks nothing of the kind.
The AAD narrows what leaks: values are equal only under the same AAD, so a per-tenant AAD keeps equal values of two tenants apart.
A beacon finds rows by equality as well and keeps the value randomised, see Beacon or deterministic field.
Usage
DeterministicCipher seals and opens with a DataEncryptionKey of
DeterministicAlgorithm::key_len() bytes, 64 for AES-SIV, and refuses a key of another length
with INVALID_KEY_LENGTH, such as the 16 or 32 bytes of a randomised algorithm’s key. The
application keeps track of which key serves which algorithm, as kms-aead-keyring does with its
key ids.
use kms_aead::backends::Ring;
use kms_aead::formats::Deterministic;
use kms_aead::{DeterministicAlgorithm, DeterministicCipher, Encrypted, KeyId};
use secret_vault_value::SecretValue;
fn main() -> kms_aead::KmsAeadResult<()> {
let cipher = DeterministicCipher::<Ring>::new(DeterministicAlgorithm::Aes256Siv)?;
let dek = cipher.generate_dek()?;
let aad = b"customers/email";
let first: Encrypted<SecretValue, Deterministic> =
cipher.encrypt(KeyId::new(1), aad, &SecretValue::from("user@example.com"), &dek)?;
let second: Encrypted<SecretValue, Deterministic> =
cipher.encrypt(KeyId::new(1), aad, &SecretValue::from("user@example.com"), &dek)?;
// Equal values give equal cipher texts, to look up by
assert_eq!(first, second);
let decrypted: SecretValue = cipher.decrypt(aad, &first, &dek)?;
assert_eq!(decrypted, SecretValue::from("user@example.com"));
Ok(())
}
An Encrypted<T, Deterministic> is [0x01][key id: u32 BE][synthetic IV: 16][ciphertext]: Tink’s
5-byte TINK prefix that names the key by its KeyId, then the output of RFC 5297, so Tink and
BigQuery read it with a keyset of that key, see
One fixed key. Its equality compares in constant time,
and its Debug prints only the length, since the bytes tell which values are equal.
Keys encrypted by a KMS
The envelope generates and decrypts deterministic keys with its provider:
use kms_aead::backends::Ring;
use kms_aead::formats::Deterministic;
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::{DeterministicAlgorithm, DeterministicCipher, Encrypted, KeyId, KmsAeadEnvelope};
use secret_vault_value::SecretValue;
#[tokio::main(flavor = "current_thread")]
async fn main() -> kms_aead::KmsAeadResult<()> {
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("example".to_string())?)?;
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::new(provider)?;
let algorithm = DeterministicAlgorithm::Aes256Siv;
// A new key, and the same key encrypted, to store
let (dek, encrypted_dek) = envelope.generate_encrypted_deterministic_dek(algorithm).await?;
// Later: decrypt the stored key
let decrypted_dek = envelope.decrypt_deterministic_dek(algorithm, &encrypted_dek).await?;
let cipher = DeterministicCipher::<Ring>::new(algorithm)?;
let sealed: Encrypted<SecretValue, Deterministic> =
cipher.encrypt(KeyId::new(1), b"aad", &SecretValue::from("value"), &dek)?;
let decrypted: SecretValue = cipher.decrypt(b"aad", &sealed, &decrypted_dek)?;
assert_eq!(decrypted, SecretValue::from("value"));
Ok(())
}
decrypt_deterministic_dek calls the provider every time and does not go through the envelope’s
DEK cache. To cache deterministic keys, use DekCache::with_dek, which holds the keys of both
kinds of algorithm.
DeterministicCipher::generate_encrypted_dek does the same with any provider.
Formats
Every byte layout the library reads or writes is on this page.
The library writes one envelope format for every provider, Tink’s KMS envelope. A value encrypted
by KmsAeadEnvelope::encrypt opens with Tink’s KmsEnvelopeAead on the same KMS key and
algorithm, and a value Tink encrypts opens with KmsAeadEnvelope::decrypt.
| Layout | Type | Bytes | Read by | Written by |
|---|---|---|---|---|
| Tink KMS envelope | Encrypted<T, Envelope> | [w: u32 BE][encrypted DEK: w][nonce: 12][ciphertext][tag: 16] | kms-aead, Tink | 0.30.0 and later |
| Bare value | Encrypted<T, Bare> | [nonce: 12][ciphertext][tag: 16] | kms-aead, Tink | every release |
| Encrypted DEK | EncryptedDataEncryptionKey | the provider’s bytes, see Encrypted DEKs | kms-aead, Tink for GCP and AWS keys | 0.30.0 and later |
| Serialised DEK | the plaintext of an encrypted DEK, but Azure’s | Tink’s key message, see Serialised DEKs | kms-aead, Tink | 0.30.0 and later |
| TINK-prefixed value | Encrypted<T, Randomized>, Encrypted<T, Deterministic>, named by a KeyId | [0x01][key id: u32 BE][value] | kms-aead, Tink, BigQuery | 0.30.0 and later, and kms-aead-keyring through it |
| Tink keyset | TinkKeyset | google.crypto.tink.Keyset | Tink, BigQuery | 0.30.0 and later, and kms-aead-keyring through it |
| Tink encrypted keyset | TinkKeyset::into_encrypted_bytes | google.crypto.tink.EncryptedKeyset, [0x12][w: varint][encrypted keyset: w] | Tink with a GCP, AWS or Vault Transit key | 0.30.0 and later |
| Envelope layout v1 | Encrypted<T, Envelope> with legacy-formats | [0x01][k: u8][key version: k][w: u16 BE][encrypted DEK: w][cipher text] | kms-aead with legacy-formats | 0.29 |
| Encrypted DEK layout v1 | stored EncryptedDataEncryptionKey | [0xFF][0x01][k: u8][key version: k][encrypted DEK] | kms-aead with legacy-formats | 0.29 |
| Length-prefixed envelope | Encrypted<T, Envelope> with legacy-formats | [w: u64 BE][encrypted DEK: w][cipher text], and [w: u32 BE] from 32-bit targets before 0.24.0 | kms-aead with legacy-formats | 0.8.0 to 0.28.1 |
| Raw encrypted DEK | EncryptedDataEncryptionKey | the KMS ciphertext of the DEK’s 16 or 32 raw bytes | kms-aead with legacy-formats | up to 0.29 |
k = 0 records no key version. A key version is 1 to 255 ASCII alphanumeric characters. The Azure
provider encrypts the raw DEK in every release, see Encrypted DEKs.
Tink’s KMS envelope
KmsAeadEnvelope::encrypt writes Tink’s KMS envelope, and Encrypted::join writes it from a
bare value and an encrypted DEK. separate reads it back:
use kms_aead::backends::Ring;
use kms_aead::formats::Envelope;
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::{Encrypted, KmsAeadEnvelope};
use secret_vault_value::SecretValue;
#[tokio::main(flavor = "current_thread")]
async fn main() -> kms_aead::KmsAeadResult<()> {
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("example".to_string())?)?;
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::new(provider)?;
let encrypted: Encrypted<SecretValue, Envelope> =
envelope.encrypt(b"aad", &SecretValue::from("secret")).await?;
let (bare, encrypted_dek) = encrypted.separate()?;
let joined = Encrypted::<SecretValue, Envelope>::join(&bare, &encrypted_dek)?;
assert_eq!(joined.value(), encrypted.value());
Ok(())
}
wis 1 to 4,096 bytes, the limit of every Tink implementation, on write and on read;- the encrypted DEK is the provider’s
EncryptedDataEncryptionKey, the KMS ciphertext of the serialised DEK, or of the raw DEK for Azure; - the AAD goes to the DEK’s AEAD only. The KMS encryption of the DEK has no AAD, as in Tink, so Tink’s AWS client sends no encryption context and its GCP client no additional authenticated data;
- the rest is a
Barevalue.
The envelope records neither the algorithm nor the KMS key version, so the writer and the reader, kms-aead or Tink, are configured with the same algorithm. Tink calls it the DEK template:
| Algorithm | Tink key type | Tink for Java and Go | Tink for C++ and Python |
|---|---|---|---|
| ChaCha20-Poly1305 | ChaCha20Poly1305Key | reads and writes | no |
| AES-128-GCM, AES-256-GCM | AesGcmKey | reads and writes | reads and writes |
| AES-256-GCM-SIV | AesGcmSivKey | reads and writes | reads and writes |
AES-256-GCM is the default of KmsAeadEnvelope::new, so every Tink implementation reads the
values of an envelope that names no algorithm. The default of 0.29 and earlier was
ChaCha20-Poly1305, see Migrating from 0.29.
Tink for Python reads a kms-aead value with the key URI of the provider’s key:
from tink import aead
from tink.integration import gcpkms
aead.register()
key_uri = 'gcp-kms://projects/p/locations/l/keyRings/r/cryptoKeys/k'
remote = gcpkms.GcpKmsClient(key_uri).get_aead(key_uri)
envelope = aead.KmsEnvelopeAead(aead.aead_key_templates.AES256_GCM, remote)
plain_text = envelope.decrypt(value, aad)
Checked on a Cloud KMS key and an AWS KMS key, both ways: AES-128-GCM, AES-256-GCM and
AES-256-GCM-SIV with Tink 1.16.1 for Python, and ChaCha20-Poly1305 with Tink 1.24.0 for Java on
the Cloud KMS key. tests/tink_interop.py and examples/tink_interop.rs repeat the Python check.
Encrypted DEKs
EncryptedDataEncryptionKey is the bytes the provider returned from an encryption, which are also
the bytes to store and the bytes it decrypts. Each provider’s bytes name the KMS key version that
encrypted the DEK where the KMS needs one to decrypt:
| Provider | Bytes | Key version |
|---|---|---|
| GCP | the Cloud KMS ciphertext, starting with 0x0a | inside the ciphertext |
| AWS | the AWS KMS ciphertext blob, starting with 0x01 | not exposed |
| Azure | [0x01][k: u8][key version: k][Key Vault encrypted DEK] | in the layout |
| Vault Transit | the Transit ciphertext, vault:vN:… | the N |
| local | [0x01][i: u8][KEK id: i][KEK version: u32 BE][nonce: 12][sealed DEK][tag: 16] | in the layout |
| an application’s own | whatever it returns |
The Azure layout is the library’s own: Key Vault decrypts only with the key version that encrypted,
and no Tink client exists for Azure. For the same reason, the Key Vault encrypted DEK is the KMS
ciphertext of the raw DEK of 16, 32 or 64 bytes, as in 0.29, not of the serialised DEK: AES key
wrap, A128KW to A256KW, takes only multiples of 8 bytes, which no serialised DEK is. The local
KMS seals the DEK with AES-256-GCM under the KEK version, with its header up to the version as the
AAD, so neither the id nor the version can be changed without the decryption failing.
Store value() or to_hex_string(), and read them back with EncryptedDataEncryptionKey::from
or try_from_hex_string, see
Storing encrypted DEKs.
Serialised DEKs
The plaintext a provider encrypts is the DEK serialised as Tink serialises a key, the message of its
Tink key type with version 0, which protobuf leaves out. The Azure provider encrypts the raw DEK
instead, see Encrypted DEKs:
| Algorithm | Message | Bytes |
|---|---|---|
| ChaCha20-Poly1305 | ChaCha20Poly1305Key | [0x12][0x20][key: 32] |
| AES-128-GCM | AesGcmKey | [0x1a][0x10][key: 16] |
| AES-256-GCM | AesGcmKey | [0x1a][0x20][key: 32] |
| AES-256-GCM-SIV | AesGcmSivKey | [0x1a][0x20][key: 32] |
| AES-256-SIV, deterministic | AesSivKey | [0x12][0x40][key: 64] |
A beacon key of HMAC-SHA256 is serialised as [0x1a][0x20][key: 32], its 32 bytes in
key_value, field 3, as in Tink’s HmacPrfKey without its parameters: the bytes of a 32-byte
AesGcmKey. It is encrypted as a DEK is, raw under an Azure key.
KmsAeadEnvelope::generate_encrypted_dek, generate_encrypted_deterministic_dek and the
generate_encrypted_dek of both ciphers and of BeaconMac encrypt their keys the same way. The
message does not name its type, and AES-256-GCM and AES-256-GCM-SIV keys are written alike, so the
algorithm of the reader decides how the key is used, as in Tink.
A decrypted DEK that is no serialised Tink key, or under an Azure key no raw DEK, fails with
INVALID_KEY_FORMAT. An encrypted Tink
keyset decrypts under the same KEK too, and this is what keeps it out of the DEK cache.
Tink keysets and the TINK prefix
Two items describe the TINK-prefixed values:
kms_aead::KeyId: the key id of Tink’s output prefix of type TINK,[0x01][key id: u32 BE], the start of everyRandomizedandDeterministicvalue and of every beacon. It picks the key that opens the value, and Tink and BigQuery pick the key of a keyset by it. It is not authenticated;kms_aead::formats::TinkKeyset: a serialisedgoogle.crypto.tink.Keysetof enabled, symmetric, TINK-prefixed keys, the bytestinkeywrites for the same keys. Encrypted by a Cloud KMS key, it is the keyset of BigQuery’sKEYS.KEYSET_CHAIN: an application writes it for its one key, see One fixed key, and a keyring exports it for its versions.
TinkKeyset::into_encrypted_bytes writes the keyset encrypted by a KMS key, as Tink writes an
encrypted keyset: a google.crypto.tink.EncryptedKeyset whose encrypted_keyset, field 2, is the
KMS ciphertext of the serialised keyset, encrypted with no associated data. Tink’s binary keyset
writers leave out keyset_info, field 3, and so does the library. Tink’s KeysetHandle.read
reads it with the KMS client of KekRef::tink_key_uri, which Tink has for GCP and AWS keys, and
Tink for Go for Vault Transit keys too.
Formats of Encrypted
Every value the library encrypts is an Encrypted<T, F>: the bytes of one layout, named by its
format F from kms_aead::formats. Encrypted::try_from and Encrypted::try_from_hex_string
check stored bytes against it:
| Format | Layout | Shortest value | key_id() | Written by |
|---|---|---|---|---|
Envelope | Tink’s KMS envelope | an encrypted DEK of 1 to 4,096 bytes, then 28 bytes | none | KmsAeadEnvelope::encrypt, Encrypted::join |
Bare | [nonce: 12][ciphertext][tag: 16] | 28 bytes | none | AeadCipher::encrypt_bare, KmsAeadEnvelope::encrypt_with_dek and encrypt_with_encrypted_dek, Encrypted::separate |
Randomized | [0x01][key id: u32 BE][nonce: 12][ciphertext][tag: 16] | 33 bytes | KeyId | AeadCipher::encrypt, and kms-aead-keyring through it |
Deterministic | [0x01][key id: u32 BE][synthetic IV: 16][ciphertext] | 21 bytes | KeyId | DeterministicCipher::encrypt, and kms-aead-keyring through it |
A Bare value is Tink’s AEAD output under a key with the output prefix RAW, and the part of every
Envelope value after the encrypted DEK. It names no key: the application keeps track of the key
that opens it.
The check reads the framing only, and whether the bytes are authentic is known when they are
decrypted. With legacy-formats, an Envelope value also holds the envelope layout v1 of 0.29 and
the length-prefixed envelope, checked by their lengths, so a column decodes its old rows into the
same type until they are upgraded, see Upgrading stored values.
Without the feature, Encrypted::try_from refuses them with LEGACY_FORMAT. separate refuses
them in every build. The layout of 32-bit targets before 0.24.0 has Tink’s framing, so it reads as
an Envelope value in every build, and KmsAeadEnvelope::decrypt opens it with legacy-formats.
The plaintext inside is the encoding of the value’s Plaintext type: UTF-8 for String, the
bytes themselves for Vec<u8> and SecretValue, true or false for bool, decimal ASCII for
the integers, and compact JSON for Json<T>. See
Typed fields.
Beacons
A Beacon<T> is a prefix and a truncated HMAC-SHA256:
[0x01][key id: u32 BE][HMAC-SHA256: the first ceil(bits / 8) bytes]
0x01is the version of the beacon layout. A later layout takes another first byte, so that beacons of both layouts can share a column during a re-index;- the key id names the beacon key that calculated the beacon;
- the HMAC keeps its first
bitsbits, and the unused low bits of the last byte are zero. A beacon is 6 to 37 bytes.
The HMAC covers the bits, the context and the plaintext, as the Plaintext type encodes it:
HMAC-SHA256(key, [bits: u16 BE][context length: u32 BE][context][plaintext])
so that beacons of two fields, or of two bit counts, never match. Beacon::try_from checks the
prefix and that 1 to 32 bytes of HMAC follow, and fails with INVALID_BEACON_FORMAT otherwise.
Telling layouts apart
The first bytes of a stored value:
| First bytes | Layout |
|---|---|
00 00 00 00 | the length-prefixed envelope of 0.28.1 and earlier |
00, then a length of 1 to 4,096 | Tink’s KMS envelope, or the 4-byte length prefix of 32-bit targets before 0.24.0 |
01 | envelope layout v1 of 0.29, or a TINK-prefixed value |
FF 01 | encrypted DEK layout v1 of 0.29 |
Only the decrypted DEK tells a Tink envelope from the 32-bit layout before 0.24.0: a raw key of
16 or 32 bytes against a serialised Tink key, whose lengths differ. An encrypted DEK of AWS, Azure
or the local KMS also starts with 0x01, and Tink’s LEGACY and CRUNCHY output prefixes with 0x00.
A column that holds two layouts:
- Tink envelopes and TINK-prefixed values: the first byte,
0x00or0x01; - Tink envelopes and 0.29 envelopes: the first byte,
0x00or0x01; - Tink envelopes and 0.28.1 envelopes: the first four bytes;
- 0.29 envelopes and TINK-prefixed values: no byte tells them apart, so the layout is recorded outside the value, such as in another column;
- bare values and any other layout: a bare value has no framing, and its random nonce can start with any byte, so a column of bare values holds no other layout;
- encrypted DEKs of 0.29 and of 0.30:
0xFF 0x01starts no encrypted DEK of a built-in provider, so the library tells them apart for those. It never inspects the bytes of an application’s own provider: strip the header[0xFF][0x01][k][key version]of its 0.29 keys before reading them.
Values written by 0.29 and earlier
The layouts of 0.29 and earlier are read only with the deprecated legacy-formats feature:
KmsAeadEnvelope::decryptreads the three envelope layouts, whichEncrypted<T, Envelope>holds with the feature;- the
*_with_encrypted_dekcalls and the DEK cache read the encrypted DEK layout v1 of 0.29 and raw encrypted DEKs.
Encrypted::separate never reads the older envelope layouts: for Azure, the encrypted DEK they
hold needs the provider to become its bytes.
Without the feature, reading them fails with KmsAeadError::LegacyFormat, the code
LEGACY_FORMAT, and one of:
KmsAeadLegacyFormat::EnvelopeV1;KmsAeadLegacyFormat::LengthPrefixedEnvelope;KmsAeadLegacyFormat::EncryptedDekV1;KmsAeadLegacyFormat::RawEncryptedDek, for an encrypted DEK that decrypts to the raw key. The values of 32-bit targets before 0.24.0 fail with it, since their framing is Tink’s.
The error never echoes the bytes read. With the feature, the first read of each layout in a
process logs one WARN event that names the upgrade methods. Without it, the KMS AEAD decrypt
spans of such values record error_code=LEGACY_FORMAT, see Observability.
Upgrading stored values
With legacy-formats, two methods of KmsAeadEnvelope rewrite a stored value in Tink’s format,
and return None when it already is in it, so a sweep writes only the rows that change:
upgrade_legacy_value, for envelope values;upgrade_legacy_encrypted_dek, for encrypted DEKs stored apart, such as the bytes of 0.29’sto_bytes.
Each decrypts the DEK with the provider, without the DEK cache, and encrypts it again as a serialised Tink key when it was in an older layout: one KMS call for a current value, two for an old one. A value in Tink’s framing needs the decryption to be told from the 32-bit layout before 0.24.0. The AEAD bytes stay as they are.
The envelope’s algorithm is the one the values were sealed with: ChaCha20-Poly1305 for values that
0.29 and earlier sealed under their default. A raw DEK does not record its algorithm, and the
encrypted DEK of the upgrade records it as a Tink key type, so upgrade_legacy_value takes the
value’s AAD and opens the value before it writes anything. Under another algorithm or another AAD
it fails with DECRYPT. upgrade_legacy_encrypted_dek has no value to open, so it records the
envelope’s algorithm for any key of its length. Be aware not to upgrade a DEK stored apart under
another algorithm than its values were sealed with: use upgrade_legacy_value wherever the values
are at hand.
A sweep over stored rows:
use kms_aead::backends::AeadBackend;
use kms_aead::formats::Envelope;
use kms_aead::{Encrypted, KmsAeadEncryptionProvider, KmsAeadEnvelope, KmsAeadResult};
use secret_vault_value::SecretValue;
/// A stored envelope value and the AAD it was sealed under.
struct Row {
aad: Vec<u8>,
value: Vec<u8>,
}
/// Rewrites the envelope values of `rows` in Tink's format, and returns how many changed.
#[allow(deprecated)]
async fn upgrade_values<P, B>(
envelope: &KmsAeadEnvelope<P, B>,
rows: &mut [Row],
) -> KmsAeadResult<usize>
where
P: KmsAeadEncryptionProvider + Send + Sync,
B: AeadBackend,
{
let mut upgraded = 0;
for row in rows.iter_mut() {
let stored = Encrypted::<SecretValue, Envelope>::try_from(row.value.clone())?;
if let Some(value) = envelope.upgrade_legacy_value(&row.aad, &stored).await? {
row.value = value.into_value();
upgraded += 1;
}
}
Ok(upgraded)
}
use kms_aead::backends::Ring;
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::{AeadAlgorithm, AeadCipher, KmsAeadEnvelopeOptions, ValueStruct};
#[tokio::main(flavor = "current_thread")]
async fn main() -> KmsAeadResult<()> {
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("example".to_string())?)?;
// A value as 0.28.1 wrote it: [w: u64 BE][encrypted raw key][cipher text]
let cipher = AeadCipher::<Ring>::new(AeadAlgorithm::ChaCha20Poly1305)?;
let dek = cipher.generate_dek()?;
let bare = cipher.encrypt_bare(b"aad", &SecretValue::from("secret"), &dek)?;
let encrypted_dek = provider.encrypt_data_encryption_key(&dek).await?;
let mut legacy = u64::try_from(encrypted_dek.value().len()).unwrap().to_be_bytes().to_vec();
legacy.extend_from_slice(encrypted_dek.value());
legacy.extend_from_slice(bare.value());
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::with_options(
provider,
KmsAeadEnvelopeOptions::new().with_algorithm(AeadAlgorithm::ChaCha20Poly1305),
)?;
let mut rows = vec![Row { aad: b"aad".to_vec(), value: legacy }];
assert_eq!(upgrade_values(&envelope, &mut rows).await?, 1);
assert_eq!(upgrade_values(&envelope, &mut rows).await?, 0);
let upgraded = Encrypted::<SecretValue, Envelope>::try_from(rows[0].value.clone())?;
let decrypted: SecretValue = envelope.decrypt(b"aad", &upgraded).await?;
assert_eq!(decrypted, SecretValue::from("secret"));
Ok(())
}
Upgrade every reader to 0.30 before the sweep writes: 0.29 and earlier cannot read Tink’s format.
Deprecation
Reading the layouts of 0.29 and earlier is deprecated since 0.30.0 and will be removed in a later
breaking release, together with the legacy-formats feature and the upgrade methods, which carry
#[deprecated] from their first release, so every call site warns. Without the feature, the
older layouts keep failing with LEGACY_FORMAT.
Error handling
Every fallible call returns KmsAeadResult<T>, whose error is kms_aead::errors::KmsAeadError:
SystemError: the configuration or the environment, such as invalid provider options or a KMS client that cannot be built;NetworkError: a KMS that could not be reached or did not answer in time, a server error or a rate limit from Azure Key Vault or Vault, and every gRPC status from Google Cloud KMS;EncryptionError: encrypting or decrypting a value or a DEK failed, or a stored value could not be read. Includes every AWS KMS error, and the requests Azure Key Vault and Vault refused;UnsupportedAlgorithm: the backend does not implement the algorithm, from constructors only;LegacyFormat: a stored value in a layout of 0.29 or earlier, which only thelegacy-formatsfeature reads, see Formats.
The enum is #[non_exhaustive], so a match on it needs a _ arm.
Codes
code() returns a code to match on: one of the library’s own codes, or the code the KMS returned,
such as AWS’s IncorrectKeyException. It is also the error_code field of the spans.
use kms_aead::backends::Ring;
use kms_aead::errors::KmsAeadError;
use kms_aead::formats::Bare;
use kms_aead::{AeadAlgorithm, AeadCipher, Encrypted};
use secret_vault_value::SecretValue;
fn main() -> kms_aead::KmsAeadResult<()> {
let cipher = AeadCipher::<Ring>::new(AeadAlgorithm::Aes256Gcm)?;
let dek = cipher.generate_dek()?;
let cipher_text: Encrypted<SecretValue, Bare> =
cipher.encrypt_bare(b"orders/1042", &SecretValue::from("secret"), &dek)?;
match cipher.decrypt_bare(b"orders/1043", &cipher_text, &dek) {
Err(KmsAeadError::EncryptionError(error)) if error.public.code == "DECRYPT" => {
// Not authentic under this key and AAD
}
other => panic!("{other:?}"),
}
let too_short = Encrypted::<SecretValue, Bare>::try_from(vec![0; 8]);
assert!(matches!(&too_short, Err(e) if e.code() == "INVALID_CIPHER_TEXT_FORMAT"));
Ok(())
}
The library’s codes:
| Code | Variant | Meaning |
|---|---|---|
ENCRYPT, DECRYPT | encryption | sealing failed, or a value is not authentic under the key and the AAD |
ENCRYPT_KEY, DECRYPT_KEY | encryption | the backend refused a key, or a KMS request could not be built |
RANDOM_GENERATOR | encryption | the CSPRNG failed, for a nonce or a key |
INVALID_KEY_LENGTH | encryption | a key that does not fit the algorithm, or a KMS generator that returned another length |
UNSUPPORTED_ALGORITHM | unsupported algorithm | the backend does not implement the algorithm |
UNKNOWN_ALGORITHM | encryption | FromStr of an algorithm name it does not know |
INVALID_CIPHER_TEXT_FORMAT | encryption | a value too short, a length past its end, or a Randomized or Deterministic value without Tink’s output prefix |
UNKNOWN_CIPHER_TEXT_LAYOUT | encryption | an empty envelope value, or a first byte that starts no layout |
INVALID_PLAINTEXT | encryption | a decrypted value that is not an encoding of its type, or a Json value that does not serialise |
INVALID_KEY_FORMAT | encryption | a decrypted DEK that is not a serialised Tink key, such as an encrypted keyset |
INVALID_ENCRYPTED_KEY_FORMAT | encryption | the encrypted DEK layout v1 of 0.29 that cannot be read, with legacy-formats |
INVALID_KEY_VERSION | encryption | a key version that is not ASCII alphanumeric, in an Azure encrypted DEK or a layout of 0.29 |
INVALID_HEX | encryption | try_from_hex_string of text that is not hex |
INVALID_BEACON_FORMAT | encryption | stored bytes that are not [0x01][key id: u32 BE] and 1 to 32 bytes of HMAC, at Beacon::try_from |
INVALID_BEACON_CONTEXT | encryption | a beacon context longer than 2^32 - 1 bytes |
BEACON | encryption | the backend failed to calculate a beacon’s HMAC |
LEGACY_FORMAT | legacy format | a layout of 0.29 or earlier without legacy-formats, at Encrypted::try_from or at decryption, and the envelope layouts of 0.29 and earlier in separate |
INVALID_DEK_CACHE_TTL | system | a DEK cache TTL of zero |
INVALID_DEK_REUSE_POLICY | system | a reuse policy out of range, or beside a disabled cache |
INVALID_LOCAL_KEK_ID, LOCAL_* | system, encryption | the local KMS |
AZURE_* | system, encryption | the Azure provider |
VAULT_* | system, network, encryption | the Vault Transit provider |
The KMS codes are the gRPC status codes of Google Cloud KMS, such as PermissionDenied, the AWS
error codes, such as NotFoundException, and Key Vault’s error codes, such as KeyNotFound.
Retries
The library retries nothing itself. The AWS and Azure SDKs apply their own retry policies, which
the Azure provider takes in client_options.
A NetworkError is the class to consider for a retry: a KMS that was not reached, a timeout, a
server error or a rate limit. For Google Cloud KMS every failed call is a NetworkError, so check
its code as well: Unavailable, DeadlineExceeded or ResourceExhausted are worth a retry,
PermissionDenied or InvalidArgument are not. An EncryptionError from a decrypt does not
change on a retry.
What errors never carry
No error carries plaintext, key material in any form, plaintext or encrypted, AAD content or cipher
text bytes, in Display or in Debug. An application can log errors with {} and {:?}.
message is meant for a person reading logs. public holds the code, the part safe to return to
the application’s own callers, and with the serde feature it is Serialize and Deserialize.
std::error::Error::source() returns the root cause, the error of the SDK or library that
failed, when there is one.
Observability
The library uses tracing. Every envelope call and every KMS
call opens one span at the DEBUG level, and the spans carry what a call used besides how long it
took: the mode, the algorithm and backend, whether the DEK came from the cache, the KMS key and
its version, and the error code.
The spans are:
KMS AEAD encrypt: the envelope’sencrypt,encrypt_with_dekandencrypt_with_encrypted_dek;KMS AEAD decrypt: the envelope’sdecrypt,decrypt_with_dekanddecrypt_with_encrypted_dek;KMS AEAD generate DEK:generate_encrypted_dekandgenerate_encrypted_deterministic_dek;KMS AEAD decrypt DEK:decrypt_deterministic_dek;KMS AEAD beacon: the envelope’sbeacon;KMS encrypt DEK: each provider’s encryption of a DEK;KMS decrypt DEK: each provider’s decryption of a DEK;KMS random: a call to the GCP or AWS KMS generator, when the provider uses it.
The KMS spans nest inside the envelope span, so a DEK the envelope had to decrypt shows its KMS call
as a child, and a DEK served by the cache shows none. AeadCipher and DeterministicCipher open
no spans: they run once per value inside the caller’s span.
Showing the spans
Any tracing subscriber works. With tracing-subscriber, an EnvFilter that enables DEBUG for
the library and span close events, every span is printed with its fields when it ends:
use kms_aead::backends::Ring;
use kms_aead::formats::Envelope;
use kms_aead::providers::{LocalKekId, LocalKmsProvider};
use kms_aead::{Encrypted, KmsAeadEnvelope};
use secret_vault_value::SecretValue;
use tracing_subscriber::fmt::format::FmtSpan;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let subscriber = tracing_subscriber::fmt()
.with_env_filter("kms_aead=debug")
.with_span_events(FmtSpan::CLOSE)
.finish();
tracing::subscriber::set_global_default(subscriber)?;
let provider = LocalKmsProvider::<Ring>::ephemeral(LocalKekId::try_from("example".to_string())?)?;
let envelope = KmsAeadEnvelope::<LocalKmsProvider<Ring>, Ring>::new(provider)?;
let aad = b"orders/1042";
let encrypted: Encrypted<SecretValue, Envelope> =
envelope.encrypt(aad, &SecretValue::from("secret")).await?;
let decrypted: SecretValue = envelope.decrypt(aad, &encrypted).await?;
assert_eq!(decrypted, SecretValue::from("secret"));
Ok(())
}
The decrypt prints, without timestamps:
DEBUG KMS AEAD decrypt{/kms_aead/mode="per_call" /kms_aead/algorithm=aes-256-gcm /kms_aead/backend=ring /kms_aead/dek_cache=disabled}:KMS decrypt DEK{/kms_aead/provider=local /kms_aead/key="example" /kms_aead/key_version="1" /kms_aead/response_time=0}: kms_aead::spans::kms: close time.busy=16.4µs time.idle=6.95µs
DEBUG KMS AEAD decrypt{/kms_aead/mode="per_call" /kms_aead/algorithm=aes-256-gcm /kms_aead/backend=ring /kms_aead/dek_cache=disabled}: kms_aead::spans: close time.busy=62.0µs time.idle=12.3µs
Full example available here,
run it with cargo run --example tracing --features ring.
OpenTelemetry
With tracing-opentelemetry every span field
becomes a span attribute with the same name, such as /kms_aead/key_version, so the KMS calls and
cache hits show up next to the latency of a request in Cloud Trace, Jaeger, etc.
The fields are declared empty when the span opens and recorded once their value is known, which
tracing-opentelemetry exports the same way as fields given at the start. Add its layer to the
subscriber as usual:
use opentelemetry::trace::TracerProvider;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_batch_exporter(exporter) // any exporter, e.g. opentelemetry-otlp
.build();
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new("info,kms_aead=debug"))
.with(tracing_opentelemetry::layer().with_tracer(provider.tracer("my-service")))
.init();
Be aware the library spans are DEBUG, so the filter has to enable that level for kms_aead.
Fields
Every field name has the /kms_aead/ prefix, such as /kms_aead/mode. A value nobody reported is
left unrecorded, never recorded as 0 or an empty string.
KMS AEAD encrypt and KMS AEAD decrypt
| Field | Value |
|---|---|
mode | per_call for encrypt and decrypt, store_less for both on a store-less envelope, caller_dek for the *_with_dek and *_with_encrypted_dek calls |
algorithm | chacha20-poly1305, aes-128-gcm, aes-256-gcm or aes-256-gcm-siv |
backend | ring, aws-lc or rustcrypto |
dek_cache | disabled for an envelope without a cache; hit or miss for a call that decrypts through an enabled cache, a failed decryption recording miss; unrecorded for a call that looks nothing up |
error_code | the code of the error the call returned |
KMS AEAD generate DEK and KMS AEAD decrypt DEK
| Field | Value |
|---|---|
purpose | randomized or deterministic |
algorithm | the algorithm of the key, aes-256-siv for deterministic keys |
backend | generate DEK only: the envelope’s backend, ring, aws-lc or rustcrypto |
dek_cache | decrypt DEK only: bypassed beside an enabled cache, since the call never goes through it, disabled otherwise |
error_code | the code of the error the call returned |
KMS encrypt DEK, KMS decrypt DEK and KMS random
| Field | Value |
|---|---|
provider | gcp, aws, azure, hc_vault_transit, local, or the name of an application’s own provider |
key | the KEK, see below; for KMS random the GCP location, and unrecorded on AWS |
key_version | KMS encrypt DEK and KMS decrypt DEK: from the KMS response on encryption, from the encrypted DEK’s bytes on decryption for Azure, Vault Transit and the local KMS; unrecorded when neither has one, as for AWS and a GCP decryption |
len | random only: the bytes requested from the KMS generator |
response_time | milliseconds the KMS call took |
error_code | the code of the error the call returned |
The key of each provider:
- GCP: the CryptoKey name,
projects/<project>/locations/<location>/keyRings/<ring>/cryptoKeys/<key>; - AWS: the key or alias ARN, or the configured key id when no region is known;
- Azure:
<vault URL>/keys/<name>; - Vault Transit:
<vault address>/<namespace>/<mount>/keys/<name>, without the namespace segment when none is set; - local: the KEK id.
Events
Besides the spans, the library logs:
WARN, inside the KMS span: a GCP or Vault Transit response without a key version;WARN, once per process and layout: the first read of a layout of 0.29 or earlier withlegacy-formats;WARN, when a Vault Transit provider is created: anhttpaddress to another host, or a system CA certificate that cannot be loaded;INFO, when a local KMS provider is created: its KEK id and versions, each with where it came from, in thekeyandkey_versionsfields.
The library retries nothing itself, so there is no retry field.
What is never logged
No span field or event carries:
- plaintext, DEKs in any form, plaintext or encrypted, or KEK material;
- AAD content, which can hold entity ids and so personal data;
- cipher text bytes;
- the length of a value or of its AAD. For AES-256-GCM the plaintext length is the value’s length
minus a fixed overhead, and a length tells short values such as
yes,noor a country code apart.
Key identifiers are recorded: KEK names, key versions and local KEK ids. A KEK name identifies the
project, the location and the key, and a per-tenant KEK name carries the tenant id, so treat the
key field accordingly in a trace pipeline.
Errors follow the same rules, see Error handling.
Security considerations
Threat model
Envelope encryption protects stored values from whoever can read the store but cannot use the KEK: a database dump, a backup, a replica, a data export or an operator with read access to the tables. Opening a value needs a KMS decryption, and the KMS checks the caller’s permission and, for the cloud KMSs, writes an audit log entry.
It does not protect values from:
- a process, or whoever controls it, that may decrypt with the KEK: it can read every value it can fetch;
- whoever can read the memory of a process while it holds plaintext DEKs, in the DEK cache or as a store-less writer’s DEK;
- a reader of the store learning which values are equal, for values sealed with deterministic encryption.
The AAD protects a value from being moved: a value copied into another row or column, with an AAD that names its place, fails to open there. The AAD itself is not encrypted, and neither is the length of the encrypted DEK at the start of Tink’s KMS envelope, which is not authenticated either.
An encrypted DEK carries no context that the KMS authenticates. Whoever can write where encrypted DEKs are stored, such as a database table or a keyring store, can copy an encrypted DEK to another record or relabel it, and the KMS decrypts it for any principal allowed to decrypt with that KEK. The controls for this are in the deployment:
- write access to the stored keys is kept apart from the right to decrypt with the KEK;
- each isolation boundary, such as a tenant or an environment, has a KEK of its own, so a DEK copied across it fails to decrypt;
- the KMS audit log of decrypt calls, turned on where the KMS leaves it off by default, shows which principal decrypted which key.
The local KMS has no IAM, no audit log and no HSM, see its limits.
Nonces
Every AEAD algorithm takes a random 96-bit nonce from the backend’s CSPRNG for each value, and writes it in front of the cipher text. NIST SP 800-38D allows 2^32 encryptions under one key with random 96-bit nonces, and the library applies the same bound to every algorithm:
- the per-call envelope seals one value per DEK, so the bound does not apply to it;
- a store-less envelope renews its DEK at
max_uses, 2^24 by default and at most 2^32; - with
encrypt_with_dekandAeadCipher, the application counts the values it seals under one key, and moves to a new key before 2^32; - the local KMS seals each DEK it encrypts with AES-256-GCM, so a KEK version is good for about 2^32 encryptions. Add a new version before that.
AES-256-GCM-SIV (RFC 8452) resists nonce misuse: a repeated nonce reveals only whether the two values sealed with it are equal, and does not break integrity.
Deterministic encryption with AES-SIV takes no random nonce.
Data volume
AES-GCM also has a bound on the data under one key, whatever the nonces: RFC 8446, section 5.5, allows 2^24.5 records of 2^14 bytes, 2^38.5 bytes, for an AE advantage of about 2^-57. The bound applies to AES-GCM keys this way:
- a store-less envelope renews its DEK at
max_bytes, 2^38 bytes by default under AES-GCM and at most 2^52, see Volume limit. AES-256-GCM-SIV and ChaCha20-Poly1305 have no default volume limit; - with
encrypt_with_dekandAeadCipher, the application counts the bytes it seals under one AES-GCM key too, and moves to a new key before 2^38 bytes.
Key lifetime and zeroisation
DataEncryptionKey, LocalKek and the Vault token are zeroised on drop and shown as *** by
Debug. The DEK cache zeroises a key when its entry leaves the cache and no with_dek closure
holds it.
Some copies are out of the library’s reach:
- the KMS SDKs and HTTP clients the providers use keep copies of each plaintext DEK in request and response buffers, which kms-aead cannot zeroise. The same holds for the Vault token in the HTTP stack;
- ring does not zeroise its expanded key schedules, so they stay in freed memory after each call;
- aws-lc-rs clears its key context through AWS-LC’s allocator, as long as the application leaves that allocator in place. The record keys AES-256-GCM-SIV derives for each call stay on the stack;
- the RustCrypto backend zeroises its key objects and the per-call Poly1305 and POLYVAL keys;
- an expired DEK stays in the cache’s memory until the next call on the cache or
purge_expired; - a plaintext value returned to the application is a
SecretValue, zeroised on drop, and what the application does with it afterwards is its own.
What rotation does not protect
Rotating the KEK in the KMS makes the KMS encrypt new DEKs under the new key version. It does not:
- re-encrypt the DEKs already stored: they still decrypt with the old key version, which the KMS must keep enabled for as long as those values are read;
- re-encrypt any value: a DEK that leaked keeps opening every value sealed under it, whatever the KEK version that encrypts it. To retire a leaked DEK, decrypt and encrypt the values again under new DEKs;
- reach DEKs already in a DEK cache: they keep serving until their entries expire, or until
invalidateorclear. The same holds for disabling a KMS key or revoking an IAM grant; - renew a store-less writer’s DEK: it is renewed by its policy, or once it leaves the cache.
With the local KMS, rotation is a new KEK version. The old version has to stay in the provider for as long as DEKs encrypted under it are read, and crypto-shredding means destroying every copy of it, backups included.
OSS
Open source code is created through voluntary collaboration of software developers. The original authors license the code so that anyone can see it, modify it, and distribute new versions of it. You should manage all OSS using the same procedures and tools that you use for commercial products. As always, train your employees on cyber security best practices that can help them securely use and manage software products. You should not solely rely on individuals, especially on the projects like this reading sensitive information.
Versioning
Please don’t use broad version dependency management not to include a new version of dependency automatically without auditing the changes.
Reporting a vulnerability
See SECURITY.md.
Migrating from 0.29
0.30 replaces the ring-only API with a choice of crypto backends, adds store-less DEK reuse,
the DEK cache, deterministic encryption and the local KMS, and makes ring optional. It writes
Tink’s KMS envelope format for every provider, so values written by 0.29 and earlier read only
with the deprecated legacy-formats feature, which also rewrites them in the new format, see
Old values. The default algorithm of the envelope is AES-256-GCM, see
Default algorithm.
Features
| 0.29 | 0.30 |
|---|---|
ring-aead-encryption | ring |
| a provider feature alone, which turned on ring | the provider feature plus ring, aws-lc, aws-lc-fips or rustcrypto |
vault-transit-kms-encryption | hashicorp-vault-transit-kms-encryption |
vault-transit-kms-encryption-base | hashicorp-vault-transit-kms-encryption-base |
vault-transit-kms-encryption-tls-roots, -tls-webpki | hashicorp-vault-transit-kms-encryption-tls-roots, -tls-webpki |
vault-transit-kms-encryption-tls-aws-lc-rs, -tls-ring | hashicorp-vault-transit-kms-encryption-tls-aws-lc-rs, -tls-ring |
ring is no longer turned on by the provider features. A 0.29 build that relied on it fails with
“kms-aead needs a crypto backend for its KMS providers”, so add a backend feature. aws-lc adds
no second crypto library to a cloud provider build, see Choosing a
backend.
The crate declares rust-version = "1.99".
Names
| 0.29 | 0.30 |
|---|---|
KmsAeadRingEnvelopeEncryption<P> | KmsAeadEnvelope<P, Ring>, with the backend as a type parameter |
KmsAeadRingEnvelopeEncryption::new(provider).await? | KmsAeadEnvelope::<P, Ring>::new(provider)?, not async, on AES-256-GCM rather than ChaCha20-Poly1305, see Default algorithm |
KmsAeadRingEnvelopeEncryption::with_algorithm(provider, &ring::aead::AES_256_GCM) | KmsAeadEnvelope::<P, Ring>::with_options(provider, KmsAeadEnvelopeOptions::new().with_algorithm(AeadAlgorithm::Aes256Gcm)) |
with_options, with_algorithm_options | KmsAeadEnvelope::<P, Ring>::with_options(provider, options) |
KmsAeadRingEnvelopeEncryptionOptions | KmsAeadEnvelopeOptions |
KmsAeadRingEncryptionProvider | KmsAeadEncryptionProvider, see Your own provider |
module kms_aead::ring_encryption | removed; AeadCipher<Ring>, with AeadCipher at the crate root and Ring in kms_aead::backends |
RingAeadEncryption::new() | AeadCipher::<Ring>::new(AeadAlgorithm::ChaCha20Poly1305) |
RingAeadEncryption::with_algorithm(&ring::aead::AES_256_GCM, rand) | AeadCipher::<Ring>::new(AeadAlgorithm::Aes256Gcm) |
RingAeadEncryption::with_rand, with_options, with_algorithm_options | removed; the backend owns its CSPRNG, and the random nonce was the only option |
RingAeadEncryptionOptions, RingAeadEncryptionNonceKind | removed |
RingAeadEncryption.algo | AeadCipher::algorithm() |
RingAeadEncryption::generate_data_encryption_key() | AeadCipher::generate_dek() |
kms_aead::kms_envelope_encryption::… | the same names at the crate root |
CipherText(bytes) | Encrypted::<SecretValue, Bare>::try_from(bytes)?, which checks the layout |
CipherTextWithEncryptedKey(bytes) | Encrypted::<SecretValue, Envelope>::try_from(bytes)?, which checks the layout |
CipherTextWithEncryptedKey::new(&cipher_text, &encrypted_dek) | Encrypted::<SecretValue, Envelope>::join(&bare, &encrypted_dek)?, in Tink’s KMS envelope format |
cipher_text.to_hex_string() | value.to_hex_string(), read back with Encrypted::try_from_hex_string |
cipher_text == other | value.value() == other.value() |
EncryptedDataEncryptionKey::new(encrypted, key_version) | EncryptedDataEncryptionKey::new(bytes): the provider’s bytes, which carry the key version where the KMS needs one |
encrypted_dek.wrapped(), to_bytes()?, TryFrom<&[u8]> | encrypted_dek.value() and EncryptedDataEncryptionKey::from(bytes) |
encrypted_dek.to_hex_string()? | encrypted_dek.to_hex_string(), infallible |
encrypted_dek.key_version() | removed; the key_version field of the KMS spans |
kms_aead::KmsKeyVersion | kms_aead::providers::KmsKeyVersion, the type of AzureKmsKeyRef::key_version |
DataEncryptionKey(secret), dek.0 | DataEncryptionKey::from(secret), dek.value() |
errors::BoxedError | removed |
HashiCorp Vault Transit types have the Hc prefix:
| 0.29 | 0.30 |
|---|---|
VaultTransitProvider | HcVaultTransitProvider |
VaultTransitProviderOptions | HcVaultTransitProviderOptions |
VaultTransitKeyRef | HcVaultTransitKeyRef |
VaultTransitToken | HcVaultTransitToken |
examples/vault_transit_kms.rs | examples/hc_vault_transit_kms.rs |
Traits replaced by inherent methods
AeadEncryption and KmsAeadEnvelopeEncryption are gone. Their methods are inherent methods of
AeadCipher and KmsAeadEnvelope, so delete the trait imports. The AAD is any
&(impl AsRef<[u8]> + ?Sized), like 0.29’s Aad: AsRef<[u8]>, so &aad still compiles, and the
calls that make no KMS call are not async:
| 0.29 | 0.30 |
|---|---|
use kms_aead::KmsAeadEnvelopeEncryption; envelope.encrypt_value(&aad, &value).await? | envelope.encrypt(&aad, &value).await? |
envelope.encrypt_value_with_dek(&aad, &value, &dek).await? | envelope.encrypt_with_dek(&aad, &value, &dek)? |
envelope.decrypt_value(&aad, &cipher_text).await? | envelope.decrypt(&aad, &value).await? |
envelope.decrypt_value_with_dek(&aad, &cipher_text, &dek).await? | envelope.decrypt_with_dek(&aad, &bare, &dek)? |
encrypt_value_with_encrypted_dek, decrypt_value_with_encrypted_dek | encrypt_with_encrypted_dek, decrypt_with_encrypted_dek |
KmsAeadEnvelopeEncryption::<Aad>::generate_new_dek(&envelope) | envelope.generate_encrypted_dek() |
use kms_aead::AeadEncryption; cipher.encrypt_value(&aad, &value, &key).await? | cipher.encrypt_bare(&aad, &value, &key)? |
cipher.decrypt_value(&aad, &cipher_text, &key).await? | cipher.decrypt_bare(&aad, &bare, &key)? |
Every cipher text is an Encrypted<T, F>, see Formats. It
has its own value() and into_value(). Those of EncryptedDataEncryptionKey come from
ValueStruct, which the crate re-exports: use kms_aead::ValueStruct. DataEncryptionKey no
longer implements ValueStruct, and has its own value().
Your own provider
An application’s own implementation of the provider trait:
- implements
KmsAeadEncryptionProviderinstead ofKmsAeadRingEncryptionProvider; - adds
fn kek(&self) -> &KekRef, withKekProvider::Otherand a key name that identifies the key; - deletes
generate_encryption_keywhen it returned the backend’s key. The replacement,generate_key_material(&self, len), has a default implementation that lets the backend generate the key, and is implemented only for a KMS generator; - returns
EncryptedDataEncryptionKey::new(bytes). A KMS that needs the key version to decrypt gets it from those bytes, as the Azure provider’s do.
The plaintext the provider encrypts is the DEK serialised as a Tink key, see Formats. The library never inspects the provider’s own bytes.
Errors
KmsAeadError is #[non_exhaustive], so an exhaustive match on it needs a _ arm. It has two
new variants, UnsupportedAlgorithm and LegacyFormat. code() returns the code of any variant,
and source() returns the root cause.
The From conversions of SDK errors into KmsAeadError are gone: gcloud_sdk::error::Error,
gcloud_sdk::tonic::Status, the AWS SdkError, the hyper client error, azure_core::Error and
SystemTimeError. Map an SDK error in the application.
ConstantTimeEq is no longer implemented on the public types. == still compares in constant
time. The Debug of Encrypted prints only the length, and == is gone from randomised values:
only an Encrypted<T, Deterministic> has it.
Error codes that changed:
| 0.29 | 0.30 | Where |
|---|---|---|
INVALID_CIPHERTEXT | INVALID_CIPHER_TEXT_FORMAT | Encrypted::try_from of a Bare value shorter than a nonce and a tag |
ENCRYPTION | RANDOM_GENERATOR | a CSPRNG failure, for a nonce or a key |
DECRYPT_KEY | ENCRYPT_KEY | the GCP encryption, when its request cannot be built |
DECRYPT_KEY | RANDOM_GENERATOR | the GCP KMS generator, when its request cannot be built |
SYSTEM_TIME_ERROR | removed | |
UNKNOWN_ENCRYPTED_KEY_LAYOUT | removed | stored encrypted DEKs are the provider’s bytes |
AZURE_KEY_VERSION_MISSING | AZURE_ENCRYPTED_DEK_FORMAT | the Azure decryption of bytes outside its layout; the old code stays for a 0.29 value without a key version, with legacy-formats |
The other 0.29 codes keep their spelling. The new codes are listed in Error handling.
AWS KMS decryption names the key
The AWS provider sends the configured key as KeyId on every decryption. In 0.29 a decryption left
it out, so AWS KMS decrypted under whichever key the encrypted DEK named, for any key the principal
may decrypt with. A 0.30 process refuses a DEK encrypted under another key with
IncorrectKeyException. A deployment that relied on decrypting under a second key configures a
provider per key.
An alias name such as alias/orders becomes an alias ARN, where 0.29 built a key ARN of it.
Configure the key ARN rather than an alias, see AWS KMS.
Default algorithm
The default algorithm of KmsAeadEnvelope is AES-256-GCM, which every Tink implementation reads
in a KMS envelope. The default of 0.29 was ChaCha20-Poly1305.
A value does not record its algorithm. Values sealed under the default of 0.29 open only with
ChaCha20-Poly1305 configured, and an envelope on the new default fails them with DECRYPT. A 0.29
envelope that named no algorithm becomes:
use kms_aead::backends::Ring;
use kms_aead::providers::{GcpKmsKeyRef, GcpKmsProvider};
use kms_aead::{AeadAlgorithm, KmsAeadEnvelope, KmsAeadEnvelopeOptions};
async fn example(key_ref: GcpKmsKeyRef) -> kms_aead::KmsAeadResult<()> {
// 0.29: KmsAeadRingEnvelopeEncryption::new(provider).await?
let envelope = KmsAeadEnvelope::<GcpKmsProvider, Ring>::with_options(
GcpKmsProvider::new(&key_ref).await?,
KmsAeadEnvelopeOptions::new().with_algorithm(AeadAlgorithm::ChaCha20Poly1305),
)?;
let _ = envelope;
Ok(())
}
Configure it on every envelope that reads those values, on the one that upgrades them, see Old values, and on every envelope that writes new values beside them: a reader cannot tell the two algorithms apart. AES-256-GCM suits a column that holds no 0.29 values.
Old values
0.30 writes Tink’s KMS envelope, and encrypts every DEK but Azure’s as a serialised Tink key. 0.29
wrote envelope layout v1 and the encrypted DEK layout v1 [0xFF][0x01]…, and 0.28.1 and earlier a
length-prefixed envelope, all with the raw DEK encrypted. 0.30 reads them only with the deprecated
legacy-formats feature, and fails with LEGACY_FORMAT without it, see
Formats.
The envelopes that read and upgrade those values keep the algorithm they were sealed with, see Default algorithm.
To move stored values to the new format:
- upgrade every process that reads the values to 0.30 with
legacy-formats. 0.29 and earlier cannot read Tink’s format, so no process writes it before every reader has the upgrade; - rewrite the stored values with
upgrade_legacy_valueandupgrade_legacy_encrypted_dekofKmsAeadEnvelope, one or two KMS calls per value, see Upgrading stored values.upgrade_legacy_valuetakes the value’s AAD, and fails withDECRYPTon a value that does not open under the envelope’s algorithm and that AAD; - turn the feature off.
Encrypted::<T, Envelope>::separate reads Tink’s format only, with or without the feature, so
code that separated stored 0.29 values runs after the sweep.