A restored backup contains an encrypted JSON record for user-1842, yet the application accepts it after an attacker changes stored bytes and the recovered role becomes admin. The attacker did not need the encryption key or access to the API. They only needed write access to the database backup or its metadata. Our running artifact is one Python module that protects this record, containing id, email, role, and created_at, while it moves between a client, an API, a database, and a backup. Symmetric encryption uses the same secret key to turn plaintext into ciphertext and to recover plaintext later. It hides the record, but hiding alone does not make altered bytes trustworthy.
The defender’s question is therefore narrower than which cipher has the strongest name. When role=admin returns from storage, how can the API prove that the bytes and their record context are exactly those originally protected? The boundaries for user-1842 show where plaintext must stop, where the key must remain separate, and where an attacker can alter ciphertext without learning its contents. Let’s trace the record across those boundaries:
This diagram shows that the database and backup need ciphertext, a nonce, and selected metadata, but never the encryption key. A nonce is a value used once with a given key to ensure repeated protection operations do not reuse the same cryptographic inputs. The nonce may be stored beside ciphertext; secrecy is not its job. The key must remain secret and must arrive from protected application configuration rather than from the record or backup.
Because the attacker can alter stored bytes, the record contract requires confidentiality and integrity together. Authenticated encryption encrypts the JSON bytes and verifies that neither those bytes nor selected context changed before returning plaintext. For user-1842, the API will authenticate user_id=user-1842 and schema_version=1 as associated data, meaning metadata verified by the cryptographic operation but left unencrypted. A copied ciphertext cannot then be silently relabeled as another user’s record, and a schema value cannot be changed without causing verification to fail.
Plaintext exposes the email and role directly. AES-CTR and AES-CBC can conceal those fields, but neither independently rejects modification. AES-GCM provides the needed authenticated-encryption contract when used with a fresh nonce for every encryption under one key. Can an encryption-only mode detect that a backup byte changed before the API parses role? The following comparison weighs the storage choices against that requirement:
This comparison makes the design decision concrete. AES-GCM rejects a modified ciphertext, nonce, key, or associated data with an authentication failure, while confidentiality-only choices leave the application without that proof. AES-GCM does not repair nonce reuse. Reusing one nonce with the same key compromises security, so nonce generation belongs inside the protection path rather than being caller-supplied metadata.
Record rule Store the nonce, ciphertext, and associated data with the record. Store the AES key separately and keep every nonce unique per key.
With the contract fixed, the module can protect user-1842 without exposing a partially verified record to its caller. The JSON serializer produces bytes because AESGCM.encrypt operates on bytes. The module generates a 256-bit AES key for the example, creates a fresh 12-byte nonce, and stores a package shaped like {nonce, ciphertext, ad}. In deployment, key generation and custody move to the application’s approved key-management boundary, while the record package remains safe to store in the database and backup.
Here is the smallest complete protection and recovery path: