Introducing HMACs
Introducing HMACs
Overview
Encrypted fields can't be searched or uniqueness-checked directly: the IV makes ciphertext non-deterministic, so the same plaintext encrypts differently every time. This stage introduces @Hmac/@HmacKeyId, mango4j-crypto's answer, before choosing between the two storage strategies in later stages. Facilitators should pair this with Introducing HMACs, which explains why HMAC (not encryption) is the right primitive for this: it's deterministic and doesn't use an IV. Be explicit that this stage stops at proving determinism - it doesn't yet use the HMAC for search or uniqueness. That's Single HMAC Strategy, next.
This stage comes as two projects:
starter/- what you work in. It compiles and runs, butcardNumberisn't@Hmac'd yet, so both HMAC fields come backnull. Look for the// TODOcomments.complete/- the finished reference, with a working HMAC.
Follow along
cd stages/06-Introducing-HMACs/starter
stages/06-Introducing-HMACs/starter as its own project.
A field can be both confidential and searchable
@Hmac is independent of @Encrypt - a field can carry both. Where @Encrypt says "hide this," @Hmac says "also compute a deterministic fingerprint of this," and mango4j-crypto happily does both in the same encrypt() call.
@Hmac
Your turn: in starter/.../PaymentCardEntity.java, add @Hmac above cardNumber, alongside the existing @Encrypt.
What the Single HMAC Strategy needs on the entity
The class-level @SingleHmacStrategy annotation (already on PaymentCardEntity) picks the simplest of mango4j-crypto's HMAC storage strategies - what it's good for, and where it falls short, is next stage's topic. For now, it requires two things per @Hmac field:
// The Single HMAC Strategy requires this field to exist, named after
// the source field plus "Hmac" (mandatory naming convention). The
// library writes the computed HMAC value here.
private String cardNumberHmac;
@HmacKeyId
private String hmacKeyId;
cardNumberHmac is already there - a plain persisted field, named after the source field plus Hmac, where the library writes the computed value. hmacKeyId needs its @HmacKeyId annotation, playing the same role for HMACs that @EncryptionKeyId-style tracking plays for encryption: recording which key produced the value.
Your turn: add @HmacKeyId above hmacKeyId.
A key that's just for HMACs
@Override
public List<CryptoKey> getCurrentHmacKeys() {
// The previous stages' answer: no HMAC keys, because nothing needed one.
List<CryptoKey> currentHmacKeys = List.of();
currentHmacKeys = List.of(getById(CURRENT_HMAC_KEY_ID));
return currentHmacKeys;
}
getCurrentHmacKeys() is resolved the exact same way getCurrentEncryptionKey() was in the previous stage: by id, through getById(). It returns a List - not because this stage uses more than one, but because List HMAC Strategy will. The key itself (workshop-hmac-key) is entirely separate from workshop-encryption-key: encryption and HMAC keys are never the same key.
Your turn: in starter/.../InMemoryCryptoKeyProvider.java, return List.of(getById(CURRENT_HMAC_KEY_ID)) instead of the empty list.
Running it
starter/ before your changes compiles and runs, but with cardNumber not yet @Hmac'd, PaymentCardEntity has no HMAC field at all as far as the library can see - both cardNumberHmac and hmacKeyId stay null:
first encryptedData: {"cryptoKeyId":"workshop-encryption-key","data":{...,"iv":"tvTNkl2Uv1ZVY9VPMuWtRg==",...}}
second encryptedData: {"cryptoKeyId":"workshop-encryption-key","data":{...,"iv":"sNhAfqK95k1b4DzZWj+tPQ==",...}}
same ciphertext? false
first cardNumberHmac: null
second cardNumberHmac: null
same HMAC? true
hmacKeyId: null
After all three changes:
first encryptedData: {"cryptoKeyId":"workshop-encryption-key","data":{...,"iv":"lk6cOpz/b67KmKY9yxG3ng==",...}}
second encryptedData: {"cryptoKeyId":"workshop-encryption-key","data":{...,"iv":"...",...}}
same ciphertext? false
first cardNumberHmac: vqGr/6T0RMdQlvu2bP9FTH9nx2vqNW6Nh2NaKHLnbmQ=
second cardNumberHmac: vqGr/6T0RMdQlvu2bP9FTH9nx2vqNW6Nh2NaKHLnbmQ=
same HMAC? true
hmacKeyId: workshop-hmac-key
The interesting comparison is between the two blocks above: two entities built from the identical card number get two different ciphertexts (fresh IV each time, so same ciphertext? is false) but the exact same HMAC, every run (same HMAC? is true in both runs - only the value becomes non-null once @Hmac is wired up). That determinism is the whole reason HMAC - not encryption - is what search and uniqueness end up built on.