Skip to content

Encrypted Partitions

Zack Didcott edited this page Apr 7, 2025 · 1 revision

Summary

Partition minor 255, along with a few others, are encrypted by default since IGEL OS v11. A custom encrypted partition can also be configured by the system administrator. Partition minor 255 contains the wfs (presumably "writable filesystem") partition. /wfs contains various configuration files, such as group.ini and setup.ini (partially XML-formatted, despite the file extension), which store the user-configured registry data. These files may be gzip-compressed.

IGEL encrypted filesystems contain two extents - of type WRITEABLE and LOGIN respectively - and are handled internally by various tools:

  1. /usr/bin/mkigelefs, chkigelefs and rmigelefs - extent filesystem
  2. /etc/igel/crypt/* - filesystem and key tools
  3. /usr/bin/kml/* - key management

Extent Filesystems

Encrypted filesystems, such as partition minor 255, contain two partition extents of types WRITEABLE and LOGIN respectively. Extents of type WRITEABLE contain models encrypted using the XChacha20-Poly1305 (AEAD) cryptosystem, with a key derived from the boot_id (see CryptoHelper.get_extent_key).

The key can also be found using the method described in LD_PRELOAD, overriding crypto_aead_xchacha20poly1305_ietf_decrypt instead of add_key. This will reveal the ciphertext, authenticated data, cryptographic nonce and key.

The authenticated data and nonce are stored in the header of the extent filesystem. The header is 48 bytes, with a data section of 1048528 bytes; the actual payload size is also specified in the header. The decrypted data is an LZF-compressed tar archive.

Install the additional dependencies and use igelfs.models.efs.ExtentFilesystem to handle these extents, for example:

key = CryptoHelper.get_extent_key(boot_id)  # Derive key from boot ID
models = ExtentFilesystem.from_bytes_to_collection(extent)
for model in models:
    data = model.decrypt(key)  # Decrypt payload with key
    decompressed = ExtentFilesystem.decompress(data)  # Decompress LZF data
    ExtentFilesystem.extract(decompressed, path)  # Extract tar archive to path

The tar archive contains a JSON configuration file, called kmlconfig.json, which stores the required information to open the encrypted volumes.

The required JSON sections are: system, slots and keys, and optionally tpm.

Encryption Keys

Once the writable extent has been decrypted and kmlconfig.json has been extracted, it is possible to derive the master key for decrypting individual filesystem keys.

The master key is derived in the following way (see CryptoHelper.get_master_key):

  • Argon2ID KDF with the following parameters:
    • size: 32 bytes
    • password: first 20 bytes of CryptoHelper.get_extent_key(boot_id) (base64 decoded, then re-encoded)
    • salt: from system.salt
    • opslimit and memlimit: dependent on system.level
  • slots[n].pub (32 bytes) is appended to result = 64 bytes
  • Result is hashed with SHA-512 (64 bytes)
  • Digest is used as key to decrypt slots[n].priv with AES-XTS, where the initialisation vector is the second half of the key ([32:])

This master key is then used to decrypt each key in the same way.

Use igelfs.kml.Keyring and KmlConfig to manage these keys in an abstract manner:

keyring  = Keyring.from_filesystem(filesystem)
keyring.get_keys()
keyring.get_key(255)

Encryption Type

Once the keys have been obtained, the encrypted filesystem can be decrypted; in older IGEL OS versions, it appears these are often LUKS containers, but in later versions, often are encrypted in plain mode, with the cipher aes-xts-plain64 and a key size of 512 bits (halved in XTS mode, i.e. 2x AES-256).

Plain mode (aes-xts-plain64):

cryptsetup open \
    --type=plain \
    --cipher=aes-xts-plain64 \
    --key-size=512 \
    --key-file=<keyfile> \
    <device> \
    <name>

LUKS:

cryptsetup --master-key-file=<keyfile> open <device> <name>

Keyring

The tools in /usr/bin/kml/ internally add keys to the kernel's key management facility with add_key, which can be viewed in /proc/keys and managed by keyctl.

These keys have type logon, meaning the keys are not readable from user space. The following methods can be used to find these keys:

Binary Patching

If these keys are required, it is possible to patch the binary /usr/bin/kml/load_cred - which is responsible for the key-derivation logic - to add these keys with type user instead, allowing them to be read. This binary can be patched with a reverse engineering tool, such as Ghidra, or with sed as below:

# Check string "logon" exists in binary
strings /usr/bin/kml/load_cred | grep logon
# Patch the binary
# If the binary changes significantly, this method may require modification
sed 's/logon/user\x00/g' /usr/bin/kml/load_cred > load_cred_patched
# Make the patched binary executable
chmod +x ./load_cred_patched
# Run the patched binary
./load_cred_patched -D

# Read the added keys, see also keyctl print or pipe
# Common keys: kml:255, kml:248 and kml:default
keyctl read $(keyctl request user kml:255)
# Write the output from keyctl (as bytes) into a keyfile
# Open the encrypted image with aes-xts-plain64 mode
cryptsetup open \
    --type=plain \
    --cipher=aes-xts-plain64 \
    --key-size=512 \
    --key-file=<keyfile> \
    <image> \
    <name>

LD_PRELOAD

Alternatively, the syscall add_key can be intercepted, by writing a library which overrides the add_key function, then loading it first using LD_PRELOAD:

// add_key.c

#include <stdio.h>
#include <string.h>

int add_key(const char *type, const char *description, const char *payload,
            const int size, const int keyring)
{
    printf("add_key() call intercepted\n");
    printf("Type: %s\n", type);
    printf("Description: %s\n", description);
    printf("Payload: ");
    for (int i = 0; i < strlen(payload); i++) {
        printf("%02x ", (unsigned char)payload[i]);
    }
    printf("\nSize: %d\n", size);
    printf("Keyring: %d\n\n", keyring);

    return 0;
}

Compile on host: gcc -Wall -fPIC -shared add_key.c -o add_key.so

Run on guest: LD_PRELOAD=/path/to/add_key.so /usr/bin/kml/load_cred -D

add_key will be loaded by the user-modified library add_key.so first, which will cause any calls to add_key from load_cred to simply output the passed arguments; the keys will not actually be added to the keyring, but it will return 0 for success to the caller.

Clone this wiki locally