Field security as a contract.
Mark a field encrypted, hashed, or blind-indexed. Vadyl applies the right cryptography after validation, before plan building. Reads decrypt only after access evaluation — masked fields never trigger decryption. Native acceleration kicks in where supported. AEAD AAD binds every ciphertext to its tenant, entity, field, and key version. Cross-context paste yields authentication failure — by design.
Composable, not a flat enum.
FieldSecurityDescriptor declares six independent axes. Encrypted, Hashed, BlindIndexed are specific compositions. The flat enum trap — a single security mode that can't combine — is forbidden by construction.
StorageProtectionKind
None / Aead / Hash / Hmac. The actual protection on the column. Aead = randomized envelope. Hmac = the field IS the blind index.
SearchabilityKind
Full / EqualityViaBlindIndex / None. Determines what filters the read planner allows. None blocks every comparison; EqualityViaBlindIndex permits =, !=, IN only.
VerificationKind
None / CandidateVerify. For password / token fields. Timing-safe verification through IFieldVerificationService — never bare comparison.
HashAlgorithmKind
Pbkdf2Sha256 / Bcrypt / Argon2id. For verifiable hashes. Self-describing format — the algorithm + parameters travel with the hash.
NativeAccelerationKind
None / AlwaysEncrypted / PgCrypto / MySqlAes. Native column-level encryption when the provider declares the capability and the operator opts in. Defense in depth, not the correctness layer.
KeyRingId
Which IKeyRing key set protects this field. Multiple rings let you rotate independently — customer-PII on a different schedule than internal-tokens.
Versioned wire format. AAD-bound to context.
Every AEAD-encrypted field rides in a versioned envelope. The AAD binds the ciphertext to its tenant, entity, field name, and key version. Cross-context copy-paste cannot decrypt — GCM authentication fails by construction.
Wire format
[version:1][keyVer:4][algo:1][flags:1][nonce:12][tag:16][ciphertext:N]
Versioned. Forward-compatible. The reader knows which key to fetch from the ring.
AAD binding
tenantId ‖ 0x00 ‖ entityName ‖ 0x00 ‖ fieldName ‖ 0x00 ‖ keyVersion
A ciphertext from tenant A in entity Customer can never be decrypted as if it lives in tenant B's Order — same key, different AAD, GCM fails.
Encryption after validation. Decryption after access.
Write path
Validate → IFieldSecurityInterceptor.protectFieldsAsync (encrypt / hash / hmac) → MutationPlan → provider. Field-level write permissions evaluate first; protected fields without CanWrite never reach the interceptor.
Read path
Provider returns rows → AccessEngine evaluates field masks → FieldProjectionBuilder applies Omit / Null / Redacted → IFieldSecurityInterceptor.unprotectFieldsAsync runs only on fields that survived masking. Masked fields never trigger decryption.
Re-key path
DataProtectionBackfill runs as an online checkpoint-resumable backfill — the same plumbing that handles schema transitions. Old key marked DecryptOnly. Reads work. Writes use the new key. Old key retires once the backfill drains.
Composable, not flat
Native crypto = defense in depth
Tenant, entity, field, key version
Resumable, no downtime
Encryption that composes with the rest of your model.
Mark a field encrypted. Vadyl handles the keys, the AAD, the masking, the searchability rules, the re-key path. No shadow encryption layer. No double bookkeeping.