What this is about
Some values should not be readable in the database, such as an IBAN or a tax number. Whoever gets hold of the database or a backup should not be able to use them.
For that you set the rule encrypted (@encrypted) on the text field in the hub. CDMS then encrypts the value with AES-256-GCM before storing it and decrypts it when reading. The client notices nothing: it sends plain text and gets plain text back.
The path of a value
sequenceDiagram
participant C as Client
participant D as CDMS
participant DB as Database
C->>D: POST iban = "DE44 5001 …"
D->>D: check the rules (on the plain text)
D->>D: encrypt, new random IV
D->>DB: stores "enc:v1:fdd5117f:InOkKngP…"
D->>DB: reads the object for the response
D->>D: decrypt
D-->>C: 200, iban = "DE44 5001 …"
The stored form always starts with enc:v1:, followed by the id of the key, then the ciphertext. Every encryption takes a new random value (IV). Two equal IBANs are therefore stored differently.
The encryption is done by a field hook that ships with CodamAI: EncryptedFieldHook from the module cdms-field-encryption. You write no code for it. The generator attaches the hook to every field with the rule, and the project frame adds the module to the pom.xml. If the field has field hooks of its own, they see the plain text: encryption is the last step before the database.
Setting the key
The application needs a key of 32 random bytes, written in Base64:
openssl rand -base64 32
| Environment variable | Property | Meaning |
|---|---|---|
CODAMAI_CDMS_ENCRYPTION_KEY | codamai.cdms.encryption.key | the active key every value is written with |
CODAMAI_CDMS_ENCRYPTION_PREVIOUS_KEYS | codamai.cdms.encryption.previous-keys | earlier keys, comma separated, only read |
The key belongs in a secret of your environment, never in the model and never in the repository. Only an application in which a field is encrypted needs it. There it does not start without it:
field-encryption-key-missing|codamai.cdms.encryption.key: a model encrypts a field (@encrypted), …
A key that is not 32 bytes long stops the start as well (field-encryption-key-invalid).
Changing the key
-
1Operationscreates a new key
-
2Operationssets it as
CODAMAI_CDMS_ENCRYPTION_KEYand moves the old one toCODAMAI_CDMS_ENCRYPTION_PREVIOUS_KEYS -
3CDMSreads old values with the old key, recognised by its id in the stored value
-
4CDMSencrypts every value that is written again with the new key
Remove an old key only once no value is stored with it any more. If the key of a stored value is missing, CDMS does not answer it but responds with 500 field-encryption-key-unknown|<id>.
Values still stored as plain text
If you make a field encrypted later, its existing values are still plain text in the database. CDMS recognises them by the missing enc:v1: and answers them unchanged. The next time the field is written, the value is encrypted. A write that does not send the field, such as a PATCH of another field, leaves it as it is.
What an encrypted field cannot do
| Attempt | Answer |
|---|---|
| read the field, also in a list or in the history | yes, decrypted |
| write the field (POST, PUT, PATCH) | yes, CDMS encrypts |
| filter by the field | 400 field-not-searchable|<field> |
| sort by the field | 400 field-not-searchable|<field> |
| also make the field "unique" | not possible: the hub refuses it, the generator stops |
| encrypt a number or date field | not possible: the rule exists on text fields only |
Filters and sorting work in the database, and therefore on the stored form. That form looks different for every value. A filter would never find anything, and a sort order would be random. Instead of answering wrong, CDMS refuses both. For the same reason the database cannot check uniqueness.
Pitfalls
Where to go next
- How a field hook works in general: Field hooks
- Hiding values from particular people: Protected values
- Which rules the hub offers: Modelling in the hub