Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

macOS Keychain Copy

macos-keychain-copy creates a generic-password item that command-line automation can read through macOS's /usr/bin/security tool.

The secret is entered interactively on the controlling terminal. It is never supplied as a command argument, written to shell history, or printed by this tool.

Why

Credentials stored in Passwords/iCloud Keychain may require user presence and may not be available through the legacy security CLI. This tool creates a separate, explicitly weaker generic-password copy for automation while leaving the original credential untouched.

Use it only when that trade-off is appropriate for the credential.

Requirements

  • macOS
  • Python 3.10 or newer
  • /usr/bin/security

Installation

With pipx:

pipx install git+https://github.com/Glutexo/macos-keychain-copy.git

Or in a virtual environment:

python3 -m venv .venv
. .venv/bin/activate
python -m pip install .

Usage

Store a new copy

keychain-copy store com.example.automation user@example.com

The underlying system command asks twice:

password data for new item:        # enter the secret
retype password for new item:      # enter the same secret again

Input is intentionally invisible. If macOS asks to unlock the login Keychain, authenticate in the separate system dialog; the command waits without imposing its own timeout.

store deliberately refuses to update an existing matching item. The system CLI cannot reliably narrow an existing item's older access-control list during -U updates. To replace a copy, explicitly run delete, review the result, and then run store again.

By default, /usr/bin/security is trusted to read the new item. Supplying any --trusted-app option replaces that default; repeat the option to name every application that should be trusted. Each value must be an absolute path to an existing executable:

keychain-copy store com.example.automation user@example.com \
  --trusted-app /usr/bin/security \
  --trusted-app /absolute/path/to/example-client

If /usr/bin/security is omitted from --trusted-app, the tool does not perform its usual read-back verification after storing. This avoids requesting access through an application you deliberately chose not to trust. A later check may require authorization or report the item as inaccessible.

Optional metadata:

keychain-copy store com.example.automation user@example.com \
  --label "Example automation credential" \
  --kind "Application password"

Check accessibility without displaying the secret

keychain-copy check com.example.automation user@example.com

Output and exit status:

  • accessible, exit 0
  • not accessible, exit 1

Delete the copy

keychain-copy delete com.example.automation user@example.com

Successful deletion prints deleted and exits 0. A missing item prints not found and exits 1; other deletion failures are reported as errors on standard error and also exit 1.

Consume the secret safely

Resolve the default user Keychain, perform a targeted lookup there, and consume the result inside the same process. Do not print it:

import shlex
import subprocess

security = "/usr/bin/security"
default_result = subprocess.run(
    [security, "default-keychain", "-d", "user"],
    check=True,
    capture_output=True,
    text=True,
)
keychains = shlex.split(default_result.stdout)
if len(keychains) != 1:
    raise RuntimeError("could not determine the default user Keychain")

secret = subprocess.run(
    [
        security,
        "find-generic-password",
        "-s",
        "com.example.automation",
        "-a",
        "user@example.com",
        "-w",
        keychains[0],
    ],
    check=True,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
).stdout.rstrip(b"\r\n")

# Use secret in this process without logging it or placing it in child argv.
del secret

For network requests, prefer an in-process client so the secret does not appear in process arguments.

Security model

  • The original credential is not read, modified, or deleted.
  • A new generic-password item is created in the default Keychain.
  • The default user Keychain path is resolved immediately before store. The system add command writes to that default; the cached exact path is then passed explicitly to check and delete, avoiding ambiguous matches elsewhere in the Keychain search list.
  • The secret is entered directly into /usr/bin/security through the terminal.
  • The -w option is last and has no value, so the secret never appears in argv.
  • check discards both secret output and diagnostics.
  • Keychain ACL entries are limited to the executable paths passed with -T; the default is /usr/bin/security. Trusting /usr/bin/security still allows any process running as the same user to invoke that binary and request the copied credential, so it is not caller-level isolation.
  • This copy is intentionally easier to automate and therefore weaker than a credential protected by user-presence checks.

See SECURITY.md for disclosure and operational guidance.

Development

Run unit tests:

PYTHONPATH=src python3 -m unittest discover -s tests -v

Run the opt-in real-Keychain integration test on macOS:

RUN_KEYCHAIN_INTEGRATION=1 \
PYTHONPATH=src \
python3 -m unittest tests.test_macos_integration -v

The integration test uses a PTY to exercise both real hidden prompts with a randomly generated synthetic value, verifies targeted retrieval, cleans up the temporary item, and never uses a real credential.

License

MIT

About

Safely create CLI-readable macOS Keychain credential copies for automation

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages