feat: add AES-256-CBC encrypted password storage (v0.2.0)

Add `login --save --encrypt` flag: passwords are encrypted with
openssl AES-256-CBC (PBKDF2) and stored as `enc:<base64>` in the
config file. A master passphrase is prompted once per session and
cached in memory. Both load_config() and resolve_registry_alias()
detect the enc: prefix and decrypt transparently. The passphrase is
passed to openssl via a temp file to avoid argv/env exposure.
openssl is an optional dependency, checked on demand.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 14:52:48 +01:00
parent 661de2f3d8
commit a59e416789
3 changed files with 204 additions and 24 deletions

View File

@@ -10,11 +10,48 @@ authentication detection (bearer token or HTTP basic auth).
---
## What Changed (Session 2026-02-21)
### Encrypted config values (`--encrypt`)
Added `openssl` AES-256-CBC encryption for passwords stored in the config file.
`openssl` is an **optional** dependency — it is only required when `enc:` prefixed values
are present in the config, or when `login --encrypt` is used.
**New functions** (Encryption helpers section):
| Function | Purpose |
| --- | --- |
| `_require_openssl()` | Die with a clear message if `openssl` is not installed |
| `_prompt_master_pass()` | Prompt once per session via `/dev/tty`; cache in `HM_MASTER_PASS` |
| `_prompt_set_master_pass()` | Prompt + confirm a new passphrase (used by `login --encrypt`) |
| `_encrypt_value(plaintext)` | AES-256-CBC encrypt → base64 ciphertext (no newlines) |
| `_decrypt_value(ciphertext)` | Decrypt base64 ciphertext → plaintext; die on wrong passphrase |
**Passphrase security**: passed to `openssl` via a `mktemp` file (`-pass file:`) to avoid
exposure in the process argument list (`ps aux`). The temp file is registered in
`HM_TMPFILES` and removed on exit.
**Config format**: encrypted values use an `enc:` prefix, e.g.:
```txt
PASSWORD=enc:U2FsdGVkX1+...base64ciphertext...
REGISTRY_PROD_PASSWORD=enc:U2FsdGVkX1+...
```
Both `load_config()` and `resolve_registry_alias()` detect the prefix and call
`_decrypt_value` transparently.
**Version bump**: `0.1.0``0.2.0`
---
## What Was Built (Session 2025-02-21)
### Primary file
`hubmanager` — executable Bash script, ~600 lines, no dependencies beyond `curl`, `jq`, Bash 4+.
`hubmanager` — executable Bash script, ~680 lines, no mandatory dependencies beyond `curl`, `jq`, Bash 4+.
`openssl` is required only when encrypted config values are used.
### Subcommands implemented
@@ -47,6 +84,7 @@ set -euo pipefail
# --- Global state ---
# --- Output / Formatting helpers ---
# --- Dependency check ---
# --- Encryption helpers --- _encrypt_value(), _decrypt_value(), _prompt_master_pass()
# --- Config loading ---
# --- HTTP helpers --- raw_http(), get_response_header()
# --- Authentication --- probe_registry_auth(), get_bearer_token(), make_auth_header()
@@ -96,6 +134,15 @@ main "$@"
- Blob already at destination (`HEAD` returns 200) → skip
- Otherwise → download to temp file → `POST` initiate upload → `PUT` with digest
9. **Encrypted config values** (v0.2.0):
- `login --save --encrypt` prompts for a master passphrase (with confirmation), encrypts
the password with `openssl enc -aes-256-cbc -pbkdf2 -a`, and writes `PASSWORD=enc:<b64>`.
- Passphrase is passed to `openssl` via a temp file (`-pass file:`) — never via argv or env.
- `load_config` and `resolve_registry_alias` both check for the `enc:` prefix and call
`_decrypt_value`, which triggers `_prompt_master_pass` (once per session, cached in
`HM_MASTER_PASS`).
- `openssl` is an optional dependency: not checked at startup, only on first `enc:` encounter.
---
## Global Variables (key ones)
@@ -110,6 +157,7 @@ main "$@"
| `HM_AUTH_REALM["registry"]` | Bearer token endpoint URL |
| `HM_TOKEN_CACHE["registry\|scope"]` | Cached bearer token |
| `HM_TOKEN_EXPIRY["registry\|scope"]` | Token expiry (epoch seconds) |
| `HM_MASTER_PASS` | Master passphrase for `enc:` config values (session-cached) |
| `HM_LAST_HTTP_CODE` | HTTP status of most recent request |
| `HM_LAST_HEADERS_FILE` | Temp file path with response headers |
| `HM_TMPFILES` | Array of temp files, cleaned up via `trap EXIT` |