How can I programmatically determine the exact start byte and exact end byte of a LUKS header on a block storage device?
I use Linux Unified Key Setup (LUKS) for Full Disk Encryption (FDE), so all of the data on my drive is encrypted using a strong master key that’s not derived from my passpharse, and I’m working on a script that will securely wipe the drive in an panic/emergency shutdown situation (ie: someone is physically stealing your laptop).
Let’s say I have a 1000T drive and time is too short (~30 seconds) in my emergency scenario to actually fill the drive with random bytes. Instead, I’d just like to overwrite the header because all of the data on the drive is worthless if the header (containing the keyslots with the master key and salts) is lost–even if the passphrase were recovered via rubber-hose cryptanalysis.
How can I programmatically safely determine the start byte and end byte of the LUKS header so I know what to overwrite?
Note: The solution provided must be valid for both LUKS1 (released in 2014) and LUKS2 (released in 2018).
In LUKS1, I’ve found that the start is 0 and the end is determined by multiplying the payload-offset
field from the container’s binary header by 512. For example
root@disp4117:~# hexdump -Cs 6 -n 2 luksVol1 00000006 00 01 |..| 00000008 root@disp4117:~# hexdump -Cs 104 -n 4 luksVol1 00000068 00 00 10 00 |....| 0000006c root@disp4117:~#
In this case, the LUKS1 header ends at byte 4096 * 512
= 2097152
.
In LUKS2, it’s much trickier as it requires parsing the JSON metadata object in the LUKS header.
Advertisement
Answer
Here’s a quick python script that will output the start and end bytes for a given LUKS container:
#!/usr/bin/python ################################################################################ # File: luksHeaderBounds.py # Purpose: Determine the start and end bytes of a LUKS header # Authors: Michael Altfield <michael@michaelaltfield.net> # Created: 2020-03-18 # Updated: 2020-03-18 # Version: 0.1 ################################################################################ # DEPENDS import sys, struct, json; # MAIN BODY if len(sys.argv) != 2: print "Usage: luksHeaderBounds.py <device>" exit(1) device = sys.argv[1] # first we get the LUKS version with open(device, "rb") as f: # the LUKS version field is a big endian unsigned short (uint16_t = '>H') # * https://docs.python.org/2/library/struct.html # the LUKS version field starts at offset 6 bytes and is 2 bytes long # so we get the first 8 bytes and the last 2 bytes of that # * https://gitlab.com/cryptsetup/LUKS2-docs/blob/master/luks2_doc_wip.pdf luksVersion = struct.unpack( '>H', f.read(8)[-2:] )[0]; if luksVersion == 1: # LUKS1 - https://gitlab.com/cryptsetup/cryptsetup/-/wikis/LUKS-standard/on-disk-format.pdf with open(device, "rb") as f: # the payload-offset field is a big endian unsigned long (uint32_t = '>L') # * https://docs.python.org/2/library/struct.html # in LUKS1, the payload-offset field starts at offset 104 bytes and is 4 bytes long # so we get the first 108 bytes and the last 4 bytes of that # * https://gitlab.com/cryptsetup/cryptsetup/-/wikis/LUKS-standard/on-disk-format.pdf payloadOffset = struct.unpack( '>L', f.read(108)[-4:] )[0]; # the payload-offset is just the number of (512-byte sized) sectors # to get bytes we must multiply it by 512 luksHeaderEnd = 512 * payloadOffset elif luksVersion == 2: # LUKS2 - https://gitlab.com/cryptsetup/LUKS2-docs/blob/master/luks2_doc_wip.pdf # first we get the header size from the primary binary header with open(device, "rb") as f: # the hdr_size field is a big endian unsigned long long (uint64_t = '>Q') # * https://docs.python.org/2/library/struct.html # in LUKS2, the hdr_size field starts at offset 8 bytes and is 8 bytes long # so we get the first 16 bytes and the last 8 bytes of that # * https://gitlab.com/cryptsetup/LUKS2-docs/blob/master/luks2_doc_wip.pdf hdr_size=struct.unpack( '>Q', f.read(16)[-8:] )[0]; # the JSON plaintext metadata object starts after the header (4096 bytes) # and ends at (hdr_size - 4096) jsonSize = hdr_size - 4096; with open(device, "rb") as f: luksMetadata = f.read(hdr_size)[-jsonSize:]; decoder = json.JSONDecoder() jsonMetadata = decoder.raw_decode(luksMetadata) # the LUKS2 header ends at the start of the first data segment luksHeaderEnd=jsonMetadata[0]['segments']['0']['offset'] else: print "ERROR: Unable to determine LUKS version" exit(1) print "Device: " +str(device) print " LUKS Header Start Byte: " +str(0) print " LUKS Header End Byte: " +str(luksHeaderEnd) exit(0)
And an example execution for a LUKS1 volume:
root@disp4117:~# ./luksHeaderBounds.py luksVol1 Device: luksVol1 LUKS Header Start Byte: 0 LUKS Header End Byte: 2097152 root@disp4117:~#
And an example execution for a LUKS2 volume:
root@disp4117:~# ./luksHeaderBounds.py luksVol2 Device: luksVol2 LUKS Header Start Byte: 0 LUKS Header End Byte: 16777216 root@disp4117:~#