2
$\begingroup$

I have an existing system which takes a cryptographically secure BLOB from user and pass it to lower API which calls HKDF-Expand to get 2 keys: one is effectively discarded in this scenario and second is actually used.

Previously this second key was used to authenticate-encrypt using AES256-GCM data from user (again get from user and passed to lower API). However this turned out to be problematic and some metadata needs to be added to the format in some cases. However it is hard to do while maintaining backward compatibility.

I'm consider 'versioning' the API by generating the key twice - once with key from user and once with key from user XORed with 1 - as it will allow to avoiding changing of API or similar tricks. My intuition is that I should get a two unrelated keys due to avalanche properties.

However I'm not a cryptographer so I'm not sure if there isn't something fishy about such schemes.

// info is publicly known; operation discards first 256-bits for key that
// is not used
(_, key1) <- HKDF-Expand(key, info)
(_, key2) <- HKDF-Expand(key ⊕ 1, info)

def decrypt(data) {
    decryptedData <- AES256-GCM-Decrypt(key2, data)
    if decryptedData is not null {
        return parseFormatWithMetadata(decryptedData)
    }
    # Try to use old format
    decryptedData <- AES256-GCM-Decrypt(key1, data)
    if decryptedData is not null {
        return (decryptedData, default metadata for v1)
    }
    return null;
}

def encrypt(data, metadata) {
    if metadata != default metadata for v1 {
        return AES256-GCM-Encrypt(key2, metadata || data)
    } else {
        return AES256-GCM-Encrypt(key1, data)
    }
}
$\endgroup$
  • 1
    $\begingroup$ Yes, HKDF should destroy any potential relation between key1 and key2. However, why don't you "simply" amend the info field with the version? $\endgroup$ – SEJPM Sep 6 '18 at 13:40
  • $\begingroup$ @SEJPM Because it is set by underlying library. $\endgroup$ – Maciej Piechotka Sep 6 '18 at 14:50
1
$\begingroup$

By itself, this is fine. Running a KDF with two different inputs generates uncorrelated sequences: knowing the output of HKDF(K1, L) doesn't give any information about the output of HKDF(K2, L) unless K1 = K2.

However, as a system design, this is fragile. What if one day someone decides that they need a new version of this format, and they decide to flip the last bit of key?

Given the constraint that you can only change the key input to HKDF, if key has a fixed size, I would append a suffix to key to indicate the format version. Version 0 uses an empty suffix, version 1 will use the 4-byte suffix {0x00, 0x00, 0x00, 0x01}, etc.

If there is an additional constraint that key has to remain the same size, I would run it through a KDF first. To avoid running into potential trouble with related keys, key should still only be used for a single purpose, which is as the key input to HKDF. Use a label that cannot possibly be equal to any valid value for info.

key' <- HKDF-Expand(key, "not a possible prefix of info; version 1")
(_, key1) <- HKDF-Expand(key, info)
(_, key2) <- HKDF-Expand(key', info)
$\endgroup$

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.