How the encrypted copy is stored
A recoverable key is still verified by its hash. Beside the hash, Unkey stores an AES-256-GCM ciphertext of the key. The data key is 32 bytes, the nonce is 12 bytes, and the keyring is the workspace, so another workspace can’t decrypt it.keys.getKey and apis.listKeys decrypt it only when you pass decrypt: true, and only with a root key that has decrypt_key.
A key is only recoverable if its keyspace has Store encrypted keys turned on (ask support@unkey.com) and the key was created with recoverable: true after that. Older keys can’t be recovered.
Two separate permissions
Creating a recoverable key needsapi.*.encrypt_key or api.<api_id>.encrypt_key on top of create_key. Reading a key back needs api.*.decrypt_key or api.<api_id>.decrypt_key on top of read_key. Without decrypt_key, a root key can create and list keys but never read them back. Give decrypt_key only to the one service that needs to show keys, for the one keyspace that needs it.
Limiting the exposure
With recovery on, a copy of our database is no longer useless on its own. Someone who also got your workspace’s encryption keys, or a leaked root key withdecrypt_key, could read your keys. To limit that:
- Only turn on recovery for keyspaces that need it.
- Never pass
decrypt: truein code that serves end users. - Use the developer portal, where users roll their own keys instead of reading them back.
- Watch the audit log for unexpected
key.createentries in keyspaces with encryption on.