kc-cli 0.1.0

Read and write macOS keychain files without the Security framework
Documentation

kc-cli — direct access to macOS keychain files

The kc binary (crate kc-cli) creates, reads, and updates macOS .keychain-db databases without using the Security framework or securityd. It implements the file format directly, so it also works in environments where the system APIs are unavailable or impractical—for example, over SSH, in CI, or with a keychain copied from another Mac.

The implementation is interoperable with Apple's tooling:

  • Keychains created by kc can be unlocked, searched, read, and updated with security.
  • Keychains created by security can be read and updated with kc, and remain usable with security afterward.
$ kc create ~/demo.keychain
Keychain password: ····
Confirm: ····
created /Users/you/demo.keychain

$ kc add generic -a alice -s github.com -D token ~/demo.keychain
Keychain password: ····
Secret: ····
Confirm: ····
stored

$ security find-generic-password -a alice -s github.com -w ~/demo.keychain
gh-token-abc

The final command uses Apple's security tool to read an item written by kc.

Commands

kc create [--idle-timeout SECS] [--no-lock-on-sleep] FILE
kc info FILE
kc show [-d] [--all] FILE
kc ls FILE
kc verify FILE

kc add generic     -a ACCOUNT -s SERVICE [-w SECRET] [-l LABEL] [-D KIND]
                   [-j COMMENT] [-G GENERIC] [-T APP]... FILE
kc add internet    -a ACCOUNT -s SERVER [-w SECRET] [-l LABEL] [-D KIND]
                   [-j COMMENT] [-S DOMAIN] [--path PATH] [-P PORT]
                   [-r PROTOCOL] [-t AUTHTYPE] [-T APP]... FILE
kc add appleshare  -a ACCOUNT -v VOLUME [-w SECRET] [-l LABEL] [-D KIND]
                   [-j COMMENT] [-s SERVER] [--address ADDR]
                   [--signature SIG] [-T APP]... FILE
kc add identity    -c CERT -k KEY [-l LABEL] [-T APP]... FILE

kc find generic    [-a ACCOUNT] [-s SERVICE] [-l LABEL] [-D KIND]
                   [-j COMMENT] [-G GENERIC] [--attr NAME=VALUE]... [-w] FILE
kc find internet   [-a ACCOUNT] [-s SERVER] [-S DOMAIN] [--path PATH]
                   [-P PORT] [-l LABEL] [-D KIND] [-j COMMENT]
                   [--attr NAME=VALUE]... [-w] FILE
kc find appleshare [-a ACCOUNT] [-v VOLUME] [-s SERVER] [--address ADDR]
                   [--signature SIG] [-l LABEL] [-D KIND] [-j COMMENT]
                   [--attr NAME=VALUE]... [-w] FILE
kc find identity   [-l LABEL] FILE

kc completions SHELL

Supplying the keychain password

Commands that decrypt or modify data accept the keychain password from one of three mutually exclusive sources:

kc find generic -a alice -w -e KC_PASSWORD ~/demo.keychain
kc find generic -a alice -w -f ~/.kc-pw    ~/demo.keychain
kc find generic -a alice -w -f -           ~/demo.keychain
  • -e NAME reads the password from an environment variable.
  • -f FILE reads the first line of a file.
  • -f - reads the password from standard input.

Without either option, kc reads from standard input when it is connected to a pipe and prompts when it is connected to a terminal.

There is no option that places the keychain password directly in the process arguments. Command arguments are visible to other processes and are often retained in shell history. The optional -w SECRET form for an item secret has the same exposure; omit it to read the secret interactively or from standard input.

Querying attributes

--attr NAME=VALUE filters on any attribute printed by kc show. Attribute names and rendered values use the same representation in both commands, so a filter such as --attr ptcl=htps works even though the protocol is stored as an integer. Repeat the option to require multiple attributes.

Adding identities

kc add identity accepts a certificate and an unencrypted PKCS#8 private key in PEM or DER form. It does not parse PKCS#12 bundles directly. A bundle can be split first:

openssl pkcs12 -in bundle.p12 -nokeys -out cert.pem
openssl pkcs12 -in bundle.p12 -nocerts -nodes |
  openssl pkcs8 -topk8 -nocrypt -out key.pem

kc add identity -c cert.pem -k key.pem ~/demo.keychain

Output formats

--format controls command output:

  • text prints aligned field lists and is the default.
  • json prints a stable JSON envelope.
  • secret prints only the decrypted secret.

--json is shorthand for --format json. On find, -w is shorthand for secret-only output. On show, --format secret prints one secret per item and implies -d.

kc find generic -a alice --format secret ~/demo.keychain | pbcopy
kc --format json find internet -s example.com ~/demo.keychain |
  jq .item.port

Exit codes are stable:

Code Meaning
0 Success
44 No item matched
45 Wrong password or locked keychain
46 Duplicate item
1 Other error

Item attributes are not encrypted and can be read without the password. Decrypting secrets requires it:

kc show ~/demo.keychain
kc show -d ~/demo.keychain

Verification

kc verify checks the parts of the database that can be validated by a reader:

  • the database blob signature;
  • every key blob signature;
  • the presence and successful unwrapping of each item's key; and
  • complete understanding of every index region.
$ kc verify ~/demo.keychain
database signature   ok
key signatures       2/2 verified
items readable       2/2
index regions        11/11 understood

File format

The container format is documented in dtformats: MacOS keychain database file format. Multibyte values are big-endian.

file header (20 bytes: "kych", version, tables offset)
tables array: size, count, count × offset
  table: header (28 bytes), record-slot array, records, index region
    record: header (24 bytes), attribute offsets, key data, attributes
commit counter (u32 following the tables array)

A keychain is a CSSM database with a self-describing schema. Four schema tables define the attributes of the remaining relations. kc reads that schema rather than hard-coding password and key-record layouts.

Several format details are particularly important:

Detail Representation
Attribute order The attribute-offset array follows schema-table order, not attribute-ID order.
Attribute names Password relations generally use four-character codes such as acct, svce, and srvr; PrintName and Alias use string names.
Offsets Attribute offsets are relative to the record and have their low bit set. Index offsets are relative to the table.

Integers are stored as four raw bytes. Dates use the fixed 16-byte form YYYYMMDDhhmmssZ, followed by a NUL and no length prefix. Other values use a four-byte length followed by data padded to a four-byte boundary.

Record slots and free lists

A table's slot array is indexed by record number:

Slot value Meaning
0 Never used
Even and nonzero Record offset relative to the table
Odd Free-list link used by macOS's record allocator

Odd entries are easy to mistake for offsets because they may not appear in a keychain that has never had records deleted. kc preserves existing free-list links and does not reuse those slots.

Blob versions and signatures

Apple keychains use more than one blob version. Keychains written by Apple's tools commonly use 0x100 for legacy files and 0x200 for partition-aware files. The blob version determines the signature algorithm:

Version Signature
0x100 Legacy BSafe-compatible HMAC behavior
0x101, 0x200 HMAC-SHA1

Apple's dbcrypto.cpp selects the legacy algorithm only for 0x100. A blob must therefore be signed according to its declared version.

Encryption

The container specification does not describe all cryptographic details. The implementation follows Apple's published ssblob.h, dbcrypto.cpp, and HmacSha1Legacy.c sources and is checked against keychains produced by macOS.

password
  └─ PBKDF2-HMAC-SHA1(salt, 1000 iterations, 24 bytes) → master key

master key
  └─ 3DES-EDE3-CBC(DbBlob.iv) → encryption key (24) + signing key (20)

encryption key
  └─ unwrap(KeyBlob) → item key (24)

item key
  └─ 3DES-EDE3-CBC(ssgp.iv) → item secret

Each item stores its encrypted secret in an ssgp payload containing a four-byte magic value, a 16-byte label, an eight-byte IV, and ciphertext. The label identifies the symmetric-key record containing that item's wrapped key.

Apple's custom key wrapping uses two CBC passes with a byte reversal between them:

inner = 3DES-CBC(db key, iv, descriptive_data_length(0) || item key)
blob  = 3DES-CBC(db key, MAGIC_CMS_IV, reverse(iv || inner))

The ssgp payload carries its own IV. Its label matches the Label attribute of the key record that protects it, forming the link between the item and its key.

Version 0x100 blobs use Apple's legacy HMAC behavior for compatibility. The implementation reproduces that behavior when the version requires it; later blob versions use standard HMAC-SHA1.

Indexes

Each table ends with an index region:

region := size, count, count × index offset
index  := size, id, kind, attribute count, attribute ids,
          entry count, entry offsets, record numbers, entries
entry  := payload size, key values

Offsets are relative to the table. Entries are sorted by key, and the record-number array follows the same order. Because inserting a record changes table-relative offsets, kc rebuilds index regions from records whenever it writes the database.

Unique-index attributes must be present even when their values are empty. For example, an internet-password item without a port stores port = 0 and an empty path. This matches the records written by macOS and keeps the item represented in the relation's unique index.

Evidence and interoperability testing

The implementation combines three sources of evidence:

  • the dtformats container specification;
  • Apple's published source and CSSM/CDSA headers; and
  • byte-level comparison with keychains written by macOS.

The test suite checks, among other things:

  • byte-identical parsing and serialization of existing keychains;
  • preservation of record slots, free-list links, unknown fields, and ACLs;
  • signature selection by blob version;
  • schema-defined attribute order and naming;
  • key derivation, wrapping, and secret decryption;
  • index reconstruction and ordering;
  • duplicate detection through relation unique indexes;
  • identity records and on-demand relation creation; and
  • interoperability in both directions with Apple's security tool.

Unknown values are named accordingly—for example, unknown_free_list and AclEntry::subject_words—and preserved rather than inferred.

Running the tests

cargo test -p kc-cli
cargo test -p kc-cli --features trust-apps

The suite covers keychains created by both kc and security. System-generated fixtures are created during the test run instead of being committed as binary files. Tests that require security skip when it is unavailable.

tests/keychain_interop.rs provides the principal end-to-end check: it creates and updates keychains with kc, then requires Apple's tool to unlock, search, read, and update them.

To extract the schema from another macOS version:

security create-keychain -p x /tmp/fresh.keychain
python3 xtask/extract-schema.py /tmp/fresh.keychain > src/apple_schema.rs

Security considerations

The legacy keychain format uses cryptography that is weak by current standards: PBKDF2-HMAC-SHA1 with 1,000 iterations protects 3DES key material. Anyone who obtains a keychain file can attempt password guesses offline. Use a strong, random password and protect the file as secret material.

Additional considerations:

  • kc creates files with mode 0600 and preserves the mode of existing files.
  • 3DES and SHA-1 are properties of the format and are retained for interoperability.
  • Secret buffers are cleared when dropped where supported by the implementation.
  • Passing an item secret with -w exposes it through process arguments; prefer an interactive prompt or standard input.
  • kc preserves ACL forms it does not model instead of rewriting them.
  • Direct file access bypasses securityd and therefore bypasses its ACL enforcement. Anyone with both the file and its password can decrypt its secrets.

Access control

An item's key blob contains an owner entry and authorization entries. kc supports both unrestricted and application-restricted forms:

kc add generic -a u -s svc ~/k.keychain
kc add generic -a u -s svc -T /usr/bin/security ~/k.keychain
kc add generic -a u -s svc \
  -T /usr/bin/security \
  -T /bin/ls \
  ~/k.keychain

A restricted entry stores one subject block per application:

subject type 116, then for each application:
  20-byte legacy CDSA code hash
  binary path
  designated requirement

When built with --features trust-apps, -T obtains the application's designated requirement with macho-codesign. A requirement can also be supplied explicitly:

csreq -r='identifier "com.example.app" and anchor apple' -b /tmp/req.bin
kc add generic -a u -s svc \
  --trust-requirement '/Applications/App.app=/tmp/req.bin' \
  ~/k.keychain

The requirement stored in the ACL matches the application's signed designated requirement. The accompanying 20-byte value is a legacy CDSA code hash rather than the modern cdhash. Testing confirms that macOS accepts a zero value on the allowed access path, so kc uses zeros instead of synthesizing an unsupported value.

macOS applies the application restriction to the six-tag authorization entry (24, 28, 37, 38, 59, 115) while leaving the single-tag entry (35) unrestricted. kc follows that structure when generating restricted ACLs.

These ACLs govern access through securityd. They do not restrict kc itself, because kc decrypts the database directly.

Source layout

src/format.rs          kych tables, records, and attribute values
src/schema.rs          relations and attributes read from the file
src/index.rs           index parsing, rebuilding, and sorting
src/crypto.rs          key derivation, signatures, and key wrapping
src/cssm.rs            typed CSSM key headers, GUIDs, and dates
src/acl.rs             ACL blob structures
src/records.rs         typed item, key, and certificate attributes
src/requirement.rs     designated requirements for trusted applications
src/der.rs             certificate DER field location
src/db.rs              open, unlock, query, and decrypt operations
src/write.rs           keychain creation and item insertion
src/secret.rs          cleared key material and CSPRNG support
src/output.rs          text and JSON output
src/apple_schema.rs    generated Apple schema data
src/bin/kc.rs          command-line interface
xtask/extract-schema.py schema extraction from a macOS keychain

License

MIT. The format documentation draws on the GFDL-licensed dtformats specification and Apple's APSL-licensed source. No code from either is included.