Metadata-Version: 2.4
Name: credstore
Version: 0.3.24
Summary: Cross-platform credential storage — OS keyring with AES-encrypted file backup
Project-URL: Homepage, https://github.com/juzcn/slife
Project-URL: Repository, https://github.com/juzcn/slife
Project-URL: Documentation, https://github.com/juzcn/slife/blob/main/credstore/README.md
Author-email: juzcn <zhangjun@cueb.edu.cn>
License: MIT
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.10
Requires-Dist: keyring-wincred>=0.1.0
Requires-Dist: keyring>=25.0.0
Requires-Dist: keyrings-cryptfile>=1.3.0
Description-Content-Type: text/markdown

# credstore

Cross-platform credential storage — OS keyring with AES-encrypted file backup.

A standalone secret manager that ships with [Slife](https://github.com/juzcn/slife) but has **no dependency on it**. Depends only on `keyring`, `keyring-wincred`, and `keyrings-cryptfile`.

Supports **Windows**, **macOS**, **Linux** (desktop + headless), and **WSL** (Windows Credential Manager via PowerShell bridge).

## Install

```bash
pip install credstore
# or bundled with Slife:
uv tool install git+https://github.com/juzcn/slife.git
```

Verify: `credstore status`

No configuration needed. Run `credstore set-password` to enable encrypted backup.

## CLI

### Setup

```bash
credstore set-password    # creates ~/.credstore/credentials.crypt
```

Path overridable via `CREDSTORE_FILE` env var or `credstore.json5`.

### Commands

| Command | Auth | Description |
|---------|------|-------------|
| `set-password` | sets it | Create or change master key (≥8 chars) |
| `status` | — | Show backend health |
| `set KEY` | master + secret | Atomic dual-write: cryptfile → keyring. Rolls back on keyring failure |
| `get KEY` | — | Keyring only, masked output (`sk-5f…b722`) |
| `get KEY -p` | master | Dual-query keyring + cryptfile, plaintext. Fails on mismatch |
| `delete KEY` | master | Remove from both stores |
| `list` | master¹ | Triple-read: keyring + cryptfile + env. Shows sync status per key |
| `inject KEY… [--shell]` | — | Persist to system env: registry (Win) or shell profile (Unix) |
| `uninject KEY… [--shell]` | — | Remove from system env |
| `reset-keyring` | master | Restore all from cryptfile → keyring (disaster recovery) |
| `reset-backup` | master | Sync keyring → cryptfile |

¹ Master password required only if cryptfile exists.

### `get` Modes

| Mode | Reads from | Output | Use case |
|------|-----------|--------|----------|
| `get KEY` | Keyring only | Masked | Quick check, safe for screen sharing |
| `get KEY -p` | Keyring + cryptfile | Plaintext | Verify consistency, pipe to another tool |

`-p` mode performs a dual-query consistency check:
- Both stores have the value AND they match → prints plaintext
- One store missing → error with recovery instructions
- Values differ → error, tells you which tool to run

### `inject` / `uninject`

`inject` reads a secret from the keyring and persists it to the system environment:

| Platform | Persistence | Activation |
|----------|-------------|------------|
| Windows | Registry (`HKCU\Environment`) + broadcast | Restart shell, or `Invoke-Expression (credstore inject KEY)` |
| Unix | Shell profile (`~/.bashrc`) | New shell, or `eval "$(credstore inject KEY)"` |

When stdout is a TTY, `inject` prints an activation hint instead of the secret. The actual export command only flows through a pipe.

```bash
eval "$(credstore inject DEEPSEEK_API_KEY)"           # Bash/Zsh — activate now
Invoke-Expression (credstore inject DEEPSEEK_API_KEY)  # PowerShell — activate now
```

`uninject` reverses the operation — removes from registry or profile and prints the unset command.

### `list` Output

```
  KEY                  SYSTEM KEYRING   CRYPTFILE        ENV    STATUS
  ────────             ──────────────   ──────────────   ────   ──────
  ANTHROPIC_API_KEY    ✔                ✔                —      synced
  DEEPSEEK_API_KEY     ✔                ✔                ✔      synced
  OPENAI_API_KEY       —                ✔                —      cryptfile only
  ────────             ──────────────   ──────────────   ────   ──────
  3 credential(s) — synced: 2, cryptfile only: 1, env: 1
```

| Column | Meaning |
|--------|---------|
| `SYSTEM KEYRING` | ✔ = stored in OS keyring |
| `CRYPTFILE` | ✔ = stored in encrypted backup |
| `ENV` | ✔ = currently set as environment variable |
| `STATUS` | `synced`, `keyring only`, `cryptfile only`, or `MISMATCH ⚠` |

## Memory Safety

Secrets are immutable Python `str` objects — they cannot be zeroed in place. Mitigations:

1. **Never batch-load** — `list` collects only key names. Sync comparison fetches one value at a time and immediately `del`s it.
2. **Prefer existence checks** — `exists_credential()` / `list_credential_keys()` never retrieve secret content.
3. **Explicit cleanup** — every CLI handler `del`s secret references on all exit paths including error branches.

| Operation | Cleanup |
|-----------|---------|
| `get` / `get_credential()` | Caller must `del` the returned value |
| `set` | `del secret` + `del master_pw` after dual-write |
| `list` | Values fetched one-at-a-time, compared, `del`ed immediately |
| `inject` | Value read → persisted → `del`ed. TTY: no secret on stdout |
| `reset-keyring` | Each value `del`ed after writing to keyring |
| `reset-backup` | Batch load unavoidable; `del entries` + `del master_pw` after sync |

`masked_input()` echoes `*` per keystroke — paste works, actual value never displayed.

## Python API

```python
import credstore

# Read / check / delete (system keyring only, no prompt)
credstore.get_credential("myapp/api_key")      # → str | None
credstore.exists_credential("myapp/api_key")   # → bool  (NEVER returns secret)
credstore.list_credential_keys()               # → list[str]  (NEVER returns values)
credstore.set_credential("myapp/api_key", "sk-…")
credstore.delete_credential("myapp/api_key")   # → bool

# keyring: URI resolution
credstore.is_keyring_uri("keyring:myapp/k")    # → True
credstore.resolve_uri("keyring:myapp/k")       # → the secret value (or KeyError)
credstore.parse_keyring_uri("keyring:srv/k")   # → ("srv", "k") | None

# Shell formatting
credstore.format_export("KEY", "secret", "bash")   # → "export KEY='secret'"
credstore.format_unset("KEY", "bash")              # → "unset KEY"

# Diagnostics
credstore.check_backend()      # → {"available": True, "backend": "…", …}
credstore.get_backend_name()   # → "system keyring + cryptfile (dual-write)"
```

**Python API talks to system keyring only** — no master password, no prompt. Dual-write (keyring + cryptfile) is handled by the CLI.

Callers of `get_credential()` and `resolve_uri()` must `del` the returned value after use. Prefer `exists_credential()` when you only need to know if a credential exists.

## Configuration

Optional `credstore.json5` (searched at `./credstore.json5` then `~/.credstore/config.json5`):

```json5
{
  // Override default cryptfile path
  cryptfile_path: "/custom/path/credentials.crypt",
}
```

Priority: `CREDSTORE_FILE` env var → `credstore.json5` → `~/.credstore/credentials.crypt` (or `./credentials.crypt` in Slife dev mode).

## Architecture

### Backend Matrix

Credstore auto-selects the best available backend via keyring's priority system:

| Platform | Backend | Priority | Mechanism |
|----------|---------|----------|-----------|
| **Windows** | WinCredKeyring | 9.0 | Windows Credential Manager (pywin32) |
| **WSL** | WslBackend | 9.5 | PowerShell → advapi32.dll CredReadW/CredWriteW (C# P/Invoke) |
| **macOS** | Keychain | 5.0 | macOS Keychain |
| **Linux desktop** | SecretService | 5.0 | D-Bus Secret Service (GNOME Keyring / KWallet) |
| **Linux headless** | KeyutilsBackend | 1.5 | Kernel keyring via `add_key`/`keyctl` syscalls (ctypes, zero deps) |

### Dual-Write Flow

```
┌──────────────────────────────────────────────────┐
│  CLI (__main__.py)                               │
│  Interactive: masked_input(), master password     │
│  Atomic dual-write: cryptfile → keyring           │
│  Rollback on keyring failure                      │
├──────────────────────────────────────────────────┤
│  Python API (__init__.py)                        │
│  Programmatic: no prompt, system keyring only     │
├────────────────────┬─────────────────────────────┤
│  System Keyring    │  Cryptfile Backup           │
│  (primary)         │  (encrypted)                │
│  ────────────────  │  ───────────────────────    │
│  Win CredMan       │  keyrings.cryptfile         │
│  WSL (PowerShell)  │  AES-encrypted INI          │
│  macOS Keychain    │  Survives OS pw changes     │
│  Linux Secret Svc  │                             │
│  Linux keyutils    │                             │
└────────────────────┴─────────────────────────────┘
```

### WSL Backend

On WSL, no Linux desktop keyring is available. `WslBackend` bridges to Windows Credential Manager by calling `powershell.exe` with embedded C# that P/Invokes `advapi32.dll` (`CredReadW`, `CredWriteW`, `CredDeleteW`). Priority 9.5 beats `WinCredKeyring` (9.0) when both are installed — our backend fixes target-format and encoding issues.

### Keyutils Backend

On headless Linux (no D-Bus session), `KeyutilsBackend` stores credentials in the Linux kernel's persistent keyring (`@p`). Calls `add_key` and `keyctl` syscalls directly through `ctypes` — zero Python dependencies beyond stdlib. Each credential is a `"user"` key with description `"credstore:<service>/<key>"`.

### Credential Enumeration

`credstore list` reads keys from the OS credential store using platform-specific APIs:

| Platform | API |
|----------|-----|
| **Windows** | `win32cred.CredEnumerate` |
| **WSL** | `powershell.exe` + inline C# `CredEnumerateW` via `advapi32.dll` |
| **Other** | Unsupported — re-run `credstore set <KEY>` to populate cryptfile |

Enumeration retrieves key names only — secret values are never batch-loaded. Sync comparison fetches one value at a time and immediately discards it.

## License

MIT
