Docs / Forensics

Field guide · macOS security

macOS security

Inside the macOS Keychain: from your password to the assembly

By Abhimanyu Gupta, Founder & Principal Operator

On this page

The macOS Keychain is the operating system’s vault for secrets, unlocked by your login password. This guide opens it up: first in plain words, then down to the PBKDF2 and Triple DES that guard it, the access-control gate that decides who reads what, and how attackers pick the lock while you learn to stop them.

Read with a purpose. Everything unwraps from your login password and the file can be stolen, so a strong password and FileVault matter most. Then tighten ACLs and partition IDs, block code injection into trusted apps, and bind crown-jewel secrets to the Secure Enclave.

At a glance

What it is
The macOS Keychain is the operating system’s vault for secrets: passwords, keys, and certificates, encrypted on disk and unlocked by your login password
Where it lives
Your secrets sit in ~/Library/Keychains/login.keychain-db; system-wide ones in /Library/Keychains/System.keychain
How it locks
Your password is stretched with PBKDF2 into a key that unwraps a master key, which unwraps per-item keys, which finally decrypt each secret
The soft spot
The file has no protection of its own. Copy it, learn the login password, and it decrypts offline. Unlocked, a local attacker can often read it with no prompt at all
The gate
Access control lists and a partition ID decide which signed applications may read an entry without asking you first

What a keychain is, in plain words

Every serious operating system needs a safe place to keep secrets so that apps do not scatter passwords across plain text files. On macOS that safe is the Keychain. When Safari offers to remember a password, when your mail client stores a token, when the Wi-Fi remembers a network, the secret goes into the Keychain rather than onto disk in the clear. It is a small encrypted database, and the key that opens it is derived from your login password. Log in, and the system quietly unlocks the vault; lock the screen or log out, and it seals again.

That is the whole idea in one breath: one password protects many. It is genuinely good design. But a vault is only as strong as the lock, the walls, and the rules about who may open a drawer without the owner watching, and each of those has an offensive story. The rest of this guide takes the vault apart, first the files, then the cryptography down to the bytes and the instructions that move them, then the gatekeeper that decides who gets in, and finally how attackers pick the lock and how you stop them.

Where the secrets live

There are two keychains that matter on a Mac, plus a system store of certificates.

  • The user (login) keychain, ~/Library/Keychains/login.keychain-db. Your application passwords, internet passwords, private keys, and personal certificates. It unlocks with your login password.
  • The system keychain, /Library/Keychains/System.keychain. Machine-wide secrets: Wi-Fi passwords, system private keys, and root certificates. It is readable by root.
  • Bundled certificates under /System/Library/Keychains/, such as the built-in certificate authorities.

On iOS there is a single keychain in /private/var/Keychains/, alongside the TrustStore and OCSP caches, and every app is fenced into its own slice of it by application identifier. Two formats are worth separating in your head. The classic file above, despite the -db suffix, is the long-standing securityd keychain format inherited from CSSM: a single binary blob with a schema, key blobs, and record blobs. The newer data-protection keychain that iOS uses, and that macOS increasingly uses for high-value items, is an SQLite database whose class keys are held by the Secure Enclave, a separate coprocessor the main CPU cannot read keys out of. This guide focuses on the classic login keychain, because that is the one sitting in a file you can copy.

The file itself is not the secret’s protection. login.keychain-db has no special permissions magic; a process running as you can read the bytes, and so can anyone who exfiltrates the file. What protects the secrets is the encryption inside, and that encryption rests entirely on the strength of your login password. Everything offensive below follows from that single fact.

The cryptography, byte by byte

Unlocking the keychain is a chain of unwrappings, not a single decryption. It is worth following link by link, because every attack targets one specific link.

Link one: stretch the password. Your login password is not used as a key directly. It is run through PBKDF2 with HMAC-SHA1, salted with a value stored in the keychain’s database blob, for a fixed iteration count, to produce a 24-byte key. The iterations exist to make guessing slow: each password candidate costs thousands of hash operations.

The key hierarchy, in C-like pseudocode
// 1. Stretch the login password into a 24-byte 3DES key.
derived = PBKDF2_HMAC_SHA1(password, dbblob.salt, dbblob.iterations, 24);

// 2. Use it to decrypt the master key stored in the DbBlob (3DES-EDE-CBC).
master = DES3_CBC_decrypt(key=derived, iv=dbblob.iv, dbblob.encrypted_master);

// 3. The master key unwraps each item's own key blob...
item_key = DES3_CBC_decrypt(key=master, iv=keyblob.iv, keyblob.wrapped_key);

// 4. ...and that key finally decrypts the secret itself.
secret = DES3_CBC_decrypt(key=item_key, iv=recordblob.iv, recordblob.ciphertext);

The cipher throughout is Triple DES in EDE-CBC mode with an 8-byte block and an 8-byte initialisation vector stored next to each blob. CBC means each block is XORed with the previous ciphertext block before decryption, so the decryptor keeps a running “previous block” register. Here is that inner chaining loop at the instruction level: not a verbatim disassembly of Apple’s binary, but a faithful arm64 rendering of what the CBC XOR compiles to.

CBC block chaining, illustrative arm64
// x0 = plaintext out, x1 = raw DES output, x2 = previous cipher block (IV first)
// XOR the freshly-decrypted block with the previous ciphertext block.
cbc_xor:
    ldp     x3, x4, [x1]        // 8 bytes of DES output (two words)
    ldp     x5, x6, [x2]        // 8 bytes of the previous cipher block
    eor     x3, x3, x5          // out[0..3] = dec ^ prev
    eor     x4, x4, x6          // out[4..7] = dec ^ prev
    stp     x3, x4, [x0]        // write the recovered plaintext
    ret

Link two: the on-disk layout. The blobs the pseudocode reads from are laid out with fixed fields. Simplified, the database blob that holds the encrypted master key looks like this:

DbBlob, the fields that matter (offsets simplified)
offset  field
0x00    magic          // 0xFADE0711, marks an Apple DbBlob
0x04    version
0x08    crypto_offset  // where the encrypted master key begins
0x0C    total_len
...     iv             // 8-byte IV for the 3DES-CBC unwrap
...     salt           // PBKDF2 salt (feeds link one)
...     iterations     // PBKDF2 iteration count
...     encrypted_key  // the master key, wrapped under 'derived'

Follow the arrows: the salt and iteration count at the bottom drive the PBKDF2 in link one; the resulting key plus the IV decrypt encrypted_key into the master key; the master key walks the item blobs. A tool such as Chainbreaker is precisely a parser for these structures plus the four decryptions above. Give it the file and the login password and it reconstructs every secret offline, no macOS involved.

login password+ salt, iters PBKDF2derived key master keyfrom DbBlob item keyper entry secretclear each arrow is a 3DES-CBC unwrap; break the first and the rest fall

The gatekeeper: ACLs and partition IDs

Decryption is only half the story. Even an unlocked keychain does not hand every secret to every process. Each entry carries an access control list that names which authorizations are allowed and which applications may use them without prompting you. The authorizations you care about are blunt:

  • kSecACLAuthorizationExportClear: read the secret in the clear. This is the one an attacker wants.
  • kSecACLAuthorizationExportWrapped: read it, but re-encrypted under a password you supply.
  • kSecACLAuthorizationAny: do anything.

Each authorization has a trusted application list, and its value decides the prompt behaviour. Nil means no authorization is required and everyone is trusted. An empty list means nobody is trusted and every access prompts. A specific list names apps or binaries (/Applications/Slack.app, /usr/libexec/airportd) that may proceed silently. Layered on top is the partition ID (kSecACLAuthorizationPartitionID), which pins access to a code-signing identity: a teamid the caller must match, apple meaning the caller must be Apple-signed, or a specific cdhash. A process only reads a secret silently if it satisfies the authorization, matches the partition ID, and matches a trusted app. Understanding those three tests is understanding exactly what an attacker has to defeat.

The offensive side

There are two families of attack, and they map cleanly onto the two halves above: break the cryptography offline, or defeat the gatekeeper at runtime.

Offline: take the file and crack the password. Because the keychain file is just bytes you can read as the user, the simplest attack is to copy it and attack it elsewhere, at leisure, with no prompts and no macOS. The only secret you need is the login password, and PBKDF2 is the wall between a stolen file and its contents.

The classic offline path
# 1. Exfiltrate the file (readable as the user, no special rights).
cp ~/Library/Keychains/login.keychain-db /tmp/loot.db

# 2. Offline, feed it and a guessed password to a parser like Chainbreaker,
#    which does the four unwraps and prints every secret. No pop-ups.
chainbreaker --password 'guessed-password' /tmp/loot.db

# Weak or reused login password = the whole vault, quietly, off the machine.

Runtime: defeat the gatekeeper without the password. On an unlocked machine, the master key is already resident in securityd’s memory, so no cracking is needed; the fight is purely about the ACL. The built-in security tool exposes the whole surface.

security(1): the built-in interface
security list-keychains                       # enumerate keychains
security dump-keychain -a -d                     # dump metadata + secrets (storm of prompts)
security find-generic-password -a "Slack" -g       # one app's secret
security set-generic-password-partition-list \    # rewrite an entry's partition list
         -s "test service" -a "test account" -S

The subtlety is avoiding the prompts. The Security framework’s SecItemCopyMatching takes a dictionary of attributes, and the difference between a loud attack and a silent one is a single flag.

Enumerate silently, then decrypt only what is free
// kSecReturnData = false: return metadata only, decrypt nothing, prompt nothing.
// Map the whole vault first, quietly, then pick the entries you can take.
query = {
    kSecClass:            kSecClassGenericPassword,
    kSecReturnAttributes: true,
    kSecReturnData:       false,   // <- the difference between loud and silent
    kSecMatchLimit:       kSecMatchLimitAll,
};
SecItemCopyMatching(query, &items);   // no decryption, so no user pop-up

From the map, the attacker reads each entry’s ACL with SecAccessCopyACLList and looks for entries that will decrypt without a prompt. Three cases decide the move:

  • Partition ID is apple. Any Apple-signed process qualifies, so the attacker simply asks from one: an osascript or a system Python interpreter can read the secret with no injection at all.
  • Exactly one trusted app is listed. Then the attacker must become that app: inject a dylib into it (or otherwise run code inside it) so the request comes from a process with the right code signature, and the secret exports silently.
  • The keychain is unlocked and root is available. The master key is in securityd; a privileged attacker can scrape it from process memory and unwrap everything, sidestepping the ACL entirely.

Tooling automates this: Chainbreaker for the offline crack, and enumerators such as LockSmith that walk the keychain and pull exactly the secrets that will not raise a prompt. One more gift to attackers: entries carry an Invisible flag to hide from the Keychain Access UI, and a General field for metadata that is not encrypted. Applications have been caught storing live authentication tokens in that unencrypted field, handing them over with no cryptography to break at all.

How to defend it

The defences line up against the attacks one for one.

  • Make the offline crack hopeless: a long, unique login password. PBKDF2 only buys time proportional to how unguessable the password is. A strong passphrase turns a stolen file into a wall; a weak or reused one turns it into a formality. This is the single highest-value control.
  • Keep the file encrypted at rest with FileVault. Full-disk encryption means a keychain file lifted from a powered-down or stolen machine is unreadable before the password even enters the picture.
  • Lock the vault, and consider a separate one. An unlocked keychain is the runtime attacker’s whole opportunity. Set it to lock on sleep and after a short idle, and keep your crown-jewel secrets in a separate keychain with a different password and an aggressive auto-lock, so a live session does not expose everything.
  • Tighten ACLs and partition IDs. Never leave a secret trusting all applications or a partition ID of apple when it does not need to; that is what lets osascript walk in. Pin high-value entries to a specific teamid or cdhash.
  • Block code injection into trusted apps. The “inject into the one trusted app” attack dies if that app ships with the hardened runtime and library validation, which refuse to load unsigned or foreign-team libraries. Developers should enable both and avoid the get-task-allow and disable-library-validation entitlements in release builds.
  • Put real secrets behind the Secure Enclave. For anything that matters, use the data-protection keychain with an access control that requires user presence: create items with SecAccessControlCreateWithFlags using .biometryCurrentSet or .userPresence, and an accessibility of kSecAttrAccessibleWhenUnlockedThisDeviceOnly. The key never leaves the Enclave, so scraping securityd memory yields nothing usable, and every read demands Touch ID or the passcode.
  • Never use the unencrypted General field for secrets. It is metadata, in the clear. Tokens and passwords belong in the encrypted data, always.
  • Watch for the loud tells. Endpoint tooling can flag the behaviours these attacks need: security dump-keychain runs, processes calling task_for_pid against securityd, dylib injection into signed apps, and reads of login.keychain-db by unexpected processes.

The password is the keystone, the Enclave is the upgrade. Classic file keychains ultimately reduce to “how good is the login password,” because everything unwraps from it and the file can be stolen. The modern answer is to stop relying on that entirely: bind the highest-value secrets to the Secure Enclave with biometric access control, so there is no exportable key for an attacker to reach, on disk or in memory.

The short version. The macOS Keychain is an encrypted vault whose contents unwrap from your login password through PBKDF2 to a master key to per-item keys, all under Triple DES. The file has no protection of its own, so a stolen keychain plus a weak password is game over offline, and an unlocked keychain is defeated at runtime by abusing the ACL: an apple partition ID invites any Apple-signed process, a single trusted app invites code injection, and root invites a memory scrape of securityd. Defend with a strong login password and FileVault, tight ACLs and partition IDs, hardened-runtime apps that reject injection, and, above all, Secure Enclave-backed items with biometric gating so the crown jewels never exist as an exportable key.

References & further reading

  1. Cody Thomas, “Lock Picking the macOS Keychain” (OBTS v5.0). The ACL, partition ID, and silent-export mechanics in depth.
  2. Apple, Keychain data protection. The data-protection keychain, accessibility classes, and the Secure Enclave.
  3. Apple Developer, SecItemCopyMatching and the Keychain Services API. The attributes, including kSecReturnData, referenced above.
  4. n0fate, Chainbreaker. An open parser and decryptor for the file format described here, useful for understanding it and for authorised forensics.
← All guides Red teaming →

Want this tested on you?

Reading about it is one thing. Seeing it proven on your own systems is another.