Files
SeedPass/scripts/generate_ts_port_fixtures.py
T
thePR0M3TH3ANandClaude Opus 5 c6cb85e840 feat(vault): port index0/atlas, and keep it current on every mutation
index0 is the per-vault activity ledger under `_system.index0`: an append-only
event stream per writer, daily checkpoints rolling those events up, and
derived canonical views (children_of, counts_by_kind, recent_activity).

It was the last piece carried through verbatim rather than computed — which
was safe for round-tripping but meant a TypeScript client never UPDATED it. A
vault written by this port went stale the moment it was touched, and a Python
client reading it afterwards saw an activity log missing everything the TS
client had done. Worse, `nostr restore` kept the local block verbatim, so a
restore silently discarded the remote's history.

Hash parity is the entire contract here: event ids, integrity hashes,
checkpoint summaries and view hashes are SHA-256 over Python's canonical JSON,
and the merge picks winners by comparing those hashes — one byte of encoding
disagreement means the two implementations permanently disagree about which
record wins. Everything routes through the existing canonicalJson (Python's
ensure_ascii escaping, code-point key ordering, CPython number formatting)
rather than JSON.stringify, and the tests compare whole structures via
canonical JSON rather than toEqual, so they pass only if the bytes that get
hashed are identical. Verified against Python fixtures covering the append
chain, compaction, manifest metadata, views and a two-replica merge —
including non-ASCII labels and tags, a non-numeric entry id, two writers
across two days, and a managed-account scope path.

Two Python quirks are replicated deliberately rather than "fixed", because
diverging would change which events exist rather than merely how they hash:
`str(None)` is "None" (so a JSON null in a required field is a non-empty
string there), and a stored integrity_hash is preserved even when it
disagrees — this normalizes, it does not verify, and silently rewriting a
mismatched hash would hide the tampering the hash exists to reveal.

Events are emitted by DIFFING the index at the single mutation funnel each
surface goes through, rather than from each call site as Python does. That is
deliberate: a call-site emitter is something a new code path can forget, and a
silently incomplete ledger is worse than an absent one because it reads as
"nothing happened". The event vocabulary matches Python's, so a stream from
either implementation reads the same. Emission is best-effort — the atlas is
derived state, and it must never fail a committed vault write.

mergeIndexPayloads now defaults to merging index0 with the same deterministic
rules; "preserve-current" and "reject" remain for callers that want them, and
`nostr restore` no longer discards the remote's history.

Also made gen_high_risk deterministic. Fernet embeds a random IV and the
current time, so that fixture regenerated differently every run and would have
failed the fixture-drift CI job each time — it now pins both, with a note that
production must never do this.

Verified end to end: the TS binary performed four mutations, and Python read
the resulting atlas, normalized it without dropping a record, and compacted it
into checkpoints and views.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-19 11:13:15 -04:00

1608 lines
56 KiB
Python

#!/usr/bin/env python3
"""Generate deterministic parity fixtures for the TypeScript port.
Writes JSON fixture files to js/packages/test-vectors/fixtures/. The
TypeScript core must reproduce every expected output byte-for-byte; see
docs/typescript_web_extension_port_plan.md sections 6, 8 and 21.
Safety: every fixture derives from the public BIP-39 test mnemonics below.
Never point this script at a real profile or seed.
Determinism: running this twice on the same commit must produce identical
bytes. Anything time- or randomness-dependent (entry timestamps, the vault
fixture nonce, KDF salts) is pinned to fixed values. The pinned nonce/salt
are FIXTURE-ONLY conveniences — production code draws them from os.urandom.
Usage:
.venv/bin/python scripts/generate_ts_port_fixtures.py
"""
from __future__ import annotations
import base64
import hashlib
import json
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from bip_utils import Bip39SeedGenerator # noqa: E402
from cryptography.hazmat.primitives.ciphers.aead import AESGCM # noqa: E402
from local_bip85.bip85 import BIP85 # noqa: E402
from nostr.coincurve_keys import Keys # noqa: E402
from nostr.key_manager import NOSTR_KEY_APP_ID # noqa: E402
from seedpass.core.password_generation import ( # noqa: E402
CURRENT_PASSWORD_GEN_VERSION,
LEGACY_PASSWORD_GEN_VERSION,
PasswordGenerator,
PasswordPolicy,
)
from seedpass.core.totp import TotpManager # noqa: E402
from utils.fingerprint import generate_fingerprint # noqa: E402
from utils.key_derivation import derive_index_key, derive_totp_secret # noqa: E402
FIXTURES_DIR = REPO / "js" / "packages" / "test-vectors" / "fixtures"
FIXTURE_VERSION = 1
FIXED_UNIX = 1700000000 # 2023-11-14T22:13:20Z — pins entry timestamps
# Public, well-known BIP-39 test vectors. Never real funds, never a real vault.
MNEMONICS = {
"abandon12": (
"abandon abandon abandon abandon abandon abandon "
"abandon abandon abandon abandon abandon about"
),
"legal12": (
"legal winner thank year wave sausage worth useful "
"legal winner thank yellow"
),
"zoo24": (
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo "
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote"
),
}
PRIMARY = "abandon12"
POLICIES = {
"default": {},
"exclude_ambiguous": {"exclude_ambiguous": True},
"safe_special": {"special_mode": "safe"},
"no_special": {"include_special_chars": False},
"custom_special": {"allowed_special_chars": "!@#"},
"high_minima": {
"min_uppercase": 5,
"min_lowercase": 5,
"min_digits": 5,
"min_special": 5,
},
}
# (text, ec_level) — spans all four EC levels, versions 1..17, and the
# version-7 boundary where version-information blocks start being written.
QR_CASES = [
(
"otpauth://totp/email-2fa?secret=JBSWY3DPEHPK3PXP"
"&issuer=SeedPass&period=30&digits=6",
"M",
),
("nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5", "M"),
("JBSWY3DPEHPK3PXP", "L"),
("https://example.com/a/fairly/long/path?with=query&params=yes", "Q"),
("x", "H"),
("A" * 200, "M"),
("A" * 600, "L"),
("mixed \u00e9\u00e8 unicode \u2713 bytes", "M"),
("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleKeyMaterialHere user@host", "M"),
("0123456789" * 8, "Q"),
]
# (policy, length, index) — lengths cross the 32-byte v1 stream-wrap boundary
PASSWORD_CASES = [
("default", 8, 0),
("default", 16, 0),
("default", 16, 1),
("default", 16, 7),
("default", 16, 4095),
("default", 20, 3),
("default", 32, 2),
("default", 33, 2),
("default", 64, 5),
("default", 128, 11),
("exclude_ambiguous", 16, 0),
("exclude_ambiguous", 40, 2),
("safe_special", 16, 0),
("safe_special", 40, 2),
("no_special", 16, 0),
("no_special", 40, 2),
("custom_special", 16, 0),
("high_minima", 24, 0),
]
class _SeedDeriver:
"""Mirrors EncryptionManager.derive_seed_from_mnemonic without a vault."""
def derive_seed_from_mnemonic(self, mnemonic, passphrase=""):
return Bip39SeedGenerator(mnemonic).Generate(passphrase)
def _write(name: str, data: dict) -> None:
path = FIXTURES_DIR / name
path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n")
print(f"wrote {path.relative_to(REPO)}")
def _bip85(mnemonic: str) -> BIP85:
return BIP85(Bip39SeedGenerator(mnemonic).Generate())
def gen_bip39() -> dict:
cases = []
for mid, mnemonic in MNEMONICS.items():
seed = Bip39SeedGenerator(mnemonic).Generate()
cases.append(
{
"id": mid,
"mnemonic": mnemonic,
"passphrase": "",
"seed_hex": seed.hex(),
}
)
return {"description": "BIP-39 mnemonic -> 64-byte seed", "cases": cases}
def gen_bip85_entropy() -> dict:
cases = []
specs = [
# (app_no, index, entropy_bytes, word_count) — word_count only for 39
(32, 0, 64, None),
(32, 1, 64, None),
(32, 0, 32, None),
(39, 0, 16, 12),
(39, 0, 32, 24),
(39, 3, 16, 12),
(NOSTR_KEY_APP_ID, 0, 32, None),
]
for mid in ("abandon12", "zoo24"):
bip85 = _bip85(MNEMONICS[mid])
for app_no, index, nbytes, word_count in specs:
entropy = bip85.derive_entropy(
index=index,
entropy_bytes=nbytes,
app_no=app_no,
word_count=word_count,
)
cases.append(
{
"mnemonic_id": mid,
"app_no": app_no,
"index": index,
"entropy_bytes": nbytes,
"word_count": word_count,
"entropy_hex": entropy.hex(),
}
)
return {
"description": (
"BIP-85 entropy: BIP32-SLIP10-secp256k1 path derivation, then "
"HMAC-SHA512(key=b'bip-entropy-from-k', child_privkey)[:entropy_bytes]. "
"Paths: app 39 -> m/83696968'/39'/0'/{word_count}'/{index}'; "
"app 32 -> m/83696968'/32'/{index}'; "
"other -> m/83696968'/{app_no}'/{index}'"
),
"cases": cases,
}
def gen_passwords(gen_version: int) -> dict:
cases = []
for mid in (PRIMARY,):
mnemonic = MNEMONICS[mid]
bip85 = _bip85(mnemonic)
for policy_name, length, index in PASSWORD_CASES:
policy = PasswordPolicy(**POLICIES[policy_name])
pg = PasswordGenerator(_SeedDeriver(), mnemonic, bip85, policy=policy)
password = pg.generate_password(
length=length, index=index, gen_version=gen_version
)
cases.append(
{
"mnemonic_id": mid,
"policy": policy_name,
"policy_params": POLICIES[policy_name],
"length": length,
"index": index,
"password": password,
}
)
return {
"description": f"Deterministic password derivation, gen_version={gen_version}",
"gen_version": gen_version,
"cases": cases,
}
def gen_totp() -> dict:
cases = []
for mid in (PRIMARY, "zoo24"):
mnemonic = MNEMONICS[mid]
for index in (0, 1, 7):
secret = derive_totp_secret(mnemonic, index)
codes = {
str(ts): TotpManager.current_code_from_secret(secret, ts)
for ts in (0, FIXED_UNIX, FIXED_UNIX + 30)
}
cases.append(
{
"mnemonic_id": mid,
"index": index,
"secret_b32": secret,
"period": 30,
"digits": 6,
"codes_at": codes,
}
)
return {
"description": (
"TOTP secret derivation: path m/83696968'/39'/1414485072'/{index}' "
"(1414485072 = 0x544F5450 = int.from_bytes(b'TOTP')), entropy = "
"HMAC-SHA512(b'bip-entropy-from-k', k), secret = "
"base32(SHA256(entropy[:32])[:20]); codes are RFC 6238 SHA-1 6-digit"
),
"cases": cases,
}
def gen_nostr_keys() -> dict:
cases = []
for mid in (PRIMARY, "legal12"):
bip85 = _bip85(MNEMONICS[mid])
for index in (0, 1, 7):
entropy = bip85.derive_entropy(
index=index, entropy_bytes=32, app_no=NOSTR_KEY_APP_ID
)
keys = Keys(priv_k=entropy.hex())
cases.append(
{
"mnemonic_id": mid,
"account_index": index,
"private_key_hex": keys.private_key_hex(),
"public_key_hex": keys.public_key_hex(),
"npub": Keys.hex_to_bech32(keys.public_key_hex(), "npub"),
"nsec": Keys.hex_to_bech32(keys.private_key_hex(), "nsec"),
}
)
return {
"description": (
"Nostr keys: BIP-85 app 1237, 32 bytes -> secp256k1 private key; "
"public key is x-only (compressed pubkey minus prefix byte); "
"npub/nsec are bech32"
),
"cases": cases,
}
def gen_managed_seeds() -> dict:
cases = []
for mid in (PRIMARY, "zoo24"):
bip85 = _bip85(MNEMONICS[mid])
for words in (12, 24):
for index in (0, 1):
child = bip85.derive_mnemonic(index=index, words_num=words)
cases.append(
{
"mnemonic_id": mid,
"words": words,
"index": index,
"child_mnemonic": child,
"child_fingerprint": generate_fingerprint(child),
}
)
return {
"description": (
"BIP-85 child mnemonics (managed accounts / derived seed phrases), "
"BIP-39 English wordlist"
),
"cases": cases,
}
def gen_fingerprints() -> dict:
cases = [
{
"mnemonic_id": mid,
"mnemonic": mnemonic,
"fingerprint": generate_fingerprint(mnemonic),
}
for mid, mnemonic in MNEMONICS.items()
]
# Normalization: fingerprint input is strip().lower()
cases.append(
{
"mnemonic_id": "abandon12-mixed-case-padded",
"mnemonic": " " + MNEMONICS[PRIMARY].upper() + " ",
"fingerprint": generate_fingerprint(" " + MNEMONICS[PRIMARY].upper() + " "),
}
)
return {
"description": (
"Profile fingerprint: SHA256(mnemonic.strip().lower()) hex, "
"first 16 chars, uppercased"
),
"cases": cases,
}
def gen_index_keys() -> dict:
cases = []
for mid, mnemonic in MNEMONICS.items():
key_b64 = derive_index_key(mnemonic).decode()
cases.append({"mnemonic_id": mid, "index_key_urlsafe_b64": key_b64})
return {
"description": (
"Vault index key (seed-only mode): seed = BIP39(mnemonic); "
"master = HKDF-SHA256(seed, salt=None, info=b'seedpass:v1:master'); "
"key = HKDF-SHA256(master, salt=None, info=b'seedpass:v1:storage'); "
"urlsafe base64"
),
"cases": cases,
}
def _build_entries_index(post_add=None) -> dict:
"""Build a real entries index with one entry of each kind, timestamps pinned.
``post_add(em)`` runs extra EntryManager operations before the index is
read back (used by the modification-parity fixture).
"""
from seedpass.core.backup import BackupManager
from seedpass.core.config_manager import ConfigManager
from seedpass.core.entry_management import EntryManager
from seedpass.core.encryption import EncryptionManager
from seedpass.core.vault import Vault
from utils.key_derivation import derive_key_from_password
mnemonic = MNEMONICS[PRIMARY]
# Pin every timestamp EntryManager writes
EntryManager._now_unix = staticmethod(lambda: FIXED_UNIX)
with tempfile.TemporaryDirectory() as tmp:
dir_path = Path(tmp)
fp = generate_fingerprint(mnemonic)
seed_key = derive_key_from_password("fixture-password", fp)
EncryptionManager(seed_key, dir_path).encrypt_parent_seed(mnemonic)
index_key = derive_index_key(mnemonic)
enc_mgr = EncryptionManager(index_key, dir_path)
vault = Vault(enc_mgr, dir_path)
cfg_mgr = ConfigManager(vault, dir_path)
backup_mgr = BackupManager(dir_path, cfg_mgr)
em = EntryManager(vault, backup_mgr)
em.add_entry(
"example.com",
16,
username="alice",
url="https://example.com",
notes="password note",
tags=["web"],
)
em.add_totp("example-totp", mnemonic, deterministic=True, tags=["otp"])
em.add_totp(
"imported-totp",
secret="JBSWY3DPEHPK3PXP",
period=45,
digits=8,
)
em.add_ssh_key("example-ssh", mnemonic, notes="ssh note")
em.add_nostr_key("example-nostr", mnemonic)
em.add_key_value("api-token", "token", "abc123", tags=["api"])
em.add_document("example-doc", "hello fixture world", file_type="txt")
em.add_seed("example-seed", mnemonic, words_num=24)
em.add_managed_account("example-managed", mnemonic)
em.add_pgp_key("example-pgp", mnemonic, user_id="[email protected]")
if post_add is not None:
post_add(em)
index = vault.load_index()
entries = {
k: v for k, v in index.items() if k != "_system"
}
return entries
def gen_entries_and_vault() -> tuple[dict, dict]:
entries = _build_entries_index()
plaintext = json.dumps(entries, indent=2, sort_keys=True).encode()
key = base64.urlsafe_b64decode(derive_index_key(MNEMONICS[PRIMARY]))
# FIXTURE-ONLY fixed nonce so the blob is reproducible; production uses
# os.urandom(12) per encryption (see EncryptionManager.encrypt_data).
nonce = hashlib.sha256(b"seedpass-ts-fixture-nonce").digest()[:12]
blob = b"V3|" + nonce + AESGCM(key).encrypt(nonce, plaintext, None)
entries_fixture = {
"description": (
"Decrypted entries index (one entry of each kind), _system "
"stripped, timestamps pinned to FIXED_UNIX"
),
"fixed_unix": FIXED_UNIX,
"mnemonic_id": PRIMARY,
"entries": entries,
}
vault_fixture = {
"description": (
"Encrypted vault payload, format b'V3|' + nonce(12) + AES-GCM "
"ciphertext+tag, key = raw bytes of index_key fixture. Nonce is "
"fixed for fixture reproducibility only."
),
"mnemonic_id": PRIMARY,
"nonce_hex": nonce.hex(),
"payload_b64": base64.b64encode(blob).decode(),
"plaintext_sha256": hashlib.sha256(plaintext).hexdigest(),
"plaintext_canonical_json": "entries_index.json#entries (indent=2, sort_keys)",
}
return entries_fixture, vault_fixture
def gen_password_kdf() -> dict:
from utils.key_derivation import (
KdfConfig,
derive_key_from_password,
derive_key_from_password_argon2,
)
fp = generate_fingerprint(MNEMONICS[PRIMARY])
pbkdf2_cases = []
for password in ("fixture-password", "correct horse battery staple", " pässwörd "):
for iterations in (50_000, 100_000):
key = derive_key_from_password(password, fp, iterations=iterations)
pbkdf2_cases.append(
{
"password": password,
"fingerprint": fp,
"iterations": iterations,
"key_urlsafe_b64": key.decode(),
}
)
argon2_cases = []
for params in (
{"time_cost": 2, "memory_cost": 64 * 1024, "parallelism": 8},
{"time_cost": 1, "memory_cost": 8 * 1024, "parallelism": 1},
):
kdf = KdfConfig(
name="argon2id",
version=1,
params=params,
salt_b64=base64.b64encode(b"fixture-salt-16b").decode(),
)
key = derive_key_from_password_argon2("fixture-password", kdf)
argon2_cases.append(
{
"password": "fixture-password",
"kdf": {
"name": kdf.name,
"version": kdf.version,
"params": kdf.params,
"salt_b64": kdf.salt_b64,
},
"key_urlsafe_b64": key.decode(),
}
)
return {
"description": (
"Password-based key derivation. PBKDF2: NFKD-normalized+stripped "
"password, salt = SHA256(fingerprint)[:16], PBKDF2-HMAC-SHA256, "
"32 bytes, urlsafe b64. Argon2id: NFKD+strip, salt from config, "
"hash_len 32, urlsafe b64. Salts pinned for fixtures only."
),
"pbkdf2_cases": pbkdf2_cases,
"argon2id_cases": argon2_cases,
}
def gen_legacy_payloads() -> dict:
from cryptography.fernet import Fernet
mnemonic = MNEMONICS[PRIMARY]
key_b64 = derive_index_key(mnemonic)
raw_key = base64.urlsafe_b64decode(key_b64)
fernet = Fernet(key_b64)
plaintext = b'{"legacy": true, "hello": "fixture"}'
iv = hashlib.sha256(b"seedpass-ts-fixture-fernet-iv").digest()[:16]
token = fernet._encrypt_from_parts(plaintext, FIXED_UNIX, iv)
nonce = hashlib.sha256(b"seedpass-ts-fixture-v2-nonce").digest()[:12]
v2_gcm = b"V2:" + nonce + AESGCM(raw_key).encrypt(nonce, plaintext, None)
v2_fernet = b"V2:" + token
# Serialized parent-seed file: JSON {"kdf": ..., "ct": b64(V3 blob)},
# encrypted with the password-derived key (not the index key).
from utils.key_derivation import derive_key_from_password
fp = generate_fingerprint(mnemonic)
seed_key_b64 = derive_key_from_password("fixture-password", fp)
seed_key = base64.urlsafe_b64decode(seed_key_b64)
seed_nonce = hashlib.sha256(b"seedpass-ts-fixture-seed-nonce").digest()[:12]
seed_ct = b"V3|" + seed_nonce + AESGCM(seed_key).encrypt(
seed_nonce, mnemonic.encode(), None
)
kdf_dict = {
"name": "pbkdf2-sha256",
"version": 1,
"params": {"iterations": 100000},
"salt_b64": "",
}
wrapper = json.dumps(
{"kdf": kdf_dict, "ct": base64.b64encode(seed_ct).decode()},
separators=(",", ":"),
).encode()
return {
"description": (
"Legacy/migration payload formats the TS port must read: raw "
"Fernet token, V2-prefixed AES-GCM, V2-prefixed Fernet "
"(wrong-header case), and the JSON kdf/ct file wrapper around a "
"V3 blob. IVs/nonces/timestamps pinned for fixtures only."
),
"mnemonic_id": PRIMARY,
"plaintext_utf8": plaintext.decode(),
"fernet_token_b64": base64.b64encode(token).decode(),
"v2_gcm_payload_b64": base64.b64encode(v2_gcm).decode(),
"v2_fernet_payload_b64": base64.b64encode(v2_fernet).decode(),
"parent_seed_file": {
"password": "fixture-password",
"fingerprint": generate_fingerprint(mnemonic),
"wrapper_b64": base64.b64encode(wrapper).decode(),
"expected_seed_mnemonic_id": PRIMARY,
},
}
def _derive_key_index(mnemonic: str) -> bytes:
from utils.key_hierarchy import kd
seed = Bip39SeedGenerator(mnemonic).Generate()
master = kd(seed, b"seedpass:v1:master")
return kd(master, b"seedpass:v1:index")
def gen_nostr_snapshot(vault_payload_b64: str) -> dict:
import gzip as gzip_mod
import hmac as hmac_mod
from nostr.snapshot import prepare_snapshot
mnemonic = MNEMONICS[PRIMARY]
encrypted = base64.b64decode(vault_payload_b64)
# mtime=0 pins the gzip header; production uses current time, which only
# affects the compressed bytes, not decompression.
compressed = gzip_mod.compress(encrypted, mtime=0)
limit = 800
manifest, chunks = prepare_snapshot(encrypted, limit)
# Re-chunk the pinned compression so chunk bytes/hashes are deterministic
pinned_chunks = [compressed[i : i + limit] for i in range(0, len(compressed), limit)]
metas = [
{
"id": f"seedpass-chunk-{i:04d}",
"size": len(c),
"hash": hashlib.sha256(c).hexdigest(),
"event_id": None,
}
for i, c in enumerate(pinned_chunks)
]
nonce = hashlib.sha256(b"seedpass-ts-fixture-manifest-nonce").digest()[:16]
key_index = _derive_key_index(mnemonic)
manifest_id = hmac_mod.new(
key_index, b"manifest|" + nonce, hashlib.sha256
).hexdigest()
manifest_json = json.dumps(
{
"ver": 1,
"algo": "gzip",
"chunks": metas,
"delta_since": FIXED_UNIX,
"nonce": base64.b64encode(nonce).decode(),
"index0": None,
}
)
return {
"description": (
"Nostr snapshot model: gzip(encrypted index) split into <=limit "
"chunks; chunk id 'seedpass-chunk-%04d', sha256 hash; manifest "
"kind 30070 (d-tag = manifest id), chunks kind 30071 (d-tag = "
"chunk id, content = b64), deltas kind 30072. Manifest id = "
"HMAC-SHA256(key_index, b'manifest|' + nonce) hex, key_index = "
"HKDF chain master->'seedpass:v1:index'. gzip mtime pinned to 0 "
"for the fixture."
),
"mnemonic_id": PRIMARY,
"event_kinds": {"manifest": 30070, "snapshot_chunk": 30071, "delta": 30072},
"chunk_limit": limit,
"encrypted_b64": vault_payload_b64,
"compressed_b64": base64.b64encode(compressed).decode(),
"chunks_b64": [base64.b64encode(c).decode() for c in pinned_chunks],
"chunk_metas": metas,
"key_index_hex": key_index.hex(),
"manifest_nonce_b64": base64.b64encode(nonce).decode(),
"manifest_id": manifest_id,
"manifest_json": manifest_json,
"unpinned_chunk_count": len(chunks),
}
def _merge_case(name: str, current: dict, incoming: dict, source_tag: str) -> dict:
import copy
from seedpass.core.sync_conflict import merge_index_payloads
merged = merge_index_payloads(
copy.deepcopy(current), copy.deepcopy(incoming), source_tag=source_tag
)
return {
"name": name,
"current": current,
"incoming": incoming,
"source_tag": source_tag,
"merged": merged,
}
def gen_sync_merge() -> dict:
t = FIXED_UNIX
def entry(label: str, ts: int, **kw) -> dict:
base = {
"type": "password",
"kind": "password",
"label": label,
"length": 16,
"archived": False,
"notes": "",
"tags": [],
"modified_ts": ts,
}
base.update(kw)
return base
cases = [
_merge_case(
"newer-incoming-wins",
{"schema_version": 4, "entries": {"0": entry("site", t)}},
{"schema_version": 4, "entries": {"0": entry("site-renamed", t + 10)}},
"tag-newer",
),
_merge_case(
"older-incoming-loses",
{"schema_version": 4, "entries": {"0": entry("site", t + 10)}},
{"schema_version": 4, "entries": {"0": entry("site-old", t)}},
"tag-older",
),
_merge_case(
"equal-ts-field-union",
{
"schema_version": 4,
"entries": {
"0": entry("site", t, notes="", tags=["a", "c"], username="")
},
},
{
"schema_version": 4,
"entries": {
"0": entry(
"site", t, notes="from-incoming", tags=["b", "a"],
username="alice", archived=True,
)
},
},
"tag-equal",
),
_merge_case(
"incoming-delete-creates-tombstone",
{"schema_version": 4, "entries": {"0": entry("site", t), "1": entry("keep", t)}},
{
"schema_version": 4,
"entries": {"0": {**entry("site", t + 5), "_deleted": True}},
},
"tag-delete",
),
_merge_case(
"entry-newer-than-tombstone-survives",
{
"schema_version": 4,
"entries": {},
"_sync_meta": {
"tombstones": {
"0": {"deleted_ts": t, "entry_hash": "", "event_hash": "", "source": "x"}
}
},
},
{"schema_version": 4, "entries": {"0": entry("revived", t + 20)}},
"tag-revive",
),
_merge_case(
"tombstone-beats-older-entry",
{
"schema_version": 4,
"entries": {},
"_sync_meta": {
"tombstones": {
"0": {"deleted_ts": t + 20, "entry_hash": "", "event_hash": "", "source": "x"}
}
},
},
{"schema_version": 4, "entries": {"0": entry("stale", t)}},
"tag-stale",
),
_merge_case(
"tombstone-vs-tombstone-newer-wins",
{
"schema_version": 4,
"entries": {},
"_sync_meta": {
"tombstones": {
"0": {"deleted_ts": t + 1, "entry_hash": "aa", "event_hash": "", "source": "one"}
}
},
},
{
"schema_version": 4,
"entries": {},
"_sync_meta": {
"tombstones": {
"0": {"deleted_ts": t + 9, "entry_hash": "bb", "event_hash": "", "source": "two"}
}
},
},
"tag-tt",
),
_merge_case(
"empty-current-adopts-incoming",
{},
{"schema_version": 4, "entries": {"3": entry("new", t)}},
"tag-adopt",
),
]
return {
"description": (
"merge_index_payloads parity cases. merged output includes the "
"normalized empty _system.index0 skeleton and _sync_meta with "
"strategy modified_ts_hash_tombstone_v2."
),
"cases": cases,
}
def gen_delta_replay() -> dict:
import copy
from seedpass.core.index0 import ensure_index0_payload
from seedpass.core.sync_conflict import merge_index_payloads
key = base64.urlsafe_b64decode(derive_index_key(MNEMONICS[PRIMARY]))
t = FIXED_UNIX
def entry(label: str, ts: int) -> dict:
return {
"type": "password", "kind": "password", "label": label,
"length": 16, "archived": False, "notes": "", "tags": [],
"modified_ts": ts,
}
snapshot_index = {"schema_version": 4, "entries": {"0": entry("base", t)}}
delta1 = {"schema_version": 4, "entries": {"0": entry("base-renamed", t + 10), "1": entry("added", t + 10)}}
delta2 = {"schema_version": 4, "entries": {"1": {**entry("added", t + 20), "_deleted": True}}}
def encrypt(payload: dict, nonce_seed: bytes) -> bytes:
nonce = hashlib.sha256(nonce_seed).digest()[:12]
plaintext = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
return b"V3|" + nonce + AESGCM(key).encrypt(nonce, plaintext, None)
enc_deltas = [
encrypt(delta1, b"seedpass-ts-fixture-delta-1"),
encrypt(delta2, b"seedpass-ts-fixture-delta-2"),
]
# Reproduce decrypt_and_save_index_from_nostr(merge=True) semantics
state = ensure_index0_payload(copy.deepcopy(snapshot_index))
for enc, payload in zip(enc_deltas, [delta1, delta2]):
source_tag = hashlib.sha256(enc).hexdigest()[:16]
incoming = ensure_index0_payload(copy.deepcopy(payload))
state = merge_index_payloads(state, incoming, source_tag=source_tag)
return {
"description": (
"Delta replay: start from a snapshot index, decrypt each V3 delta "
"payload, merge in order with source_tag = "
"sha256(encrypted_delta)[:16]. Nonces pinned (fixture-only)."
),
"mnemonic_id": PRIMARY,
"snapshot_index": snapshot_index,
"delta_payloads_b64": [base64.b64encode(e).decode() for e in enc_deltas],
"delta_plaintexts": [delta1, delta2],
"final_state": state,
}
def gen_portable_backup(entries: dict) -> dict:
from utils.checksum import canonical_json_dumps, json_checksum
mnemonic = MNEMONICS[PRIMARY]
index_data = entries
canonical = canonical_json_dumps(index_data)
checksum = json_checksum(index_data)
key = base64.urlsafe_b64decode(derive_index_key(mnemonic))
nonce = hashlib.sha256(b"seedpass-ts-fixture-export-nonce").digest()[:12]
payload = b"V3|" + nonce + AESGCM(key).encrypt(nonce, canonical.encode(), None)
encrypted_wrapper = {
"format_version": 1,
"created_at": FIXED_UNIX,
"fingerprint": generate_fingerprint(mnemonic),
"encryption_mode": "seed-only",
"cipher": "aes-gcm",
"checksum": checksum,
"payload": base64.b64encode(payload).decode(),
}
plaintext_wrapper = {
"format_version": 1,
"created_at": FIXED_UNIX,
"fingerprint": generate_fingerprint(mnemonic),
"encryption_mode": "none",
"cipher": "none",
"checksum": checksum,
"payload": base64.b64encode(canonical.encode()).decode(),
}
return {
"description": (
"Portable backup wrapper (portable_backup.py, format_version 1). "
"payload = V3 AES-GCM over canonical JSON of the index, key = "
"index key of the parent seed; checksum = "
"sha256(canonical_json(index)). Nonce/created_at pinned "
"(fixture-only)."
),
"mnemonic_id": PRIMARY,
"index": index_data,
"canonical_json_sha256": checksum,
"encrypted_wrapper": encrypted_wrapper,
"plaintext_wrapper": plaintext_wrapper,
}
def gen_entry_secrets() -> dict:
"""Expected reveal values for the fixture vault, via the same low-level
functions EntryManager retrieval uses."""
from nostr.coincurve_keys import Keys
from seedpass.core.password_generation import (
PasswordGenerator,
PasswordPolicy,
derive_seed_phrase,
)
mnemonic = MNEMONICS[PRIMARY]
bip85 = _bip85(mnemonic)
# Entry 0: password, length 16, gen_version 2, derivation index 0
pg = PasswordGenerator(_SeedDeriver(), mnemonic, bip85, policy=PasswordPolicy())
password = pg.generate_password(length=16, index=0, gen_version=2)
# Entry 1: deterministic TOTP, derivation index 0
totp_secret = derive_totp_secret(mnemonic, 0)
totp_code = TotpManager.current_code_from_secret(totp_secret, FIXED_UNIX)
# Entry 4: nostr key entry — DEFAULT BIP-85 app 39 (not 1237)
entropy = bip85.derive_entropy(index=4, entropy_bytes=32)
nostr_keys = Keys(priv_k=entropy.hex())
nsec = Keys.hex_to_bech32(nostr_keys.private_key_hex(), "nsec")
npub = Keys.hex_to_bech32(nostr_keys.public_key_hex(), "npub")
# Entry 7: seed entry, index 7, 24 words; entry 8: managed account, 12 words
seed_phrase = derive_seed_phrase(bip85, 7, 24)
managed_phrase = derive_seed_phrase(bip85, 8, 12)
return {
"description": (
"Expected plaintext values for entries in entries_index.json, "
"computed via the same functions EntryManager retrieval uses. "
"Nostr entry keys use the DEFAULT BIP-85 app 39 path, unlike the "
"sync client identity (app 1237)."
),
"mnemonic_id": PRIMARY,
"password_entry_0": password,
"totp_entry_1_code_at": {str(FIXED_UNIX): totp_code},
"nostr_entry_4": {"nsec": nsec, "npub": npub},
"seed_entry_7_mnemonic": seed_phrase,
"managed_entry_8_mnemonic": managed_phrase,
}
def gen_nostr_events(snapshot: dict, delta_replay: dict) -> dict:
"""Signed Nostr events for the snapshot/chunk/delta model.
Event ids are deterministic (NIP-01 serialization); BIP-340 signatures
use random aux data and are NOT. To keep the generator byte-deterministic
we reuse the previously committed signature whenever the event id is
unchanged — a fresh signature is only produced for new/changed events.
"""
import json as json_mod
from nostr_sdk import EventBuilder, EventId, Keys, Kind, Tag, Timestamp
# Sync client identity: BIP-85 app 1237, account index 0 (nostr_keys.json)
bip85 = _bip85(MNEMONICS[PRIMARY])
entropy = bip85.derive_entropy(index=0, entropy_bytes=32, app_no=NOSTR_KEY_APP_ID)
keys = Keys.parse(entropy.hex())
existing_sigs: dict[str, str] = {}
existing_path = FIXTURES_DIR / "nostr_events.json"
if existing_path.exists():
try:
for ev in json_mod.loads(existing_path.read_text())["events"]:
existing_sigs[ev["event"]["id"]] = ev["event"]["sig"]
except Exception:
pass
def build(name: str, kind: int, content: str, tags: list) -> dict:
builder = (
EventBuilder(Kind(kind), content)
.tags(tags)
.custom_created_at(Timestamp.from_secs(FIXED_UNIX))
)
event = json_mod.loads(builder.sign_with_keys(keys).as_json())
cached_sig = existing_sigs.get(event["id"])
if cached_sig:
event["sig"] = cached_sig
return {"name": name, "event": event}
events = [
build(
"chunk-0",
30071,
snapshot["chunks_b64"][0],
[Tag.identifier(snapshot["chunk_metas"][0]["id"])],
),
build(
"manifest",
30070,
snapshot["manifest_json"],
[Tag.identifier(snapshot["manifest_id"])],
),
build(
"delta-1",
30072,
delta_replay["delta_payloads_b64"][0],
[Tag.event(EventId.parse(snapshot["manifest_id"]))],
),
]
return {
"description": (
"Signed Nostr events from the Python-side nostr_sdk (rust-nostr) "
"with pinned created_at. Ids are NIP-01-deterministic; signatures "
"are cached across generator runs (BIP-340 aux randomness)."
),
"mnemonic_id": PRIMARY,
"signer_private_key_hex": entropy.hex(),
"signer_public_key_hex": keys.public_key().to_hex(),
"events": events,
}
MOD_UNIX = FIXED_UNIX + 100
def gen_entry_mods() -> dict:
"""Apply a modification sequence via the real EntryManager and capture
the result; TS replicates the same ops and must match byte-for-byte."""
from seedpass.core.entry_management import EntryManager
def mods(em: EntryManager) -> None:
# Later timestamp so touched entries visibly differ from creation
EntryManager._now_unix = staticmethod(lambda: MOD_UNIX)
em.modify_entry(
0,
username="alice2",
url="https://example.org",
notes="updated note",
tags=["web", "prod"],
min_digits=3,
special_mode="safe",
)
em.modify_entry(1, period=60, digits=8)
em.modify_entry(5, label="api-token-renamed", value="rotated-value")
em.archive_entry(6)
em.restore_entry(6)
em.archive_entry(3)
em.add_link(0, 1, relation="totp", note="2fa for site")
em.add_link(0, 5, relation="related_to")
em.add_link(5, 0)
em.remove_link(0, 5)
entries = _build_entries_index(post_add=mods)
return {
"description": (
"Entries index after a modification sequence (modify_entry, "
"archive/restore, add_link/remove_link) applied by the real "
"EntryManager at MOD_UNIX. TS replays the same ops from the "
"creation state and must match."
),
"fixed_unix": FIXED_UNIX,
"mod_unix": MOD_UNIX,
"mnemonic_id": PRIMARY,
"entries": entries,
}
def gen_migrations() -> dict:
"""Legacy index payloads at every historical schema version, with the
result Python's apply_migrations produces for each."""
import copy
from seedpass.core.migrations import LATEST_VERSION, apply_migrations
# v0: no schema_version, passwords keyed by index, "website" not "label"
v0 = {
"passwords": {
"0": {
"website": "legacy-site.example",
"length": 16,
"username": "olduser",
"url": "https://legacy.example",
},
"1": {"website": "second-site.example", "length": 12},
}
}
# v1: schema_version present, still the passwords shape
v1 = {"schema_version": 1, **copy.deepcopy(v0)}
# v2: entries exist, but without custom_fields/origin/tags
v2 = {
"schema_version": 2,
"entries": {
"0": {
"type": "password",
"kind": "password",
"label": "v2-site.example",
"length": 20,
"notes": "",
"username": "u",
},
"1": {
"type": "totp",
"kind": "totp",
"label": "v2-totp",
"index": 0,
"period": 30,
"digits": 6,
"notes": "",
},
},
}
# v3: has custom_fields/origin but no tags
v3 = copy.deepcopy(v2)
v3["schema_version"] = 3
for entry in v3["entries"].values():
entry["custom_fields"] = []
entry["origin"] = ""
cases = []
for name, payload in (("v0", v0), ("v1", v1), ("v2", v2), ("v3", v3)):
cases.append(
{
"name": name,
"input": copy.deepcopy(payload),
"migrated": apply_migrations(copy.deepcopy(payload)),
}
)
return {
"description": (
"Schema migrations 0->4 as applied by seedpass.core.migrations."
" v0->v1 injects schema_version; v1->v2 renames passwords to"
" entries and website to label; v2->v3 adds custom_fields/origin"
" defaults; v3->v4 adds tags defaults."
),
"latest_version": LATEST_VERSION,
"cases": cases,
}
def gen_ssh_keys() -> dict:
from seedpass.core.password_generation import derive_ssh_key_pair
cases = []
for mid in (PRIMARY, "zoo24"):
mnemonic = MNEMONICS[mid]
bip85 = _bip85(mnemonic)
for index in (0, 1, 7):
priv_pem, pub_pem = derive_ssh_key_pair(mnemonic, index)
entropy = bip85.derive_entropy(index=index, entropy_bytes=32, app_no=32)
cases.append(
{
"mnemonic_id": mid,
"index": index,
"entropy_hex": entropy.hex(),
"private_key_pem": priv_pem,
"public_key_pem": pub_pem,
}
)
return {
"description": (
"SSH key pairs: BIP-85 app 32, 32 bytes of entropy used directly "
"as an Ed25519 private key. Serialized as PKCS#8 PEM (private) "
"and SubjectPublicKeyInfo PEM (public), matching Python's "
"cryptography output byte-for-byte."
),
"cases": cases,
}
def gen_pgp_keys() -> dict:
from seedpass.core.password_generation import derive_pgp_key
cases = []
for mid in (PRIMARY, "zoo24"):
bip85 = _bip85(MNEMONICS[mid])
for index, user_id in ((0, "[email protected]"), (3, ""), (7, "Test User <[email protected]>")):
priv, pub, fp = derive_pgp_key(bip85, index, "ed25519", user_id)
cases.append(
{
"mnemonic_id": mid,
"index": index,
"user_id": user_id,
"fingerprint": fp,
"private_key_armored": priv,
"public_key_armored": pub,
}
)
return {
"description": (
"Deterministic PGP (Ed25519/EdDSA) keys from BIP-85 app 32, "
"created 2000-01-01T00:00:00Z, as PGPy serializes them. EdDSA "
"signatures are deterministic, so the armored output is "
"byte-reproducible."
),
"created_at": 946684800,
"cases": cases,
}
def gen_kdf_metadata() -> dict:
return {
"description": (
"Vault KDF metadata shapes the TS port must parse. Salts here are "
"pinned for the fixture; production salts come from os.urandom."
),
"current_kdf_version": 1,
"configs": [
{
"name": "argon2id",
"version": 1,
"params": {"time_cost": 2, "memory_cost": 65536, "parallelism": 8},
"salt_b64": base64.b64encode(b"fixture-salt-16b").decode(),
},
{
"name": "pbkdf2-sha256",
"version": 1,
"params": {"iterations": 100000, "dklen": 32},
"salt_note": "salt = SHA256(fingerprint)[:16] — deterministic",
},
],
}
def gen_qr() -> dict:
"""QR reference matrices for the TypeScript encoder.
Byte mode is forced: the library auto-selects alphanumeric mode for an
all-uppercase string, which is a different encoding of the same text
rather than a different answer to the same question, and the TS encoder
is byte-mode only.
The chosen mask is recorded so the TS tests can pin it. The two
implementations disagree about mask SELECTION -- the spec scores the
finished symbol, this library scores one with its format modules blanked
(`makeImpl(test=True, ...)`) -- and every one of the eight masks is a
valid, scannable code, so the tests compare matrices at a fixed mask
rather than reproducing the quirk.
"""
import qrcode
import qrcode.util as qr_util
from qrcode.constants import (
ERROR_CORRECT_H,
ERROR_CORRECT_L,
ERROR_CORRECT_M,
ERROR_CORRECT_Q,
)
levels = {
"L": ERROR_CORRECT_L,
"M": ERROR_CORRECT_M,
"Q": ERROR_CORRECT_Q,
"H": ERROR_CORRECT_H,
}
format_ec = {"L": 1, "M": 0, "Q": 3, "H": 2}
def format_bits(level: str, mask: int) -> int:
data = (format_ec[level] << 3) | mask
rem = data
for _ in range(10):
rem = (rem << 1) ^ ((rem >> 9) * 0x537)
return ((data << 10) | rem) ^ 0x5412
def read_mask(matrix, level: str) -> int:
bits = 0
for i in range(15):
if i < 6:
r, c = i, 8
elif i < 8:
r, c = i + 1, 8
elif i == 8:
r, c = 8, 7
else:
r, c = 8, 14 - i
if matrix[r][c]:
bits |= 1 << i
for mask in range(8):
if format_bits(level, mask) == bits:
return mask
raise AssertionError("no mask matched the encoded format information")
cases: list[dict] = []
for text, level in QR_CASES:
q = qrcode.QRCode(error_correction=levels[level], border=0)
q.add_data(qr_util.QRData(text, mode=qr_util.MODE_8BIT_BYTE))
q.make(fit=True)
matrix = [[bool(v) for v in row] for row in q.get_matrix()]
cases.append(
{
"text": text,
"level": level,
"version": q.version,
"size": len(matrix),
"mask": read_mask(matrix, level),
"modules": matrix,
}
)
return {
"description": "QR matrices (byte mode) from the qrcode library",
"cases": cases,
}
def gen_semantic() -> dict:
"""Reference records and rankings for the retrieval index.
Two properties are being pinned. The rankings must match so a query
answered on one implementation is answered the same way on the other. And
the records must contain no secret: the file this produces is plaintext
beside an encrypted vault, so anything indexed is readable without the
master password. The key_value case below carries a value that must not
appear anywhere in the output.
"""
import tempfile
from seedpass.core.semantic_index import SemanticIndex
entries = [
{"id": 0, "kind": "password", "label": "first-ever.example",
"username": "zero", "notes": "the very first entry a profile creates",
"tags": ["edge"]},
{"id": 1, "kind": "password", "label": "bank.example", "username": "alice",
"url": "https://bank.example", "notes": "main current account",
"tags": ["money", "daily"]},
{"id": 2, "kind": "key_value", "label": "deploy-token", "key": "DEPLOY_TOKEN",
"value": "SECRET-MUST-NOT-APPEAR", "notes": "ci pipeline", "tags": ["ops"]},
{"id": 3, "kind": "document", "label": "recovery notes",
"content": "how to recover the bank account if locked out", "tags": ["docs"]},
{"id": 4, "kind": "totp", "label": "email-2fa", "issuer": "Fastmail",
"notes": "", "tags": ["email"]},
{"id": 5, "kind": "ssh", "label": "prod-server", "fingerprint": "SHA256:abcdef",
"notes": "production access", "tags": ["ops"]},
{"id": 6, "kind": "nostr", "label": "social", "npub": "npub1example",
"notes": "", "tags": []},
{"id": 7, "kind": "seed", "label": "child seed", "notes": "not an indexed kind"},
{"id": 8, "kind": "document", "label": "", "content": "", "notes": "", "tags": []},
{"id": 9, "kind": "password", "label": "linked", "notes": "", "tags": [],
"links": [{"relation": "depends_on", "note": "see recovery notes"}]},
{"kind": "document", "label": "no id at all", "content": "skipped"},
]
queries = [
"bank",
"bank account",
"ops production",
"email",
"recover locked",
"nothing matches here",
"DEPLOY_TOKEN",
"first entry",
]
with tempfile.TemporaryDirectory() as tmpdir:
index = SemanticIndex(Path(tmpdir))
index.build(entries)
records = json.loads(
(Path(tmpdir) / "semantic_index" / "records.json").read_text(
encoding="utf-8"
)
)
results = {q: index.search(q, k=10) for q in queries}
kind_filtered = index.search("ops", k=10, kind="ssh")
blob = json.dumps(records)
assert "SECRET-MUST-NOT-APPEAR" not in blob, "the index leaked a stored secret"
return {
"description": "semantic index records and rankings",
"entries": entries,
"records": records,
"queries": results,
"kind_filtered": kind_filtered,
}
def gen_high_risk() -> dict:
"""Interop vectors for the high-risk partition.
Both implementations read and write these two files, so the fixture is
what proves the format is genuinely shared rather than merely
self-consistent. A partition only one side can open would be worse than
none: the entries are SSH keys, PGP keys and seeds, and a user who cannot
open them has lost them.
The salt and partition key are fixed so the envelope is reproducible;
production draws both at random.
"""
import tempfile
from cryptography.fernet import Fernet
from seedpass.core import agent_secret_isolation as iso
from seedpass.core import high_risk_partition_store as store
factor = "second-factor-passphrase"
salt = bytes(range(16))
partition_key = base64.urlsafe_b64encode(bytes(range(32))).decode("ascii")
# Fernet embeds a random IV and the current time, so `encrypt` would emit
# different bytes on every run and the fixture-drift CI job would fail
# each time it regenerated. `_encrypt_from_parts` pins both. Production
# must never do this -- a repeated IV under one key breaks CBC -- which is
# why it appears only here, in a generator producing test vectors from a
# fixed key.
fixed_iv = bytes(range(16))
fixed_time = 1700000000
wrapping_key = iso._derive_wrapping_key(factor, salt, iso.PARTITION_KDF_ITERATIONS)
envelope = {
"version": iso.PARTITION_ENVELOPE_VERSION,
"kdf": "pbkdf2-sha256",
"iterations": iso.PARTITION_KDF_ITERATIONS,
"salt_b64": base64.b64encode(salt).decode("ascii"),
"wrapped_partition_key": Fernet(wrapping_key)
._encrypt_from_parts(partition_key.encode("utf-8"), fixed_time, fixed_iv)
.decode("utf-8"),
}
tag = iso._partition_key_tag(partition_key)
entries = {
"3": {
"kind": "ssh", "type": "ssh", "label": "prod-server", "index": 3,
"notes": "deploy", "archived": False, "modified_ts": 1700000000,
},
"5": {
"kind": "pgp", "type": "pgp", "label": "signing", "index": 5,
"notes": "", "archived": False, "modified_ts": 1700000100,
},
}
# Same reason: build the partition blob with a pinned IV and time rather
# than calling save_partition_entries, whose Fernet output is random.
payload = json.dumps(
{
"schema_version": 1,
"partition": "high_risk",
"updated_at_utc": float(fixed_time),
"entries": entries,
},
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
file_key = hashlib.sha256(tag.encode("utf-8")).digest()
blob = Fernet(base64.urlsafe_b64encode(file_key))._encrypt_from_parts(
payload, fixed_time, fixed_iv
)
# Prove the pinned construction is what the real writer/reader accept.
with tempfile.TemporaryDirectory() as tmpdir:
profile = Path(tmpdir) / "PROFILE"
profile.mkdir()
(profile / store.PARTITION_FILENAME).write_bytes(blob)
assert store.load_partition_entries(profile, tag) == entries
return {
"description": "high-risk partition envelope and file, for cross-impl checks",
"factor": factor,
"partition_key": partition_key,
"tag": tag,
"envelope": envelope,
"entries": entries,
"partition_file_b64": base64.b64encode(blob).decode("ascii"),
}
def gen_index0() -> dict:
"""index0/atlas reference state for the TypeScript port.
Everything in index0 is hashed with SHA-256 over Python's canonical JSON,
and the merge picks winners by comparing those hashes -- so a single byte
of encoding disagreement makes the two implementations permanently
disagree about which record wins. These vectors are compared whole, via
canonical JSON, rather than field by field.
The inputs deliberately include non-ASCII labels and tags (ensure_ascii
escaping), a non-numeric entry id (view ordering must degrade, not throw),
two writers on two days (checkpoint grouping and retention), and a managed
account (the second scope path).
"""
from seedpass.core import index0 as index0_mod
fp_dir = "/home/user/.seedpass/C557EEC878DFD852"
managed_dir = "/home/user/.seedpass/C557EEC878DFD852/accounts/AABBCCDDEEFF0011"
entries = {
"0": {"kind": "password", "label": "bank.example", "modified_ts": 1700000100,
"tags": ["money", "daily"],
"links": [{"target_id": "2", "relation": "depends_on", "note": "recovery"}]},
"1": {"kind": "totp", "label": "email-2fa", "modified_ts": 1700000200,
"archived": True},
"2": {"kind": "ssh", "label": "prod \u2014 \u00fcn\u00efcod\u00e9 \u2713",
"modified_ts": 1700000300, "tags": ["ops", "\u00fcn\u00ef"]},
"10": {"kind": "document", "label": "notes", "modified_ts": 1700000400},
"not-a-number": {"kind": "password", "label": "weird id",
"modified_ts": 1700000500},
}
payload = {"schema_version": 4, "entries": entries}
specs = [
(1700000100, "entry.created", "0", "password"),
(1700000200, "entry.created", "1", "totp"),
(1700086400, "entry.updated", "0", "password"),
(1700086500, "entry.archived", "1", "totp"),
]
for i, (ts, event_type, subject_id, subject_kind) in enumerate(specs):
payload = index0_mod.append_index0_event(
payload,
event_type=event_type,
subject_type="entry",
subject_id=subject_id,
subject_kind=subject_kind,
modified_ts=ts,
fingerprint_dir=fp_dir,
tags=["auto"],
summary=f"event {i} \u2014 \u00fcn\u00ef",
)
payload = index0_mod.append_index0_event(
payload,
event_type="entry.created",
subject_type="entry",
subject_id="7",
subject_kind="seed",
modified_ts=1700090000,
fingerprint_dir=managed_dir,
)
compacted = index0_mod.compact_index0_payload(payload, fingerprint_dir=fp_dir)
other = index0_mod.append_index0_event(
{"schema_version": 4, "entries": entries},
event_type="entry.created",
subject_type="entry",
subject_id="99",
subject_kind="password",
modified_ts=1700099999,
fingerprint_dir=fp_dir,
)
scope = index0_mod.derive_index0_context(fp_dir)["scope_path"]
return {
"description": "index0/atlas events, checkpoints, views and merge",
"entries": entries,
"context_root": index0_mod.derive_index0_context(fp_dir),
"context_managed": index0_mod.derive_index0_context(managed_dir),
"appended": payload,
"compacted": compacted,
"manifest_meta": index0_mod.build_manifest_index0_metadata(
payload, fingerprint_dir=fp_dir
),
"other": other,
"merged": index0_mod.merge_system_index0(
compacted["_system"]["index0"], other["_system"]["index0"]
),
"views": index0_mod.list_canonical_views(compacted["_system"]["index0"]),
"one_view": index0_mod.get_canonical_view(
compacted["_system"]["index0"],
view_type="counts_by_kind",
scope_path=scope,
),
}
def main() -> None:
FIXTURES_DIR.mkdir(parents=True, exist_ok=True)
commit = subprocess.run(
["git", "rev-parse", "HEAD"], cwd=REPO, capture_output=True, text=True
).stdout.strip()
files: dict[str, dict] = {
"bip39_seeds.json": gen_bip39(),
"bip85_entropy.json": gen_bip85_entropy(),
"passwords_v1.json": gen_passwords(LEGACY_PASSWORD_GEN_VERSION),
"passwords_v2.json": gen_passwords(CURRENT_PASSWORD_GEN_VERSION),
"totp.json": gen_totp(),
"nostr_keys.json": gen_nostr_keys(),
"managed_seeds.json": gen_managed_seeds(),
"fingerprints.json": gen_fingerprints(),
"index_keys.json": gen_index_keys(),
"kdf_metadata.json": gen_kdf_metadata(),
"migrations.json": gen_migrations(),
"ssh_keys.json": gen_ssh_keys(),
"pgp_keys.json": gen_pgp_keys(),
"password_kdf.json": gen_password_kdf(),
"legacy_payloads.json": gen_legacy_payloads(),
"qr.json": gen_qr(),
"semantic.json": gen_semantic(),
"high_risk.json": gen_high_risk(),
"index0.json": gen_index0(),
}
entries_fixture, vault_fixture = gen_entries_and_vault()
files["entries_index.json"] = entries_fixture
files["vault_v3_payload.json"] = vault_fixture
files["nostr_snapshot.json"] = gen_nostr_snapshot(vault_fixture["payload_b64"])
files["portable_backup.json"] = gen_portable_backup(entries_fixture["entries"])
files["entry_secrets.json"] = gen_entry_secrets()
files["entry_mods.json"] = gen_entry_mods()
files["sync_merge.json"] = gen_sync_merge()
files["delta_replay.json"] = gen_delta_replay()
files["nostr_events.json"] = gen_nostr_events(
files["nostr_snapshot.json"], files["delta_replay.json"]
)
for name, data in files.items():
data["fixture_version"] = FIXTURE_VERSION
_write(name, data)
manifest = {
"fixture_version": FIXTURE_VERSION,
"python_commit": commit,
"generator": "scripts/generate_ts_port_fixtures.py",
"redaction_policy": (
"All fixtures derive from public BIP-39 test mnemonics "
"(abandon.../legal.../zoo...). Secret-shaped outputs are "
"generated test material, never from a live profile."
),
"determinism": (
"Byte-identical across runs on the same commit: timestamps pinned "
"to fixed_unix, vault nonce and KDF salts pinned (fixture-only)."
),
"files": sorted(files.keys()),
}
_write("manifest.json", manifest)
if __name__ == "__main__":
main()