Data-at-Rest Encryption

LockMyCal implements a robust security layer for protecting sensitive user and client data. All OAuth tokens, access passwords for CalDAV accounts and other sensitive data are stored in the PostgreSQL database in encrypted form, using a custom implementation of the AES-256-GCM algorithm.

Note

Even if the whole SQL dump of the database were leaked without authorization, an attacker without the main cryptographic key would not gain access to the tokens of connected calendars or to other stored credentials.


How the encryption works

graph LR
    A["OAuth Tokens / CalDAV Passwords"] --> B["LockMyCal Cryptographic Layer (AES-256-GCM)"]
    K["DATA_ENCRYPTION_KEY"] --> B
    B --> C["Encrypted text (Ciphertext + IV + Tag)"]
    C --> D["PostgreSQL Database"]

1. Generating the DATA_ENCRYPTION_KEY encryption key

You generate it with any of the following commands:

Using OpenSSL:

openssl rand -base64 48

Using Elixir in Docker:

docker run --rm elixir:alpine elixir -e ":crypto.strong_rand_bytes(48) |> Base.encode64() |> IO.puts()"

2. Setting the environment variable

Put the generated string into your .env file:

DATA_ENCRYPTION_KEY=YourGeneratedBase64PasswordOfLength48Bytes==

If you don't set DATA_ENCRYPTION_KEY in .env, the app derives the encryption key from SECRET_KEY_BASE — it works, but it ties the protection of data at rest to the key for signing sessions. We recommend setting a separate DATA_ENCRYPTION_KEY, so that SECRET_KEY_BASE can be rotated if necessary without any impact on the stored data.

Caution

Don't change DATA_ENCRYPTION_KEY after setting it. Data encrypted with the original key could then not be decrypted, and the connected calendars and video services would have to be connected again. Back the key up securely together with .env.

3. Moving from a key derived from SECRET_KEY_BASE

If you have run the instance so far without DATA_ENCRYPTION_KEY, the stored data is encrypted with a key derived from SECRET_KEY_BASE. After setting DATA_ENCRYPTION_KEY, re-encrypt it once:

docker compose exec app bin/tymeslot eval 'Ecto.Migrator.with_repo(Tymeslot.Repo, fn _ -> IO.inspect(Tymeslot.Security.CredentialReencryption.run(), label: "reencryption") end)'

The command is safe to run repeatedly – repeat it until it reports migrated_values: 0. After that, the two keys are independent and SECRET_KEY_BASE can be rotated without any impact on the stored data.