jsonlinuxencryptionluks

How to programatically find the start and end bytes of LUKS header?


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.


Solution

  • 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:~#