35 lines
3.3 KiB
Markdown
35 lines
3.3 KiB
Markdown
# seal parity (tai.py -> taicpp)
|
|
|
|
Namespace `detz::seal`, sources in `src/seal/`, one class per `.h`/`.cpp` pair, headers prefixed `seal_`. No dependency beyond the standard library: every primitive tai.py takes from `hashlib`/`hmac`/`base64`/`os.urandom` is written here.
|
|
|
|
## Algorithm (must match byte for byte)
|
|
|
|
- master key: `PBKDF2-HMAC-SHA256(passphrase utf-8, salt, 200000 rounds, 32 bytes)`.
|
|
- `enc_key = HMAC(master, "tai-enc-1")`, `mac_key = HMAC(master, "tai-mac-1")`, `check = hex(HMAC(master, "tai-check-1"))`.
|
|
- `<home>/.seal`: `json.dumps({"salt": b64(16 random bytes), "check": check})` (mode 0600), created on first use; later opens compare `check`, mismatch is `SealError("wrong TAI_PASSPHRASE")`, unreadable JSON or salt is `SealError("seal file is corrupt")`. An empty file counts as absent (python `if raw`).
|
|
- keystream: concatenated `HMAC(enc_key, nonce || be32(counter))`, counter from 0.
|
|
- envelope: `tai1$` + `b64(nonce16)$b64(cipher)$b64(HMAC(mac_key, nonce||cipher))`. `lock` of an already sealed text or with a disabled seal returns the text unchanged.
|
|
- `unlock` errors: `not sealed`, `sealed value is corrupt` (not 4 `$` pieces or bad base64), `seal check failed` (MAC mismatch, constant time compare).
|
|
- Empty passphrase: seal disabled, no `.seal` is read or written.
|
|
|
|
## Parity table
|
|
|
|
| tai.py | taicpp | status |
|
|
|---|---|---|
|
|
| `SealError` | `store::SealError` (store/errors.h, shared) | same messages |
|
|
| `Seal` (`lock`, `unlock`, `safe_unlock`, `stream`, `enabled`, `default_key`, `load_master`) | `seal::Seal : store::SealPort` | ported, `safe_unlock` inherited from `SealPort` |
|
|
| `DEFAULT_PASSPHRASE` | `Seal::kDefaultPassphrase` | |
|
|
| `resolve_passphrase` | `repl::Boot::resolve_passphrase` (foundation) | not duplicated |
|
|
| `rotate_seal(config, old_seal, new)` | `SealRotation::rotate(Paths, old, new)` | events, secrets value+meta (with or without `profile` column), schedules, settings, mcp_servers, `*.sys.md`, `*.session.json`, `*.bots.json`; logs `vault/seal_rotated` |
|
|
| default-key upgrade in `boot()` | `SealFactory::make(home, passphrase, default_key)` | prints `seal upgraded from default key to TAI_PASSPHRASE` |
|
|
| hashlib sha256 / hmac / pbkdf2_hmac / base64 | `SealSha256`, `SealHmac`, `SealPbkdf2`, `SealBase64` | RFC vectors in `seal_test` |
|
|
|
|
Gaps (listed, not hidden): `SealBase64::decode` is strict about data after `=` where python silently discards it (never produced by either implementation). Invalid UTF-8 inside a sealed value is returned as bytes where python raises `UnicodeDecodeError`. Rotation paths are derived from the home (`memory.db`, `profiles/`) because the factory signature carries only the home. NULL values in `events`, `secrets` and `schedules` are not guarded in python either (python crashes, C++ re-seals an empty string).
|
|
|
|
## Tests
|
|
|
|
`make -f tests/seal.mk test` from `taicpp/`:
|
|
|
|
- `tests/seal_test.cpp`: `test_seal.py` ported 1:1 (all 16 cases) against `detz::store::Store`, plus RFC 4231/7914/FIPS vectors, factory upgrade and corrupt `.seal` cases.
|
|
- `tests/seal_diff.py` with `tests/seal_probe.cpp`: master key and keystream equality, python seals / C++ opens and the reverse, `.seal` file written by C++ and opened by python, wrong passphrase, tamper, whole vaults written by one side and decrypted by the other, rotation in both directions, boot upgrade.
|