# kc-cli — `kc` for 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`. The file format implementation lives in
[`keychain-rs`](https://crates.io/crates/keychain-rs); this crate is the CLI.
It 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] KEYCHAIN
kc info [KEYCHAIN]
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 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 set SELECTOR [-w [SECRET]] [--set-label NAME] [--set-kind KIND]
[--set-comment TEXT] [--set-generic BYTES]
[--set NAME=VALUE]... FILE
kc rm item SELECTOR [--all] FILE
kc rm identity [-l LABEL] [--hash HEX] FILE
kc rm cert [-l LABEL] [--hash HEX] FILE
[-T APP]... SOURCE DESTINATION
kc export cert [-l LABEL] [--der] [-o FILE] KEYCHAIN
kc export key [-l LABEL] [--der] [-o FILE] KEYCHAIN
kc export identity [-l LABEL] [-o FILE] [--pkcs12 [--pem]] KEYCHAIN
kc passwd [--new-password-env NAME] [--new-password-file FILE] FILE
kc settings [-t SECONDS | --no-timeout] [-l | --no-lock-on-sleep] FILE
kc config show
kc config set keychains.default KEYCHAIN
kc config set search.paths PATH [PATH...]
kc config append search.paths PATH [PATH...]
kc config prepend search.paths PATH [PATH...]
kc access show [KEYCHAIN]
kc access set [--mode extended|native|hybrid] [--default allow|prompt|deny]
[-T APP]... [--trust-requirement PATH=FILE]... [KEYCHAIN]
kc access clear [KEYCHAIN]
kc access apply [--to securityd] [KEYCHAIN]
kc access audit [--against securityd] [KEYCHAIN]
kc completions SHELL
```
Every `KEYCHAIN` operand accepts an explicit path or a bare name. Bare names
are searched under `~/Library/Keychains` as `NAME.keychain-db`,
`NAME.keychain`, then `NAME`; additional directories can be set with
`kc config set search.paths "path1" "path2"`. `system` means
`/Library/Keychains/System.keychain`. When the operand is omitted, `login` is
used unless changed with `kc config set keychains.default machina`; settings
are stored in `~/.config/keychain.kdl`.
`SELECTOR` is the same set of flags for every mutating command — the way `find`
names an item, in one place:
```text
[-c generic|internet|appleshare] [-a ACCOUNT] [-s SERVICE] [--server HOST]
[--security-domain REALM] [--path PATH] [-P PORT] [-v VOLUME] [--label NAME]
[--kind KIND] [--comment TEXT] [--generic BYTES] [--attr NAME=VALUE]...
```
For add/find metadata, `-A`, `-S`, `-L`, and `-C` are visible aliases for
account, service, label, and comment. The lowercase `-a`, `-s`, `-l`, and `-j`
forms remain compatible. Selector-based commands use `-L` and `-C`; `kc trust`
keeps `-A` for its established `--any` meaning.
A mutation with no selector is refused rather than applied to whatever happens
to be first, and one that matches more than one item is refused unless the
command takes `--all`.
### Changing what is already there
```bash
kc set -a alice -w ~/demo.keychain # prompt for a new secret
kc set -a alice --set-comment "rotated" ~/demo.keychain # attributes only, no password needed
kc rm item -a alice ~/demo.keychain
kc trust -a alice -T /usr/bin/security ~/demo.keychain # restrict, or -A for any application
kc cp -a alice ~/demo.keychain ~/other.keychain
kc passwd ~/demo.keychain
kc settings -t 900 ~/demo.keychain
```
An update keeps the item's record, its record number and its item key, preserves
`cdat`, and moves `mdat`; only the payload is re-sealed, under a fresh IV.
Attributes that identify the item — the ones in its relation's unique index,
such as `acct` and `svce` — are refused: changing one makes it a different item,
which is a delete and an add.
`kc rm item` deletes the item **and** the key record that protects it, exactly as
macOS does; `kc rm cert` deletes a certificate and leaves its private key behind,
the way `security delete-certificate` does, while `kc rm identity` removes both.
`kc passwd` keeps the database's master keys and re-seals them under the new
password with a fresh salt and IV, so every existing item stays readable. It
refuses before writing anything if the old password is wrong.
### Keychain-wide access policy
Apple's database format stores ACLs per item, not on the keychain itself.
`kc access` provides the higher-level keychain policy and can project it onto
every password and private-key item:
```bash
kc access set --mode hybrid --default prompt \
-T /usr/bin/security machina
kc access apply --to securityd -E KC_PASSWORD machina
kc access audit --against securityd machina
```
Modes separate the two enforcement systems:
- `extended` makes direct `kc` secret reads honor `allow`, `prompt`, or `deny`.
- `native` inherits trusted applications on new items for `securityd`.
- `hybrid` does both.
A `prompt` decision is refused in scripts unless `--interactive` is present;
then `kc` asks through `/dev/tty`. Native ACLs authenticate signed applications
using their designated requirements. A shell invocation cannot securely attest
which application called it, so `kc` never treats a process path as a signed
caller identity.
`access apply` is explicit because it rewrites existing item ACLs. Apple's
native ACL can represent “any application” or trusted applications with a
prompt for other callers; it cannot represent an unconditional deny, so a
`deny` policy remains an extended-policy feature and is rejected for native
projection.
The policy is stored alongside keychain search settings in
`~/.config/keychain.kdl`. Per-item `kc trust` remains available as an override.
New items inherit native/hybrid policy unless explicit `-T` or
`--trust-requirement` options are supplied.
### Supplying the keychain password
Commands that decrypt or modify data accept mutually exclusive password
sources:
```bash
kc find generic -a alice -w -p "$(secret-tool lookup kc login)" login
kc find generic -a alice -w -E KC_PASSWORD ~/demo.keychain
kc find generic -a alice -w login -E
kc find generic -a alice -w -F ~/.kc-pw ~/demo.keychain
kc create --password-gen machina
```
- `-p PASSWORD` supplies a value directly. It is visible in process arguments;
prefer a shell expression such as `-p "$(command)"`, or use `-E`/`-F`.
- `-E [ENV_VAR]` reads an environment variable; bare `-E` uses `KC_PASSWORD`.
- `-F FILE` reads the first line of a file; `-F -` reads standard input.
- `--password-gen[=TEMPLATE]` generates a new password; without a template it
uses `sha1`, producing 20 random bytes as 40 lowercase hexadecimal characters.
- `--password-policy alphanumeric|mixed-case|symbol|secure` changes the
generated alphabet. `mixed-case` guarantees lowercase, uppercase, and a
digit; `secure` additionally guarantees punctuation. It requires
`--password-gen`.
Without an option, `kc` reads from standard input when it is connected to a
pipe and prompts without echoing when connected to a terminal.
### 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 either a certificate plus an unencrypted PKCS#8 or
traditional PKCS#1 RSA private key in PEM or DER form, or a PKCS#12/PFX
container holding one identity:
```bash
kc add identity -c cert.pem -k key.pem ~/demo.keychain
kc add identity --pkcs12 identity.p12 ~/demo.keychain
kc add identity --pkcs12 identity.p12 \
--pkcs12-password-env P12_PASSWORD ~/demo.keychain
```
The PKCS#12 password can come from `--pkcs12-password[=PASSWORD]`,
`--pkcs12-password-env NAME`, or `--pkcs12-password-file FILE`; otherwise it is
read from a pipe or prompted for.
Containers with multiple identities are refused rather than choosing one
implicitly. The bundle's friendly name becomes the identity label unless
`--label` overrides it.
`kc import identity` auto-detects a combined certificate/private-key PEM
document or a PKCS#12 container. PKCS#12 input may be its conventional binary
DER representation or PEM with a `PKCS12` label. `kc export identity` writes
combined PEM by default; `--pkcs12` writes encrypted DER PKCS#12, and
`--pkcs12 --pem` writes the same container in PEM form.
PKCS#12 support is provided by `keychain-rs`; publish that crate before
packaging `kc-cli`, then run `mise run kc-cli:package`.
### 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 — a record's number *is* the
slot it occupies:
| Even and nonzero | Record offset relative to the table |
| Odd | Free-list link: `(28 + 4 × slot) | 1`, pointing at the next free slot |
| `0` | Free, and the end of the free-list chain |
The table header's sixth word is the head of that chain. It points at the
**highest** free slot, which links down to the next, and so on to a slot holding
`0`; a table with no free slots carries `0`. A freshly created table carries
`0x1d`, which is simply the link to its single free slot 0. The chain is a
function of which slots are free, not of the order they were freed in: two
keychains that reach the same state by opposite deletion orders are byte-identical.
Deleting a record frees its slot and leaves the array length alone, unless the
freed slot is the last one — then that entry is removed and the count drops by
one, never cascading past other trailing free slots and never below a single
slot. Inserting takes the head of the chain and reuses that slot's record
number, and the new record's bytes go at the *end* of the records region even
when the slot reused is a low-numbered one. `kc` reproduces all of this: a
`kc rm item` produces a file byte-identical to what
`security delete-generic-password` produces from the same input.
### 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;
- byte-identical agreement with `security delete-generic-password` for deletes
at the first, middle, and last slot;
- in-place updates, password changes, settings changes, ACL rewrites, copies,
and certificate and identity deletion, each read back with Apple's tool; and
- interoperability in both directions with Apple's `security` tool.
Unknown values are named accordingly—for example, `free_list_head` 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
cargo test -p keychain-rs
```
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 ../keychain/xtask/extract-schema.py /tmp/fresh.keychain > ../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, and
`kc trust` refuses to rewrite an ACL it could not parse rather than replacing
it with a canonical one.
- `kc export key` and `kc export identity` write private key material; the file
they create is mode `0600`, and an existing file's mode is taken down to
`0600` rather than inherited.
- Direct file access bypasses `securityd` and therefore bypasses its ACL
enforcement. Anyone with both the file and its password can decrypt its
secrets.
## A running `securityd` caches what it has opened
`kc` replaces a keychain by writing a temporary file and renaming it, which is
atomic but changes the file's inode. A `securityd` that already has that path
open keeps serving the version it read: after `kc passwd`, `security` may still
unlock with the *old* password, and after `kc rm` it may still return a deleted
item — until something makes it re-open the file.
```bash
security lock-keychain ~/demo.keychain # before the change, if it may be cached
kc passwd ~/demo.keychain
security unlock-keychain ~/demo.keychain # after
```
Two related macOS behaviours are worth knowing, both established by experiment:
`security unlock-keychain` on an **already unlocked** keychain returns success
without checking the password at all, so verifying a rotation means locking
first; and `security show-keychain-info` prints to standard error, not standard
output.
## 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/bin/kc.rs command-line interface (uses keychain-rs)
```
Format internals live in [`keychain-rs`](../keychain/README.md).
## License
MIT. The format documentation draws on the GFDL-licensed dtformats
specification and Apple's APSL-licensed source. No code from either is included.