# Authenticated Encryption Migration
Date reviewed: 2026-09-16
## Implemented Clean-Break Decision
The migration is implemented as a deliberate clean break. Common now writes and reads only the `SACSENC2.` AES-GCM envelope, using a fresh random 12-byte nonce, a 16-byte authentication tag, and required purpose-bound associated data. The nonce is the GCM equivalent of an IV: it is stored with the ciphertext, is not secret, and must never repeat with the same key.
The existing in-code key strings are retained as agreed. `SacsAesGcmCrypter` derives a 256-bit key from its existing string with SHA-256; raw-key consumers continue to supply their existing Base64-encoded keys. Fixed IVs, per-session IV state, CBC operations, and legacy decryption have been removed from active APIs. Invalid, tampered, wrong-key, wrong-purpose, and legacy values fail closed rather than being returned as plaintext.
Existing encrypted cookies, links, session parameters, and service messages are intentionally invalidated. Coupled service clients and servers must be deployed together. Moving keys into protected configuration and adding key identifiers and rotation remains a future hardening task, not part of this compatibility-free change.
Tests cover empty, ASCII, and Unicode round trips; URL-safe envelopes; nonce uniqueness; tampering; wrong keys and purposes; malformed, incomplete, oversized, and legacy input; invalid key sizes; and both shared wrappers.
## Superseded Staged-Migration Proposal
The remainder of this document records the earlier compatibility-preserving proposal for context. Its dual-read, retention-window, key-ring, and staged-rollout requirements were superseded by the clean-break decision above and are not implementation instructions.
## Scope
This document proposed the shared design for migrating the former `SacsAesIvCrypter` API, now replaced by `SacsAesGcmCrypter`, and `SacsEncryptionHelper` from unauthenticated AES-CBC to versioned authenticated encryption. BusinessMgr-specific consumers and retention requirements are recorded in its repository-owned [`docs/encryption-consumers.md`](../../../BusinessMgr/Workspace/docs/encryption-consumers.md).
The recent static-PBKDF2 change deliberately preserved the legacy format. It removed the .NET 10 warning but did not address CBC integrity, fixed password salt, low iteration count, embedded application keys, or key rotation.
## Current Formats
| API | Key material | IV handling | Serialized form | Compatibility concern |
|---|---|---|---|---|
| Former `SacsAesIvCrypter` string API | Password converted to 256-bit key with PBKDF2-SHA256, a fixed eight-byte salt, and 300 iterations | Random 16-byte IV appended to each value | `;` | Used for BusinessMgr service request and response traffic. Existing clients and servers must continue to understand the legacy form during a coordinated rollout. |
| `SacsEncryptionHelper` | Caller supplies a Base64-encoded 256-bit key | Caller supplies a 16-byte IV; several BusinessMgr flows reuse a fixed IV | Base64 ciphertext only | Used for cookies, emailed links, and session URLs. The IV and key are external to the value, so the ciphertext is not self-describing and cannot support algorithm or key rotation safely. |
Both formats use AES-CBC without an authentication tag. Successful decryption therefore does not prove that the ciphertext and associated context are authentic.
## Proposed Version 2 Envelope
Use AES-GCM through .NET's `AesGcm` API with a fresh random 12-byte nonce and a 16-byte authentication tag for every encryption operation. Never reuse a nonce with the same key.
The serialized value should be URL-safe and self-describing:
```text
SACSENC2......
```
The exact binary/text encoding should be finalised with test vectors before implementation. Required properties are:
- An unambiguous version marker that cannot be confused with either legacy format.
- A non-secret key identifier so decryptors can select the correct key during rotation.
- Explicit KDF identity and parameters for password-derived keys; raw-key callers use a `none` KDF marker.
- A fresh 12-byte nonce and a 16-byte tag stored with every ciphertext.
- Optional authenticated associated data identifying the application and purpose, such as `BusinessMgr:Control:KeepLoggedIn`, to prevent a valid value being replayed in another context.
- Base64url encoding for cookies and query-string values without additional URL-encoding ambiguity.
Password-based encryption must use a new random salt per value and a separately agreed, benchmarked PBKDF2 iteration count. Legacy fixed-salt/300-iteration derivation remains read-only compatibility code and must not be relabelled as version 2.
## API Shape
Keep legacy methods available for compatibility, but prevent new callers from accidentally creating legacy ciphertext. The implementation should introduce purpose-aware versioned operations along these lines:
```csharp
String EncryptVersioned(String plaintext, EncryptionKey key, String purpose);
Boolean TryDecryptVersioned(String value, EncryptionKeyRing keys, String purpose, out String plaintext, out EncryptionFormat format);
```
`EncryptionFormat` would have distinguished authenticated version 2, the former `SacsAesIvCrypter` format, legacy `SacsEncryptionHelper`, and unencrypted input. Password-based and raw-key entry points could have remained separate internally, but both would have emitted the same versioned envelope rules.
Authentication failures must return a distinct failure result, must not return attacker-controlled plaintext, and must be logged without recording keys, ciphertext, cookies, tokens, or decrypted personal data.
## Rollout Sequence
1. Add envelope parsing, AES-GCM encryption/decryption, deterministic test vectors, tamper tests for every envelope field, wrong-key/purpose tests, and legacy-read tests in Common.
2. Introduce configuration-backed key rings with an active write key and retained read keys. Do not commit production keys.
3. Add dual-read behavior while retaining legacy writes; deploy it to every producer and consumer of a shared format.
4. Switch one consumer category at a time to version 2 writes. New writes use AES-GCM; reads dispatch by the envelope marker and continue to accept the applicable legacy format.
5. Retire legacy reads only after the category-specific retention window and rollout constraints in BusinessMgr's inventory have elapsed.
6. Remove embedded keys and fixed IVs after configured key deployment is verified.
## Required Tests
| Area | Required coverage |
|---|---|
| Envelope | Version parsing, Base64url round trip, empty and maximum expected plaintext, Unicode, unknown versions, missing/extra fields, and size limits before allocation. |
| AES-GCM | Fixed test vector, random nonce uniqueness, modified nonce/ciphertext/tag rejection, wrong key, wrong purpose/associated data, and unsupported platform behavior. |
| Password derivation | Fixed version 2 vector, random salt uniqueness, parameter bounds, wrong password, and exact legacy ciphertext compatibility. |
| Key rotation | Active-key writes, retained-key reads, unknown and retired key identifiers, and no fallback from a failed authenticated value to legacy parsing. |
| Consumers | Legacy and version 2 cookies/links/service messages, expiration handling, mixed-version service rollout, and failure behavior that never treats invalid ciphertext as plaintext. |
## Decisions Required Before Implementation
- Where each application obtains its encryption key ring and how keys are deployed and rotated.
- Whether service request and response traffic can move in one coordinated release or needs explicit capability negotiation.
- The maximum lifetime of login cookies and emailed links, which determines how long legacy reads remain.
- The purpose strings and key separation required for Control, Portal, service requests, service responses, password reset, demo registration, and session parameters.
- The benchmarked PBKDF2 iteration count if password-derived version 2 encryption remains necessary; configured random keys are preferable for server applications.