Skip to content

Parse macOS 26 FileVault keybags: 22-byte KEK metadata, expanded container VEK entry, and big-endian DER long-form lengths - #89

Open
willmcginnis wants to merge 3 commits into
libyal:mainfrom
willmcginnis:macos26-filevault-keybag
Open

Parse macOS 26 FileVault keybags: 22-byte KEK metadata, expanded container VEK entry, and big-endian DER long-form lengths#89
willmcginnis wants to merge 3 commits into
libyal:mainfrom
willmcginnis:macos26-filevault-keybag

Conversation

@willmcginnis

@willmcginnis willmcginnis commented Jul 26, 2026

Copy link
Copy Markdown

Summary

libfsapfs 20240429 cannot open a FileVault-encrypted APFS container written by a recent macOS (observed on macOS 26.5.2, build 25F84). It aborts while parsing the volume keybag's KEK record with:

libfsapfs_key_encrypted_key_read_data: unsupported KEK metadata attribute value data size: 22

Two on-disk shapes have outgrown fixed-size assumptions in libfsapfs_key_encrypted_key.c, and there is one latent DER-length bug that this also fixes. None of this changes the cryptography — it is a parsing/interoperability fix (see "Scope" at the end).

What changed on disk

Each keybag record is a DER SEQUENCE containing context-specific implicitly tagged fields. Two record kinds are affected.

1. The volume-keybag KEK records — keyblob[2] metadata is 22 bytes (was 8)

A per-user KEK record from a disposable test VM (login password admin):

30 81 9f                                     SEQUENCE (159)
   80 01 00                                  [0] version = 0
   81 20  <32 bytes>                         [1] hmac
   82 08  07dae24ec205fe57                   [2] salt (8)
   a3 6e                                     [3] keyblob SEQUENCE (110)
      80 01 00                               [0] version = 0
      81 10  faa413cd...c40732               [1] uuid (16)
      82 16  49000000 0200                   [2] metadata (22, was 8)
             96d725ec34d842ac9764e43f81804f57
      83 28  <40 bytes>                      [3] wrapped_key (40)
      84 03  088950                          [4] iterations = 559440
      85 10  <16 bytes>                      [5] salt (16)

The legacy 8-byte struct interprets offsets 0–3 as encryption_method and 4–7 as unknown1[2]/unknown2/unknown3. In the observed 22-byte value, offsets 0–3 decode little-endian as 0x49 — outside libfsapfs's accepted set {0, 2, 16}, so they are evidently a flags field, not a method — and offsets 6–21 are a 16-byte, UUID-shaped value (the six-byte prefix is retained; the UUID replaces the old unknown2/unknown3 bytes). This 22-byte layout is not novel — apfs-fuse has modeled it since 2023 as struct key_info_t { uint32_t flags; uint8_t unk_04; uint8_t unk_05; uint8_t uuid[16]; } — libfsapfs simply has not been updated for it. Mapping this record to method 0 (below) is an implementation inference based on the 40-byte wrap.

2. The container-keybag VEK entry (KB_TAG_VOLUME_KEY) — larger, with long-form lengths

30 82 01 80                                  SEQUENCE (384)   <- 2-byte long-form length
   80 01 00                                  [0] version = 0
   81 20  <32 bytes>                         [1] hmac
   82 08  <8 bytes>                          [2] salt
   a3 82 01 4d                               [3] keyblob SEQUENCE (333)  <- 2-byte long-form length
      80 01 00                               [0] version = 0
      81 10  66eb31c4...52884e9c             [1] uuid (volume, 16)
      82 16  29000000 0101 96d725ec...4f57   [2] metadata (22)
      83 28  <40 bytes>                      [3] wrapped_vek (40)
      84 10  <16 bytes>                      [4] 16 bytes (documented layout: <=8-byte iter count)
      85 03  <3 bytes>                       [5]  3 bytes (documented layout: 16-byte salt)
      86 10  <16 bytes>                      [6] 16 bytes (not in documented layout)
      87 10  <16 bytes>                      [7] 16 bytes (not in documented layout)
      88 81 b8  <184 bytes>                  [8] 184 bytes (1-byte long-form length; not in documented layout)

This entry exceeds 255 bytes, so the outer SEQUENCE and the [3] keyblob use 0x82 (2-byte) long-form lengths, and [8] uses an 0x81 (1-byte) long-form length. Its [4] is 16 bytes and [5] is 3 bytes (both differ from the documented password-protected layout — this entry is password-independent, so those slots are not a PBKDF2 iteration count / salt), and it carries three extra tags [6]/[7]/[8].

The fixes (all in libfsapfs_key_encrypted_key.c)

  1. Read 0x82 long-form lengths big-endian. DER lengths are big-endian, but the reader used byte_stream_copy_to_uint16_little_endian (four call sites). On the VEK entry above, a3 82 01 4d is otherwise read as 0x4d01 = 19713 instead of 333. This is a latent bug independent of macOS 26: it misdecodes any 0x82-encoded (2-byte) long-form length whose two length octets differ (e.g. 01 4d).
  2. Accept 0x81 (1-byte) long-form lengths in the nested wrapped-KEK sub-parser. The top-level parser already handles long-form (it reads the KEK record's own 30 81 9f), but the nested wrapped-KEK-object sub-parser accepts only 0x82, so [8]'s 88 81 b8 is rejected.
  3. Derive the nested wrapped-KEK object header size from the length form (0x81 -> 3, 0x82 -> 4; short-form lengths keep the 2-byte header) instead of a hard-coded 2, so the sub-object starts on its real tag byte.
  4. Accept a 22-byte keyblob[2] in addition to 8. In the 22-byte layout the first 4 bytes are a flags field, not a valid encryption_method (the observed value is outside {0, 2, 16}), so override the parsed method to 0 — whose sizing (32-byte key, 40-byte wrap) matches the observed 40-byte wrapped_key — and parsing proceeds.
  5. Relax the [4]/[5] handling in the shared record parser (this affects both record kinds): read [4] as a PBKDF2 iteration count only for lengths 1–8 (zero is still rejected), and copy [5] as a salt only when exactly 16 bytes; otherwise leave them uninterpreted. For the KEK records this is a no-op (their [4] is 1–8 bytes and [5] is 16); it lets the container VEK entry's 16-byte [4] and 3-byte [5] be skipped.

The extra tags [6]/[7]/[8] need no per-tag handling — they fall through the existing default: break once the length decoding above lets the parser advance past them.

Testing

The patch applies cleanly to current main and, built with --enable-debug-output, compiles without any warning on the changed file (and is -std=c89 -pedantic clean there). Against a real macOS 26.5.2 (25F84) FileVault container from a disposable VM, fsapfsinfo then parses the container keybag and both KEK records where it previously aborted. (The sample bytes above are from that throwaway VM, whose login password is the literal admin.) I have not run the tests/ suite; the change is confined to the DER/keybag record parser.

Scope — this is a parsing fix, not an unlock

Importantly, on the guest examined here the password does not unwrap the KEK offline: PBKDF2-HMAC-SHA256(password, salt, iterations) followed by RFC-3394 unwrap of the 40-byte wrapped_key fails the A6A6A6A6A6A6A6A6 integrity check with the correct password (checked across ~150 salt/iteration/key-length variants and two independent unwrap implementations). The KEK appears bound to a wrapping context that is not present on disk (on a Virtualization.framework guest, plausibly the out-of-process virtual Secure Enclave). So this PR makes libfsapfs parse these keybags rather than aborting; it does not by itself decrypt such a volume.

@joachimmetz

Copy link
Copy Markdown
Member

Can you describe the format case that cannot be read? Let's make sure it is also captured in https://github.com/dfirlabs/apfs-specimens

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 29.09%. Comparing base (af6712f) to head (3d8afbe).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #89      +/-   ##
==========================================
+ Coverage   28.05%   29.09%   +1.03%     
==========================================
  Files          73       73              
  Lines       15918    15907      -11     
  Branches     3659     3671      +12     
==========================================
+ Hits         4466     4628     +162     
+ Misses      10281    10093     -188     
- Partials     1171     1186      +15     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@willmcginnis

Copy link
Copy Markdown
Author

Thanks for looking at this. Answering the format question first, then the
specimen, then the CI.

The format case

Two independent differences from the layout libfsapfs models. Only the first
produces an error; the second is silently mis-parsed once the first is fixed.

1. keyblob[2] metadata is 22 bytes, not 8. Every KEK record on the
observed container carries a 6-byte header plus a 16-byte identifier:

82 16  49000000 0200 96d725ec34d842ac9764e43f81804f57

libfsapfs_key_encrypted_key_read_data requires exactly 8 here, so it fails
with unsupported KEK metadata attribute value data size: 22 and the container
will not open. This layout is not new — apfs-fuse has modelled it since 2023
(be0f05af, "Move crypto code into own library, improve ASN.1 parsing code")
as struct key_info_t { uint32_t flags; uint8_t unk_04; uint8_t unk_05; uint8_t uuid[16]; }, copied in DecodeKeyHeader() under if (info_len > 0x16) return nullptr; — i.e. any length up to 22 is accepted. The first 4 bytes are that
flags field rather than encryption_method; the observed 0x49 is not one
of the valid methods {0, 2, 16}, and the 40-byte keyblob[3] implies
AES-256, so the patch normalises the method to 0 for the 22-byte variant.

2. The container key bag VEK entry is 388 bytes, not 122.

30 82 01 80                          SEQUENCE (384)             <-- 2-byte long form
   80 01 00                          [0] version
   81 20  ...                        [1] hmac (32)
   82 08  ...                        [2] unknown, salt? (8)
   a3 82 01 4d                       [3] keyblob (333)          <-- 2-byte long form
      80 01 00                       [0] version
      81 10  ...                     [1] volume uuid
      82 16  ...                     [2] metadata (22)
      83 28  ...                     [3] wrapped_vek (40)
      84 10  ...                     [4] 16 bytes   (documented: <= 8-byte iteration count)
      85 03  ...                     [5]  3 bytes   (documented: 16-byte PBKDF2 salt)
      86 10  ...                     [6] 16 bytes   (not in documented layout)
      87 10  ...                     [7] 16 bytes   (not in documented layout)
      88 81 b8  ...                  [8] 184 bytes  (not in documented layout, 1-byte long form)

Outer 3 + 34 + 10 + 337 = 384; keyblob
3 + 18 + 24 + 42 + 18 + 5 + 18 + 18 + 187 = 333. Three things break:

  • DER long-form lengths are big-endian, and the existing code reads the
    0x82 form little-endian at four sites. A KEK record never exposes this
    because its 159-byte content fits the 0x81 form. 0x82 01 80 is 384, not
    32769.
  • The 0x81 form was not accepted in the nested wrapped-KEK sub-parser, so
    keyblob[8] (88 81 b8) is rejected.
  • [4] and [5] are not an iteration count and a salt here. This entry is
    unwrapped with a key rather than a password, so there is no PBKDF2 state.
    Today the > 8 and != 16 guards return( -1 ), rejecting the whole
    record; the patch skips just those two attributes instead. It gates that on
    the 22-byte [2] metadata rather than doing it unconditionally, because a
    corrupt legacy password KEK — say a 15-byte [5] — would otherwise parse
    with an all-zero salt and be reported as a wrong password rather than a
    corrupt key bag. There is a test for exactly that case.

apfs-fuse does not model this entry either — DecodeVEK() parses through [3]
and returns success while ignoring [4][8], stopping at
// HW encrypt has more here ... TODO.

I have labelled the outer [2] "unknown, salt?" above rather than "salt", to
stay with the hedge in documentation/Apple File System (APFS).asciidoc. The
keyblob's [5] is the documented PBKDF2 salt and is a different field.

Bounds checking that this change makes necessary

Accepting 0x81 in the nested sub-parser consumes a length byte before the
existing bounds check, so a record ending in a bare <tag> 0x81 reads one byte
past the buffer:

30 07  a3 05  80 01 00  84 81                 9 bytes, 0x81 is the last one

With a guard page immediately after the record, current main returns a clean
-1 for that input and the 0x81-accepting version faults. The same shape is
already reachable today via the 0x82 form, and via the attribute tag/length
pair at all four tag-length decode sites — six minimal inputs fault at
data_size = 2 on current main. Since libfsapfs is on OSS-Fuzz and this code
is reachable from a crafted image, the branch adds an availability check ahead
of each of those reads, and two related corrections:

  • the object value data size is now checked against the bytes remaining after
    the length bytes rather than against a hardcoded 2-byte header, which
    under-counts by 1 or 2 for the long forms; and
  • attributes are bounded by the enclosing object's value data rather than by
    the supplied buffer, so trailing bytes past the SEQUENCE are no longer
    parsed. Previously a second a3 packed object appended after a record
    silently replaced the real one.

If you would rather keep these separate from the format change I am happy to
split them into their own commit or their own PR — say the word.

On capturing it in dfirlabs/apfs-specimens

I tried to reproduce it the way generate-specimens-macos.sh does and it
does not reproduce
, which I think is the useful finding. On macOS 26.4.1
(25E253, Apple Silicon), hdiutil create -fs APFS followed by
diskutil apfs encryptVolume ... -user disk -passphrase test, dumped with an
--enable-debug-output fsapfsinfo:

Field diskutil-encrypted, 26.4.1 FileVault system volume, 26.5.2 VZ guest
KEK keyblob[2] metadata 8 bytes 22 bytes
VEK entry, outer content 122 bytes 384 bytes
VEK keyblob 73 bytes, tags [0][3] 333 bytes, tags [0][8]
Opens with stock libfsapfs yes no

So the trigger is not the macOS version — it is a real FileVault-enabled
system volume with a Secure-Enclave-backed crypto user. A hdiutil +
diskutil script on a data image produces the legacy layout on macOS 26.
Getting a faithful specimen means installing macOS 26 in a VM, enabling
FileVault in the guest, shutting it down and taking the key bag from the guest
image — which is more than the existing scripts do, and the resulting image is
far larger than the 4 MB specimens in that repo.

I am happy to do whichever you prefer:

  1. contribute a generate-specimens-encrypted-macos.sh that produces a
    genuinely encrypted volume (see the note below) and documents the gap, or
  2. contribute just the three key bag records as a small static test vector, or
  3. work out with you what a VM-produced FileVault specimen should look like so
    it is small enough to live in that repo.

I have unit tests in this PR that cover every new branch, so the parser change
is covered either way. To be precise about what is in them: three of the
vectors are byte-exact records from the container above — the per-user KEK, the
personal recovery key KEK (crypto user
EBC6C064-0000-11AA-AA11-00306543ECAC), and the VEK entry. The remainder are
synthetic records, built rather than hand-typed so every DER length is correct
by construction: a legacy 8-byte-metadata record to hold the original path
still, two long-form-length records for the 0x81/0x82 sites the real
records do not reach, and minimal truncated records for the bounds checks.

While checking: in the current generate-specimens-macos.sh the
apfs_single_volume_encrypted case does not actually encrypt. The active path
is hdiutil create -fs 'APFS' ... -volname SingleVolume with no passphrase;
-passphrase test only appears in the commented-out diskutil apfs addVolume
fallback. That would explain why there is no coverage of the key bag paths at
all today.

On the failing CI

I looked at each one; none of them are caused by this change.

  • build (windows-2025, 2026) fails in synczlib.ps1 line 54 with
    Cannot find path 'zlib-1.3.2', before any libfsapfs source is compiled.
    zlib132.zip at zlib.net does still contain a zlib-1.3.2/ directory, so
    the URL is fine. The cause looks like the ExtractZip fallback: when
    C:\Program Files\7-Zip\7z.exe is absent it uses
    Shell.Application's CopyHere, which is asynchronous — the script then
    immediately does Remove-Item on the zip and Move-Item on a directory that
    may not exist yet. windows-2022 passed the same step in the same run, which
    fits a 7-Zip-present/absent difference between runner images.
  • build (windows-2022, 2022) and build (windows-11-arm, 2022) were
    cancelled 4 seconds after windows-2025 failed (The operation was cancelled, mid synclibs.ps1). The build matrix has no
    fail-fast: false, so these are collateral.
  • coverage_cygwin fails compiling fsapfstools/fsapfsmount.c:483 with
    an incompatible pointer type between
    int (*)(const char *, struct fuse_stat *, struct fuse_file_info *) and
    mount_fuse_stat_t. That file is not touched by this PR.
  • codecov/patch was the one real gap — the new branches had no test.
    That is fixed by the tests added here; patch coverage of the changed lines
    measured locally with gcov is 100% (89 of 89 executable added lines).

On the shape of the change itself: all new locals are declared with the rest of
the function's declarations, the one size_t-to-uint16_t conversion is
explicit (the class MSVC reports as C4267), and the keyblob[4] and
keyblob[5] cases use guard clauses so the surrounding statements keep their
original indentation. Compiling the file under
-std=c89 -pedantic -Wall -Wextra -Wdeclaration-after-statement -Wconversion -Wsign-conversion -Wshadow, with and without --enable-debug-output, produces
exactly the same diagnostics as the pre-branch version of the file — no new
ones.

@joachimmetz

Copy link
Copy Markdown
Member

On capturing it in dfirlabs/apfs-specimens

Let's start with a small write up (script in the broadest sense) how the volume was created and make it reproduceable. Independent of this PR. The PR can use stand-alone key bag records for unit testing

synczlib.ps1

Yeah this sometimes happens due to connectivity issues on the action runners.

coverage_cygwin

This is due a recent-ish change in the Github action cygwin configuration, I'll push an update.

@joachimmetz joachimmetz self-assigned this Jul 28, 2026
@willmcginnis

Copy link
Copy Markdown
Author

Sounds good — I'll keep the specimen work out of this PR and write it up separately. Happy to put it wherever suits; dfirlabs/apfs-specimens seemed the natural home but say if you'd rather it went elsewhere.

The PR already matches what you describe — the tests are stand-alone key bag records compiled into tests/fsapfs_test_key_encrypted_key.c, with no specimen image involved — so nothing changes here on that front.

One thing worth saying up front, because it decides what a FileVault specimen would even be good for. The 22-byte KEK metadata ends in 16 bytes that are constant per install: identical for both crypto users within one install, and different between two independently installed machines that use the same account name and password. They are bound to that machine's Secure Enclave. So a FileVault system-volume image cannot be unlocked by anyone who did not create it — it would be parsing-coverage only, never an unlock test vector. That is the main reason I think stand-alone records are the right call here regardless of how the specimen question lands.

Separately, and more useful in the short term: the apfs_single_volume_encrypted case in generate-specimens-macos.sh does not encrypt, which I mentioned last time and have now confirmed on macOS 26.4.1 — the active hdiutil create -fs 'APFS' ... -volname SingleVolume path leaves FileVault: No. Adding

diskutil apfs encryptVolume ${VOLUME_DEVICE} -user disk -passphrase test

after create_test_file_entries, then waiting for the background conversion, gives a volume that stock libfsapfs reports as locked and unlocks with test — the first specimen that would exercise the key bag paths at all. Two things to watch in that wait loop, both measured: diskutil apfs list reports Encryption Progress: NN.0% for APFS (Conversion Progress is the CoreStorage field and never appears), and that line often reads (Paused) while the percentage is still climbing, so (Paused) isn't a terminal state. I'll send that as its own small change.

Still open from last time whenever you get to it: happy to split the bounds checking out of the format change into its own commit or PR if you'd prefer them separate.

@joachimmetz

Copy link
Copy Markdown
Member

I'll keep the specimen work out of this PR and write it up separately.

Thanks that is fine. Using the dfirlabs/apfs-specimens project is preferred.

reports Encryption Progress: NN.0% for APFS (Conversion Progress is the CoreStorage field and never appears), and that line often reads (Paused) while the percentage is still climbing, so (Paused) isn't a terminal state. I'll send that as its own small change.

That is fine the scripts are intended for creating test data (not production code), both encryption in progress and completed are useful test scenarios.

So a FileVault system-volume image cannot be unlocked by anyone who did not create it — it would be parsing-coverage only, never an unlock test vector

That is fine, having it scripted, at minimum allows someone to recreate comparable test data.

Still open from last time whenever you get to it: happy to split the bounds checking out of the format change into its own commit or PR if you'd prefer them separate.

Fine to keep them in this one.

@joachimmetz

Copy link
Copy Markdown
Member

Regarding https://github.com/libyal/libfsapfs/actions/runs/30320701280/job/90181237038?pr=89 rebasing with HEAD should resolve this

@willmcginnis

Copy link
Copy Markdown
Author

A correction to my previous comment — I overstated the "cannot be unlocked" claim, and since this is a parsing library it's worth scoping properly.

Architecture. What I described is specific to the Apple-Silicon (VZ / VMAPPLE) guest I measured. On Intel, the equivalent device key is derived in software from a machine identifier (PBKDF2(IOPlatformUUID)), which is present on the machine — so a same-shaped record on an Intel/legacy volume can be offline-unlockable. My unqualified "cannot be unlocked" dropped that scope (the PR body has it — "the guest examined here").

Mechanism. I leaned on the 16 constant bytes as though they were the binding; they aren't. They're a per-install label (a wrapping-context identifier — and on this container it is the public cirruslabs base-image value, shared by anyone who pulls that image, which is also why nothing private is in the test vectors). The actual machine-binding is upstream of the on-disk record: the password-derived key is combined with an off-disk Secure-Enclave secret before the RFC-3394 unwrap, so the KEK is not a function of on-disk bytes alone.

What's actually established. On the Apple-Silicon guest, a reader VM with a different Secure-Enclave context rejects both the correct password and the personal recovery key, and replacing just the VM's auxiliary storage (nvram.bin) while holding the disk constant breaks unlock — so the unlock secret lives in the aux storage, not on the disk. The honest statement is therefore about the disk image alone: it carries no unlock secret. The flip side: a copy-on-write clone of the whole VM bundle (which copies the aux) does unlock — so it's the disk-alone that's inert, not the bundle — and I have not tested whether that aux is portable to a different host, so I'm not claiming universal unopenability.

None of this changes the specimen conclusion: stand-alone key bag records for parsing coverage is the right call, and a disk-image FileVault specimen exercises the parser but is not an unlock test vector on this platform. Apologies for the imprecision in the earlier version.

Will McGinnis added 3 commits July 28, 2026 14:20
…ainer VEK entry, big-endian DER long-form lengths)
Reduced the diff of the previous commit and added the bounds checking that
change makes necessary.

* Gated the relaxed keyblob[4] and keyblob[5] guards on a 22-byte keyblob[2]
  metadata attribute, so that a corrupt legacy password KEK, such as one with
  a 15-byte keyblob[5], is still rejected instead of parsing with an all-zero
  salt and being reported as a wrong password.
* Added availability checks ahead of every DER tag and length byte read.
  Accepting the 0x81 long form consumed a length byte before the existing
  bounds check, so a record ending in a bare <tag> 0x81 read past the data.
  The same was already reachable through the 0x82 form, and through the
  attribute tag and length byte pair at all four tag and length decode sites.
* Bound the object value data size by the number of bytes remaining after the
  length bytes instead of by a hardcoded 2-byte header, which under-counts by
  1 or 2 bytes for the long forms.
* Bound the attributes by the object value data instead of by the data, so
  that trailing data is no longer parsed as an attribute.
* Corrected the number of iterations debug output format specifier to PRIu64.
* Removed a debug output statement unrelated to the format change.
Covers every branch the macOS 26 keybag change adds, which previously had
no test at all.

Real records, from a disposable macOS 26.5.2 virtual machine whose account
password is "admin":
- data1: per-user KEK record (162 bytes) with a 22-byte keyblob[2] metadata
  attribute.
- data2: personal recovery key KEK record (162 bytes) of the same container,
  a second independent record with a 22-byte keyblob[2] metadata attribute
  and a different HMAC, identifier, wrapped KEK, iteration count and salt.
- data3: container key bag VEK entry (388 bytes), which uses 2-byte long-form
  DER lengths on the object and on the keyblob, a 1-byte long-form DER length
  on keyblob[8], a 16-byte keyblob[4] and a 3-byte keyblob[5].

Records derived from those:
- data4: KEK record with the pre-macOS 26 8-byte keyblob[2] metadata
  attribute, to safeguard the original code path.
- data5 and data6: records that use the 1-byte and 2-byte long-form DER
  lengths on the object, on the keyblob and on the keyblob attributes. The
  unsupported long-form attributes precede keyblob[4] and keyblob[5], so a
  change that stopped parsing at them is caught by the number of iterations
  and salt asserts.
- data7: record followed by trailing data that itself looks like a wrapped
  KEK packed object attribute, which must not be parsed.
- error_data1 to error_data4: an unsupported 12-byte keyblob[2] metadata
  attribute, an 8-byte keyblob[2] metadata attribute combined with a 15-byte
  keyblob[5] or with a 16-byte keyblob[4], and an empty keyblob[4].
- error_data5 to error_data14: minimal records truncated at a DER tag or
  length byte, and records with attribute value data running past the object
  that contains it.

Every regular case starts from a freshly initialized key encrypted key, since
libfsapfs_key_encrypted_key_read_data only sets the attributes a record
actually carries, and asserts the parsed HMAC, identifier, wrapped KEK, salt,
number of iterations and encryption method.
@willmcginnis
willmcginnis force-pushed the macos26-filevault-keybag branch from 731b4da to 3d8afbe Compare July 28, 2026 21:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants