---
title: "ADR-002: Envelope encryption"
manual: "nr-vault"
version: "1.0"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-vault:adr-002-envelope-encryption@1.0"
source: "Developer/Adr/ADR-002-EnvelopeEncryption.rst"
rendered: "2026-09-18T07:37:50+00:00"
---

# ADR-002: Envelope encryption {#adr-002-envelope-encryption-1}

**Table of contents**

-   [Status](https://docs.typo3.org/permalink/netresearch/nr-vault:status@1.0)
-   [Date](https://docs.typo3.org/permalink/netresearch/nr-vault:date@1.0)
-   [Context](https://docs.typo3.org/permalink/netresearch/nr-vault:context@1.0)
-   [Problem statement](https://docs.typo3.org/permalink/netresearch/nr-vault:problem-statement@1.0)
-   [Decision drivers](https://docs.typo3.org/permalink/netresearch/nr-vault:decision-drivers@1.0)
-   [Considered options](https://docs.typo3.org/permalink/netresearch/nr-vault:considered-options@1.0)
-   [Decision](https://docs.typo3.org/permalink/netresearch/nr-vault:decision@1.0)
-   [Implementation](https://docs.typo3.org/permalink/netresearch/nr-vault:implementation@1.0)
-   [Consequences](https://docs.typo3.org/permalink/netresearch/nr-vault:consequences@1.0)
-   [References](https://docs.typo3.org/permalink/netresearch/nr-vault:references@1.0)

## Status {#status}

Accepted

## Date {#date}

2026-01-03

## Context {#context}

The nr-vault extension needs to encrypt secrets at rest in the database.
The encryption approach must:

-   Protect secrets even if the database is compromised
-   Allow efficient key rotation without re-encrypting all secret values
-   Use well-audited, modern cryptographic primitives
-   Integrate with PHP's native cryptography libraries

## Problem statement {#problem-statement}

How should secrets be encrypted to provide strong security while enabling
efficient operations like key rotation?

## Decision drivers {#decision-drivers}

-   **Security**: Must use authenticated encryption (AEAD)
-   **Key rotation**: Master key changes should not require re-encrypting values
-   **Performance**: Encryption/decryption must be fast
-   **Simplicity**: Use PHP's built-in libsodium, no external dependencies
-   **Memory safety**: Sensitive data must be cleared from memory

## Considered options {#considered-options}

### Option 1: Direct encryption with master key {#option-1-direct-encryption-with-master-key}

Encrypt each secret directly with the master key.

**Pros:**

-   Simple implementation
-   Single key to manage

**Cons:**

-   Master key rotation requires re-encrypting ALL secrets
-   Same key used for all secrets (higher exposure risk)

### Option 2: Envelope encryption (DEK/KEK) {#option-2-envelope-encryption-dek-kek}

Two-layer encryption: unique Data Encryption Key (DEK) per secret,
encrypted with Master Key (KEK).

**Pros:**

-   Master key rotation only re-encrypts DEKs (fast)
-   Each secret has unique encryption key
-   Industry-standard pattern (AWS KMS, Google Cloud KMS)

**Cons:**

-   Slightly more complex implementation
-   More data to store (encrypted DEK + nonces)

## Decision {#decision}

We chose **envelope encryption** with AES-256-GCM (primary) or
XChaCha20-Poly1305 (fallback) because:

1.  **Efficient key rotation**: Only DEKs need re-encryption, not secret values
1.  **Defense in depth**: Unique key per secret limits blast radius
1.  **Industry standard**: Proven pattern used by major cloud providers
1.  **Modern algorithms**: Both are AEAD with strong security properties

## Implementation {#implementation}

### Encryption flow {#encryption-flow}

**Envelope encryption process**

```text
1. Generate unique DEK (32 bytes) for the secret
2. Generate two random nonces (12 or 24 bytes each)
3. Encrypt DEK with master key: encryptedDek = AEAD(DEK, masterKey, dekNonce)
4. Encrypt secret with DEK: encryptedValue = AEAD(secret, DEK, valueNonce)
5. Calculate SHA-256 checksum for change detection
6. Clear sensitive data from memory (sodium_memzero)
7. Store: encryptedValue, encryptedDek, dekNonce, valueNonce, checksum
```

### Decryption flow {#decryption-flow}

**Envelope decryption process**

```text
1. Retrieve master key from provider
2. Decrypt DEK: DEK = AEAD_decrypt(encryptedDek, masterKey, dekNonce)
3. Decrypt secret: secret = AEAD_decrypt(encryptedValue, DEK, valueNonce)
4. Clear DEK and master key from memory
5. Return plaintext secret
```

### Algorithm selection {#algorithm-selection}

**Classes/Crypto/EncryptionService.php**

```php
private function useAes256Gcm(): bool
{
    // Use AES-256-GCM if hardware acceleration available
    // Otherwise fall back to XChaCha20-Poly1305
    if (!sodium_crypto_aead_aes256gcm_is_available()) {
        return false;
    }

    return !$this->configuration->preferXChaCha20();
}

private function getNonceLength(): int
{
    return $this->useAes256Gcm()
        ? SODIUM_CRYPTO_AEAD_AES256GCM_NPUBBYTES      // 12 bytes
        : SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES;  // 24 bytes
}
```

### Memory safety {#memory-safety}

**Secure memory handling**

```php
try {
    $dek = $this->generateDek();
    $encryptedValue = $this->encryptWithKey($plaintext, $dek, $valueNonce);
    // ... store encrypted data
} finally {
    sodium_memzero($dek);
    sodium_memzero($masterKey);
    sodium_memzero($plaintext);
}
```

### Master key rotation {#master-key-rotation}

With envelope encryption, rotating the master key is efficient:

**Re-encrypting DEKs only**

```php
public function reEncryptDek(
    string $encryptedDek,
    string $dekNonce,
    string $identifier,
    string $oldMasterKey,
    string $newMasterKey,
): array {
    // Decrypt DEK with old master key
    $dek = $this->decryptDek($encryptedDek, $dekNonce, $identifier, $oldMasterKey);

    // Re-encrypt DEK with new master key
    $newNonce = random_bytes($this->getNonceLength());
    $newEncryptedDek = $this->encryptWithKey($dek, $newMasterKey, $newNonce);

    sodium_memzero($dek);
    return ['encrypted_dek' => $newEncryptedDek, 'dek_nonce' => $newNonce];
}
```

### Database storage {#database-storage}

**Encrypted data columns**

```sql
encrypted_value mediumblob,           -- AEAD ciphertext + auth tag
encrypted_dek text,                   -- Base64-encoded encrypted DEK
dek_nonce varchar(24) NOT NULL,       -- Base64-encoded DEK nonce
value_nonce varchar(24) NOT NULL,     -- Base64-encoded value nonce
encryption_version int unsigned,      -- For algorithm migrations
value_checksum char(64) NOT NULL,     -- SHA-256 for change detection
```

## Consequences {#consequences}

### Positive {#positive}

-   **Fast key rotation**: Only DEKs re-encrypted, O(n) simple operations
-   **Unique keys per secret**: Compromise of one DEK doesn't expose others
-   **Hardware acceleration**: AES-256-GCM uses AES-NI when available
-   **Authenticated encryption**: Tampering is detected and rejected
-   **Memory safety**: Sensitive data cleared immediately after use

### Negative {#negative}

-   **More storage**: Each secret requires DEK + two nonces
-   **Complexity**: Two-layer encryption requires careful implementation
-   **Algorithm migration**: Changing algorithms requires re-encryption

### Risks {#risks}

-   Master key loss = all secrets unrecoverable (mitigate with secure backups)
-   Memory-based attacks could capture keys during brief window of use

## References {#references}

-   [libsodium documentation](https://doc.libsodium.org/)
-   [AWS KMS Envelope Encryption](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#enveloping)
-   [NIST SP 800-38D (GCM)](https://csrc.nist.gov/publications/detail/sp/800-38d/final)
