Skip to main content

🔑 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 facade — get_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

Release files for apwlib 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for apwlib 0.1.2
File Size Uploaded
apwlib-0.1.2.tar.gz 24.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for apwlib 0.1.2
File Interpreter ABI Platform
apwlib-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 57.1 kB

Release files / apwlib-0.1.2.tar.gz

Download URL apwlib-0.1.2.tar.gz
Size 24.8 kB
Tags Source
SHA-256 checksum
How to use checksums
f7545b27f7de62a7fb35970f640a705c41877661d63da78a9f12462b54306a95
BLAKE2b-256 checksum
How to use checksums
d15f4a11b1a149af383abce7aecc0c466dfbac3540ef93586213d3bd75cf2819
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release files / apwlib-0.1.2-py3-none-any.whl

Download URL apwlib-0.1.2-py3-none-any.whl
Size 32.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1c48b808f75af46213b47420c354bd1fb764f53edfe540248b39d3feaca17844
BLAKE2b-256 checksum
How to use checksums
244332533a9749aedc3f48ae6428c1ec671af3d718541903abddc0d1b32bc4b8
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page