Skip to content

Crypto-hash and keys

Algorithm

By default, the cryptoHash method uses keyed HMAC-SHA256. Set parameters.cryptoHashAlgorithm to blake3 to use keyed BLAKE3 instead, which is considerably faster:

fhirPathRules:
  - path: Resource.id
    method: cryptoHash
parameters:
  cryptoHashAlgorithm: blake3

This is opt-in and defaults to hmacSha256 so that upgrading doesn't silently change the hash values of an already-deployed config. BLAKE3's keyed mode requires an exact 32-byte key; since cryptoHashKey can be longer than that, it's first compressed to 32 bytes using BLAKE3's key derivation mode (with a fixed, application-specific context string), so any valid key value works unchanged.

Truncating the hash

When using the cryptoHash method on a value, the result is a hex-encoded string of 64 characters. You can truncate this to a specific maximum length using the truncateToMaxLength setting. For example:

fhirPathRules:
  - path: Resource.id
    method: cryptoHash
    truncateToMaxLength: 32

truncates the usually 64-character-long hash to the following:

{
  "resourceType": "Patient",
  "id": "b43a73c44e6d5b57644b63d89ee90cbf"
}

Hashing references and URIs

For Reference.reference elements and for uri elements whose value looks like a FHIR resource URL (e.g. Patient/123, http://example.com/fhir/Patient/123/_history/2 or Patient?identifier=http://example.com/sid|123), cryptoHash hashes only the id part and keeps the rest of the value:

{ "system": "http://example.com/fhir/NamingSystem/test" }

becomes

{ "system": "http://example.com/fhir/NamingSystem/b43a73c44e6d5b57644b63d89ee90cbf" }

This also happens for values that aren't actually references, like an identifier system whose URL happens to contain a resource name such as NamingSystem. To hash the entire value of such uri elements instead, set transformWholeUri:

fhirPathRules:
  - path: nodesByType('Identifier').system
    method: cryptoHash
    transformWholeUri: true
{ "system": "b43a73c44e6d5b57644b63d89ee90cbfb43a73c44e6d5b57644b63d89ee90cbf" }

transformWholeUri only affects uri elements; Reference.reference elements keep having only their id hashed. It can be combined with truncateToMaxLength. The pseudonymize method supports the same setting: the whole uri value is then sent to the pseudonymization service instead of just its id. The encrypt method always encrypts the entire value and so needs no such setting.

Key derivation

Instead of using cryptoHashKey/encryptKey directly (whether set via the anonymization.yaml parameters section or the Anonymization__CryptoHashKey/Anonymization__EncryptKey environment variables), each can be treated as its own HKDF (RFC 5869) master key, from which the key actually used is derived. This is enabled by setting a key derivation context - either statically via the Anonymization__KeyDerivationContext environment variable, or per config via parameters.keyDerivationContext, which takes precedence over the environment variable when both are set:

fhirVersion: R4
fhirPathRules:
  - path: Resource.id
    method: cryptoHash
parameters:
  # derives the key actually used from the Anonymization__CryptoHashKey master key
  keyDerivationContext: project-a

cryptoHashKey and encryptKey are derived independently of one another. The same (master key, context) pair always derives the same key, while different contexts derived from the same master key are cryptographically independent from one another - so a single master key per algorithm can safely serve many projects/tenants/configs without provisioning and storing a separate key for each.

Generating keys

Warning

cryptoHashKey/Anonymization__CryptoHashKey and encryptKey/Anonymization__EncryptKey should each be a randomly generated 32-byte (256-bit) secret. This matches the key size HMAC-SHA256 and AES-256 are designed for, whether used directly or as the HKDF master key when a key derivation context is set. Generate them with:

openssl rand -hex 32

Note that unlike cryptoHashKey, a statically-set encryptKey must be exactly 32 bytes (AES-256): it's used as the AES key as-is, AES rejects anything longer, and anything shorter is rejected as too short (see below). Generate it with openssl rand -base64 24 instead, which is exactly 32 characters long. A derived encryptKey (via a key derivation context) doesn't have this restriction on the master key, since the derived output is always 32 bytes.

A configured cryptoHashKey or encryptKey - wherever it's set, and whether it's used directly or as a master key - must be at least 32 bytes long (as UTF-8). Otherwise, the service fails to start, or, for a config sent along with a request, rejects that request. Only the length is checked, not how the key was generated, so still generate it as shown above.

If no key is set at all, a random one is generated on every start instead, so hashes won't be the same, and encrypted values can't be decrypted, across restarts or replicas.