# 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"))`. - `/.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.