Files
2026-08-22 09:39:13 +02:00

7.7 KiB

Delta 0.2.6-pre.017 — migration explicite .kspwallet V1 -> V2 et canaris de persistence

Base requise

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 :

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 :

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 :

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

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

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

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 :

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 :

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 :

wallet.migration_invalid

Ce qui est préservé et ce qui est renouvelé

Préservé :

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 :

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 :

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 :

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 :

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

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 :

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.