Reputation: 2785
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.
Upvotes: 0
Views: 1107
Reputation: 1
Michael's answer is fine but it is written in python2 which was EOL in January of 2020 and most new distros don't include it by default anymore. It could be updated but cryptsetup actually includes a command for this exact situation. Just run cryptsetup erase <device>
then type YES and it will clear the header for you. You can skip the confirmation with cryptsetup -q erase <device>
.
It does preserve the header but it wipes all the key slots which accomplishes the objective of making the data irrecoverable without a header backup.
Here's the man page https://man.archlinux.org/man/cryptsetup.8
Upvotes: 0
Reputation: 2785
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 <[email protected]>
# 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:~#
Upvotes: 0