kc-cli — kc for macOS keychain files
The kc binary (crate 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; 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
kccan be unlocked, searched, read, and updated withsecurity. - Keychains created by
securitycan be read and updated withkc, and remain usable withsecurityafterward.
$ 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]
[--access-mode MODE] [--access-default DECISION]
[--no-access-policy] 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 add identity (-c CERT -k KEY | --pkcs12 PFX) [-l LABEL] [-T APP]... FILE
kc import identity INPUT [-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 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
kc trust SELECTOR (-T APP... | --trust-requirement PATH=FILE...
| --prompt | -A) FILE
kc cp SELECTOR [--to-password-env NAME] [--to-password-file 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:
[-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
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:
kc create saves that hybrid / prompt policy by default. New items
pre-authorize no native application, the same ACL as security -T "", so
securityd prompts every caller. Use --no-access-policy for the old unmanaged
behavior, or select another initial policy with --access-mode and
--access-default.
Modes separate the two enforcement systems:
extendedmakes directkcsecret reads honorallow,prompt, ordeny.nativeinherits trusted applications on new items forsecurityd.hybriddoes both.
A direct kc prompt decision is refused in scripts unless --interactive
is present; then kc asks through /dev/tty. Native access goes through
securityd and uses the macOS authorization UI. 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 distinguishes any application, no pre-authorized application
(prompt every caller), and 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. kc trust --prompt removes every
pre-authorized application from one item; kc trust -A allows every
application without warning.
Supplying the keychain password
Commands that decrypt or modify data accept mutually exclusive password sources:
-p PASSWORDsupplies 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-EusesKC_PASSWORD.-F FILEreads the first line of a file;-F -reads standard input.--password-gen[=TEMPLATE]generates a new password; without a template it usessha1, producing 20 random bytes as 40 lowercase hexadecimal characters.--password-policy alphanumeric|mixed-case|symbol|securechanges the generated alphabet.mixed-caseguarantees lowercase, uppercase, and a digit;secureadditionally 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:
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:
textprints aligned field lists and is the default.jsonprints a stable JSON envelope.secretprints 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.
|
|
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:
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 — a record's number is the slot it occupies:
| Slot value | Meaning |
|---|---|
| Even and nonzero | Record offset relative to the table |
| Odd | Free-list link: `(28 + 4 × 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:
| 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;
- byte-identical agreement with
security delete-generic-passwordfor 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
securitytool.
Unknown values are named accordingly—for example, free_list_head and
AclEntry::subject_words—and preserved rather than inferred.
Running the tests
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 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:
kccreates files with mode0600and 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
-wexposes it through process arguments; prefer an interactive prompt or standard input. kcpreserves ACL forms it does not model instead of rewriting them, andkc trustrefuses to rewrite an ACL it could not parse rather than replacing it with a canonical one.kc export keyandkc export identitywrite private key material; the file they create is mode0600, and an existing file's mode is taken down to0600rather than inherited.- Direct file access bypasses
securitydand 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.
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:
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:
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/bin/kc.rs command-line interface (uses keychain-rs)
Format internals live in keychain-rs.
License
MIT. The format documentation draws on the GFDL-licensed dtformats specification and Apple's APSL-licensed source. No code from either is included.