v0.5.2-pre.006-fix-004

This commit is contained in:
2026-08-11 00:30:03 +02:00
parent 066d969f5a
commit 1ee1d0297d
17 changed files with 698 additions and 41 deletions

View File

@@ -1,8 +1,15 @@
<!-- file: ks-wallet/CHANGELOG.md -->
<!-- version: 19 -->
<!-- version: 20 -->
# CHANGELOG — ks-wallet
## `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 ;
- ajoute `inspect_transfer_file()` et `WalletTransferInspection` pour valider un fichier secret externe et n'en exposer que la pubkey dérivée et le format sélectionné, sans import ni conversion ;
- conserve les codecs secrets dans `ks-wallet` et permet au desktop de diagnostiquer les anciens keypairs de `wallets/temporary/**` un par un avant toute migration ;
- ajoute un test externe couvrant l'inspection publique JSON/Base58 et l'inventaire des formats supportés.
## `0.5.2-pre.006`
- clôt les TODO de sélection non sensible par alias et d'adaptation des consommateurs : `ks-config` ne porte que `wallet_alias`, les scénarios exigent un unlock explicite pour une sélection persistante et le desktop ne projette que des DTO sûrs ;

View File

@@ -1,5 +1,5 @@
<!-- file: ks-wallet/README.md -->
<!-- version: 11 -->
<!-- version: 12 -->
# ks-wallet
@@ -21,6 +21,8 @@ La crate fournit actuellement :
- ouverture d'un fichier explicitement sélectionné avec `WalletManager::unlock_file()` sans l'enregistrer dans le store ;
- changement du mot de passe avec `WalletManager::change_password()` en conservant exactement la même keypair/pubkey ;
- migration non destructive de `<alias>.json` vers `<alias>.kswallet` avec `WalletManager::migrate_legacy()` ;
- inspection sûre d'un fichier de transfert via `inspect_transfer_file()`, qui valide le keypair sans l'importer et ne retourne que sa pubkey publique ;
- inventaire stable des formats via `WalletTransferFormat::supported()` avec code, libellé et extension conventionnelle ;
- import de fichiers secrets avec `WalletManager::import_file()` en `SolanaCliJson` ou `SolanaPrivateKeyBase58` ;
- export de fichiers secrets avec `WalletManager::export_file()`, uniquement après authentification du mot de passe du `.kswallet` ;
- refus des collisions dalias et de pubkey lors dun import ;

View File

@@ -1,5 +1,5 @@
<!-- file: ks-wallet/TODO.md -->
<!-- version: 14 -->
<!-- version: 15 -->
# TODO — ks-wallet
@@ -35,7 +35,8 @@
- [ ] 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 ;
- [ ] permettre aux outils de diagnostic dextraire la pubkey publique des keypairs legacy compatibles pour explorer leur historique on-chain sans exiger leur conversion préalable en `.kswallet`.
- [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.
## Version ultérieure non déterminée — adaptateurs de transfert

View File

@@ -1,5 +1,5 @@
<!-- file: ks-wallet/USAGE.md -->
<!-- version: 11 -->
<!-- version: 12 -->
# Utilisation de ks-wallet
@@ -317,6 +317,27 @@ println!("pubkey={}", handle.public_key());
La matrice des formats vérifiés et reportés est maintenue dans [`../docs/WALLET_FORMAT_COMPATIBILITY.md`](../docs/WALLET_FORMAT_COMPATIBILITY.md).
### Inspecter un keypair externe sans l'importer
```rust
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`
```rust

View File

@@ -1,5 +1,5 @@
// file: ks-wallet/src/lib.rs
// version: 8
// version: 9
//! Wallet boundary for local key storage and transaction signing.
#![warn(missing_docs)]
@@ -24,6 +24,10 @@ pub use self::manager::WalletManager;
pub use self::password::WalletPassword;
/// Explicit secret import/export format supported by the wallet boundary.
pub use self::transfer::WalletTransferFormat;
/// Safe public identity extracted from one validated transfer file.
pub use self::transfer::WalletTransferInspection;
/// Validates one external transfer file and returns only its public identity.
pub use self::transfer::inspect_transfer_file;
/// Authenticated signing capability for one unlocked persistent wallet.
pub use self::unlocked::UnlockedWallet;
/// Solana keypair kept private inside the wallet boundary.

View File

@@ -1,5 +1,5 @@
// file: ks-wallet/src/transfer.rs
// version: 1
// version: 2
//! Explicit migration and secret import/export adapters.
@@ -23,6 +23,74 @@ pub enum WalletTransferFormat {
SolanaPrivateKeyBase58,
}
impl WalletTransferFormat {
/// Returns every transfer format currently supported for both import and export.
pub const fn supported() -> [Self; 2] {
return [Self::SolanaCliJson, Self::SolanaPrivateKeyBase58];
}
/// Returns the stable machine-readable code of this transfer format.
pub const fn code(self) -> &'static str {
return match self {
Self::SolanaCliJson => "solana_cli_json",
Self::SolanaPrivateKeyBase58 => "solana_private_key_base58",
};
}
/// Returns the human-readable label of this transfer format.
pub const fn label(self) -> &'static str {
return match self {
Self::SolanaCliJson => "Solana CLI keypair JSON (64 bytes)",
Self::SolanaPrivateKeyBase58 => "Solana private key Base58 (64 bytes)",
};
}
/// Returns the conventional file extension used by this transfer format.
pub const fn default_extension(self) -> &'static str {
return match self {
Self::SolanaCliJson => "json",
Self::SolanaPrivateKeyBase58 => "txt",
};
}
}
/// Safe result of validating one external secret-transfer file.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WalletTransferInspection {
public_key: std::string::String,
format: WalletTransferFormat,
}
impl WalletTransferInspection {
/// Returns the public key derived from the validated external keypair.
pub fn public_key(&self) -> &str {
return self.public_key.as_str();
}
/// Returns the transfer format used to validate the external keypair.
pub fn format(&self) -> WalletTransferFormat {
return self.format;
}
}
/// Validates one external transfer file and returns only its public identity.
///
/// The secret bytes never leave `ks-wallet`; callers receive only the derived
/// Solana public key and the format that was explicitly selected for validation.
pub async fn inspect_transfer_file(
source_path: impl std::convert::AsRef<std::path::Path>,
format: WalletTransferFormat,
) -> ks_core::Result<WalletTransferInspection> {
let keypair = match read_transfer_keypair(source_path.as_ref(), format).await {
std::result::Result::Ok(keypair) => keypair,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(WalletTransferInspection {
public_key: keypair.pubkey().to_string(),
format,
});
}
impl crate::WalletManager {
/// Imports one secret file into a new password-protected native wallet.
///
@@ -136,7 +204,7 @@ impl crate::WalletManager {
action = "export_wallet_secret",
wallet_alias = alias.as_str(),
public_key = public_key.as_str(),
transfer_format = transfer_format_code(format),
transfer_format = format.code(),
"exported wallet secret after password authentication"
);
return std::result::Result::Ok(());
@@ -189,7 +257,7 @@ async fn persist_imported_keypair(
action = "import_wallet_secret",
wallet_alias = alias.as_str(),
public_key = %public_key,
transfer_format = transfer_format_code(format),
transfer_format = format.code(),
"imported wallet secret into native protected storage"
);
return std::result::Result::Ok(crate::UnlockedWallet::new(alias, keypair));
@@ -656,10 +724,3 @@ fn native_path(manager: &crate::WalletManager, alias: &crate::WalletAlias) -> st
crate::KSWALLET_FILE_EXTENSION
));
}
fn transfer_format_code(format: crate::WalletTransferFormat) -> &'static str {
return match format {
crate::WalletTransferFormat::SolanaCliJson => "solana_cli_json",
crate::WalletTransferFormat::SolanaPrivateKeyBase58 => "solana_private_key_base58",
};
}

View File

@@ -1,5 +1,5 @@
// file: ks-wallet/tests/transfer.rs
// version: 1
// version: 2
//! External migration and secret import/export contract tests.
@@ -277,3 +277,42 @@ async fn import_rejects_invalid_sources_without_creating_native_destination() {
assert_eq!(base58_error.code(), "wallet_import_base58_invalid");
assert!(!directory.path().join("invalid-base58.kswallet").exists());
}
#[tokio::test]
async fn transfer_inspection_exposes_only_public_identity_for_supported_formats() {
let directory = tempfile::tempdir()
.unwrap_or_else(|error| panic!("temporary directory must exist: {error}"));
make_directory_private(directory.path());
let keypair = solana_keypair::Keypair::new();
let expected_public_key = keypair.pubkey().to_string();
let mut keypair_bytes = keypair.to_bytes();
let mut json = serde_json::to_vec(keypair_bytes.as_slice())
.unwrap_or_else(|error| panic!("Solana JSON fixture must encode: {error}"));
let json_path = directory.path().join("inspect.json");
write_private_file(&json_path, json.as_slice());
let json_inspection = ks_wallet::inspect_transfer_file(
&json_path,
ks_wallet::WalletTransferFormat::SolanaCliJson,
)
.await
.unwrap_or_else(|error| panic!("Solana JSON inspection must succeed: {error}"));
assert_eq!(json_inspection.public_key(), expected_public_key);
assert_eq!(json_inspection.format().code(), "solana_cli_json");
json.zeroize();
let mut base58 = bs58::encode(keypair_bytes.as_slice()).into_string();
let base58_path = directory.path().join("inspect.txt");
write_private_file(&base58_path, base58.as_bytes());
let base58_inspection = ks_wallet::inspect_transfer_file(
&base58_path,
ks_wallet::WalletTransferFormat::SolanaPrivateKeyBase58,
)
.await
.unwrap_or_else(|error| panic!("Base58 inspection must succeed: {error}"));
assert_eq!(base58_inspection.public_key(), expected_public_key);
assert_eq!(base58_inspection.format().code(), "solana_private_key_base58");
let formats = ks_wallet::WalletTransferFormat::supported();
assert_eq!(formats.len(), 2);
assert_eq!(formats[0].default_extension(), "json");
assert_eq!(formats[1].default_extension(), "txt");
base58.zeroize();
keypair_bytes.zeroize();
}