|
| 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. |
0 commit comments