Apple Encrypted Archive is a proprietary archive format that can compress, encrypt, and/or sign a file.
Its magic number is the 4 ASCII characters "AEA1
".
macOS Monterey ships an aea
command line tool that can create and extract (decrypt) these archives (see aea manpage).
They can also be handled with public Swift API and C API in libAppleArchive.dylib
.
The C API is documented in comments in include/AppleArchive/AEAContext.h
etc,
but for some reason it's not present in the developer documentation website.
It's notably used since iOS 15 for signed .shortcut files created in Shortcuts, as well as for the rootFS in iOS 18 IPSWs.
Profiles
There are five different "profiles" that an AEA file can use:
- 0: hkdf_sha256_hmac__none__ecdsa_p256 - no encryption, signed
- 1: hkdf_sha256_aesctr_hmac__symmetric__none - symmetric key encryption
- 2: hkdf_sha256_aesctr_hmac__symmetric__ecdsa_p256 - symmetric key encryption, signed
- 3: hkdf_sha256_aesctr_hmac__ecdhe_p256__none - ECDHE encryption
- 4: hkdf_sha256_aesctr_hmac__ecdhe_p256__ecdsa_p256 - ECDHE encryption, signed
- 5: hkdf_sha256_aesctr_hmac__scrypt__none - scrypt encryption (password based)
When reading a file using profile 0 or 2, you can use a EC-P256 public key to verify the signature.
When reading a file using profile 1 or 2, you need a symmetric key as input in order to decrypt it.
Profiles 3,4,5 weren't investigated yet.
In all profiles, AEA files can have "auth data" in the header, which is data that is not encrypted but is authenticated to ensure it hasn't been modified. The libAppleArchive library provides access to a simple format that can be used to put key-value pairs in auth data, but using it is not required, and some Apple-provided AEA files use different auth data formats.
Uses
Signed shortcuts are AEA files using profile 0, which means they are signed but not encrypted. The auth data is a binary plist containing the certificate used to verify the signature, and the certificate chain up to Apple's root. The payload is LZFSE-compressed, and contains an Apple Archive with the shortcut data.
For example extraction code, take a look at libshortcutsign's extract_aa_from_aea
function.
Since iOS 18 / macOS 15, the root filesystem in IPSWs is encrypted into .dmg.aea files, using AEA in profile 1 (symmetric encryption but no signing). The auth data in these files contains key-value pairs in the standard AEAAuthData format, which provide the WKMS parameters needed to derive the decryption key.
Cross-platform Go code to decrypt .dmg.aea files is available as part of the blacktop/ipsw project, in pkg/aea/decrypt.go.
Cryptographic operations
These cryptographic definitions will be referenced later.
HMAC_AD
AEA uses some kind of "HMAC with associated data" operation,
which I will call HMAC_AD here.
It's implemented in HMACDerive_SHA256
on Apple's code.
HMAC_AD(hmac_key, data, ad) = HMAC_SHA256(hmac_key, ad + data + len(ad))
where len(ad) is the 8-byte little-endian length of the associated data.
For example:
HMAC_AD(hmac_key, "hello", "auth") = HMAC_SHA256(hmac_key, "authhello\x04\x00\x00\x00\x00\x00\x00\x00")
AES_AEAD
There is also an AEAD encryption primitive using AES-CTR and HMAC,
which I will call AES_AEAD here.
It takes a 640-bit (80-byte) key, which consists of 256-bit HMAC key, 256-bit AES key, and 128-bit IV.
It's implemented in AEADDecrypt_AESCTR_MAC256_KEY640
in the code.
To decrypt some ciphertext given a key, auth_data, and expected_hmac:
- Split the key into 32-byte hmac_key, 32-byte aes_key, 16-byte aes_iv.
- Calculate
HMAC_AD(hmac_key, ciphertext, auth_data)
and compare it to expected_hmac; fail if it doesn't match. - Decrypt using
AES_256_CTR(aes_key, aes_iv, ciphertext)
.
If the archive's profile is 0 (unencrypted), AES_AEAD is defined as taking a 256-bit HMAC key alone (rather than the 640-bit keyblob) and not decrypting anything. It simply verifies the HMAC with the given key and returns the "ciphertext" unmodified, since it's already unencrypted.
This makes things easier to explain,
since I don't need "if profile is 0" exceptions all over the place,
and it actually matches how Apple implements it,
in the function AEADDecrypt_None_MAC256_KEY256
used in profile 0.
File format
Every integer in AEA files is in little-endian.
The format is mainly being reversed for profiles 0 and 1; information about other profiles is untested and likely to be incorrect.
Prologue
All AEA files start with a "prologue". It has some fixed header fields, the variable-length auth data, and then other fields that may be present or missing depending on the profile.
The "Archive ID" is the SHA-256 digest of the entire prologue.
It's returned by the AEAContextGetArchiveIdentifier
and
ArchiveEncryptionContext
APIs,
and by the aea id
command.
Length | Field | Notes |
---|---|---|
4 bytes | Magic number ("AEA1") | Always present |
3 bytes | Profile ID | Always present |
1 byte | Scrypt hardness | Always present, only meaningful on profile 5 |
4 bytes | Auth data length | Always present |
n bytes | Auth data | Length given by previous field, which may be 0 |
128/160 bytes | Prologue signature | 128 bytes on profile 0, 160 bytes on profiles 2/4, missing on 1/3/5 (unsigned) |
32/65 bytes | Encryption data | 32 bytes on profile 0, 65 bytes on profiles 3/4 (ECDHE), missing on 1/2/5 |
32 bytes | Salt | Always present |
32 bytes | Root HMAC | Always present |
48 bytes | Root header | Always present, but encrypted if profile != 0 |
32 bytes | Cluster header HMAC | Always present |
The length of the prologue, assuming empty auth data, is as follows:
Profile | Length |
---|---|
0 | 0x13c |
1 | 0x9c |
If you never deal with scrypt-profile files, you can just pretend the profile and the scrypt hardness are a single 4-byte integer.
On profile 0 files, the signature is an ECDSA-SHA256 signature, as an X.509 ECDSA-Sig-Value (ie. an ASN.1 SEQUENCE with two integers, in DER encoding). It is calculated over the whole prologue, including the auth data, but with the signature itself filled with zeros.
Note that the actual signature is usually smaller than 128 bytes. The field takes up 128 bytes in the file, but you may need to parse the DER to figure out the real length before passing it to a cryptography routine.
Root header
The root header is cleartext in profile 0, and encrypted on all other profiles.
Its structure is:
Length | Field | Description |
---|---|---|
8 bytes | Raw size | Size in bytes of the decrypted and uncompressed data inside the AEA |
8 bytes | Container size | Size of the whole AEA file |
4 bytes | Segment size | |
4 bytes | Segments per cluster | |
1 byte | Compression algorithm |
|
1 byte | Checksum algorithm |
|
22 bytes | Padding | Zeros filling the remaining space |
Main key
All AEA files have a main key, which is derived from the "encryption data". On profile-0 files, the encryption data is a field in the prologue, while on profile-1 files, you need the key provided externally to decrypt the file.
To derive the main key, use the HKDF algorithm with:
- Input key = 32-byte "encryption data" field from the prologue (on profile-0), or externally-provided decryption key (on profile-1).
- Salt = 32-byte "salt" field from the prologue.
- Info = "AEA_AMK" + profile ID (4 byte little-endian) + public signing key (on profile-0).
- Output length = 32
On profile-1 files the "info" would be "AEA_AMK\x01\x00\x00\x00".
On profile-0 files, the public ECDSA key needs to be added to the HKDF info as raw ANSI X9.63 data (65 bytes). If you have the public key as a 67-byte blob starting with 0x04 0x41, that's a DER octet string, and you need to strip out those first two bytes.
Example profile-0 info:
4145 415f 414d 4b00 0000 0004 b8af 131a AEA_AMK......... 82fb b06a f8a9 f675 c1dd c4e7 c1bd ddd2 ...j...u........ 80f4 5d33 4b0f 4850 28fc dcff 53fc c4d3 ..]3K.HP(...S... 81ef a18c 21a1 dc00 eaea fd4e e517 550c ....!......N..U. b23a b9f0 f7a5 5762 5714 437d .:....WbW.C}
Decrypting root header
Derive the root header key using HKDF with:
- Input key = 32-byte main key
- Salt = empty
- Info = "AEA_RHEK"
- Output length = 32 (on profile 0) or 80 (all others)
On encrypted profiles, the 80-byte output from HKDF contains the HMAC key, AES key, and IV. On profile 0, the derived 32-byte output is the HMAC key alone, and the root header data is already in cleartext.
Use AES_AEAD
to verify and decrypt the root header:
- Ciphertext = root header (48 bytes)
- Key = 32- or 80-byte root header key as derived above
- Associated data = concatenation of cluster_header_hmac + auth_data prologue fields
- Expected HMAC = root_hmac prologue field
The decrypted result can be parsed according to the root header section above.
Clusters
The payload of an AEA file is divided into clusters. Each cluster usually contains 256 segments, and each segment usually contains 1MB of data. The actual number of segments per cluster and the segment size are specified in the root header.
Of course, the last cluster in the file may have less segments,
and the last segment may be smaller.
The total number of clusters in an archive is ceil(container_size / (segment_size * segments_per_cluster))
.
The first cluster is immediately after the file's prologue (ie. after the "cluster header HMAC" field).
First derive the cluster key using HKDF with:
- Input key = 32-byte main key
- Salt = empty
- Info = "AEA_CK" + cluster index (4 byte little-endian)
- Output length = 32
The cluster index is the index of this cluster in the file, starting at 0.
Next, derive the cluster header encryption key, again using HKDF:
- Input key = 32-byte cluster key
- Salt = empty
- Info = "AEA_CHEK"
- Output length = 32 (on profile 0) or 80 (all others)
The cluster header consists of the first 40*segments_per_cluster bytes
from the beginning of the cluster.
Decrypt and verify this chunk using AES_AEAD
with the "cluster header encryption key" derived earlier.
For the first cluster in the file,
the expected HMAC is the "cluster header HMAC" field from the file root header.
For later clusters, the expected HMAC is read from the previous cluster.