# kc-cli — direct access to macOS keychain files
The `kc` binary (crate [`kc-cli`](https://crates.io/crates/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.
```console
$ 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
```text
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:
```bash
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:
```bash
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`.
```bash
```
Exit codes are stable:
| `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:
```bash
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.
```console
$ 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](https://github.com/libyal/dtformats/blob/main/documentation/MacOS%20keychain%20database%20file%20format.asciidoc).
Multibyte values are big-endian.
```text
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:
| 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:
| `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:
| `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.
```text
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:
```text
```
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:
```text
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
```bash
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:
```bash
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:
```bash
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:
```text
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`](https://github.com/bryanmatteson/macho). A requirement can
also be supplied explicitly:
```bash
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
```text
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.