Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Encryption Inventory

This page lists every field that Mycelium stores in an encrypted or hashed form, together with the mechanism used and its migration status relative to the envelope encryption rollout (Phases 1 and 2).


Fields encrypted with AES-256-GCM

These fields hold reversible ciphertexts. Before Phase 1 they were all encrypted with the global KEK directly (v1 format). After Phase 1 they use per-tenant DEKs wrapped by the KEK (v2 format). The two formats are distinguished by a v2: prefix in the stored value.

FieldTable / columnMechanism before Phase 1DEK scopeMigration phase
Totp::Enabled.secretuser.mfa (JSONB)Totp::encrypt_me — KEK directsystem (UUID nil)Phase 1
HttpSecret.token (webhook)webhook.secret (JSONB)WebHook::new_encryptedHttpSecret::encrypt_me — KEK directsystem (UUID nil)Phase 1
TelegramBotTokentenant.meta (JSONB key)encrypt_string — KEK directper-tenantPhase 1
TelegramWebhookSecrettenant.meta (JSONB key)encrypt_string — KEK directper-tenantPhase 1
phone_number, telegram_useraccount.meta (JSONB)plaintextper-tenantPhase 2
tenant.meta (general keys)tenant.meta (JSONB)plaintextper-tenantPhase 2
Subscription / TenantManager metadataaccount.meta (JSONB)plaintextper-tenantPhase 2

TOTP is user identity (user, manager, staff) and is never tenant-scoped; every call site passes tenant_id = None, so the secret is encrypted under the system DEK.

DEK storage

Each tenant row in the tenant table now carries two additional columns:

ColumnTypeDescription
encrypted_dekTEXT (nullable)AES-256-GCM ciphertext of the 32-byte DEK, wrapped by the KEK. NULL means the DEK has not been provisioned yet (lazy on first use).
kek_versionINTEGER NOT NULL DEFAULT 1Tracks which KEK generation was used to wrap the DEK. Used during KEK rotation.

The system tenant row (id = 00000000-0000-0000-0000-000000000000) stores the DEK used for system-level secrets (webhook HTTP secrets, all TOTP).


Fields hashed with Argon2 — outside encryption scope

These fields are one-way hashes. There is no plaintext to recover or re-encrypt. They are unaffected by envelope encryption migration.

FieldTable / columnNote
password_hashidentity_providerArgon2id — verification only, no decryption
Email confirmation tokenUserRelatedMeta.token (logical)Argon2 one-way hash

Ciphertext format versions

VersionFormatWhen writtenHow detected
v1 (legacy)base64(nonce₁₂ ‖ ciphertext ‖ tag₁₆)Before Phase 1No prefix
v2 (envelope)v2:base64(nonce₁₂ ‖ ciphertext ‖ tag₁₆)After Phase 1Starts with v2:

Decrypt functions detect the prefix automatically and route to the correct decryption path, so v1 and v2 data can coexist in the same deployment without downtime.


AAD (Authenticated Additional Data)

AAD prevents ciphertexts from being transplanted between tenants or between fields. The AAD scheme is:

aad = tenant_id.as_bytes() || field_name_bytes
Field constantBytes
AAD_FIELD_TOTP_SECRETb"totp_secret"
AAD_FIELD_TELEGRAM_BOT_TOKENb"telegram_bot_token"
AAD_FIELD_TELEGRAM_WEBHOOK_SECRETb"telegram_webhook_secret"
AAD_FIELD_HTTP_SECRETb"http_secret"

DEK wrap/unwrap uses only tenant_id.as_bytes() as AAD (no field suffix).


token_secret is multi-purpose — rotation has side-effects

The token_secret configured in AccountLifeCycle is not only the KEK source. Its bytes are also consumed directly by non-envelope code paths:

ConsumerRoleRotation impact
AccountLifeCycle::derive_kek_bytesKEK for wrap/unwrap of all DEKsRe-wrap DEKs via myc-cli rotate-kek (TODO).
encrypt_string::build_aes_key (v1 legacy path)KEK for ciphertexts written before Phase 1Stays readable only while token_secret is unchanged; migrate to v2 before rotating.
HttpSecret::decrypt_me (v1 branch)Indirect — routes through the legacy pathSame as above.
Totp::decrypt_me (v1 branch)Indirect — routes through the legacy pathSame as above.
UserAccountScope::sign_tokenHMAC-SHA512 key for connection-string signaturesNo re-signing path. All currently-issued connection strings are invalidated on rotation — treat as revoked.

Rotate token_secret only after:

  1. migrate-dek --dry-run reports zero v1 fields remaining, and
  2. The operational impact of invalidating every live connection-string signature is understood and accepted.

See Envelope Encryption Migration Guide for step-by-step operator instructions.