Skip to main content

Encryption Keys

Choose where the key for data at rest comes from and keep it stable. EUDIPLO encrypts private keys and personal session data in the database with AES-256-GCM under one 256-bit key, which the backend loads at startup from ENCRYPTION_KEY_SOURCE.

What is encrypted​

TableColumns
Key chainsrootJwk, activeJwk, previousJwk (private keys of db key chains)
Sessionscredentials (verified presentations), credentialPayload (offer claims), offer, auth_queries, responseEncryptionPrivateJwk (per-request key that decrypts the wallet response)
Interactive authorization sessionsauthorizationDetails, presentationData, completedStepsData

Public keys, certificates and configuration are stored in plain text. Keys in an external KMS never reach the database. Values written before encryption was introduced are still read and are encrypted on their next write.

The same key also derives the HMAC key for the pseudonymous subject keys of the single-active-credential policy, so the slot of a returning user is found again without storing their identity.

Choose a key source​

ENCRYPTION_KEY_SOURCEKey comes fromUse
env (default)Derived from MASTER_SECRET with HKDF-SHA256Development, single-VM installations
vaultA HashiCorp Vault KV v2 secretProduction
awsAn AWS Secrets Manager secretProduction on AWS
azureAn Azure Key Vault secretProduction on Azure

With vault, aws and azure the key exists only in the backend's memory, not in its environment, and MASTER_SECRET no longer protects stored data. The key must be 32 bytes, stored as base64 (44 characters) or hex (64 characters):

openssl rand -base64 32

Vault​

ENCRYPTION_KEY_SOURCE=vault
VAULT_ADDR=https://vault.example.com:8200
VAULT_TOKEN=<token with read access to the path>
VAULT_ENCRYPTION_KEY_PATH=secret/data/eudiplo/encryption-key # default
vault kv put secret/eudiplo/encryption-key key="$(openssl rand -base64 32)"

The backend reads <VAULT_ADDR>/v1/<VAULT_ENCRYPTION_KEY_PATH> and expects the key in the field key. VAULT_ADDR and VAULT_TOKEN can also be referenced as ${VAULT_ADDR}/${VAULT_TOKEN} from kms.json, but Vault as a key provider for signing keys is configured separately in KMS.

AWS Secrets Manager​

ENCRYPTION_KEY_SOURCE=aws
AWS_REGION=eu-central-1
AWS_ENCRYPTION_SECRET_NAME=eudiplo/encryption-key
AWS_ENCRYPTION_SECRET_KEY=key # JSON field, default "key"
aws secretsmanager create-secret --name eudiplo/encryption-key \
--secret-string "$(openssl rand -base64 32)"

The secret can be the plain key, a JSON object with the key in AWS_ENCRYPTION_SECRET_KEY, or a 32-byte binary secret. Credentials come from the AWS SDK default chain (IAM role, IRSA, or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY).

Azure Key Vault​

ENCRYPTION_KEY_SOURCE=azure
AZURE_KEYVAULT_URL=https://myvault.vault.azure.net
AZURE_ENCRYPTION_SECRET_NAME=eudiplo-encryption-key
az keyvault secret set --vault-name myvault --name eudiplo-encryption-key \
--value "$(openssl rand -base64 32)"

Credentials come from DefaultAzureCredential (managed identity, or AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET).

Checkpoint: the startup log shows Initializing encryption with key provider: <source>, and for vault, aws and azure a short key fingerprint. All replicas must log the same fingerprint.

Keep the key stable​

There is no key rotation: the backend does not re-encrypt stored data. If the key changes, or with env the MASTER_SECRET changes:

  • private keys of db key chains and stored session data can no longer be decrypted,
  • the single-active-credential policy no longer recognizes earlier subjects, so their existing credentials stop counting against the limit,
  • with the built-in OAuth2 server, all management tokens become invalid (they are signed with MASTER_SECRET).

Store the key, or MASTER_SECRET, in your secret manager with versioning, and include it in the backup plan: a database backup is useless without it.

Switch to an external key source​

The data encrypted so far is bound to the current key. To move from env to vault, aws or azure without losing data, the external secret must contain the key that env derived from your MASTER_SECRET, which you can compute offline:

node -e 'const c=require("node:crypto");console.log(Buffer.from(c.hkdfSync("sha256",process.argv[1],"","eudiplo-encryption-at-rest",32)).toString("base64"))' "$MASTER_SECRET"

Store the output as the key in the external source, then switch ENCRYPTION_KEY_SOURCE. Test the procedure on a copy of the database first.