Encryption
HatTrick.Configuration encrypts with AES-256-GCM, an authenticated cipher: decryption verifies integrity as part of decrypting, so tampered ciphertext is rejected rather than silently producing garbage plaintext. The passphrase supplied to ApplyEncryptionKey is never used directly as the AES key; it is stretched via PBKDF2-HMAC-SHA256 against a fresh random salt generated on every encrypt call:
| Parameter | Value |
|---|---|
| Cipher | AES-256-GCM |
| Key size | 256 bits |
| Nonce size | 96 bits (NIST SP 800-38D recommended size for GCM) |
| Authentication tag size | 128 bits |
| Salt size | 128 bits, fresh per encrypt call |
| Key derivation | PBKDF2-HMAC-SHA256, 600,000 iterations (OWASP 2023 minimum recommendation) |
| Minimum passphrase length | 12 chars (JsonConfigManager.MinimumEncrptionKeyLength) |
Because the salt and nonce are regenerated on every call, encrypting identical plaintext under the same key twice produces two different ciphertexts.
Configuring a key
Encryption is configured per instance, then opted into per registration. JsonConfigManager and EnvVarConfigManager both work this way:
var configMgr = JsonConfigManager.BuildInstance("default")
.ApplyEncryptionKey(() => "at-least-twelve-characters")
.Register<AppSettings>("app-settings.json") // plaintext
.Register<Secrets>("secrets.aes", encrypted: true) // encrypted
.Build();ApplyEncryptionKey takes a Func<string> rather than a raw string, so the key can be sourced lazily (an environment variable, a secrets manager, a masked console prompt) instead of being materialized as a literal first. The minimum-length floor is enforced independently by the key-derivation type itself, so it applies even to callers that bypass both managers via ConfigEncryptor directly.
Build() throws InvalidOperationException if any registration was marked encrypted: true but ApplyEncryptionKey was never called on that builder. The fluent order of Register and ApplyEncryptionKey doesn’t matter; only whether a key was supplied by the time Build() runs. Reading or writing an encrypted registration on an instance with no key configured also throws InvalidOperationException rather than treating ciphertext as plaintext.Encrypting values outside a manager
ConfigEncryptor is a public static class shipped with the library; its Encrypt/Decrypt methods are the format-agnostic way to author the ciphertext a manager expects, callable directly without a registered type or a live instance:
string cipherText = ConfigEncryptor.Encrypt(File.ReadAllText("secrets.json"), key);
File.WriteAllText("secrets.aes", cipherText);
string plainText = ConfigEncryptor.Decrypt(cipherText, key);This is what lets you produce an initial encrypted config file (or an encrypted environment variable’s value) before ever registering it. HatTrick.Configuration.Tool wraps this same call in a command-line interface, for encrypting and decrypting files and values without writing code. See CLI Tool for installation, commands, and usage.
Tampering and wrong keys
A tampered ciphertext and a wrong decryption key are indistinguishable failure modes by design: GCM’s authentication tag check throws the same CryptographicException either way.
try
{
ConfigEncryptor.Decrypt(cipherText, wrongKey);
}
catch (CryptographicException)
{
// could be a wrong key, could be corrupted/tampered ciphertext; GCM doesn't tell you which
}On the environment-variable side specifically, a wrong key surfaces as this same unaggregated CryptographicException straight out of Build(). It is not caught and folded into a ConfigValidationException alongside other validator failures. See Validation for how those aggregate instead.
Key rotation
Rotating a key means decrypting with the old key and re-encrypting with the new one. Doing this by hand, via ConfigEncryptor.Decrypt/.Encrypt or the decrypt/encrypt commands, exposes the intermediate plaintext. HatTrick.Configuration.Tool also provides dedicated rekey/rekey-value commands that do both steps internally without ever writing or printing that intermediate plaintext; see CLI Tool for usage.
Format stability
The on-disk/in-variable layout is salt (16 bytes) + nonce (12 bytes) + ciphertext (plaintext length) + tag (16 bytes), hex-encoded: 88 bytes of fixed overhead per encrypted payload before hex encoding doubles the byte count to characters. This layout, the iteration count, and the derivation algorithm are all part of the format: anything already encrypted under the current parameters needs to be re-encrypted if any of them ever change in a future library version. Rotating the key alone doesn’t address this: a parameter change needs the same decrypt-and-re-encrypt treatment as a key change, just triggered by a version upgrade instead.
Next steps
- CLI Tool: encrypt/decrypt files and values without writing code
- JSON Configuration: registering an encrypted file
- Environment Variables: registering an encrypted variable