Skip to main content

Programmatic access to Apple Passwords (iCloud Keychain) on macOS.

Project description

🔑 apwlib

Apple Passwords (iCloud Keychain) as a typed Python API. The library behind apwcli.

CI License: MIT Python 3.12+ macOS

Website · API reference · Examples · How it works


Apple brokers keychain access through a helper that only runs inside an approved browser — there is no public API. apwlib runs that plumbing for you: a background daemon hosts the real iCloud Passwords extension in a headless browser, pairs with the macOS PIN once, and your code gets a small, typed facade.

  • Typed facadeget_password, save_password, get_otp, list_otp returning dataclasses, keyed by site URL
  • Zero babysitting — the daemon auto-starts as a detached singleton and is reused across programs and runs
  • Transparent pairing — pass a pin_provider and an unpaired call pairs itself; apwlib.pinwindow collects the PIN on screen when there's no terminal
  • Nothing leaves your Mac — Apple's own extension does the crypto (SRP pairing, AES-GCM per command); apwlib is local transport around it
  • Small public surface — the package root plus apwlib.pinwindow and apwlib.diagnostics; everything else is private and underscore-named

Install

uv add apwlib    # or: pip install apwlib

Requires macOS with a supported browser installed (Chrome, Brave, Edge, or Chromium); the iCloud Passwords extension itself is downloaded automatically from the Chrome Web Store.

Quickstart

from apwlib import ApplePasswords

pw = ApplePasswords(pin_provider=lambda: input("PIN: "))

for entry in pw.get_password("github.com"):
    print(entry.username)  # entry.password holds the real value

The first call starts the daemon and, if the session isn't paired yet, macOS shows a 6-digit PIN and your pin_provider supplies it. Keep one ApplePasswords instance around and call it whenever you need a credential.

Usage

from apwlib import ApplePasswords, ApwError, SessionError

pw = ApplePasswords(pin_provider=lambda: input("PIN: "))

# Entries for a site. Matching is by registrable domain: a bare host, a full
# URL, or a subdomain all resolve to the same entries. Narrow with a username,
# or omit it for every match. An empty list means no match; reads don't raise
# for "not found".
for entry in pw.get_password("github.com", "me@example.com"):
    login(entry.username, entry.password)

# Create or update a credential.
pw.save_password("example.com", "me@example.com", "s3cret-passw0rd")

# One-time codes: what's available, and the current code.
for code in pw.list_otp("github.com"):
    print(code.username, code.domain)
for code in pw.get_otp("github.com"):
    print(code.code)

# The paired session lives in the browser and can drop; catch SessionError
# to re-pair, or ApwError for anything protocol-level.
try:
    pw.get_password("github.com")
except SessionError:
    print("session dropped — pair again")
except ApwError as exc:
    print(f"request failed (status {int(exc.status)}): {exc}")

Constructor options: pin_provider (callable returning the PIN; without it an unpaired call raises NotPairedError), auto_start=False (require an already-running daemon), socket_path (override the daemon socket).

Pairing

Pairing needs a 6-digit PIN that macOS displays, once per daemon lifetime. A pin_provider callback handles it transparently; to drive it explicitly:

from apwlib import Daemon

daemon = Daemon()
daemon.request_challenge()  # macOS shows a 6-digit PIN
paired = daemon.verify_challenge(input("PIN: "))  # blocks until settled

A pairing cannot outlive the daemon (the helper issues a fresh PIN per handshake by design), so keeping the daemon alive is what keeps the PIN rare.

No terminal to prompt in? apwlib.pinwindow.request_pin is a ready-made pin_provider that collects the PIN in a small on-screen window (six code boxes, opened chromeless in an installed browser) — it's what apwcli uses when stdin is not a TTY:

from apwlib import ApplePasswords
from apwlib.pinwindow import request_pin

pw = ApplePasswords(pin_provider=request_pin)

The window is a plain HTML page with a bundled default stylesheet. Restyle it by passing CSS text (pin_provider=lambda: request_pin(css=...)) or by dropping a replacement at ~/.apwlib/pinwindow.css.

Daemon control & diagnostics

Most programs never touch the daemon — the facade auto-starts and reuses it. For explicit lifecycle control (a setup step, a health endpoint):

from apwlib import Daemon
from apwlib.diagnostics import run_checks

daemon = Daemon()
daemon.start()      # no-op if already running; raises DaemonStartError if not ready
daemon.status()     # {"running": ..., "bridge": ..., "paired": ..., ...}
daemon.restart()    # replace a wedged daemon
daemon.stop()

for check in run_checks(daemon):  # what `apwcli doctor` renders
    print(check.key, check.ok, check.detail)

Data model

Reads return dataclasses (PasswordEntry, OTPEntry):

from apwlib import PasswordEntry

entry = PasswordEntry(username="me@example.com", domain="github.com", password="hunter2")
print(entry.username, entry.domain)
#> me@example.com github.com

PasswordEntry.password is None when the vault withholds it.

Errors

Every failure raises ApwError (or a subclass — SessionError, DaemonNotRunningError, NotPairedError, DaemonStartError, ServerError) carrying a Status:

from apwlib import SessionError

try:
    raise SessionError()
except SessionError as exc:
    print(int(exc.status))
    #> 9

Examples

Runnable scripts, ordered by complexity, live in examples/: reading entries, one-time codes, saving a credential, and explicit daemon control. Run any with uv run python examples/<dir>/main.py.

How it works

The design notes cover the whole story: Apple's helper and its kernel-enforced launch constraint, the SRP pairing and SMSG encryption, and why a (headless) browser is involved at all. The full API reference is generated from the source at michel-tricot.github.io/apwcli.

License

MIT © Michel Tricot

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

apwlib-0.1.2.tar.gz (24.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

apwlib-0.1.2-py3-none-any.whl (32.3 kB view details)

Uploaded Python 3

File details

Details for the file apwlib-0.1.2.tar.gz.

File metadata

  • Download URL: apwlib-0.1.2.tar.gz
  • Upload date:
  • Size: 24.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for apwlib-0.1.2.tar.gz
Algorithm Hash digest
SHA256 f7545b27f7de62a7fb35970f640a705c41877661d63da78a9f12462b54306a95
MD5 62dfcf02a0e416c4a0c10801ce71295d
BLAKE2b-256 d15f4a11b1a149af383abce7aecc0c466dfbac3540ef93586213d3bd75cf2819

See more details on using hashes here.

File details

Details for the file apwlib-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: apwlib-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 32.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for apwlib-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 1c48b808f75af46213b47420c354bd1fb764f53edfe540248b39d3feaca17844
MD5 13d9a7a0ab5b01a1e4168e8aac3d04f1
BLAKE2b-256 244332533a9749aedc3f48ae6428c1ec671af3d718541903abddc0d1b32bc4b8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page