mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-04 11:52:03 +00:00
style: satisfy the black gate that was failing the Tests workflow
The Tests workflow was red on every OS and Python version for one reason:
`black --check` found 28 unformatted files. Part predates this branch (the
tui_v3 screens, core/api.py, core/index0.py); part is mine from this session.
Formatting only — verified rather than assumed, because a formatter touching
363 files is exactly where an unnoticed behaviour change would hide:
- 1223 Python tests pass, unchanged
- regenerating the fixtures after reformatting the generator produces
byte-identical output, so no hashed value moved
- differential fuzz: no divergences
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
This commit is contained in:
1 parent
71db3bc498
commit
3395aadd1c
32 files changed
+686
-249
No files matched your search
@@ -29,7 +29,7 @@ CASES: list = [
|
||||
{"\U0001f600": 1, "!": 2},
|
||||
{"z": 1, "é": 2, "中": 3, "\U0001f600": 4},
|
||||
# Escaping
|
||||
{"quote\"": "back\\slash"},
|
||||
{'quote"': "back\\slash"},
|
||||
{"ctrl": "\x00\x01\x1f"},
|
||||
{"nl": "line\nbreak\ttab"},
|
||||
{"unicode": "café 中文 \U0001f512"},
|
||||
|
||||
+189
-54
@@ -38,16 +38,12 @@ sys.path.insert(0, str(REPO / "src"))
|
||||
CLI_BIN = REPO / "js" / "packages" / "cli" / "bin" / "seedpass-js.mjs"
|
||||
|
||||
# Throwaway seeds. Never used for real funds or real vaults.
|
||||
SEED_A = (
|
||||
"rival cover produce defy coconut arrow empty acid crime cereal strong icon"
|
||||
)
|
||||
SEED_A = "rival cover produce defy coconut arrow empty acid crime cereal strong icon"
|
||||
SEED_B = (
|
||||
"romance waste exercise alone pistol mushroom aunt series weasel muscle move skirt"
|
||||
)
|
||||
# Valid words, deliberately wrong checksum: both implementations must refuse it.
|
||||
SEED_INVALID = (
|
||||
"gaze stereo trend brown chunk hero pole width once tent lift bird"
|
||||
)
|
||||
SEED_INVALID = "gaze stereo trend brown chunk hero pole width once tent lift bird"
|
||||
PASSWORD = "cross-impl-check-password"
|
||||
|
||||
results: list[tuple[str, bool, str]] = []
|
||||
@@ -273,10 +269,14 @@ def py_secrets_for(index: dict, seed: str) -> dict[str, str]:
|
||||
out[label] = entry["content"]
|
||||
elif kind == "seed":
|
||||
out[label] = derive_seed_phrase(
|
||||
bip85, int(entry.get("index", int(idx))), int(entry.get("word_count", 24))
|
||||
bip85,
|
||||
int(entry.get("index", int(idx))),
|
||||
int(entry.get("word_count", 24)),
|
||||
)
|
||||
elif kind == "managed_account":
|
||||
out[label] = derive_seed_phrase(bip85, int(entry.get("index", int(idx))), 12)
|
||||
out[label] = derive_seed_phrase(
|
||||
bip85, int(entry.get("index", int(idx))), 12
|
||||
)
|
||||
elif kind == "pgp":
|
||||
from seedpass.core.password_generation import derive_pgp_key
|
||||
|
||||
@@ -326,16 +326,16 @@ def phase_a(tmp: Path) -> None:
|
||||
fp = py_create_profile(app_dir, SEED_A, PASSWORD)
|
||||
|
||||
try:
|
||||
listed = json.loads(run_cli(app_dir, "entry", "list", env_extra={"SEEDPASS_MNEMONIC": SEED_A}))
|
||||
listed = json.loads(
|
||||
run_cli(app_dir, "entry", "list", env_extra={"SEEDPASS_MNEMONIC": SEED_A})
|
||||
)
|
||||
check("TS opens the Python-created index", True)
|
||||
except Exception as exc:
|
||||
check("TS opens the Python-created index", False, str(exc))
|
||||
return
|
||||
|
||||
py_seed, py_index = py_open_profile(app_dir, fp, PASSWORD)
|
||||
py_labels = sorted(
|
||||
e["label"] for e in py_index.get("entries", {}).values()
|
||||
)
|
||||
py_labels = sorted(e["label"] for e in py_index.get("entries", {}).values())
|
||||
ts_labels = sorted(row["label"] for row in listed)
|
||||
check("entry sets match", py_labels == ts_labels, f"{py_labels} != {ts_labels}")
|
||||
|
||||
@@ -359,7 +359,11 @@ def phase_a(tmp: Path) -> None:
|
||||
with agent_running(app_dir):
|
||||
try:
|
||||
run_cli(
|
||||
app_dir, "vault", "unlock", "--ttl", "5",
|
||||
app_dir,
|
||||
"vault",
|
||||
"unlock",
|
||||
"--ttl",
|
||||
"5",
|
||||
env_extra={"SEEDPASS_PASSWORD": PASSWORD},
|
||||
)
|
||||
revealed = run_cli(app_dir, "entry", "reveal", "python-api")
|
||||
@@ -370,7 +374,9 @@ def phase_a(tmp: Path) -> None:
|
||||
)
|
||||
except Exception as exc:
|
||||
check(
|
||||
"TS unlocks the Python profile with the master password", False, str(exc)
|
||||
"TS unlocks the Python profile with the master password",
|
||||
False,
|
||||
str(exc),
|
||||
)
|
||||
|
||||
|
||||
@@ -379,25 +385,64 @@ def phase_b(tmp: Path) -> None:
|
||||
app_dir = tmp / "b"
|
||||
app_dir.mkdir()
|
||||
run_cli(
|
||||
app_dir, "fingerprint", "add", "--name", "ts-made",
|
||||
app_dir,
|
||||
"fingerprint",
|
||||
"add",
|
||||
"--name",
|
||||
"ts-made",
|
||||
env_extra={"SEEDPASS_MNEMONIC": SEED_B, "SEEDPASS_PASSWORD": PASSWORD},
|
||||
)
|
||||
env = {"SEEDPASS_MNEMONIC": SEED_B}
|
||||
run_cli(app_dir, "entry", "add", "password", "ts-site.example", "--length", "22", env_extra=env)
|
||||
run_cli(
|
||||
app_dir,
|
||||
"entry",
|
||||
"add",
|
||||
"password",
|
||||
"ts-site.example",
|
||||
"--length",
|
||||
"22",
|
||||
env_extra=env,
|
||||
)
|
||||
run_cli(app_dir, "entry", "add", "totp", "ts-totp", env_extra=env)
|
||||
run_cli(app_dir, "entry", "add", "key-value", "ts-api", "token", "ts-secret-value", env_extra=env)
|
||||
run_cli(
|
||||
app_dir,
|
||||
"entry",
|
||||
"add",
|
||||
"key-value",
|
||||
"ts-api",
|
||||
"token",
|
||||
"ts-secret-value",
|
||||
env_extra=env,
|
||||
)
|
||||
run_cli(app_dir, "entry", "add", "managed-account", "ts-managed", env_extra=env)
|
||||
run_cli(app_dir, "entry", "add", "ssh", "ts-ssh", env_extra=env)
|
||||
run_cli(app_dir, "entry", "add", "nostr", "ts-nostr", env_extra=env)
|
||||
run_cli(
|
||||
app_dir, "entry", "add", "pgp", "ts-pgp", "--user-id", "[email protected]",
|
||||
app_dir,
|
||||
"entry",
|
||||
"add",
|
||||
"pgp",
|
||||
"ts-pgp",
|
||||
"--user-id",
|
||||
"[email protected]",
|
||||
env_extra=env,
|
||||
)
|
||||
run_cli(app_dir, "entry", "add", "document", "ts-doc", "ts document body", env_extra=env)
|
||||
run_cli(
|
||||
app_dir, "entry", "add", "document", "ts-doc", "ts document body", env_extra=env
|
||||
)
|
||||
run_cli(app_dir, "entry", "add", "seed", "ts-seed", "--words", "24", env_extra=env)
|
||||
run_cli(
|
||||
app_dir, "entry", "add", "totp", "ts-totp-imported",
|
||||
"--secret", "JBSWY3DPEHPK3PXP", "--period", "45", "--digits", "8",
|
||||
app_dir,
|
||||
"entry",
|
||||
"add",
|
||||
"totp",
|
||||
"ts-totp-imported",
|
||||
"--secret",
|
||||
"JBSWY3DPEHPK3PXP",
|
||||
"--period",
|
||||
"45",
|
||||
"--digits",
|
||||
"8",
|
||||
env_extra=env,
|
||||
)
|
||||
|
||||
@@ -422,7 +467,9 @@ def phase_b(tmp: Path) -> None:
|
||||
for k in py_secrets
|
||||
if py_secrets[k].rstrip("\n") != (ts_secrets.get(k) or "").rstrip("\n")
|
||||
]
|
||||
check("every secret derives identically", not mismatches, f"mismatched: {mismatches}")
|
||||
check(
|
||||
"every secret derives identically", not mismatches, f"mismatched: {mismatches}"
|
||||
)
|
||||
|
||||
# Python's schema validation should accept the TS-written index
|
||||
from seedpass.core.migrations import LATEST_VERSION # type: ignore
|
||||
@@ -452,11 +499,17 @@ def phase_c(tmp: Path) -> None:
|
||||
vault = Vault(enc_mgr, fp_dir)
|
||||
cfg_mgr = ConfigManager(vault, fp_dir)
|
||||
backup_mgr = BackupManager(fp_dir, cfg_mgr)
|
||||
py_export = export_backup(vault, backup_mgr, fp_dir / "py-export.json", parent_seed=SEED_A)
|
||||
py_export = export_backup(
|
||||
vault, backup_mgr, fp_dir / "py-export.json", parent_seed=SEED_A
|
||||
)
|
||||
try:
|
||||
summary = json.loads(
|
||||
run_cli(
|
||||
app_a, "vault", "import", str(py_export), "--inspect",
|
||||
app_a,
|
||||
"vault",
|
||||
"import",
|
||||
str(py_export),
|
||||
"--inspect",
|
||||
env_extra={"SEEDPASS_MNEMONIC": SEED_A},
|
||||
)
|
||||
)
|
||||
@@ -473,7 +526,10 @@ def phase_c(tmp: Path) -> None:
|
||||
fp_b = generate_fingerprint(SEED_B)
|
||||
ts_export = app_b / "ts-export.json"
|
||||
run_cli(
|
||||
app_b, "vault", "export", str(ts_export),
|
||||
app_b,
|
||||
"vault",
|
||||
"export",
|
||||
str(ts_export),
|
||||
env_extra={"SEEDPASS_MNEMONIC": SEED_B},
|
||||
)
|
||||
fp_dir_b = app_b / fp_b
|
||||
@@ -481,6 +537,7 @@ def phase_c(tmp: Path) -> None:
|
||||
vault_b = Vault(enc_b, fp_dir_b)
|
||||
cfg_b = ConfigManager(vault_b, fp_dir_b)
|
||||
backup_b = BackupManager(fp_dir_b, cfg_b)
|
||||
|
||||
def user_data(index: dict) -> str:
|
||||
"""User-visible state only.
|
||||
|
||||
@@ -504,7 +561,9 @@ def phase_c(tmp: Path) -> None:
|
||||
"user data changed across the round trip",
|
||||
)
|
||||
except Exception as exc:
|
||||
check("Python imports a TS portable backup (checksum verified)", False, str(exc))
|
||||
check(
|
||||
"Python imports a TS portable backup (checksum verified)", False, str(exc)
|
||||
)
|
||||
|
||||
|
||||
def phase_d(tmp: Path) -> None:
|
||||
@@ -526,8 +585,15 @@ def phase_d(tmp: Path) -> None:
|
||||
detail = ""
|
||||
try:
|
||||
run_cli(
|
||||
app_dir, "fingerprint", "add", "--name", "should-not-exist",
|
||||
env_extra={"SEEDPASS_MNEMONIC": SEED_INVALID, "SEEDPASS_PASSWORD": PASSWORD},
|
||||
app_dir,
|
||||
"fingerprint",
|
||||
"add",
|
||||
"--name",
|
||||
"should-not-exist",
|
||||
env_extra={
|
||||
"SEEDPASS_MNEMONIC": SEED_INVALID,
|
||||
"SEEDPASS_PASSWORD": PASSWORD,
|
||||
},
|
||||
)
|
||||
detail = "TS created a profile from an invalid phrase"
|
||||
except Exception as exc:
|
||||
@@ -593,7 +659,9 @@ def phase_e(tmp: Path) -> None:
|
||||
check("Python and TS agree on the migrated entry set", py_labels == labels)
|
||||
|
||||
# And TS must not have written a downgraded index back to disk
|
||||
reloaded = Vault(EncryptionManager(derive_index_key(SEED_A), fp_dir), fp_dir).load_index()
|
||||
reloaded = Vault(
|
||||
EncryptionManager(derive_index_key(SEED_A), fp_dir), fp_dir
|
||||
).load_index()
|
||||
check(
|
||||
"Python still reads the profile after TS touched it",
|
||||
sorted(e["label"] for e in reloaded.get("entries", {}).values()) == labels,
|
||||
@@ -686,9 +754,15 @@ def phase_f(tmp: Path) -> None:
|
||||
encrypted = vault.get_encrypted_index()
|
||||
manifest, manifest_id = asyncio.run(client.publish_snapshot(encrypted))
|
||||
published = bool(manifest_id) and bool(manifest.chunks)
|
||||
check("Python publishes a snapshot to the relay", published, str(manifest_id))
|
||||
check(
|
||||
"Python publishes a snapshot to the relay", published, str(manifest_id)
|
||||
)
|
||||
except Exception as exc:
|
||||
check("Python publishes a snapshot to the relay", False, f"{type(exc).__name__}: {exc}")
|
||||
check(
|
||||
"Python publishes a snapshot to the relay",
|
||||
False,
|
||||
f"{type(exc).__name__}: {exc}",
|
||||
)
|
||||
return
|
||||
|
||||
# --- TypeScript restores from what Python published -------------------
|
||||
@@ -714,7 +788,9 @@ def phase_f(tmp: Path) -> None:
|
||||
f"{ts_labels} != {py_labels}",
|
||||
)
|
||||
revealed = run_cli(ts_dir, "entry", "reveal", "python-api", env_extra=env)
|
||||
check("restored secrets are intact", revealed == "py-secret-value", revealed)
|
||||
check(
|
||||
"restored secrets are intact", revealed == "py-secret-value", revealed
|
||||
)
|
||||
except Exception as exc:
|
||||
check("TS restores the Python-published snapshot", False, str(exc))
|
||||
|
||||
@@ -757,7 +833,9 @@ def phase_g(tmp: Path) -> None:
|
||||
links = json.loads(run_cli(app_dir, "entry", "links", "0", env_extra=env))
|
||||
check(
|
||||
"TS sees Python's link with a resolved target",
|
||||
len(links) == 1 and links[0]["relation"] == "related_to" and links[0]["target_id"] == 2,
|
||||
len(links) == 1
|
||||
and links[0]["relation"] == "related_to"
|
||||
and links[0]["target_id"] == 2,
|
||||
json.dumps(links),
|
||||
)
|
||||
archived = json.loads(run_cli(app_dir, "entry", "get", "1", env_extra=env))
|
||||
@@ -770,7 +848,11 @@ def phase_g(tmp: Path) -> None:
|
||||
_vault2, em2, _cfg2 = _py_vault(app_dir, fp, SEED_A)
|
||||
entry0 = em2.retrieve_entry(0)
|
||||
entry1 = em2.retrieve_entry(1)
|
||||
check("Python sees TS's edit", entry0.get("notes") == "edited-by-ts", str(entry0.get("notes")))
|
||||
check(
|
||||
"Python sees TS's edit",
|
||||
entry0.get("notes") == "edited-by-ts",
|
||||
str(entry0.get("notes")),
|
||||
)
|
||||
check("Python sees TS's unarchive", entry1.get("archived") is False)
|
||||
check("Python sees TS's link removal", em2.get_links(0) == [])
|
||||
|
||||
@@ -786,14 +868,25 @@ def phase_h(tmp: Path) -> None:
|
||||
"schema_version": 4,
|
||||
"entries": {
|
||||
"0": {
|
||||
"type": "password", "kind": "password", "label": "shared",
|
||||
"length": 16, "archived": False, "notes": "from-current",
|
||||
"tags": ["a"], "modified_ts": base_ts,
|
||||
"type": "password",
|
||||
"kind": "password",
|
||||
"label": "shared",
|
||||
"length": 16,
|
||||
"archived": False,
|
||||
"notes": "from-current",
|
||||
"tags": ["a"],
|
||||
"modified_ts": base_ts,
|
||||
},
|
||||
"1": {
|
||||
"type": "key_value", "kind": "key_value", "label": "only-current",
|
||||
"key": "k", "value": "v", "archived": False, "notes": "",
|
||||
"tags": [], "modified_ts": base_ts,
|
||||
"type": "key_value",
|
||||
"kind": "key_value",
|
||||
"label": "only-current",
|
||||
"key": "k",
|
||||
"value": "v",
|
||||
"archived": False,
|
||||
"notes": "",
|
||||
"tags": [],
|
||||
"modified_ts": base_ts,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -801,20 +894,33 @@ def phase_h(tmp: Path) -> None:
|
||||
"schema_version": 4,
|
||||
"entries": {
|
||||
"0": {
|
||||
"type": "password", "kind": "password", "label": "shared",
|
||||
"length": 16, "archived": True, "notes": "",
|
||||
"tags": ["b"], "modified_ts": base_ts, "username": "incoming-user",
|
||||
"type": "password",
|
||||
"kind": "password",
|
||||
"label": "shared",
|
||||
"length": 16,
|
||||
"archived": True,
|
||||
"notes": "",
|
||||
"tags": ["b"],
|
||||
"modified_ts": base_ts,
|
||||
"username": "incoming-user",
|
||||
},
|
||||
"2": {
|
||||
"type": "password", "kind": "password", "label": "only-incoming",
|
||||
"length": 20, "archived": False, "notes": "", "tags": [],
|
||||
"type": "password",
|
||||
"kind": "password",
|
||||
"label": "only-incoming",
|
||||
"length": 20,
|
||||
"archived": False,
|
||||
"notes": "",
|
||||
"tags": [],
|
||||
"modified_ts": base_ts + 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
py_merged = merge_index_payloads(
|
||||
json.loads(json.dumps(current)), json.loads(json.dumps(incoming)), source_tag="xtest"
|
||||
json.loads(json.dumps(current)),
|
||||
json.loads(json.dumps(incoming)),
|
||||
source_tag="xtest",
|
||||
)
|
||||
|
||||
# Run the TS merge through a tiny node harness against the same inputs
|
||||
@@ -829,7 +935,9 @@ def phase_h(tmp: Path) -> None:
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["node", str(script), json.dumps([current, incoming])],
|
||||
capture_output=True, text=True, cwd=str(CLI_BIN.parent),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(CLI_BIN.parent),
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
check("TS and Python merge identically", False, proc.stderr.strip()[:200])
|
||||
@@ -880,7 +988,11 @@ def phase_i(tmp: Path) -> None:
|
||||
with agent_running(app_dir):
|
||||
try:
|
||||
run_cli(
|
||||
app_dir, "vault", "unlock", "--ttl", "10",
|
||||
app_dir,
|
||||
"vault",
|
||||
"unlock",
|
||||
"--ttl",
|
||||
"10",
|
||||
env_extra={"SEEDPASS_PASSWORD": PASSWORD},
|
||||
)
|
||||
revealed = run_cli(app_dir, "entry", "reveal", "python-api")
|
||||
@@ -937,8 +1049,26 @@ def phase_k(tmp: Path) -> None:
|
||||
|
||||
# Simulate a migration window: the user works exclusively in TS for a
|
||||
# while, exercising creation, modification and archival.
|
||||
run_cli(app_dir, "entry", "add", "password", "post-migration-site", "--length", "22", env_extra=env)
|
||||
run_cli(app_dir, "entry", "add", "key-value", "post-migration-kv", "k", "rollback-value", env_extra=env)
|
||||
run_cli(
|
||||
app_dir,
|
||||
"entry",
|
||||
"add",
|
||||
"password",
|
||||
"post-migration-site",
|
||||
"--length",
|
||||
"22",
|
||||
env_extra=env,
|
||||
)
|
||||
run_cli(
|
||||
app_dir,
|
||||
"entry",
|
||||
"add",
|
||||
"key-value",
|
||||
"post-migration-kv",
|
||||
"k",
|
||||
"rollback-value",
|
||||
env_extra=env,
|
||||
)
|
||||
run_cli(app_dir, "entry", "add", "totp", "post-migration-totp", env_extra=env)
|
||||
run_cli(app_dir, "entry", "modify", "0", "--notes", "touched by ts", env_extra=env)
|
||||
run_cli(app_dir, "entry", "archive", "1", env_extra=env)
|
||||
@@ -966,12 +1096,13 @@ def phase_k(tmp: Path) -> None:
|
||||
|
||||
# And Python can still derive the secrets for what TS created.
|
||||
py_secrets = py_secrets_for(py_index, SEED_A)
|
||||
ts_secrets = ts_secrets_for(app_dir, ["post-migration-site", "post-migration-kv"], SEED_A)
|
||||
ts_secrets = ts_secrets_for(
|
||||
app_dir, ["post-migration-site", "post-migration-kv"], SEED_A
|
||||
)
|
||||
check(
|
||||
"secrets for TS-created entries match in Python",
|
||||
all(
|
||||
py_secrets[k].rstrip("\n") == ts_secrets[k].rstrip("\n")
|
||||
for k in ts_secrets
|
||||
py_secrets[k].rstrip("\n") == ts_secrets[k].rstrip("\n") for k in ts_secrets
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1007,7 +1138,11 @@ def phase_l(tmp: Path) -> None:
|
||||
"blacklisted": "foreign meaning, not our archive flag",
|
||||
"website": "foreign meaning, not our label alias",
|
||||
"words": ["foreign", "list"],
|
||||
"bitlogin": {"admins": ["npub1aaaa"], "roles": {"sales": ["npub1bbbb"]}, "policy_rev": 7},
|
||||
"bitlogin": {
|
||||
"admins": ["npub1aaaa"],
|
||||
"roles": {"sales": ["npub1bbbb"]},
|
||||
"policy_rev": 7,
|
||||
},
|
||||
}
|
||||
foreign_top_level = {"spec": "bitlogin-v1", "org_count": 1}
|
||||
|
||||
|
||||
@@ -110,7 +110,8 @@ def rand_text(rng: random.Random) -> str:
|
||||
if rng.random() < 0.5:
|
||||
return rng.choice(INTERESTING_TEXT)
|
||||
return "".join(
|
||||
rng.choice(string.ascii_letters + string.digits + " -_.") for _ in range(rng.randint(0, 12))
|
||||
rng.choice(string.ascii_letters + string.digits + " -_.")
|
||||
for _ in range(rng.randint(0, 12))
|
||||
)
|
||||
|
||||
|
||||
@@ -262,7 +263,9 @@ def case_password(rng: random.Random) -> tuple[str, Any, dict[str, Any]]:
|
||||
index = rng.randint(0, 5000)
|
||||
version = rng.choice([1, 2])
|
||||
bip85 = BIP85(Bip39SeedGenerator(mnemonic).Generate(""))
|
||||
pg = PasswordGenerator(_Deriver(), mnemonic, bip85, policy=PasswordPolicy(**policy_kwargs))
|
||||
pg = PasswordGenerator(
|
||||
_Deriver(), mnemonic, bip85, policy=PasswordPolicy(**policy_kwargs)
|
||||
)
|
||||
kwargs = {} if version == 1 else {"gen_version": version}
|
||||
try:
|
||||
result: Any = pg.generate_password(length=length, index=index, **kwargs)
|
||||
@@ -302,7 +305,9 @@ def case_merge(rng: random.Random) -> tuple[str, Any, dict[str, Any]]:
|
||||
def case_index0(rng: random.Random) -> tuple[str, Any, dict[str, Any]]:
|
||||
from seedpass.core import index0 as index0_mod
|
||||
|
||||
fp_dir = f"/tmp/seedpass/{''.join(rng.choice('0123456789ABCDEF') for _ in range(16))}"
|
||||
fp_dir = (
|
||||
f"/tmp/seedpass/{''.join(rng.choice('0123456789ABCDEF') for _ in range(16))}"
|
||||
)
|
||||
payload: dict[str, Any] = {"schema_version": 4, "entries": rand_entries(rng)}
|
||||
events = []
|
||||
# Same reasoning as the tombstone cap: occasionally produce enough events,
|
||||
@@ -313,7 +318,9 @@ def case_index0(rng: random.Random) -> tuple[str, Any, dict[str, Any]]:
|
||||
# peaked at 63 distinct subjects across 250 cases -- one short -- so the
|
||||
# eviction path stayed unreachable and a deliberate off-by-one there went
|
||||
# undetected. Bounded code has to be driven past its bound, with margin.
|
||||
event_count = rng.randint(SUBJECT_CAP + 20, SUBJECT_CAP + 60) if big else rng.randint(0, 5)
|
||||
event_count = (
|
||||
rng.randint(SUBJECT_CAP + 20, SUBJECT_CAP + 60) if big else rng.randint(0, 5)
|
||||
)
|
||||
# A large run is clustered into one day half the time, so both the subject
|
||||
# cap (per checkpoint) and the checkpoint retention limit get reached.
|
||||
day_spread = not big or rng.random() < 0.5
|
||||
@@ -359,7 +366,12 @@ def case_index0(rng: random.Random) -> tuple[str, Any, dict[str, Any]]:
|
||||
return (
|
||||
"index0",
|
||||
result,
|
||||
{"op": "index0", "fingerprintDir": fp_dir, "entries": payload.get("entries", {}), "events": events},
|
||||
{
|
||||
"op": "index0",
|
||||
"fingerprintDir": fp_dir,
|
||||
"entries": payload.get("entries", {}),
|
||||
"events": events,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -397,7 +409,9 @@ def case_semantic(rng: random.Random) -> tuple[str, Any, dict[str, Any]]:
|
||||
try:
|
||||
index.build(entries)
|
||||
records = json.loads(
|
||||
(Path(tmpdir) / "semantic_index" / "records.json").read_text(encoding="utf-8")
|
||||
(Path(tmpdir) / "semantic_index" / "records.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
hits = index.search(query, k=10)
|
||||
except Exception as exc:
|
||||
@@ -502,7 +516,9 @@ def run_ts(requests: list[dict[str, Any]]) -> list[Any]:
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--cases", type=int, default=200, help="cases per surface")
|
||||
parser.add_argument("--seed", type=int, default=1, help="RNG seed (reproduces a run)")
|
||||
parser.add_argument(
|
||||
"--seed", type=int, default=1, help="RNG seed (reproduces a run)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--only",
|
||||
action="append",
|
||||
@@ -514,7 +530,9 @@ def main() -> int:
|
||||
surfaces = args.only or sorted(BUILDERS)
|
||||
rng = random.Random(args.seed)
|
||||
|
||||
print(f"differential fuzz: seed={args.seed} cases={args.cases} surfaces={','.join(surfaces)}")
|
||||
print(
|
||||
f"differential fuzz: seed={args.seed} cases={args.cases} surfaces={','.join(surfaces)}"
|
||||
)
|
||||
|
||||
expected: list[tuple[str, int, Any]] = []
|
||||
requests: list[dict[str, Any]] = []
|
||||
@@ -528,12 +546,16 @@ def main() -> int:
|
||||
print(f" generated {len(requests)} cases; running the TypeScript side...")
|
||||
ts_results = run_ts(requests)
|
||||
if len(ts_results) != len(expected):
|
||||
raise SystemExit(f"runner returned {len(ts_results)} results for {len(expected)} cases")
|
||||
raise SystemExit(
|
||||
f"runner returned {len(ts_results)} results for {len(expected)} cases"
|
||||
)
|
||||
|
||||
divergences: list[tuple[str, int, str, str, dict[str, Any]]] = []
|
||||
known: list[tuple[str, int]] = []
|
||||
skipped = 0
|
||||
for (name, idx, python_result), ts_result, request in zip(expected, ts_results, requests):
|
||||
for (name, idx, python_result), ts_result, request in zip(
|
||||
expected, ts_results, requests
|
||||
):
|
||||
if request.get("op") == "noop":
|
||||
skipped += 1
|
||||
continue
|
||||
@@ -552,7 +574,9 @@ def main() -> int:
|
||||
for surface in sorted(by_surface):
|
||||
failures = sum(1 for d in divergences if d[0] == surface)
|
||||
mark = "PASS" if failures == 0 else "FAIL"
|
||||
print(f" [{mark}] {surface}: {by_surface[surface] - failures}/{by_surface[surface]} agree")
|
||||
print(
|
||||
f" [{mark}] {surface}: {by_surface[surface] - failures}/{by_surface[surface]} agree"
|
||||
)
|
||||
|
||||
if skipped:
|
||||
print(f" ({skipped} cases skipped: the Python side refused the input)")
|
||||
@@ -567,12 +591,21 @@ def main() -> int:
|
||||
)
|
||||
|
||||
if divergences:
|
||||
print(f"\n{len(divergences)} DIVERGENCE(S) — reproduce with --seed {args.seed}\n")
|
||||
dump = Path("/tmp/claude-1000/-home-user/4efd790e-427f-430a-b379-ef8391e6744f/scratchpad/divergences.json")
|
||||
dump.write_text(json.dumps([
|
||||
{"surface": n, "case": i, "python": p_, "ts": t, "input": r}
|
||||
for n, i, p_, t, r in divergences
|
||||
], indent=1))
|
||||
print(
|
||||
f"\n{len(divergences)} DIVERGENCE(S) — reproduce with --seed {args.seed}\n"
|
||||
)
|
||||
dump = Path(
|
||||
"/tmp/claude-1000/-home-user/4efd790e-427f-430a-b379-ef8391e6744f/scratchpad/divergences.json"
|
||||
)
|
||||
dump.write_text(
|
||||
json.dumps(
|
||||
[
|
||||
{"surface": n, "case": i, "python": p_, "ts": t, "input": r}
|
||||
for n, i, p_, t, r in divergences
|
||||
],
|
||||
indent=1,
|
||||
)
|
||||
)
|
||||
print(f" (full list written to {dump})\n")
|
||||
for name, idx, py_canon, ts_canon, request in divergences[:5]:
|
||||
print(f"--- {name} case {idx} ---")
|
||||
|
||||
@@ -45,6 +45,8 @@ def test_app_dir() -> Path:
|
||||
another ``tests`` segment on each reload.
|
||||
"""
|
||||
return Path(consts.APP_DIR) / "tests"
|
||||
|
||||
|
||||
from utils.key_derivation import derive_key_from_password, derive_index_key
|
||||
from seedpass.core.encryption import EncryptionManager
|
||||
from seedpass.core.vault import Vault
|
||||
|
||||
@@ -58,8 +58,7 @@ MNEMONICS = {
|
||||
"abandon abandon abandon abandon abandon about"
|
||||
),
|
||||
"legal12": (
|
||||
"legal winner thank year wave sausage worth useful "
|
||||
"legal winner thank yellow"
|
||||
"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 "
|
||||
@@ -326,7 +325,9 @@ def gen_fingerprints() -> dict:
|
||||
{
|
||||
"mnemonic_id": "abandon12-mixed-case-padded",
|
||||
"mnemonic": " " + MNEMONICS[PRIMARY].upper() + " ",
|
||||
"fingerprint": generate_fingerprint(" " + MNEMONICS[PRIMARY].upper() + " "),
|
||||
"fingerprint": generate_fingerprint(
|
||||
" " + MNEMONICS[PRIMARY].upper() + " "
|
||||
),
|
||||
}
|
||||
)
|
||||
return {
|
||||
@@ -412,9 +413,7 @@ def _build_entries_index(post_add=None) -> dict:
|
||||
post_add(em)
|
||||
|
||||
index = vault.load_index()
|
||||
entries = {
|
||||
k: v for k, v in index.items() if k != "_system"
|
||||
}
|
||||
entries = {k: v for k, v in index.items() if k != "_system"}
|
||||
return entries
|
||||
|
||||
|
||||
@@ -461,7 +460,11 @@ def gen_password_kdf() -> dict:
|
||||
|
||||
fp = generate_fingerprint(MNEMONICS[PRIMARY])
|
||||
pbkdf2_cases = []
|
||||
for password in ("fixture-password", "correct horse battery staple", " pässwörd "):
|
||||
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(
|
||||
@@ -534,8 +537,10 @@ def gen_legacy_payloads() -> dict:
|
||||
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
|
||||
seed_ct = (
|
||||
b"V3|"
|
||||
+ seed_nonce
|
||||
+ AESGCM(seed_key).encrypt(seed_nonce, mnemonic.encode(), None)
|
||||
)
|
||||
kdf_dict = {
|
||||
"name": "pbkdf2-sha256",
|
||||
@@ -592,7 +597,9 @@ def gen_nostr_snapshot(vault_payload_b64: str) -> dict:
|
||||
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)]
|
||||
pinned_chunks = [
|
||||
compressed[i : i + limit] for i in range(0, len(compressed), limit)
|
||||
]
|
||||
metas = [
|
||||
{
|
||||
"id": f"seedpass-chunk-{i:04d}",
|
||||
@@ -704,8 +711,12 @@ def gen_sync_merge() -> dict:
|
||||
"schema_version": 4,
|
||||
"entries": {
|
||||
"0": entry(
|
||||
"site", t, notes="from-incoming", tags=["b", "a"],
|
||||
username="alice", archived=True,
|
||||
"site",
|
||||
t,
|
||||
notes="from-incoming",
|
||||
tags=["b", "a"],
|
||||
username="alice",
|
||||
archived=True,
|
||||
)
|
||||
},
|
||||
},
|
||||
@@ -713,7 +724,10 @@ def gen_sync_merge() -> dict:
|
||||
),
|
||||
_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), "1": entry("keep", t)},
|
||||
},
|
||||
{
|
||||
"schema_version": 4,
|
||||
"entries": {"0": {**entry("site", t + 5), "_deleted": True}},
|
||||
@@ -727,7 +741,12 @@ def gen_sync_merge() -> dict:
|
||||
"entries": {},
|
||||
"_sync_meta": {
|
||||
"tombstones": {
|
||||
"0": {"deleted_ts": t, "entry_hash": "", "event_hash": "", "source": "x"}
|
||||
"0": {
|
||||
"deleted_ts": t,
|
||||
"entry_hash": "",
|
||||
"event_hash": "",
|
||||
"source": "x",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -741,7 +760,12 @@ def gen_sync_merge() -> dict:
|
||||
"entries": {},
|
||||
"_sync_meta": {
|
||||
"tombstones": {
|
||||
"0": {"deleted_ts": t + 20, "entry_hash": "", "event_hash": "", "source": "x"}
|
||||
"0": {
|
||||
"deleted_ts": t + 20,
|
||||
"entry_hash": "",
|
||||
"event_hash": "",
|
||||
"source": "x",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -755,7 +779,12 @@ def gen_sync_merge() -> dict:
|
||||
"entries": {},
|
||||
"_sync_meta": {
|
||||
"tombstones": {
|
||||
"0": {"deleted_ts": t + 1, "entry_hash": "aa", "event_hash": "", "source": "one"}
|
||||
"0": {
|
||||
"deleted_ts": t + 1,
|
||||
"entry_hash": "aa",
|
||||
"event_hash": "",
|
||||
"source": "one",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -764,7 +793,12 @@ def gen_sync_merge() -> dict:
|
||||
"entries": {},
|
||||
"_sync_meta": {
|
||||
"tombstones": {
|
||||
"0": {"deleted_ts": t + 9, "entry_hash": "bb", "event_hash": "", "source": "two"}
|
||||
"0": {
|
||||
"deleted_ts": t + 9,
|
||||
"entry_hash": "bb",
|
||||
"event_hash": "",
|
||||
"source": "two",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -798,14 +832,25 @@ def gen_delta_replay() -> dict:
|
||||
|
||||
def entry(label: str, ts: int) -> dict:
|
||||
return {
|
||||
"type": "password", "kind": "password", "label": label,
|
||||
"length": 16, "archived": False, "notes": "", "tags": [],
|
||||
"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}}}
|
||||
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]
|
||||
@@ -1158,7 +1203,11 @@ def gen_pgp_keys() -> dict:
|
||||
cases = []
|
||||
for mid in (PRIMARY, "zoo24"):
|
||||
bip85 = _bip85(MNEMONICS[mid])
|
||||
for index, user_id in ((0, "[email protected]"), (3, ""), (7, "Test User <[email protected]>")):
|
||||
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(
|
||||
{
|
||||
@@ -1300,26 +1349,85 @@ def gen_semantic() -> dict:
|
||||
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"}]},
|
||||
{
|
||||
"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 = [
|
||||
@@ -1401,12 +1509,22 @@ def gen_high_risk() -> dict:
|
||||
|
||||
entries = {
|
||||
"3": {
|
||||
"kind": "ssh", "type": "ssh", "label": "prod-server", "index": 3,
|
||||
"notes": "deploy", "archived": False, "modified_ts": 1700000000,
|
||||
"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,
|
||||
"kind": "pgp",
|
||||
"type": "pgp",
|
||||
"label": "signing",
|
||||
"index": 5,
|
||||
"notes": "",
|
||||
"archived": False,
|
||||
"modified_ts": 1700000100,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1464,16 +1582,31 @@ def gen_index0() -> dict:
|
||||
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"]},
|
||||
"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},
|
||||
"not-a-number": {
|
||||
"kind": "password",
|
||||
"label": "weird id",
|
||||
"modified_ts": 1700000500,
|
||||
},
|
||||
}
|
||||
|
||||
payload = {"schema_version": 4, "entries": entries}
|
||||
|
||||
Reference in new issue
Block a user