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.
// 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.
// 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:
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.
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.
# 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 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.
// 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: anosascriptor 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
applewhen it does not need to; that is what letsosascriptwalk in. Pin high-value entries to a specificteamidorcdhash. - 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
SecAccessControlCreateWithFlagsusing.biometryCurrentSetor.userPresence, and an accessibility ofkSecAttrAccessibleWhenUnlockedThisDeviceOnly. The key never leaves the Enclave, so scrapingsecuritydmemory yields nothing usable, and every read demands Touch ID or the passcode. - Never use the unencrypted
Generalfield 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-keychainruns, processes callingtask_for_pidagainstsecurityd, dylib injection into signed apps, and reads oflogin.keychain-dbby 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
- Cody Thomas, “Lock Picking the macOS Keychain” (OBTS v5.0). The ACL, partition ID, and silent-export mechanics in depth.
- Apple, Keychain data protection. The data-protection keychain, accessibility classes, and the Secure Enclave.
- Apple Developer, SecItemCopyMatching and the Keychain Services API. The attributes, including
kSecReturnData, referenced above. - n0fate, Chainbreaker. An open parser and decryptor for the file format described here, useful for understanding it and for authorised forensics.