Skip to content

Commit a42cd42

Browse files
committed
🧪 chore(security): add Argon2 benchmark command
Add a repeatable KDF benchmark script and just recipe so Argon2 parameters can be measured on the target server. Also save the detailed wallet crypto v2 execution plan for upcoming implementation.
1 parent a7dbbda commit a42cd42

File tree

3 files changed

+223
-0
lines changed

3 files changed

+223
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from __future__ import annotations
2+
3+
import time
4+
from statistics import mean
5+
6+
from argon2.low_level import Type, hash_secret_raw
7+
8+
9+
CASES: tuple[tuple[int, int, int], ...] = (
10+
(64 * 1024, 2, 1),
11+
(64 * 1024, 3, 1),
12+
(96 * 1024, 2, 1),
13+
(96 * 1024, 3, 1),
14+
(128 * 1024, 2, 1),
15+
(128 * 1024, 3, 1),
16+
)
17+
18+
19+
def benchmark_case(
20+
memory_kib: int, time_cost: int, parallelism: int
21+
) -> tuple[float, float, float]:
22+
password = b"benchmark-password"
23+
salt = b"0123456789abcdef"
24+
samples_ms: list[float] = []
25+
for _ in range(7):
26+
t0 = time.perf_counter()
27+
hash_secret_raw(
28+
secret=password,
29+
salt=salt,
30+
time_cost=time_cost,
31+
memory_cost=memory_kib,
32+
parallelism=parallelism,
33+
hash_len=32,
34+
type=Type.ID,
35+
)
36+
samples_ms.append((time.perf_counter() - t0) * 1000)
37+
return mean(samples_ms), min(samples_ms), max(samples_ms)
38+
39+
40+
def main() -> int:
41+
print("Argon2id benchmark (run this on target server)")
42+
for memory_kib, time_cost, parallelism in CASES:
43+
avg_ms, min_ms, max_ms = benchmark_case(memory_kib, time_cost, parallelism)
44+
print(
45+
f"m={memory_kib // 1024:>3}MiB t={time_cost} p={parallelism} "
46+
f"-> avg={avg_ms:7.1f}ms min={min_ms:7.1f}ms max={max_ms:7.1f}ms"
47+
)
48+
return 0
49+
50+
51+
if __name__ == "__main__":
52+
raise SystemExit(main())
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# wallet-crypto-v2-migration: Wallet crypto v2 with dual-format transition
2+
3+
## Context
4+
5+
Need to harden wallet secret storage against a "DB-only leak" threat model,
6+
without breaking the core product behavior:
7+
8+
- free wallets (NoPassword) are mandatory and must remain controllable by system;
9+
- non-free wallets must decrypt only with user PIN/password;
10+
- TON wallets must not break;
11+
- migration and rollback must stay operationally safe.
12+
13+
Operational rollout without feature flags:
14+
15+
- Version A (transition): read/write both formats.
16+
- One week later, Version B: v2-only reads/writes.
17+
- One more week later: manual DB cleanup of legacy columns.
18+
19+
## Files/Directories To Change (Version A)
20+
21+
- `bot/db/models.py`
22+
- `bot/core/domain/entities.py`
23+
- `bot/infrastructure/persistence/sqlalchemy_wallet_repository.py`
24+
- `bot/infrastructure/services/encryption_service.py`
25+
- `bot/core/use_cases/wallet/get_secrets.py`
26+
- `bot/core/use_cases/wallet/change_password.py`
27+
- `bot/core/use_cases/wallet/add_wallet.py`
28+
- `bot/infrastructure/services/wallet_secret_service.py`
29+
- `bot/other/stellar_tools.py`
30+
- `bot/routers/add_wallet.py`
31+
- `bot/routers/ton.py` (only if adapter updates are needed)
32+
- `bot/scripts/` (new migration script)
33+
- `bot/tests/infrastructure/test_encryption_service.py`
34+
- `bot/tests/core/test_get_wallet_secrets_with_seed.py`
35+
- `bot/tests/routers/test_add_wallet.py`
36+
- `bot/tests/routers/test_ton.py`
37+
- `bot/tests/external/` (if canary updates are needed)
38+
- `adr/` (new ADR)
39+
- `docs/runbooks/` (migration/rollback runbook)
40+
41+
## Change Plan
42+
43+
1. [ ] Add new DB field and model mapping (no legacy removal).
44+
- Add `wallet_crypto_v2` (`Text`/CLOB) to wallet table model in
45+
`bot/db/models.py`.
46+
- Add corresponding field to domain entity in `bot/core/domain/entities.py`.
47+
- Update create/read/update mappings in
48+
`bot/infrastructure/persistence/sqlalchemy_wallet_repository.py`.
49+
- Keep legacy columns `secret_key` and `seed_key` intact.
50+
51+
2. [ ] Introduce v2 crypto container format in one field.
52+
- Store JSON container in `wallet_crypto_v2`.
53+
- Required metadata:
54+
- `v` (2),
55+
- `wallet_kind` (`stellar_free`, `stellar_user`, `ton_free`),
56+
- `mode` (`free` or `user`),
57+
- `kid` (`current` or `old`),
58+
- `salt` (base64 random),
59+
- `secret` block: `{nonce, ct}`,
60+
- optional `seed` block: `{nonce, ct}`.
61+
- Keep `seed` separate from `secret` to avoid decrypting seed in normal flows.
62+
63+
3. [ ] Implement encryption/decryption primitives in `EncryptionService`.
64+
- Extend legacy flow with v2 methods for free and user modes.
65+
- Use Argon2id for user-mode key derivation.
66+
- Use AEAD cipher (AES-GCM or ChaCha20-Poly1305).
67+
- Use random `salt` and random `nonce`.
68+
- Key inputs (simple config):
69+
- `WALLET_KEK` (required),
70+
- `WALLET_KEK_OLD` (optional, for rotation window).
71+
- Writes always use `kid=current`; reads may fallback to `old`.
72+
73+
4. [ ] Update read paths to prefer v2 with legacy fallback.
74+
- If `wallet_crypto_v2` exists and is valid, use it.
75+
- Otherwise fallback to legacy `secret_key/seed_key`.
76+
- Cover:
77+
- `bot/core/use_cases/wallet/get_secrets.py`,
78+
- `bot/other/stellar_tools.py`,
79+
- any direct secret usage discovered during implementation.
80+
81+
5. [ ] Update write paths for Version A dual-write compatibility.
82+
- On create/update/password change:
83+
- always write `wallet_crypto_v2`,
84+
- and keep legacy `secret_key/seed_key` updated.
85+
- This guarantees rollback to old image remains functional.
86+
- Cover free, non-free, and TON create/update flows.
87+
88+
6. [ ] TON compatibility adaptation (must not break).
89+
- Current TON detection relies on `secret_key == "TON"` and mnemonic in
90+
`seed_key`.
91+
- For Version A:
92+
- keep legacy TON fields updated (dual-write),
93+
- write v2 representation (`wallet_kind=ton_free`) in parallel.
94+
- Update `wallet_secret_service` read logic:
95+
- v2 first,
96+
- legacy fallback.
97+
- Verify `routers/ton.py` user flows are unchanged.
98+
99+
7. [ ] Add migration script (safe and idempotent).
100+
- Create script in `bot/scripts/` to backfill `wallet_crypto_v2`.
101+
- Script behavior:
102+
- scan wallets with empty v2,
103+
- migrate legacy to v2,
104+
- keep legacy untouched,
105+
- support dry-run and batch/chunk execution.
106+
- Track counters: migrated/skipped/failed.
107+
108+
8. [ ] Add robust tests.
109+
- Infra tests (`test_encryption_service.py`):
110+
- free/user roundtrip,
111+
- wrong PIN fails,
112+
- tampered ciphertext fails,
113+
- old-key fallback works.
114+
- Core tests (`test_get_wallet_secrets_with_seed.py`):
115+
- v2 secret-only read,
116+
- seed decrypted only when requested,
117+
- legacy fallback.
118+
- Router tests:
119+
- add-wallet NoPassword/PIN/password branches,
120+
- TON wallet create/send branch.
121+
- Migration tests:
122+
- legacy -> v2 idempotency,
123+
- dual-write correctness.
124+
125+
9. [ ] Add docs and ADR.
126+
- New ADR: wallet crypto v2 design + threat model + rollout/rollback.
127+
- New/updated runbook: migration steps, validation checklist,
128+
rollback behavior.
129+
130+
10. [ ] Verify Version A readiness and rollback safety.
131+
- Confirm old image can read wallets created by Version A.
132+
- Confirm Version A can read both legacy and v2 records.
133+
- Confirm no regressions in CI gates.
134+
135+
## Version B Plan (scheduled, not in same PR)
136+
137+
1. [ ] Remove legacy read fallback from code (v2-only reads).
138+
2. [ ] Stop legacy writes (v2-only writes).
139+
3. [ ] Keep DB legacy columns temporarily for safety window.
140+
4. [ ] Run full regression and canary.
141+
5. [ ] After one more week and validation, execute manual DB cleanup:
142+
- drop `secret_key` and `seed_key`.
143+
144+
## Risks / Open Questions
145+
146+
- Risk: TON compatibility break due to legacy assumptions.
147+
- Mitigation: dual-read + dual-write in Version A and dedicated TON tests.
148+
- Risk: mixed record population during migration.
149+
- Mitigation: idempotent migration script with counters and reruns.
150+
- Risk: rollback incompatibility for newly created wallets.
151+
- Mitigation: strict dual-write in Version A.
152+
- Open question: production DDL syntax details for target Firebird environment.
153+
154+
## Verification
155+
156+
- Baseline commands:
157+
- `just lint`
158+
- `just check-fast`
159+
- `just test-e2e-smoke`
160+
- `just test-external`
161+
- Targeted checks:
162+
- wallet crypto v2 unit/integration tests,
163+
- TON router tests,
164+
- send/sign password flows.
165+
- Operational checks:
166+
- migration dry-run and real run,
167+
- total wallets vs v2-populated wallets,
168+
- rollback drill: create wallet in Version A, validate old image read path.

‎justfile‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ secret-scan:
6969
metrics:
7070
uv run python .linters/metrics_snapshot.py
7171

72+
bench-kdf:
73+
uv run --with argon2-cffi python bot/scripts/argon2_benchmark.py
74+
7275
start-task task_id title="":
7376
uv run python .linters/create_exec_plan.py {{task_id}} --title "{{title}}"
7477

0 commit comments

Comments
 (0)