Photo of Joe T. Sylve

Joe T. Sylve, Ph.D.

Digital Forensic Researcher and Educator

Wrapped Keys

In our last post, we discussed both Volume and Container Keybags and how they protect wrapped Volume Encryption and Key Encryption Keys. Depending on whether the encrypted volume was migrated from an HFS+ encrypted Core Storage volume, there are subtle differences in how these keys are used. In this post, we will discuss the structure of these wrapped keys and how they can be used to access the raw Volume Encryption Keys that encrypt data on the file system.

Key Encryption Key Blobs

Each Key Encryption Key (KEK) is encoded in a binary DER blob with the following structure:

KEKBLOB ::= SEQUENCE {
    unknown [0] INTEGER
    hmac    [1] OCTET STRING
    salt    [2] OCTET STRING
    keyblob [3] SEQUENCE {
        unknown     [0] INTEGER
        uuid        [1] OCTET STRING 
        flags       [2] INTEGER
        wrapped_key [3] OCTET STRING
        iterations  [4] INTEGER
        salt        [5] OCTET STRING
    }
}

The keys begin with a header that contains an HMAC-SHA256 hash of the key blob data. The HMAC key is generated from the SHA-256 hash of a magic value concatenated with the given salt.

hmac_key := SHA256("\x01\x16\x20\x17\x15\x05" + salt)

The key blob encodes the wrapped KEK and additional information needed for unwrapping, including a set of bit-flags.

KEK Flags

Name Value Description
KEK_FLAG_CORESTORAGE 0x00010000’0000000000 Key is a legacy CoreStorage KEK
KEK_FLAG_HARDWARE 0x00020000’0000000000 Key is hardware encrypted

If the KEK_FLAG_CORESTORAGE flag is set, then the wrapped KEK was migrated from a Core Storage encrypted HFS+ volume and used a 128-bit key to encrypt the KEK; otherwise, a 256-bit key is used.

Generate a key using the PBKDF2-HMAC-SHA256 algorithm, the user’s password, the provided salt, and the number of iterations.

// Calculate size of wrapping key (in bytes)
key_size := (flags & KEK_FLAG_CORESTORAGE) ? 16 : 32

// Generate unwrapping key from user's password
key := pbkdf2_hmac_sha256(password, salt, iterations, key_size)

// Unwrap the encrypted KEK
kek := rfc3394_unwrap(key, wrapped_key);

If the encrypted volume was migrated from Core Storage and the user changed their password afterward, it’s possible to have a non-Core-Storage wrapped KEK containing only a 128-bit key. In these instances, the last 128 bits of the unwrapped KEK will be zeros and should be ignored.

// Shorten the KEK if needed
if is_zeroed(kek[16:]) {
    kek = kek[:16];
}

Volume Encryption Key Blobs

Volume Encryption Key (VEK) blobs have a very similar structure to the KEK blobs that we just discussed. Depending on whether they were migrated from Core Storage, they can also be 128-bit or 256-bit keys.

VEKBLOB ::= SEQUENCE {
    unknown [0] INTEGER
    hmac    [1] OCTET STRING
    salt    [2] OCTET STRING
    keyblob [3] SEQUENCE {
        unknown     [0] INTEGER
        uuid        [1] OCTET STRING
        flags       [2] INTEGER
        wrapped_key [3] OCTET STRING
    }
}

VEK Flags

Name Value Description
VEK_FLAG_CORESTORAGE 0x00010000’0000000000 Key is a legacy CoreStorage VEK
VEK_FLAG_HARDWARE 0x00020000’0000000000 Key is hardware encrypted

Use the KEK to unwrap the VEK using the RFC3394 key wrapping algorithm. If the wrapped VEK is a 128-bit Core Storage VEK, then only the first 128-bits of the KEK are used.

// Calculate size of wrapping key (in bytes)
vek_size = (flags & VEK_FLAG_CORESTORAGE) ? 16 : 32;

if (vek_size == 16) {
    kek = kek[:16];
}

// Unwrap the VEK
vek = rfc3394_unwrap(kek, wrapped_key)

128-bit Core Storage VEKs must be extended to 256-bit encryption keys. This is accomplished by using the first 128 bits of the SHA256 hash of the VEK and its UUID as the second half of the key.

// 128-bit veks need to be combined with the first 128-bits of a hash
if vek_size == 16 {
    vek = append(vek, SHA256(vek + uuid)[16:])
}

RFC 3394 Key Unwrapping

Both KEK and VEK unwrapping use the RFC 3394 AES Key Wrap algorithm. This algorithm provides authenticated key transport: if the wrapping key is wrong or the wrapped data is corrupted, the unwrap will fail with a detectable integrity error.

The algorithm operates on 64-bit blocks:

  1. The wrapped key is split into n 64-bit blocks. The first block is the integrity check value (ICV) and the remaining blocks are the key material.
  2. Over 6 rounds (each iterating through all key blocks), the algorithm applies AES decryption with XOR operations that mix a round counter into the data.
  3. After all rounds, the ICV must equal 0xA6A6A6A6A6A6A6A6. If it does not, the wrapping key was incorrect or the data is corrupted.

The wrapped key is always 8 bytes longer than the unwrapped key (the ICV overhead). So a 256-bit (32-byte) key is stored as a 40-byte wrapped blob, and a 128-bit (16-byte) key as a 24-byte wrapped blob.

Per-File Encryption State

On volumes that use per-file key encryption (APFS_FS_SCALEABLE_PFK), each file’s encryption state is stored in a wrapped_crypto_state_t structure within APFS_TYPE_CRYPTO_STATE records in the File System Tree. Note that per-file key encryption requires hardware encryption; the software decryption path described here applies to single-key (APFS_FS_ONEKEY) volumes:

typedef struct wrapped_crypto_state {
    uint16_t major_version;             // 0x00
    uint16_t minor_version;             // 0x02
    uint32_t cpflags;                   // 0x04
    uint32_t persistent_class;          // 0x08
    uint32_t key_os_version;            // 0x0C
    uint16_t key_revision;              // 0x10
    uint16_t key_len;                   // 0x12
    uint8_t persistent_key[];           // 0x14
} wrapped_crypto_state_t;

The persistent_class determines when the key is available for unwrapping (see Protection Classes). The key_os_version is encoded as a packed integer: (major << 24) | (minor << 16) | build.

On single-key volumes (APFS_FS_ONEKEY), per-file crypto state objects are absent. A single placeholder APFS_TYPE_CRYPTO_STATE record still exists with the identifier CRYPTO_SW_ID (4), and all fields of its j_crypto_val_t are zero. Instead of per-file keys, all files are decrypted with the volume-level VEK as an AES-XTS key, using the crypto_id from each APFS_TYPE_FILE_EXTENT record (j_file_extent_val_t) as the tweak.

Conclusion

In this post, we discussed using the wrapped keys stored in APFS Keybags to gain access to the Volume Encryption Key that protects a user’s data in APFS. The RFC 3394 algorithm provides authenticated unwrapping, while the wrapped_crypto_state_t structure enables per-file encryption with individual protection classes. In a the next post in this series, we will continue our discussion about APFS encryption by describing how to identify and decrypt protected information using these keys.

Find an issue or technical inaccuracy in this post? Please file an issue so that it may be corrected.