Field encryption
Fields with the encrypted tag are encrypted by the engine before writing
(AES-256-GCM) and transparently decrypted on read — identical on all
backends, the DB only ever sees ciphertext (BYTEA/BLOB).
type ProviderAccount struct { ID orm.ID `orm:"pk"` Name string `orm:"index"` APIKey string `orm:"encrypted,required"`}
db, err := orm.Open(orm.Postgres(dsn), orm.Encryption(orm.StaticKey(keyFromKMS)), // required as soon as a model uses `encrypted`)Rules:
orm.Encryption(provider)is anOpenoption; without itMigratefails for models withencryptedfields.orm.StaticKey([]byte)(32 bytes) is the simplest provider; theorm.KeyProviderinterface (current key + lookup by key ID) is rotation-ready from day one — every ciphertext carries the ID of the key used, rotation happens lazily on the next write.encryptedapplies tostringand[]bytefields (also pointers) and is not combinable withpk/index/unique/json/version/default/enum/ref.- Encrypted fields are not indexable, filterable or sortable — the DB cannot meaningfully compare ciphertext.
- v1 scope:
encryptedworks on CRUD models. On event-sourced models it is currently rejected atMigrateand follows in a later version.
Example: key rotation
Section titled “Example: key rotation”StaticKey is fine to get started, but it’s a single key with no rotation.
For production, implement orm.KeyProvider yourself — for example against a
KMS that tracks multiple key versions:
type kmsKeys struct{ kms *kms.Client }
// CurrentKey returns the key used to write NEW ciphertext.func (k kmsKeys) CurrentKey() (id string, key []byte, err error) { return k.kms.CurrentKeyID(), k.kms.Fetch(k.kms.CurrentKeyID()), nil}
// Key resolves a key ID stored in ciphertext when READING.func (k kmsKeys) Key(id string) ([]byte, error) { return k.kms.Fetch(id), nil}
db, err := orm.Open(orm.Postgres(dsn), orm.Encryption(kmsKeys{kms: kmsClient}),)Rotation needs no migration step: as soon as the KMS returns a new
CurrentKeyID(), every subsequent write encrypts with it. Old rows stay
readable because their ciphertext carries the original key ID and Key(id)
resolves it — old and new keys coexist until a batch migration script (see
Migration) re-encrypts the existing rows whenever
convenient.