v0.5.2-pre.007
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
<!-- file: ks-wallet/CHANGELOG.md -->
|
||||
<!-- version: 20 -->
|
||||
<!-- version: 21 -->
|
||||
|
||||
# CHANGELOG — ks-wallet
|
||||
|
||||
## `0.5.2-pre.007`
|
||||
|
||||
- retire les chemins locaux inutiles du `Debug`, des logs et des erreurs du `TemporaryWalletStore` legacy tout en conservant les getters explicites nécessaires aux fixtures ;
|
||||
- ajoute un canari externe garantissant que les erreurs/`Debug` legacy ne divulguent pas le répertoire local ;
|
||||
- ajoute un test concurrent de création native confirmant qu'un seul créateur publie un alias et que la destination `.kswallet` reste valide ;
|
||||
- clôt la documentation `0.5.2`, archive le plan de version et reporte explicitement l'inventaire de `wallets/temporary/**` à `0.5.4`.
|
||||
|
||||
## `0.5.2-pre.006-delta-fix-004`
|
||||
|
||||
- ajoute `WalletTransferFormat::supported()`, `code()`, `label()` et `default_extension()` afin que les consommateurs puissent présenter exactement les formats réellement compilés ;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<!-- file: ks-wallet/README.md -->
|
||||
<!-- version: 13 -->
|
||||
<!-- version: 14 -->
|
||||
|
||||
# ks-wallet
|
||||
|
||||
`ks-wallet` fournit la frontière wallet Solana générale du workspace Khadhroony.
|
||||
|
||||
## État actuel
|
||||
## Contrat `0.5.2`
|
||||
|
||||
En `0.5.2-pre.005`, la crate sait en plus migrer sans destruction un legacy Solana JSON, importer/exporter le format Solana CLI JSON et transférer un keypair complet via le format privé Base58 utilisé comme adaptateur tiers de référence pour Phantom. La dérivation Argon2id et le chiffrement authentifié XChaCha20-Poly1305 restent la protection du conteneur natif `.kswallet`.
|
||||
`ks-wallet` constitue la frontière générale de stockage, authentification et signature des wallets Solana du workspace. Le format persistant recommandé est `.kswallet`; le JSON Solana historique reste une compatibilité legacy et une source d'import.
|
||||
|
||||
La crate fournit actuellement :
|
||||
|
||||
@@ -40,25 +40,11 @@ La crate fournit actuellement :
|
||||
|
||||
La persistance legacy écrit encore directement le contenu dans le chemin final : elle n'est pas une publication atomique crash-safe par fichier temporaire + renommage.
|
||||
|
||||
## Cible `0.5.2`
|
||||
## Format natif et compatibilité
|
||||
|
||||
`ks-wallet` doit devenir capable de :
|
||||
Le format `.kswallet` utilise Argon2id v19 pour la dérivation depuis le mot de passe et XChaCha20-Poly1305 pour le chiffrement authentifié. Le layout normatif est documenté dans [`../docs/NATIVE_FORMAT.md`](../docs/NATIVE_FORMAT.md). Le payload secret v1 est exactement la keypair Solana brute de 64 octets ; le header, l'alias, le sel et le nonce sont authentifiés comme AAD avant qu'une capacité `UnlockedWallet` puisse être produite. Changer le mot de passe ne change jamais la keypair/pubkey.
|
||||
|
||||
- découvrir dans le store les fichiers `<alias>.kswallet` valides et gérer plusieurs wallets persistants accessibles par alias ;
|
||||
- conserver des wallets temporaires/jetables pour tests et scénarios ;
|
||||
- stocker les wallets persistants dans un format natif binaire `<alias>.kswallet` ;
|
||||
- protéger chaque wallet persistant par un mot de passe modifiable ;
|
||||
- fournir une capacité de signature sans exposer les bytes privés ;
|
||||
- importer le format legacy et d'autres formats explicitement supportés ;
|
||||
- exporter volontairement vers des formats externes supportés ;
|
||||
- exiger un mot de passe valide pour tout export contenant le secret ;
|
||||
- préserver exactement la même keypair lors d'un changement de mot de passe ;
|
||||
- importer et exporter obligatoirement le format keypair JSON des binaires Solana ;
|
||||
- documenter les formats compatibles des principaux wallets Solana, implémenter un adaptateur tiers d'exemple et reporter les autres au TODO.
|
||||
|
||||
Le format `.kswallet` est propre à `ks-wallet`, mais sa protection cryptographique utilise des primitives établies : Argon2id v19 pour la dérivation depuis le mot de passe et XChaCha20-Poly1305 pour le chiffrement authentifié. Le layout normatif est documenté dans [`../docs/NATIVE_FORMAT.md`](../docs/NATIVE_FORMAT.md). Le payload secret v1 est exactement la keypair Solana brute de 64 octets ; avec le tag AEAD, le ciphertext est fixé à 80 octets. Le header, l'alias, le sel et le nonce sont authentifiés comme AAD avant qu'une capacité `UnlockedWallet` puisse être produite. Changer le mot de passe ne change jamais la keypair : une modification réelle du secret Ed25519 produirait une autre pubkey et donc un autre wallet. Le changement du mot de passe reprotège le fichier persistant ; il ne révoque pas une capacité `UnlockedWallet` déjà détenue par un consommateur, qui doit être explicitement `lock()`/dropée selon son propre cycle de vie.
|
||||
|
||||
Le scan automatique reste strictement borné au répertoire fourni à `WalletManager`. Un programme peut néanmoins demander l'inspection d'un autre fichier `.kswallet` choisi explicitement, par exemple via un file browser desktop, puis l'ouvrir avec `unlock_file()` et le mot de passe fourni. Ces opérations ne modifient pas le store et ne rendent pas le chemin public.
|
||||
Le scan automatique reste strictement borné au répertoire fourni à `WalletManager`. Un programme peut inspecter explicitement un autre `.kswallet` puis l'ouvrir avec `unlock_file()` sans modifier le store. Les erreurs, logs et `Debug` du store JSON legacy ne projettent plus ses chemins locaux.
|
||||
|
||||
## Relations
|
||||
|
||||
@@ -77,5 +63,6 @@ Une application desktop doit projeter les informations autorisées dans ses prop
|
||||
- [USAGE.md](USAGE.md)
|
||||
- [TODO.md](TODO.md)
|
||||
- [CHANGELOG.md](CHANGELOG.md)
|
||||
- [plan temporaire `0.5.2`](../docs/plans/V0_5_2_KS_WALLET_RESTRUCTURING_PLAN.md)
|
||||
- [Guide wallets](../docs/guides/WALLETS.md)
|
||||
- [Rapport de validation 0.5.2](../docs/validation/V0_5_2_WALLET_VALIDATION_REPORT.md)
|
||||
- [ROADMAP général](../ROADMAP.md)
|
||||
|
||||
@@ -1,47 +1,19 @@
|
||||
<!-- file: ks-wallet/TODO.md -->
|
||||
<!-- version: 15 -->
|
||||
<!-- version: 16 -->
|
||||
|
||||
# TODO — ks-wallet
|
||||
|
||||
## `0.5.2`
|
||||
## `0.5.4` — réconciliation des keypairs de fixtures
|
||||
|
||||
- [x] caractériser par tests externes le format legacy `<alias>.json`, ses erreurs et ses permissions.
|
||||
- [x] définir l'identité publique minimale d'un wallet et le lookup par alias.
|
||||
- [x] introduire le manager, le scan borné et le lookup des `.kswallet` structurellement valides dans le store résolu.
|
||||
- [x] conserver les wallets temporaires ou jetables purement en mémoire et leur frontière de signature actuelle.
|
||||
- [x] permettre l'inspection explicite d'un `.kswallet` hors store sans mutation de la configuration ni exposition publique du chemin.
|
||||
- [x] spécifier le format natif binaire v1 `<alias>.kswallet` et implémenter son décodage/validation stricts pour le scan.
|
||||
- [x] `0.5.2-pre.004` — implémenter l'encodage de création et la publication atomique/no-clobber du conteneur natif.
|
||||
- [x] sélectionner et documenter Argon2id v19 / XChaCha20-Poly1305 et leurs paramètres/bornes v1.
|
||||
- [x] créer/ouvrir un wallet persistant avec mot de passe sans exposer les bytes privés.
|
||||
- [x] permettre le changement de mot de passe en rechiffrant exactement la même keypair et donc en conservant la même pubkey.
|
||||
- [x] préserver la frontière `solana_signer::Signer` compatible avec les consommateurs sans dépendance de `ks-lib` vers `ks-wallet`.
|
||||
- [x] fournir cette même capacité depuis un wallet natif ouvert par mot de passe.
|
||||
- [x] importer le legacy Solana JSON vers `.kswallet` avec écriture atomique, rollback et vérification de pubkey.
|
||||
- [x] importer et exporter le format keypair JSON standard des binaires Solana.
|
||||
- [x] produire une matrice documentée des formats Phantom, Solflare, Backpack, Trust Wallet, Coinbase/Base et autres wallets Solana pertinents.
|
||||
- [x] implémenter dans `0.5.2` un seul adaptateur wallet tiers d'exemple : Base58 du keypair Solana complet, avec Phantom comme cible de référence documentée.
|
||||
- [x] reporter les autres adaptateurs tiers faisables vers une version ultérieure non déterminée après validation de la matrice.
|
||||
- [x] ne jamais synthétiser une recovery phrase supposée préserver une keypair arbitraire sans mnemonic/seed d'origine.
|
||||
- [x] exiger le mot de passe valide pour tout export contenant le secret.
|
||||
- [x] définir les collisions d'alias et de pubkey sans écrasement silencieux.
|
||||
- [x] normaliser la sélection par alias dans `ks-config` sans secret.
|
||||
- [x] adapter les consommateurs et le desktop uniquement via des surfaces non sensibles.
|
||||
- [ ] retirer secrets et chemins locaux inutiles des logs, erreurs, diagnostics et DTO.
|
||||
- [x] compléter les tests `pre.005` de migration non destructive, round-trip Solana CLI JSON/Base58, permissions privées, refus d'écrasement, collision de pubkey et export refusé avec mauvais mot de passe ; les tests de concurrence restent à réconcilier dans la finalisation selon les surfaces réellement utilisées.
|
||||
- [ ] produire le guide de sécurité et la documentation finale avant clôture de `0.5.2`.
|
||||
|
||||
## Version ultérieure non déterminée — migration des keypairs de fixtures
|
||||
|
||||
- [ ] inventorier et classifier non destructivement les fichiers keypair JSON sous `wallets/temporary/**` avant toute conversion de masse : distinguer wallets/signers, autorités, mints, recipients et autres keypairs de fixtures ;
|
||||
- [ ] proposer une migration sélective des seuls signers devant devenir persistants vers `.kswallet`, en préservant les fichiers JSON sources et les pubkeys ;
|
||||
- [x] permettre l'inspection explicite d'un fichier keypair legacy compatible et l'extraction de sa pubkey publique sans conversion préalable en `.kswallet`.
|
||||
- [ ] ajouter un scanner borné de répertoire pour inventorier plusieurs candidats legacy avant migration, sans conversion automatique ni classification métier inventée à partir du secret seul.
|
||||
- [ ] ajouter un scanner borné de répertoire pour inventorier plusieurs candidats legacy sous `wallets/temporary/**`, sans conversion automatique ;
|
||||
- [ ] produire uniquement des faits locaux vérifiables : format supporté/non supporté, keypair valide/invalide et pubkey lorsqu'elle est dérivable ;
|
||||
- [ ] proposer une migration sélective des seuls signers devant devenir persistants vers `.kswallet`, en préservant les sources et les pubkeys ;
|
||||
- [ ] laisser toute classification on-chain ou métier au consommateur/scénario, sans dépendance `ks-wallet -> ks-onchain-transport`.
|
||||
|
||||
## Version ultérieure non déterminée — adaptateurs de transfert
|
||||
|
||||
- [ ] caractériser le wire Solana exact accepté par l'import `Private key` de Backpack avant d'ajouter un codec de marque ou un alias de format.
|
||||
- [ ] caractériser le wire d'import Solana de Trust Wallet depuis une documentation suffisamment précise avant implémentation.
|
||||
- [ ] caractériser le format keystore Solflare et son mot de passe uniquement si son conteneur public est suffisamment stable et spécifié pour un round-trip testé.
|
||||
- [ ] réévaluer Base app / ex-Coinbase Wallet si une documentation officielle expose un import direct de keypair Solana arbitraire ; ne jamais synthétiser une recovery phrase.
|
||||
- [ ] caractériser le wire Solana exact accepté par l'import `Private key` de Backpack avant d'ajouter un codec de marque ou un alias de format ;
|
||||
- [ ] caractériser le wire d'import Solana de Trust Wallet depuis une documentation suffisamment précise avant implémentation ;
|
||||
- [ ] caractériser le format keystore Solflare et son mot de passe uniquement si son conteneur public est suffisamment stable et spécifié pour un round-trip testé ;
|
||||
- [ ] réévaluer Base app / ex-Coinbase Wallet si une documentation officielle expose un import direct de keypair Solana arbitraire ; ne jamais synthétiser une recovery phrase ;
|
||||
- [ ] ajouter d'autres adaptateurs wallets uniquement à partir d'un format secret officiellement documenté, strictement validable et testable.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: ks-wallet/USAGE.md -->
|
||||
<!-- version: 13 -->
|
||||
<!-- version: 14 -->
|
||||
|
||||
# Utilisation de ks-wallet
|
||||
|
||||
## Statut
|
||||
|
||||
En `0.5.2-pre.005`, le manager couvre aussi la migration non destructive du legacy, l'import/export du keypair JSON Solana CLI et le transfert Base58 d'une keypair Solana complète. Tout export secret repart d'un `.kswallet` authentifié avec son mot de passe.
|
||||
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
|
||||
|
||||
@@ -494,3 +494,13 @@ Phantom est la cible tierce de référence du Base58 et Solflare documente l'imp
|
||||
- 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 :
|
||||
|
||||
```bash
|
||||
cargo test -p ks-wallet-demo-scenarios
|
||||
```
|
||||
|
||||
Le guide opérationnel complet est [`../docs/guides/WALLETS.md`](../docs/guides/WALLETS.md).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: ks-wallet/src/wallet.rs
|
||||
// version: 11
|
||||
// version: 12
|
||||
|
||||
//! Local wallet storage and signing primitives.
|
||||
|
||||
@@ -158,11 +158,17 @@ impl crate::TemporaryWallet {
|
||||
}
|
||||
|
||||
/// Filesystem-backed store for development and integration-test wallets.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct TemporaryWalletStore {
|
||||
directory: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for crate::TemporaryWalletStore {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.debug_struct("TemporaryWalletStore").finish();
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::TemporaryWalletStore {
|
||||
/// Creates a wallet store rooted at the supplied directory.
|
||||
pub fn new(directory: impl std::convert::Into<std::path::PathBuf>) -> ks_core::Result<Self> {
|
||||
@@ -193,7 +199,7 @@ impl crate::TemporaryWalletStore {
|
||||
std::result::Result::Ok(exists) => std::result::Result::Ok(exists),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_file_exists_check_failed",
|
||||
format!("{}: {error}", path.display()),
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
@@ -218,7 +224,6 @@ impl crate::TemporaryWalletStore {
|
||||
action = "create_temporary_wallet",
|
||||
wallet_alias = alias.as_str(),
|
||||
public_key = %keypair.pubkey(),
|
||||
wallet_path = %path.display(),
|
||||
"created persistent temporary wallet"
|
||||
);
|
||||
return std::result::Result::Ok(crate::TemporaryWallet {
|
||||
@@ -240,7 +245,6 @@ impl crate::TemporaryWalletStore {
|
||||
action = "load_temporary_wallet",
|
||||
wallet_alias = alias.as_str(),
|
||||
public_key = %keypair.pubkey(),
|
||||
wallet_path = %path.display(),
|
||||
"loaded persistent temporary wallet"
|
||||
);
|
||||
return std::result::Result::Ok(crate::TemporaryWallet {
|
||||
@@ -279,7 +283,7 @@ async fn prepare_wallet_directory(directory: &std::path::Path) -> ks_core::Resul
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_directory_create_failed",
|
||||
format!("{}: {error}", directory.display()),
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
}
|
||||
@@ -292,7 +296,7 @@ async fn prepare_wallet_directory(directory: &std::path::Path) -> ks_core::Resul
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_directory_permissions_failed",
|
||||
format!("{}: {error}", directory.display()),
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
}
|
||||
@@ -335,10 +339,7 @@ async fn write_new_keypair(
|
||||
} else {
|
||||
"wallet_file_create_failed"
|
||||
};
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
code,
|
||||
format!("{}: {error}", task_path.display()),
|
||||
));
|
||||
return std::result::Result::Err(ks_core::Error::new(code, error.to_string()));
|
||||
},
|
||||
};
|
||||
if let std::result::Result::Err(error) = file.write_all(encoded.as_slice()) {
|
||||
@@ -346,7 +347,7 @@ async fn write_new_keypair(
|
||||
let _ = std::fs::remove_file(&task_path);
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_file_write_failed",
|
||||
format!("{}: {error}", task_path.display()),
|
||||
error.to_string(),
|
||||
));
|
||||
}
|
||||
encoded.zeroize();
|
||||
@@ -354,7 +355,7 @@ async fn write_new_keypair(
|
||||
let _ = std::fs::remove_file(&task_path);
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_file_sync_failed",
|
||||
format!("{}: {error}", task_path.display()),
|
||||
error.to_string(),
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
@@ -364,7 +365,7 @@ async fn write_new_keypair(
|
||||
std::result::Result::Ok(result) => result,
|
||||
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_file_task_failed",
|
||||
format!("{}: {error}", path.display()),
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
@@ -379,7 +380,7 @@ async fn read_keypair(path: &std::path::Path) -> ks_core::Result<solana_keypair:
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_file_read_failed",
|
||||
format!("{}: {error}", path.display()),
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
@@ -390,7 +391,7 @@ async fn read_keypair(path: &std::path::Path) -> ks_core::Result<solana_keypair:
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_keypair_json_invalid",
|
||||
format!("{}: {error}", path.display()),
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
@@ -400,8 +401,7 @@ async fn read_keypair(path: &std::path::Path) -> ks_core::Result<solana_keypair:
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_keypair_length_invalid",
|
||||
format!(
|
||||
"{} contains {length} bytes instead of {}",
|
||||
path.display(),
|
||||
"wallet keypair contains {length} bytes instead of {}",
|
||||
crate::SOLANA_KEYPAIR_LENGTH
|
||||
),
|
||||
));
|
||||
@@ -412,7 +412,7 @@ async fn read_keypair(path: &std::path::Path) -> ks_core::Result<solana_keypair:
|
||||
std::result::Result::Ok(keypair) => std::result::Result::Ok(keypair),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_keypair_invalid",
|
||||
format!("{}: {error}", path.display()),
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
@@ -423,14 +423,14 @@ async fn validate_wallet_file_metadata(path: &std::path::Path) -> ks_core::Resul
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_file_metadata_failed",
|
||||
format!("{}: {error}", path.display()),
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
if metadata.file_type().is_symlink() || !metadata.is_file() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_file_type_invalid",
|
||||
format!("{} must be a regular file and not a symlink", path.display()),
|
||||
"wallet file must be a regular file and not a symlink",
|
||||
));
|
||||
}
|
||||
#[cfg(unix)]
|
||||
@@ -440,7 +440,7 @@ async fn validate_wallet_file_metadata(path: &std::path::Path) -> ks_core::Resul
|
||||
if mode & 0o077 != 0 {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"wallet_file_permissions_too_open",
|
||||
format!("{} has mode {mode:o}; expected no group or other access", path.display()),
|
||||
format!("wallet file has mode {mode:o}; expected no group or other access"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: ks-wallet/tests/legacy_characterization.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! External characterization tests for the legacy Solana JSON wallet format.
|
||||
|
||||
@@ -69,6 +69,31 @@ async fn legacy_missing_file_preserves_current_error_contract() {
|
||||
assert_eq!(error.code(), "wallet_file_metadata_failed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_errors_and_debug_do_not_disclose_local_path() {
|
||||
let directory = tempfile::tempdir()
|
||||
.unwrap_or_else(|error| panic!("temporary directory must exist: {error}"));
|
||||
let canary_directory = directory.path().join("wallet-path-canary");
|
||||
std::fs::create_dir_all(&canary_directory)
|
||||
.unwrap_or_else(|error| panic!("canary directory must be created: {error}"));
|
||||
let store = ks_wallet::TemporaryWalletStore::new(&canary_directory)
|
||||
.unwrap_or_else(|error| panic!("unexpected store error: {error}"));
|
||||
let debug = format!("{store:?}");
|
||||
assert!(!debug.contains("wallet-path-canary"));
|
||||
assert!(!debug.contains(canary_directory.to_string_lossy().as_ref()));
|
||||
let alias = ks_wallet::WalletAlias::parse("missing-canary")
|
||||
.unwrap_or_else(|error| panic!("unexpected alias error: {error}"));
|
||||
let error = store
|
||||
.load(alias)
|
||||
.await
|
||||
.err()
|
||||
.unwrap_or_else(|| panic!("missing legacy wallet must fail"));
|
||||
let message = error.to_string();
|
||||
assert_eq!(error.code(), "wallet_file_metadata_failed");
|
||||
assert!(!message.contains("wallet-path-canary"));
|
||||
assert!(!message.contains(canary_directory.to_string_lossy().as_ref()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_corrupted_json_and_short_keypair_are_distinct() {
|
||||
let directory = tempfile::tempdir()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: ks-wallet/tests/native_password.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! External password-protected native wallet lifecycle tests.
|
||||
|
||||
@@ -137,6 +137,45 @@ async fn native_creation_is_private_and_refuses_overwrite() {
|
||||
assert_eq!(handle.public_key(), public_key);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn concurrent_native_creation_has_one_winner_and_preserves_valid_destination() {
|
||||
let directory = tempfile::tempdir()
|
||||
.unwrap_or_else(|error| panic!("temporary directory must exist: {error}"));
|
||||
make_directory_private(directory.path());
|
||||
let left_manager = ks_wallet::WalletManager::new(directory.path())
|
||||
.unwrap_or_else(|error| panic!("left manager must be created: {error}"));
|
||||
let right_manager = ks_wallet::WalletManager::new(directory.path())
|
||||
.unwrap_or_else(|error| panic!("right manager must be created: {error}"));
|
||||
let alias = ks_wallet::WalletAlias::parse("concurrent-create")
|
||||
.unwrap_or_else(|error| panic!("alias must be valid: {error}"));
|
||||
let left_alias = alias.clone();
|
||||
let right_alias = alias.clone();
|
||||
let left = left_manager.create(left_alias, password("left-password"));
|
||||
let right = right_manager.create(right_alias, password("right-password"));
|
||||
let (left_result, right_result) = tokio::join!(left, right);
|
||||
let (winner, loser_error) = match (left_result, right_result) {
|
||||
(std::result::Result::Ok(wallet), std::result::Result::Err(error)) => (wallet, error),
|
||||
(std::result::Result::Err(error), std::result::Result::Ok(wallet)) => (wallet, error),
|
||||
(std::result::Result::Ok(_), std::result::Result::Ok(_)) => {
|
||||
panic!("concurrent creation must not publish two wallets for one alias");
|
||||
},
|
||||
(std::result::Result::Err(_), std::result::Result::Err(_)) => {
|
||||
panic!("exactly one concurrent creation must succeed");
|
||||
},
|
||||
};
|
||||
assert_eq!(loser_error.code(), "wallet_native_already_exists");
|
||||
let public_key = winner.public_key();
|
||||
winner.lock();
|
||||
let verifier = ks_wallet::WalletManager::new(directory.path())
|
||||
.unwrap_or_else(|error| panic!("verification manager must be created: {error}"));
|
||||
let handle = verifier
|
||||
.lookup(&alias)
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("published wallet lookup must succeed: {error}"))
|
||||
.unwrap_or_else(|| panic!("published wallet must exist"));
|
||||
assert_eq!(handle.public_key(), public_key);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authenticated_header_tampering_fails_before_signing_capability() {
|
||||
let directory = tempfile::tempdir()
|
||||
|
||||
Reference in New Issue
Block a user