Files
khadhroony-bot3/ks-wallet/USAGE.md
2026-08-11 15:15:03 +02:00

20 KiB

Utilisation de ks-wallet

Statut

Depuis 0.5.2, le manager couvre la persistance native protégée, la migration non destructive du legacy, l'inspection de fichiers de transfert, l'import/export Solana CLI JSON et Base58, ainsi que le changement de mot de passe sans changement de pubkey. Tout export secret repart d'un .kswallet authentifié avec son mot de passe.

Valider un alias

let alias = match ks_wallet::WalletAlias::parse(
    "devnet-operator",
) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};

assert_eq!(alias.as_str(), "devnet-operator");

Un alias doit contenir entre 1 et 64 octets, commencer par un caractère alphanumérique ASCII et ne contenir ensuite que des caractères alphanumériques, _ ou -.

Générer un wallet temporaire en mémoire

let alias = match ks_wallet::WalletAlias::parse(
    "ephemeral",
) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};

let wallet = ks_wallet::TemporaryWallet::generate(alias);
let identity = wallet.identity();

assert_eq!(
    identity.persistence,
    ks_wallet::WalletPersistence::Temporary,
);
println!("public key={}", identity.public_key);

Cette capacité est durable : 0.5.2 doit continuer à permettre des wallets temporaires/jetables pour tests, démonstrations et scénarios sans imposer une persistance ou un mot de passe.

Signer un message

let signature = match wallet.sign_message(
    b"khadhroony demo authorization",
) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};

println!("signature={signature}");

as_signer() retourne une référence au trait Solana Signer sans exposer les octets du keypair.

Pour une API async qui conserve la référence au travers d'un await et impose un futur Send, as_sync_signer() préserve explicitement le marqueur Sync du keypair concret.

Cette frontière de signature doit rester disponible après restructuration : les consommateurs ne doivent pas connaître le format persistant.

Stockage legacy actuel

let store = match ks_wallet::TemporaryWalletStore::new(
    "./data/wallets",
) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};

Le store actuel persiste un keypair au format Solana JSON dans :

<alias>.json

Ce format est legacy pour 0.5.2.

Créer un wallet legacy persistant

let alias = match ks_wallet::WalletAlias::parse(
    "devnet-payer",
) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};

let wallet = match store.create(alias).await {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};

let identity = wallet.identity();
assert_eq!(
    identity.persistence,
    ks_wallet::WalletPersistence::Persistent,
);

create refuse d'écraser un fichier existant.

Charger ou créer sans écrasement

let alias = match ks_wallet::WalletAlias::parse(
    "shared-devnet",
) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};

let wallet = match store.load_or_create(alias).await {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};

println!("wallet={}", wallet.public_key());

load_or_create utilise une création exclusive et reprend la lecture lorsqu'une création concurrente a déjà réservé le même alias.

Le contenu legacy est toutefois écrit directement dans le chemin final. La publication n'est pas encore crash-safe/atomique par fichier temporaire + renommage.

Manager multi-wallet et codec v1

Le manager reçoit le répertoire déjà résolu par le consommateur/configuration. Son scan automatique ne sort jamais de cette racine :

let manager = match ks_wallet::WalletManager::new(
    configured_wallet_directory,
) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};

let wallets = match manager.scan().await {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};

scan() est non récursif et ne traite que les <alias>.kswallet. Chaque candidat doit décoder entièrement selon le format v1 : magic/version, flags, IDs crypto, paramètres KDF, pubkey déclarée, alias, sel, nonce et ciphertext de taille exacte. Le nom du fichier et l'alias encodé doivent correspondre.

Le lookup par alias reste limité à la racine configurée :

let wallet = match manager.lookup(&alias).await {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};

Pour un fichier choisi explicitement par l'utilisateur, par exemple via un file browser Tauri, le chemin peut être extérieur au store :

let handle = match manager.inspect_file(selected_path).await {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};

println!("alias={}", handle.alias().as_str());
println!("pubkey={}", handle.public_key());
println!("format={}", handle.format_version());

WalletFileHandle conserve le chemin en interne mais ne fournit aucun getter public vers ce chemin et son Debug ne l'affiche pas. Sa pubkey reste une identité déclarée tant que le fichier n'a pas été ouvert : unlock()/unlock_file() authentifient l'AEAD puis vérifient que la pubkey de la keypair déchiffrée correspond exactement au header avant de retourner UnlockedWallet. inspect_file() ne modifie pas la configuration et n'ajoute pas le fichier sélectionné au résultat de scan().

Cycle de vie natif protégé par mot de passe

WalletPassword prend possession du mot de passe pour une seule opération. Il n'est ni clonable ni sérialisable et son Debug est toujours redacted. L'appelant construit donc une nouvelle valeur pour chaque création, ouverture ou changement de mot de passe.

Créer un wallet persistant natif

let alias = match ks_wallet::WalletAlias::parse("operator") {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
let password = match ks_wallet::WalletPassword::new(password_string) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
let wallet = match manager.create(alias.clone(), password).await {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
println!("pubkey={}", wallet.public_key());

create() génère une keypair Solana, la protège avec Argon2id + XChaCha20-Poly1305, publie <alias>.kswallet sans écraser une destination existante et retourne une capacité UnlockedWallet. Aucun getter public ne fournit les bytes privés.

Ouvrir par alias

let password = match ks_wallet::WalletPassword::new(password_string) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
let wallet = match manager.unlock(&alias, password).await {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
let signature = match wallet.sign_message(b"authenticated operation") {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
println!("signature={signature}");
wallet.lock();

Un mauvais mot de passe ou une altération des données authentifiées échoue avant la création de la capacité de signature. lock() consomme explicitement UnlockedWallet; laisser la valeur sortir de portée a le même effet de durée de vie.

Ouvrir un fichier choisi hors du store

let handle = match manager.inspect_file(selected_path).await {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
let password = match ks_wallet::WalletPassword::new(password_string) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
let wallet = match manager.unlock_file(&handle, password).await {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};

Le handle est revalidé contre le fichier avant l'ouverture authentifiée. Le fichier externe reste extérieur au scan/configuration du manager.

Changer le mot de passe

let current_password = match ks_wallet::WalletPassword::new(current_password_string) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
let new_password = match ks_wallet::WalletPassword::new(new_password_string) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
let handle = match manager
    .change_password(&alias, current_password, new_password)
    .await
{
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
println!("pubkey={}", handle.public_key());

change_password() n'altère pas la keypair : il authentifie l'ancien conteneur, rechiffre exactement le même matériau avec un nouveau sel/nonce et remplace atomiquement le .kswallet. La pubkey reste identique. Cette opération ne peut pas révoquer rétroactivement une UnlockedWallet déjà remise à un consommateur ; cette capacité doit être lock()/dropée par son propriétaire.

Migration et import/export secrets

0.5.2-pre.005 expose deux formats de transfert explicites :

  • WalletTransferFormat::SolanaCliJson : tableau JSON standard des 64 octets du keypair Solana ;
  • WalletTransferFormat::SolanaPrivateKeyBase58 : Base58 du keypair Solana complet de 64 octets, utilisé comme adaptateur tiers de référence pour Phantom et compatible avec le flux Phantom -> Solflare documenté.

La matrice des formats vérifiés et reportés est maintenue dans ../docs/WALLET_FORMAT_COMPATIBILITY.md.

Inspecter un keypair externe sans l'importer

let inspection = match ks_wallet::inspect_transfer_file(
    source_path,
    ks_wallet::WalletTransferFormat::SolanaCliJson,
)
.await
{
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
println!("pubkey={}", inspection.public_key());

inspect_transfer_file() applique les mêmes validations bornées que l'import mais ne crée aucun .kswallet. Le résultat ne contient que la pubkey dérivée et le format explicitement sélectionné. WalletTransferFormat::supported() fournit la liste compilée des formats afin qu'un consommateur n'ait pas à maintenir une seconde matrice.

Cette inspection confirme uniquement qu'un fichier contient un keypair Solana valide dans le format choisi. Le wire secret ne permet pas de déduire de manière fiable le rôle historique de la clé (wallet, mint, authority, recipient, etc.).

Migrer un legacy <alias>.json

let password = match ks_wallet::WalletPassword::new(password_string) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
let wallet = match manager.migrate_legacy(alias.clone(), password).await {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
println!("pubkey={}", wallet.public_key());

migrate_legacy() lit <store>/<alias>.json, crée <store>/<alias>.kswallet, vérifie la pubkey et ne modifie ni ne supprime le legacy. Une destination native déjà existante est refusée. Le .json reste donc disponible pour rollback jusqu'à suppression explicite par l'opérateur.

Importer un fichier Solana CLI JSON

let alias = match ks_wallet::WalletAlias::parse("imported-cli") {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
let password = match ks_wallet::WalletPassword::new(password_string) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
let wallet = match manager
    .import_file(
        alias,
        password,
        source_path,
        ks_wallet::WalletTransferFormat::SolanaCliJson,
    )
    .await
{
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};

Une source d'import secret doit être un fichier régulier non symlink et privé sous Unix. L'import refuse les collisions de destination et les pubkeys déjà présentes sous un autre alias natif.

Importer une private key Base58

Le même appel utilise :

ks_wallet::WalletTransferFormat::SolanaPrivateKeyBase58

La forme acceptée est strictement le Base58 du keypair Solana complet de 64 octets. Une seed de 32 octets ou une recovery phrase n'est pas interprétée implicitement.

Exporter un secret

let password = match ks_wallet::WalletPassword::new(password_string) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
};
match manager
    .export_file(
        &alias,
        password,
        destination_path,
        ks_wallet::WalletTransferFormat::SolanaCliJson,
    )
    .await
{
    std::result::Result::Ok(()) => {},
    std::result::Result::Err(error) => {
        return std::result::Result::Err(error);
    },
}

L'export secret exige toujours le mot de passe valide du .kswallet. La destination n'est créée qu'après authentification, n'est jamais écrasée silencieusement et est publiée en fichier privé (0600 sous Unix). Le répertoire parent peut être un répertoire explicitement choisi par le consommateur, par exemple via un file browser ; il n'est pas assimilé au store wallet configuré.

Pour produire le Base58 tiers, remplacer le format par WalletTransferFormat::SolanaPrivateKeyBase58.

Format binaire v1

Le layout exact, les bornes et la politique de publication sont documentés dans ../docs/NATIVE_FORMAT.md. pre.004 implémente désormais l'encodage, la lecture, la protection et la publication. Le payload secret v1 est strictement borné à une keypair Solana de 64 octets et produit un ciphertext/tag de 80 octets.

Le format exécute Argon2id v19 avec un profil d'écriture par défaut de 64 MiB / 3 passes / 4 lanes et XChaCha20-Poly1305 avec nonce 24 octets. Les paramètres lus depuis un fichier restent bornés avant toute exécution du KDF.

Cible persistante 0.5.2

Le nouveau stockage natif doit utiliser :

<alias>.kswallet

Le fichier .kswallet est un conteneur binaire versionné protégé par le mot de passe propre au wallet.

L'API cible doit permettre conceptuellement :

create persistent(alias, password)
open/unlock(alias, password)
change password(alias, old password, new password)
scan/discover <alias>.kswallet
list identities
lookup by alias
sign without exposing private bytes

Le store découvre les wallets persistants en scannant directement sa racine résolue (KS_WALLETS_DIRECTORY, wallets par défaut) pour les <alias>.kswallet valides. Les sous-répertoires temporary/... appartiennent aux fixtures et ne font pas partie du scan natif persistant. Le scan ne doit pas faire confiance au seul nom du fichier.

Le changement de mot de passe rechiffre la même keypair. Il n'existe pas de rotation normale du secret Ed25519 conservant la même pubkey : une nouvelle clé secrète signifie une nouvelle identité Solana.

Le modèle runtime ouvert est UnlockedWallet, capacité non clonable possédant le signer authentifié. WalletManager, WalletFileHandle, WalletIdentity, WalletPersistence, WalletPassword et UnlockedWallet font désormais partie de l'API de base.

Import/export implémenté

ks-wallet importe et exporte désormais le format keypair JSON standard des binaires Solana ainsi que le Base58 du keypair complet de 64 octets. La migration legacy réutilise explicitement le codec SolanaCliJson.

Un export public peut toujours exposer uniquement les données autorisées telles que l'alias et la pubkey. Tout export contenant ou permettant de reconstruire la clé privée demande et valide le mot de passe du wallet.

Phantom est la cible tierce de référence du Base58 et Solflare documente l'import direct de cette private key depuis Phantom. Backpack, Trust Wallet, le keystore Solflare et Base app restent reportés tant qu'un contrat Solana précis et testable n'est pas suffisamment documenté pour la surface considérée. Aucune recovery phrase n'est synthétisée à partir d'une keypair arbitraire.

Invariants

  • les octets secrets ne sont pas exposés par l'API publique par commodité ;
  • aucun mot de passe ni secret n'est projeté vers Tauri ;
  • ks-config ne stocke pas les mots de passe ;
  • WalletIdentity, WalletSummary, WalletFileHandle::Debug et les erreurs natives normales n'exposent pas le chemin local ;
  • un fichier existant n'est pas écrasé silencieusement ;
  • les liens symboliques et fichiers non réguliers sont refusés ;
  • sur Unix, les permissions privées sont vérifiées ;
  • les buffers secrets temporaires sont effacés lorsque possible ;
  • cette crate ne décide pas si une transaction est autorisée.

Tests de référence actuels

  • validation des alias ;
  • génération et signature sans persistance ;
  • création exclusive, chargement et load_or_create ;
  • refus d'écrasement ;
  • rejet des keypairs corrompus ;
  • vérification des permissions Unix privées.

pre.003 ajoute le décodage/validation v1 stricts et les bornes KDF/fichier. pre.004 ajoute l'encodage, la publication atomique, le password, la dérivation/chiffrement effectifs, l'ouverture authentifiée et le changement de password. pre.005 ajoute la migration legacy, les deux formats de transfert testés, les collisions d'alias/pubkey et la matrice de compatibilité. La tranche suivante couvre la configuration et les consommateurs.

Validation réutilisable du mot de passe

La crate sœur ks-wallet-demo-scenarios valide le cycle A → B → rejet de A → ouverture/signature avec B → restauration B → A sans Tauri et sans toucher aux wallets réels de l'opérateur :

cargo test -p ks-wallet-demo-scenarios

Le guide opérationnel complet est ../docs/guides/WALLETS.md.