libyal / libbde

Library and tools to access the BitLocker Drive Encryption (BDE) encrypted volumes
GNU Lesser General Public License v3.0
216 stars 53 forks source link

CCM key unwrapping does not validate tag and returns 16 bytes of garbage header #36

Open marcan opened 5 years ago

marcan commented 5 years ago

The entire world apparently calls what BitLocker uses to wrap keys "AES-CCM". Unfortunately, that's not what is actually implemented. The algorithm implemented by libcaes_crypt_ccm isn't CCM: it doesn't have a MAC, it doesn't have associated data, and it doesn't use the first keystream block for that. That makes it just AES-CTR.

The only thing "CCM" about it is that the nonce is prepended with a byte of value 15 - (uint8_t) nonce_size - 1 (which CCM does, and is not inherent to CTR mode). There's none of the other bits that would make it compliant with the CCM specification (RFC3610). Crucially, BitLocker key unwrapping can be implemented using a standard AES-CTR implementation (just prepend 0x02 to the nonce), but cannot be implemented using a standard AES-CCM implementation (because there is no way to disable the whole MAC machinery).

I would recommend renaming all mentions of CCM to CTR, to avoid confusion. I just spent a few hours wondering why using PyCryptodome yielded incorrect decryptions of BitLocker wrapped keys. Instead, CTR mode is what you want. In PyCryptodome:

~~python def decrypt(p, k): nonce = p[:12] nonce = bytes([15 - len(nonce) - 1]) + nonce aes = AES.new(k, AES.MODE_CTR, nonce=nonce) a = aes.decrypt(p[12:]) return a~~ ~~ is the code you'd want to unwrap a BitLocker wrapped key, with MODE_CTR, not MODE_CCM.

marcan commented 5 years ago

I take that back; it is CCM, it's just that the tag is prepended. I was really confused because there was no notion of the two chunks in the libbde codebase.

So the problem is that libcaes_crypt_ccm doesn't actually implement CCM. It implements the CTR part of CCM, and then returns 16 bytes of garbage in front of the result (what would be the tag section).

This is a correct implementation of BitLocker wrapped key decryption:

def decrypt(p, k):
    nonce = p[:12]
    tag = p[12:28]
    aes = AES.new(k, AES.MODE_CCM, nonce=nonce)
    return aes.decrypt_and_verify(p[28:], received_mac_tag=tag)

So the format of AES-CCM encrypted keys is:

And a correct implementation of AES-CCM would validate the tag correctly according to RFC3610, and return an error if it mismatches (this also lets you recognize if the wrong key was supplied).

joachimmetz commented 5 years ago

@marcan interesting, thx for the update.

This seems to be related to https://github.com/libyal/libcaes/issues/2. When time permits I'll have a look to likely move this function into libbde and remove it from libcaes.

marcan commented 5 years ago

I think it should be fine in libcaes as it is a standard mode, if the interface is changed to be standard-compliant (and then libbde is changed to use it in a standard way).