G'day. LUKS stores multiple copies of the master key in what it terms key slots. Each key-slot contains an encrypted copy of the master key, and it can vary in its password-based KDF parameters (and even algorithms). The parameters and algorithms can sometimes be wildly different, giving rise to the circumstance where forcing one key-slot may be infeasible, while another is fundamentally weak. (Both will yield the master key.)
For LUKS1, the best option for the password-based KDF is PBKDF2. For LUKS2, an additional option exists to use Argon2, which gives you the potential to make it even harder to force a corresponding key-slot... but in all cases this will depend on the hardness parameters chosen, as well as the relative strength of the password. (In LUKS2 both options can exist in parallel.)
First, create a file to play with: fallocate -l 32M testing.encrypted.iso
and xxd -c32 -l256 testing.encrypted.iso
Now turn it into a LUKS container, noting our two key-slots, both wildly different in terms of relative strength of the password-based KDF:
cryptsetup --type=luks2 --pbkdf=pbkdf2 --pbkdf-force-iterations=1000 --hash=sha384 --luks2-metadata-size=16k --luks2-keyslots-size=$((512*1024)) luksFormat testing.encrypted.iso
cryptsetup --type=luks2 --pbkdf=argon2id --pbkdf-memory=1048576 --pbkdf-parallel=4 --pbkdf-force-iterations=8 --key-slot=5 luksAddKey testing.encrypted.iso
(In fact, our Argon2 key-slot might be derived from pwd awesomedud
, while our PBKDF2 one might be 'rp6eq25dgt2orn/mnf. d4r6tj'n2uhpsqsbv.b
!)
xxd -c32 -l256 testing.encrypted.iso
cryptsetup luksDump testing.encrypted.iso
cryptsetup luksDump --key-slot=5 --key-file=- --dump-master-key testing.encrypted.iso
Now clean up: shred -z testing.encrypted.iso
and rm testing.encrypted.iso
.
MASTER KEY RECOVERY
Once the slot key is derived from the password, the alogorithm must then retrieve the encrypted master key from the key-slot area in the header. From https://gitlab.com/cryptsetup/cryptsetup/-/wikis/LUKS-standard/on-disk-format.pdf, figure 5, p11, Fruhwirth '18:
(Apologies for the screenshot, feel free to transcribe this text and replace...)
ANTI-FORENSIC SPLITTING
You may have noticed a function in the previous pseudo-code, AFmerge
? My crude understanding is as follows: in the LUKS header, the individual bytes that make up each key-slot are interspersed (intermingled, interleaved) throughout the key-slot area of the header at a particular interval. Cumulatively, the key-slot area will contain n key slots, which are all interleaved. To recover the master key from any given key-slot, you need access to all of the blocks that contribute, otherwise the key won't be recovered.
The reason for this is to reduce the likelihood that any part of the key will be recovered after the user chooses to destroy the header. It is also the reason why you should consider more than one key-slot (ie. a high-entropy recovery key), but regardless, it's critical to keep a reliable backup(s) of the header (edit if you value the data within the container):
cryptsetup luksHeaderBackup --header-backup-file=testing.enciso.header testing.encrypted.iso
More detail on this here - https://clemens.endorphin.org/nmihde/nmihde-A4-ds.pdf, s5.2 p75+, Fruhwirth '05 - but fundamentally:
If the probability to destroy a certain block of data is 0 ≤ p ≤ 1, then the probability that the block survives is 1−p. Given a set of data consisting of n data items, the probability to erase the whole data set becomes worse since p^n becomes smaller as n becomes larger.
... the remaining task is to construct a data vector from an original data item and to make the destruction of a single vector part crucial for the survival of the whole information contained in the data vector.