Easily encrypt data at rest in relational databases using drop-in Gorm Serializers.
Ok, but how?
This library draws inspiration from how SOPS handles encrypting fields in a simple configuration file as well as how
BadgerDB implements its encryption key management behind the scenes. When a []byte field is encrypted using this
library, it's formatted as follows: ENC:algorithm,fingerprint,ciphertext. The algorithm is a single byte
representing aes (1) or aes-gcm (2). This indicates which serializer was used when storing the fields in the
database. fingerprint identifies which encryption key was used to encrypt the field. This can allow multiple to be
chained together in order to handle multiple keys or even migrations (TBD). Finally, the ciphertext block is the
encrypted value.
The aes serializer uses direct key encryption. It is recommended that the aes serializer not be used to encrypt
values that may be repeated. This is because the aes serializer does not factor a unique seed in with each entry.
Instead, it's expected that the values being encrypted are unique (for example, other encryption keys).
The aes-gcm serializer uses intermediary keys that are stored in the encryption_keys table to encrypt data across
the entire database. Unlike the aes serializer, the aes-gcm serializers is safe to use on values that may be
repeated. The key itself is encrypted using the aes serializer, making it easy to rotate the root key without needing
to read, decrypt, and re-encrypt every field in the database. This makes rotations quick and strongly protects the core
encryption keys from attackers.
| Driver | Supported | Notes |
|---|---|---|
| github.com/glebarez/sqlite | ✅ | Support since day one. |
| gorm.io/driver/postgres | ✅ | - |
| gorm.io/driver/mysql | ❌ | #3 |
| gorm.io/driver/sqlserver | ✅ | - |
go get go.pitz.tech/gorm/encryptionGorm uses tags to communicate which serializer should be used when reading and writing the field to the database. This
library only supports using the aes and aes-gcm serializers on []byte fields.
package main
type Model struct {
UniqueValue []byte `gorm:"...;serializer:aes"`
NonUniqueValue []byte `gorm:"...;serializer:aes-gcm"`
}A few notes...
First, when using fixed size []byte fields, you'll need to consider the length added by the additional metadata of the
encrypted fields. The equations below roughly communicate how much additional length will be needed.
aes = data length + 50aes-gcm = data length + 62
The various lengths for the additional metadata are as follows:
- prefix = 4 bytes
- algorithm = 1 byte
- separator = 1 byte
- fingerprint = 43 bytes
- separator = 1 byte
- data =
xbytes (aes) |x+ 12 bytes (aes-gcm)
Second, you need to be mindful of how indexes are used in conjunction with encrypted fields. For example, if you're
encrypting an email_address using aes-gcm, then you can't use a unique index on that field. You can however use a
unique index on a semi-representative field such as an email_hash. Which can be in plaintext in the database. While
you could encrypt your email_address using the aes serializer, the field would require explicit rotation.
package main
import (
"go.pitz.tech/gorm/encryption"
"gorm.io/gorm"
)
func migrate(encryptionKey []byte) error {
var dialector gorm.Dialector
db, err := gorm.Open(dialector, nil)
if err != nil {
return err
}
err = encryption.Register(db, encryption.WithKey(encryptionKey), encryption.WithMigration())
if err != nil {
return err
}
return db.AutoMigrate(
// your application models...
)
}package main
import (
"go.pitz.tech/gorm/encryption"
"gorm.io/gorm"
)
func run(encryptionKey []byte) error {
var dialector gorm.Dialector
db, err := gorm.Open(dialector, nil)
if err != nil {
return err
}
err = encryption.Register(db, encryption.WithKey(encryptionKey))
if err != nil {
return err
}
// your business logic
return nil
}package main
import (
"go.pitz.tech/gorm/encryption"
"go.pitz.tech/gorm/encryption/aes"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
func run(customKey []byte) error {
schema.RegisterSerializer("custom-aes", aes.New(customKey))
// now you have `serializer:custom-aes`
var dialector gorm.Dialector
db, err := gorm.Open(dialector, nil)
if err != nil {
return err
}
// your business logic
return nil
}The code below hasn't been tested, but conveys the basic idea on how to rotate the primary encryption key.
package main
import (
"database/sql"
"go.pitz.tech/gorm/encryption"
"go.pitz.tech/gorm/encryption/database"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
func rotate(oldKey, newKey []byte) error {
var dialector gorm.Dialector
db, err := gorm.Open(dialector, nil)
if err != nil {
return err
}
// to do this in batches, you simply need to paginate the following block until you iterate through the entire table
err = encryption.Register(db, encryption.WithKey(oldKey))
if err != nil {
return err
}
allKeys := make([]database.Key, 0)
err = db.Find(&allKeys).Error
if err != nil {
return err
}
err = encryption.Register(db, encryption.WithKey(newKey))
if err != nil {
return err
}
return db.Transaction(func(txn *gorm.DB) error {
for _, key := range allKeys {
err := txn.Save(key).Error
if err != nil {
return err
}
}
return nil
})
}MIT. See LICENSE for more details.