# Delta `0.2.6-pre.017` — migration explicite `.kspwallet` V1 -> V2 et canaris de persistence ## Base requise ```text 0.2.6-pre.016-fix.002 appliquée workspace.package.version = 0.2.6-pre.16.fix.2 ``` Le checkpoint opérateur de cette base est intégralement vert : ```text cargo fmt --all OK python3 scripts/audit_rust_workspace_rules.py clean cargo check --workspace OK cargo clippy --workspace --all-targets OK cargo test --workspace OK ksp-wallet-lib 78 passed / 1 ignored Wallet Desk desktop_contract 16/16 OK Wallet Desk release_compliance 4/4 OK smokes réseau / benchmark ignored comme prévu ``` `pre.016-fix.002` est donc accepté comme base de `pre.017`. ## Signal technique Cette tranche ajoute des APIs/runtime/persistence de migration Wallet : ```text workspace.package.version = 0.2.6-pre.17 commit = v0.2.6-pre.017 ``` Aucun tag prerelease. Aucun `package.json`/`tauri.conf.json` n'est modifié. ## Objectif de la tranche `pre.017` ferme le chantier fonctionnel V1/V2 avant la candidate documentaire `pre.018` : ```text migration V1 -> V2 explicite OWNER authentifie toujours la source migration mémoire copie filesystem no-clobber remplacement in-place atomique et stale-protected identité Solana conservée metadata protégées conservées exactement note IDs stables conservés VIEW enabled/disabled conservé credential VIEW cible explicite si VIEW est activé aucune migration implicite lors d'un open/inspect canaris tampering / no-clobber / stale-state régression Wallet Desk non-migrante ``` ## APIs publiques de migration ### Snapshot mémoire ```rust migrate_wallet_v1_to_v2( source, owner_password, target_view_password, ) ``` Le `source` doit être un V1 strict. La fonction : 1. parse le V1 ; 2. authentifie OWNER ; 3. récupère en mémoire l'identité Solana autorisée et le payload metadata exact ; 4. construit un nouvel envelope V2 ; 5. restaure alias, notes et identifiants stables de notes dans le payload V2 ; 6. retourne un `WalletOwner` V2. Aucun octet source n'est modifié. ### Migration vers une nouvelle destination ```rust migrate_wallet_file_v1_to_v2( source, destination, owner_password, target_view_password, ) ``` La source V1 est conservée. La destination V2 utilise la persistence no-clobber existante : une destination existante retourne `wallet.destination_exists` et reste inchangée. ### Migration in-place ```rust migrate_wallet_file_v1_to_v2_in_place( source, owner_password, target_view_password, ) ``` L'enveloppe V1 observée avant OWNER unlock devient l'état authentifié attendu. Après construction complète du V2, la publication passe par le remplacement atomique V1 existant : ```text relire source bornée parse V1 strict vérifier signature OWNER comparer à expected V1 écrire/sync temp revérifier expected V1 juste avant publication rename atomique sync fichier / parent best-effort ``` Un autre état V1 valide publié entre-temps provoque `wallet.state_conflict` et n'est jamais écrasé. ## Politique credential VIEW pendant migration V1 et V2 ont des domaines/AAD distincts. Le VIEW key-wrap V1 ne peut donc pas être réutilisé directement dans V2. Règle `pre.017` : ```text source V1 VIEW disabled -> target_view_password = None source V1 VIEW enabled -> target_view_password = Some(...) ``` Lorsque VIEW est activé, le credential cible peut être : - le même mot de passe VIEW que V1 ; - un nouveau mot de passe VIEW choisi pendant la migration. Cela ne constitue pas une élévation supplémentaire : OWNER possède déjà en V1/V2 l'autorité de rotation du credential VIEW sans connaître l'ancien mot de passe VIEW. La migration pure ne change toutefois pas la **forme de capability** : elle ne peut ni activer VIEW sur une source désactivée ni supprimer VIEW sur une source activée. Une incohérence de cette politique retourne le nouveau code : ```text wallet.migration_invalid ``` ## Ce qui est préservé et ce qui est renouvelé Préservé : ```text Solana keypair / Pubkey alias protégé notes protégées note IDs stables état VIEW enabled/disabled credential OWNER logique fourni par le caller ``` Renouvelé en V2 : ```text owner_root metadata_key secret_key OWNER auth Ed25519 key OWNER/VIEW slot IDs Argon2 salts XChaCha nonces ciphertexts state signature AAD/transcripts V2 ``` La migration n'est donc jamais une transformation syntaxique JSON -> binaire des ciphertexts existants. Elle ouvre/authentifie V1 puis reconstruit cryptographiquement un document V2. ## Secret lifetime interne `OwnerPassword` reste publiquement non-`Clone`. La migration doit toutefois dériver : ```text V1 OWNER KDF pour authentifier la source V2 OWNER KDF pour créer la cible ``` `ksp-wallet-lib` ajoute donc uniquement un duplicate interne court-vivant du wrapper OWNER, non exposé aux consumers et toujours zéroïsé au drop. Aucun contrat public `Clone` n'est ajouté à `OwnerPassword` ou `ViewPassword`. ## Canaris `ksp-wallet-lib` Nouveau module `unit_tests/migration.rs` : - migration V1 full-vector -> V2 ; - identité Solana conservée ; - alias/notes/note IDs conservés ; - OWNER V2 rouvert avec le même credential OWNER ; - VIEW V2 rouvert avec le credential cible ; - ancien VIEW refusé lorsqu'un nouveau credential cible est choisi ; - forme VIEW activée/désactivée obligatoirement conservée ; - copie vers destination existante = no-clobber ; - copie réussie laisse V1 source inchangé ; - migration in-place produit un V2 ouvrable via APIs génériques ; - tampering OWNER-authenticated V1 rejeté avant publication ; - remplacement concurrent par un autre V1 valide = `wallet.state_conflict` ; - état concurrent plus récent conservé. Le public API canary expose les trois fonctions de migration et `ERROR_CODE_MIGRATION_INVALID` depuis la racine de crate. ## Régression Wallet Desk Wallet Desk ne reçoit volontairement **aucun bouton/commande de migration automatique** dans cette tranche. Il reste sur : ```text create/import génériques -> V2 open OWNER/VIEW générique -> V1 ou V2 inventory inspect générique -> V1 ou V2 ``` Un nouveau canari `pre_017_wallet_desk_open_paths_remain_non_migrating` interdit l'introduction d'un appel `migrate_wallet_*` dans les chemins normaux `app_state`/inventory. Cette séparation garantit qu'ouvrir un ancien V1 dans Wallet Desk ne modifie jamais le fichier à l'insu de l'utilisateur. ## Documentation synchronisée Cette tranche met à jour : ```text docs/formats/KSPWALLET_V2.md docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md crates/ksp-wallet-lib/README.md crates/ksp-wallet-lib/USAGE.md prompts/011-V0_2_6_START_PROMPT.md docs/000-README.md docs/plans/000-README.md ROADMAP.md état global uniquement ``` `CHANGELOG.md` reste réservé à `pre.018`/clôture de release conformément à la politique KSP. ## Validation opérateur requise ```bash cargo fmt --all python3 scripts/audit_rust_workspace_rules.py cargo check --workspace cargo clippy --workspace --all-targets cargo test -p ksp-wallet-lib cargo test -p ksp-app-wallet-desk cargo test --workspace ``` Aucun `cargo tauri build` dans cette tranche. Si ce checkpoint est vert, la prochaine tranche est : ```text 0.2.6-pre.018 ``` avec documentation/candidate finale, fermeture CWD/resources du bundle, parcours Tauri final, prompt `0.2.7`, puis `cargo tauri build` comme toute dernière opération de validation.