v0.2.6-pre.016
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
<!-- file: crates/ksp-wallet-lib/README.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# `ksp-wallet-lib`
|
||||
|
||||
Statut : **stable depuis KSP `0.2.5`**.
|
||||
|
||||
`ksp-wallet-lib` est la bibliothèque KSP propriétaire du Wallet Solana natif. Elle possède le format autonome `.kspwallet` V1 et, depuis `0.2.6-pre.015`, le wire binaire V2 canonique, les capacités indépendantes VIEW/OWNER, la protection du secret Solana, la signature, l'administration des metadata, les rotations de credentials, la persistence native et les adapters d'import/export explicitement supportés.
|
||||
`ksp-wallet-lib` est la bibliothèque KSP propriétaire du Wallet Solana natif. Elle possède le format autonome `.kspwallet` V1 et le format binaire V2 canonique, les capacités indépendantes VIEW/OWNER, la protection du secret Solana, la signature, l'administration des metadata, les rotations de credentials, la persistence native et les adapters d'import/export explicitement supportés. Depuis `0.2.6-pre.016`, les APIs non versionnées créent/importent en V2 par default explicite et lisent V1/V2 par détection bornée.
|
||||
|
||||
La crate est volontairement indépendante de Config, du réseau et de Tauri. Un consumer fournit les chemins, passwords et metadata ; Wallet ouvre, protège, signe et persiste sans décider d'une policy de dépense ni contacter un RPC.
|
||||
|
||||
@@ -14,7 +14,8 @@ La crate est volontairement indépendante de Config, du réseau et de Tauri. Un
|
||||
La crate possède :
|
||||
|
||||
- le format natif `.kspwallet` V1 et son parser JSON strict ;
|
||||
- le wire binaire `.kspwallet` V2, son codec borné/canonique et ses domains/transcripts distincts ;
|
||||
- le wire binaire `.kspwallet` V2, son codec borné/canonique, sa création/ouverture/persistence et ses domains/transcripts distincts ;
|
||||
- la façade générique V1/V2 et les variantes `_v1`/`_v2` permettant soit le default, soit un wire forcé ;
|
||||
- les key slots OWNER/VIEW indépendants ;
|
||||
- Argon2id pour les KDF de passwords ;
|
||||
- XChaCha20-Poly1305 pour le wrapping et les compartiments ;
|
||||
@@ -194,6 +195,20 @@ Elles couvrent le wire, Argon2id/XChaCha20-Poly1305, l'ouverture VIEW/OWNER, la
|
||||
- [`../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md) — matrice de sécurité/interoperabilité/compliance ;
|
||||
- [`../../prompts/011-V0_2_6_START_PROMPT.md`](../../prompts/011-V0_2_6_START_PROMPT.md) — reprise vers Wallet Desk après publication stable de `0.2.5`.
|
||||
|
||||
## V2 en `0.2.6-pre.015`
|
||||
## V2 en `0.2.6-pre.015` / `pre.016`
|
||||
|
||||
`pre.015` ajoute uniquement le codec structurel V2 et sa spécification. Les APIs de persistence runtime restent V1 jusqu’à `pre.016`, qui ajoute `create_wallet_file(...)` default V2, les variantes `_v1`/`_v2` et le dispatch générique de lecture. Le default est une décision explicite et ne suit pas automatiquement une future V3.
|
||||
`pre.015` a figé le wire structurel V2, son codec et ses transcripts/AAD. `pre.016` matérialise le runtime V2 complet et la façade multi-version :
|
||||
|
||||
```text
|
||||
DEFAULT_WALLET_FORMAT = V2
|
||||
LATEST_SUPPORTED_WALLET_FORMAT = V2
|
||||
|
||||
create_wallet_file(...) -> V2
|
||||
create_wallet_file_v1(...) -> V1 forcé
|
||||
create_wallet_file_v2(...) -> V2 forcé
|
||||
|
||||
open/inspect génériques -> détection V1/V2
|
||||
open/inspect _v1/_v2 -> format forcé strict
|
||||
```
|
||||
|
||||
`WalletOwner` et `WalletView` conservent le format natif qu'ils ont ouvert : metadata, rotations OWNER/VIEW, disable/recreate VIEW, self-rotation VIEW, signature et export ne transcodent jamais implicitement le fichier. Le default est une décision explicite et ne suit pas automatiquement une future V3. La migration authentifiée V1 -> V2 reste une opération séparée de `pre.017`.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!-- file: crates/ksp-wallet-lib/USAGE.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# Utilisation de `ksp-wallet-lib`
|
||||
|
||||
Ce guide présente les principales surfaces publiques de Wallet V1. La spécification cryptographique du fichier reste [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md).
|
||||
Ce guide présente les principales surfaces publiques multi-version de Wallet. V1 reste spécifié par [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md) et V2 par [`../../docs/formats/KSPWALLET_V2.md`](../../docs/formats/KSPWALLET_V2.md).
|
||||
|
||||
Les exemples utilisent des chemins explicites : Wallet ne lit ni Config ni environnement pour découvrir un répertoire.
|
||||
|
||||
@@ -11,13 +11,13 @@ Les exemples utilisent des chemins explicites : Wallet ne lit ni Config ni envir
|
||||
|
||||
```rust
|
||||
async fn create_example() -> ksp_core_lib::Result<()> {
|
||||
let metadata = ksp_wallet_lib::WalletCreateMetadataV1::new(
|
||||
let metadata = ksp_wallet_lib::WalletCreateMetadata::new(
|
||||
Some(std::string::String::from("devnet-main")),
|
||||
vec![std::string::String::from("wallet de test")],
|
||||
);
|
||||
let owner_password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD"));
|
||||
let view_password = ksp_wallet_lib::ViewPassword::new(std::string::String::from("VIEW-PASSWORD"));
|
||||
let created = ksp_wallet_lib::create_wallet_file_v1(
|
||||
let created = ksp_wallet_lib::create_wallet_file(
|
||||
"wallets/devnet-main.kspwallet",
|
||||
owner_password,
|
||||
Some(view_password),
|
||||
@@ -35,13 +35,13 @@ async fn create_example() -> ksp_core_lib::Result<()> {
|
||||
|
||||
La destination doit avoir un parent existant. Une destination existante n'est jamais remplacée par une création.
|
||||
|
||||
Pour créer uniquement en mémoire, utiliser `create_wallet_v1` puis `WalletOwner::to_json_bytes()` si le caller possède lui-même une autre boundary de stockage.
|
||||
Pour créer uniquement en mémoire avec le default, utiliser `create_wallet`. `WalletOwner::to_native_bytes()` restitue ensuite le wire natif courant. Les APIs `_v1` et `_v2` restent disponibles lorsqu’un caller doit forcer une version précise.
|
||||
|
||||
## 2. Inspecter un wallet verrouillé
|
||||
|
||||
```rust
|
||||
async fn inspect_example() -> ksp_core_lib::Result<()> {
|
||||
let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1("wallets/devnet-main.kspwallet").await;
|
||||
let inspected = ksp_wallet_lib::inspect_locked_wallet_file("wallets/devnet-main.kspwallet").await;
|
||||
let locked = match inspected {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
@@ -59,7 +59,7 @@ Cette projection ne contient volontairement ni Pubkey, ni alias, ni notes.
|
||||
```rust
|
||||
async fn open_view_example() -> ksp_core_lib::Result<()> {
|
||||
let password = ksp_wallet_lib::ViewPassword::new(std::string::String::from("VIEW-PASSWORD"));
|
||||
let opened = ksp_wallet_lib::open_wallet_view_file_v1("wallets/devnet-main.kspwallet", password).await;
|
||||
let opened = ksp_wallet_lib::open_wallet_view_file("wallets/devnet-main.kspwallet", password).await;
|
||||
let mut view = match opened {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
@@ -89,7 +89,7 @@ VIEW ne possède aucune API `sign`, `export_transfer`, `update_alias`, `add_note
|
||||
```rust
|
||||
async fn sign_example(message: &[u8]) -> ksp_core_lib::Result<[u8; ksp_wallet_lib::KSPWALLET_SOLANA_SIGNATURE_BYTES]> {
|
||||
let password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD"));
|
||||
let opened = ksp_wallet_lib::open_wallet_owner_file_v1("wallets/devnet-main.kspwallet", password).await;
|
||||
let opened = ksp_wallet_lib::open_wallet_owner_file("wallets/devnet-main.kspwallet", password).await;
|
||||
let owner = match opened {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
@@ -105,7 +105,7 @@ La signature retournée contient 64 octets Ed25519. Aucun getter public ne retou
|
||||
```rust
|
||||
async fn metadata_example() -> ksp_core_lib::Result<()> {
|
||||
let password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD"));
|
||||
let opened = ksp_wallet_lib::open_wallet_owner_file_v1("wallets/devnet-main.kspwallet", password).await;
|
||||
let opened = ksp_wallet_lib::open_wallet_owner_file("wallets/devnet-main.kspwallet", password).await;
|
||||
let mut owner = match opened {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
@@ -207,13 +207,13 @@ async fn inspect_transfer_example(source: &[u8]) -> ksp_core_lib::Result<()> {
|
||||
Import fichier vers un nouveau `.kspwallet` :
|
||||
|
||||
```rust
|
||||
let imported = ksp_wallet_lib::import_wallet_transfer_file_v1(
|
||||
let imported = ksp_wallet_lib::import_wallet_transfer_file(
|
||||
"wallets/imported.kspwallet",
|
||||
"wallets/legacy-id.json",
|
||||
ksp_wallet_lib::WalletTransferFormat::SolanaCliJson,
|
||||
ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD")),
|
||||
None,
|
||||
ksp_wallet_lib::WalletCreateMetadataV1::new(Some(std::string::String::from("imported")), vec![]),
|
||||
ksp_wallet_lib::WalletCreateMetadata::new(Some(std::string::String::from("imported")), vec![]),
|
||||
)
|
||||
.await;
|
||||
```
|
||||
@@ -246,14 +246,16 @@ Les octets retournés par `export_transfer` contiennent volontairement le secret
|
||||
Les équivalents sans I/O filesystem sont :
|
||||
|
||||
```text
|
||||
create_wallet_v1
|
||||
open_wallet_view_v1
|
||||
open_wallet_owner_v1
|
||||
inspect_locked_wallet_v1
|
||||
inspect_wallet_transfer
|
||||
create_wallet -> default V2
|
||||
create_wallet_v1 / _v2 -> version forcée
|
||||
open_wallet_view -> détection V1/V2
|
||||
open_wallet_owner -> détection V1/V2
|
||||
inspect_locked_wallet -> détection V1/V2
|
||||
open/inspect *_v1 / *_v2 -> version forcée
|
||||
inspect_wallet_transfer -> format transfer explicite
|
||||
```
|
||||
|
||||
Les handles `WalletOwner` et `WalletView` peuvent être sérialisés vers le document natif courant avec `to_json_bytes()`. Ces bytes restent un `.kspwallet` chiffré, pas un export de la keypair.
|
||||
Les handles `WalletOwner` et `WalletView` sérialisent leur document courant avec `to_native_bytes()`. `to_json_bytes()` est conservé comme compatibilité V1 et renvoie une erreur de format pour un handle V2 au lieu de transcoder implicitement.
|
||||
|
||||
## 11. Erreurs et diagnostics
|
||||
|
||||
@@ -278,13 +280,24 @@ ksp-onchain-transport-lib
|
||||
|
||||
Cette composition est le rôle de `0.2.6 — ksp-app-wallet-desk`, pas de `ksp-wallet-lib`.
|
||||
|
||||
## Wire V2 (`0.2.6-pre.015`)
|
||||
## Wire/runtime V2 (`0.2.6-pre.015` / `pre.016`)
|
||||
|
||||
Le codec structurel V2 peut être utilisé pour analyser une fixture/document V2 déjà produit :
|
||||
Le codec structurel V2 reste disponible directement pour les outils qui travaillent explicitement au niveau wire :
|
||||
|
||||
```rust
|
||||
let envelope = ksp_wallet_lib::KspWalletEnvelopeV2::parse_binary(bytes)?;
|
||||
let canonical = envelope.to_binary_bytes()?;
|
||||
```
|
||||
|
||||
La persistence applicative ne doit pas encore appeler ce codec directement pour créer un wallet : `pre.016` introduit les APIs génériques/versionnées et le default V2.
|
||||
Une application normale doit préférer les façades Wallet :
|
||||
|
||||
```text
|
||||
create_wallet_file(...) default V2
|
||||
create_wallet_file_v1(...) V1 forcé
|
||||
create_wallet_file_v2(...) V2 forcé
|
||||
open_wallet_*_file(...) auto-détection V1/V2
|
||||
open_wallet_*_file_v1/_v2 version forcée
|
||||
inspect_locked_wallet_file(...) auto-détection V1/V2
|
||||
```
|
||||
|
||||
`DEFAULT_WALLET_FORMAT` et `LATEST_SUPPORTED_WALLET_FORMAT` sont intentionnellement indépendants. L'arrivée d'un futur V3 ne changera pas automatiquement le default V2.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/constants.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
//! Wallet-owned constants.
|
||||
|
||||
@@ -91,6 +91,14 @@ pub const KSPWALLET_V1_XCHACHA_NONCE_BYTES: usize = 24;
|
||||
pub const KSPWALLET_V2_AEAD_TAG_BYTES: usize = KSPWALLET_V1_AEAD_TAG_BYTES;
|
||||
/// Argon2 version serialized by V2 key slots.
|
||||
pub const KSPWALLET_V2_ARGON2_VERSION: u32 = KSPWALLET_V1_ARGON2_VERSION;
|
||||
/// Default Argon2id iteration count for newly created V2 key slots.
|
||||
pub const KSPWALLET_V2_DEFAULT_ARGON2_ITERATIONS: u32 = KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS;
|
||||
/// Default Argon2id memory cost for newly created V2 key slots.
|
||||
pub const KSPWALLET_V2_DEFAULT_ARGON2_MEMORY_KIB: u32 = KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB;
|
||||
/// Default Argon2id parallelism for newly created V2 key slots.
|
||||
pub const KSPWALLET_V2_DEFAULT_ARGON2_PARALLELISM: u32 = KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM;
|
||||
/// Default KDF salt size generated independently for every newly created V2 key slot.
|
||||
pub const KSPWALLET_V2_DEFAULT_KDF_SALT_BYTES: usize = KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES;
|
||||
/// Byte length of an Ed25519 public key used as V2 Wallet format authority.
|
||||
pub const KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES: usize = KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES;
|
||||
/// Byte length of an Ed25519 detached V2 state signature.
|
||||
@@ -123,6 +131,8 @@ pub const KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-OWNE
|
||||
pub const KSPWALLET_V2_OWNER_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-OWNER-SLOT";
|
||||
/// Domain separator for V2 OWNER-only secret compartment AEAD AAD.
|
||||
pub const KSPWALLET_V2_SECRET_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-SECRET";
|
||||
/// Exact Solana keypair plaintext size retained by the initial V2 secret payload profile.
|
||||
pub const KSPWALLET_V2_SECRET_PLAINTEXT_BYTES: usize = KSPWALLET_V1_SECRET_PLAINTEXT_BYTES;
|
||||
/// Byte length of every V2 key-slot identifier.
|
||||
pub const KSPWALLET_V2_SLOT_ID_BYTES: usize = KSPWALLET_V1_SLOT_ID_BYTES;
|
||||
/// Domain separator for the V2 OWNER state-signature transcript.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// file: crates/ksp-wallet-lib/src/crypto.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! In-memory cryptographic primitives for native `.kspwallet` V1.
|
||||
//! In-memory cryptographic primitives shared by native `.kspwallet` V1/V2 runtime.
|
||||
|
||||
use chacha20poly1305::KeyInit; // rust-rules: trait-import
|
||||
use chacha20poly1305::aead::Aead; // rust-rules: trait-import
|
||||
|
||||
/// Exact V1 content-key and password-derived-key size in bytes.
|
||||
/// Exact content-key and password-derived-key size in bytes for the current V1/V2 profiles.
|
||||
pub(crate) const SECRET_KEY_BYTES: usize = 32;
|
||||
|
||||
/// Owned 32-byte secret key with redacted diagnostics and drop-time zeroization.
|
||||
@@ -68,6 +68,11 @@ pub(crate) fn derive_password_key(password: &[u8], kdf: &crate::WalletKdfParamet
|
||||
return derive_argon2id(password, kdf.salt(), kdf.memory_kib(), kdf.iterations(), kdf.parallelism());
|
||||
}
|
||||
|
||||
/// Derives one V2 password wrapping key from serialized Argon2id parameters.
|
||||
pub(crate) fn derive_password_key_v2(password: &[u8], kdf: &crate::WalletKdfParametersV2) -> ksp_core_lib::Result<crate::SecretKeyV1> {
|
||||
return derive_argon2id(password, kdf.salt(), kdf.memory_kib(), kdf.iterations(), kdf.parallelism());
|
||||
}
|
||||
|
||||
/// Wraps one 32-byte content key with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
|
||||
pub(crate) fn wrap_key(
|
||||
wrapping_key: &crate::SecretKeyV1,
|
||||
|
||||
111
crates/ksp-wallet-lib/src/format.rs
Normal file
111
crates/ksp-wallet-lib/src/format.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
// file: crates/ksp-wallet-lib/src/format.rs
|
||||
// version: 2
|
||||
|
||||
//! Native `.kspwallet` version selection and bounded dispatch detection.
|
||||
|
||||
/// Explicit native `.kspwallet` wire format understood by this Wallet release.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum WalletFormat {
|
||||
/// Stable historical JSON/Base64url format.
|
||||
V1,
|
||||
/// Canonical binary KSP format.
|
||||
V2,
|
||||
}
|
||||
|
||||
impl WalletFormat {
|
||||
/// Returns the serialized native format version.
|
||||
#[must_use]
|
||||
pub const fn version(self) -> u32 {
|
||||
return match self {
|
||||
Self::V1 => crate::KSPWALLET_FORMAT_VERSION_V1,
|
||||
Self::V2 => crate::KSPWALLET_FORMAT_VERSION_V2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Default format selected by the non-versioned native Wallet creation/import APIs.
|
||||
///
|
||||
/// This is intentionally independent from [`LATEST_SUPPORTED_WALLET_FORMAT`]. Adding a future format does not implicitly move this default.
|
||||
pub const DEFAULT_WALLET_FORMAT: WalletFormat = WalletFormat::V2;
|
||||
/// Highest native Wallet format understood by this release.
|
||||
pub const LATEST_SUPPORTED_WALLET_FORMAT: WalletFormat = WalletFormat::V2;
|
||||
|
||||
/// Creates a new in-memory native Wallet using [`DEFAULT_WALLET_FORMAT`].
|
||||
pub async fn create_wallet(
|
||||
owner_password: crate::OwnerPassword,
|
||||
view_password: std::option::Option<crate::ViewPassword>,
|
||||
metadata: crate::WalletCreateMetadata,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
return match DEFAULT_WALLET_FORMAT {
|
||||
WalletFormat::V1 => crate::create_wallet_v1(owner_password, view_password, metadata).await,
|
||||
WalletFormat::V2 => crate::create_wallet_v2(owner_password, view_password, metadata).await,
|
||||
};
|
||||
}
|
||||
|
||||
/// Opens VIEW from any currently supported native Wallet wire format.
|
||||
pub async fn open_wallet_view(source: &[u8], password: crate::ViewPassword) -> ksp_core_lib::Result<crate::WalletView> {
|
||||
return match detect_wallet_format(source) {
|
||||
std::result::Result::Ok(WalletFormat::V1) => crate::open_wallet_view_v1(source, password).await,
|
||||
std::result::Result::Ok(WalletFormat::V2) => crate::open_wallet_view_v2(source, password).await,
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Opens OWNER from any currently supported native Wallet wire format.
|
||||
pub async fn open_wallet_owner(source: &[u8], password: crate::OwnerPassword) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
return match detect_wallet_format(source) {
|
||||
std::result::Result::Ok(WalletFormat::V1) => crate::open_wallet_owner_v1(source, password).await,
|
||||
std::result::Result::Ok(WalletFormat::V2) => crate::open_wallet_owner_v2(source, password).await,
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Inspects the authenticated locked projection of any currently supported native Wallet wire format.
|
||||
pub fn inspect_locked_wallet(source: &[u8]) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
|
||||
return match detect_wallet_format(source) {
|
||||
std::result::Result::Ok(WalletFormat::V1) => crate::inspect_locked_wallet_v1(source),
|
||||
std::result::Result::Ok(WalletFormat::V2) => crate::inspect_locked_wallet_v2(source),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Detects the native wire format without decrypting or authenticating the document.
|
||||
///
|
||||
/// Detection only selects the strict parser. The selected V1/V2 parser remains authoritative for all structural and cryptographic validation.
|
||||
pub fn detect_wallet_format(source: &[u8]) -> ksp_core_lib::Result<WalletFormat> {
|
||||
if source.len() > crate::KSPWALLET_MAX_FILE_BYTES {
|
||||
return std::result::Result::Err(format_error("Wallet document exceeds the maximum size"));
|
||||
}
|
||||
if source.starts_with(crate::KSPWALLET_MAGIC.as_bytes()) {
|
||||
let version_start = crate::KSPWALLET_MAGIC.len();
|
||||
let version_end = version_start + 2;
|
||||
let version_bytes = match source.get(version_start..version_end) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(format_error("Wallet binary header is truncated")),
|
||||
};
|
||||
let version = u16::from_be_bytes([version_bytes[0], version_bytes[1]]);
|
||||
return match u32::from(version) {
|
||||
crate::KSPWALLET_FORMAT_VERSION_V2 => std::result::Result::Ok(WalletFormat::V2),
|
||||
other => std::result::Result::Err(version_error(other)),
|
||||
};
|
||||
}
|
||||
let first_non_whitespace = source.iter().copied().find(|byte| return !byte.is_ascii_whitespace());
|
||||
if first_non_whitespace == std::option::Option::Some(b'{') {
|
||||
return std::result::Result::Ok(WalletFormat::V1);
|
||||
}
|
||||
return std::result::Result::Err(format_error("Wallet document does not match a supported native framing"));
|
||||
}
|
||||
|
||||
fn format_error(message: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, message);
|
||||
}
|
||||
|
||||
fn version_error(version: u32) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_VERSION_UNSUPPORTED, "Wallet format version is not supported")
|
||||
.with_context("format_version", version.to_string());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/format.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/lib.rs
|
||||
// version: 12
|
||||
// version: 13
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -16,7 +16,8 @@
|
||||
//! replacement. `0.2.5-pre.008` adds bounded Solana CLI JSON and canonical full-keypair Base58 import/export adapters with safe inspection and no-clobber
|
||||
//! native import/export publication. `0.2.5-pre.009` adds adversarial security/compliance canaries and records the final dependency/interop audit before
|
||||
//! documentation closure. `0.2.6-pre.015` defines the strict binary `.kspwallet` V2 wire and its bounded canonical codec without yet changing the public
|
||||
//! persistence default or Wallet Desk dispatch. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral
|
||||
//! persistence default or Wallet Desk dispatch. `0.2.6-pre.016` adds complete V2 create/open/administration, stable version-neutral dispatch, explicit V1/V2
|
||||
//! APIs and moves the default native creation/import format to V2 without coupling that default to the latest supported version. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral
|
||||
//! observability uses only
|
||||
//! `ksp-logging-lib` with the explicit crate target defined in `src/constants.rs`.
|
||||
|
||||
@@ -24,16 +25,19 @@ mod capability;
|
||||
mod constants;
|
||||
mod crypto;
|
||||
mod error;
|
||||
mod format;
|
||||
mod metadata;
|
||||
mod owner;
|
||||
mod password;
|
||||
mod payload;
|
||||
mod persistence;
|
||||
mod runtime;
|
||||
mod transcript;
|
||||
mod transcript_v2;
|
||||
mod transfer;
|
||||
mod view;
|
||||
mod wallet;
|
||||
mod wallet_v2;
|
||||
mod wire;
|
||||
mod wire_v2;
|
||||
|
||||
@@ -131,6 +135,14 @@ pub use self::constants::KSPWALLET_V1_XCHACHA_NONCE_BYTES;
|
||||
pub use self::constants::KSPWALLET_V2_AEAD_TAG_BYTES;
|
||||
/// Argon2 version serialized by `.kspwallet` V2 key slots.
|
||||
pub use self::constants::KSPWALLET_V2_ARGON2_VERSION;
|
||||
/// Default Argon2id iteration count for newly created V2 slots.
|
||||
pub use self::constants::KSPWALLET_V2_DEFAULT_ARGON2_ITERATIONS;
|
||||
/// Default Argon2id memory cost for newly created V2 slots.
|
||||
pub use self::constants::KSPWALLET_V2_DEFAULT_ARGON2_MEMORY_KIB;
|
||||
/// Default Argon2id parallelism for newly created V2 slots.
|
||||
pub use self::constants::KSPWALLET_V2_DEFAULT_ARGON2_PARALLELISM;
|
||||
/// Default KDF salt size generated for newly created V2 slots.
|
||||
pub use self::constants::KSPWALLET_V2_DEFAULT_KDF_SALT_BYTES;
|
||||
/// Byte length of the V2 Ed25519 format-authority public key.
|
||||
pub use self::constants::KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES;
|
||||
/// Byte length of the V2 Ed25519 detached state signature.
|
||||
@@ -163,6 +175,8 @@ pub use self::constants::KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN;
|
||||
pub use self::constants::KSPWALLET_V2_OWNER_SLOT_AAD_DOMAIN;
|
||||
/// Domain separator for V2 OWNER-only secret compartment AEAD AAD.
|
||||
pub use self::constants::KSPWALLET_V2_SECRET_AAD_DOMAIN;
|
||||
/// Exact Solana keypair plaintext size retained by the initial V2 secret payload profile.
|
||||
pub use self::constants::KSPWALLET_V2_SECRET_PLAINTEXT_BYTES;
|
||||
/// Byte length of every V2 key-slot identifier.
|
||||
pub use self::constants::KSPWALLET_V2_SLOT_ID_BYTES;
|
||||
/// Domain separator for the V2 OWNER state-signature transcript.
|
||||
@@ -205,9 +219,27 @@ pub use self::error::ERROR_CODE_STATE_CONFLICT;
|
||||
pub use self::error::ERROR_CODE_TRANSFER_FORMAT_UNSUPPORTED;
|
||||
/// Error code used when a VIEW unlock attempt fails without exposing a finer cryptographic oracle.
|
||||
pub use self::error::ERROR_CODE_VIEW_UNLOCK_FAILED;
|
||||
/// Default native Wallet format selected by non-versioned create/import APIs.
|
||||
pub use self::format::DEFAULT_WALLET_FORMAT;
|
||||
/// Highest native Wallet format supported by this release.
|
||||
pub use self::format::LATEST_SUPPORTED_WALLET_FORMAT;
|
||||
/// Explicit native Wallet wire-format selector.
|
||||
pub use self::format::WalletFormat;
|
||||
/// Creates a new in-memory native Wallet using the explicit default format.
|
||||
pub use self::format::create_wallet;
|
||||
/// Detects V1 JSON versus V2 binary framing before strict parser dispatch.
|
||||
pub use self::format::detect_wallet_format;
|
||||
/// Inspects any supported in-memory native Wallet document.
|
||||
pub use self::format::inspect_locked_wallet;
|
||||
/// Opens OWNER from any supported in-memory native Wallet document.
|
||||
pub use self::format::open_wallet_owner;
|
||||
/// Opens VIEW from any supported in-memory native Wallet document.
|
||||
pub use self::format::open_wallet_view;
|
||||
/// Minimal non-secret information available while a native Wallet remains locked.
|
||||
pub use self::metadata::LockedWalletInfo;
|
||||
/// Protected initial metadata supplied to native Wallet creation.
|
||||
/// Protected initial metadata supplied to version-neutral native Wallet creation.
|
||||
pub type WalletCreateMetadata = self::metadata::WalletCreateMetadataV1;
|
||||
/// Protected initial metadata payload profile retained for explicit V1 compatibility.
|
||||
pub use self::metadata::WalletCreateMetadataV1;
|
||||
/// Safe metadata projection produced after VIEW or OWNER authorization.
|
||||
pub use self::metadata::WalletInfo;
|
||||
@@ -219,22 +251,46 @@ pub use self::owner::WalletOwner;
|
||||
pub use self::password::OwnerPassword;
|
||||
/// Owned VIEW password material with redacted diagnostics and drop-time zeroization.
|
||||
pub use self::password::ViewPassword;
|
||||
/// Creates and no-clobber persists a native Wallet using the explicit default format.
|
||||
pub use self::persistence::create_wallet_file;
|
||||
/// Creates and no-clobber persists a new native `.kspwallet` V1 file.
|
||||
pub use self::persistence::create_wallet_file_v1;
|
||||
/// Creates and no-clobber persists a new native `.kspwallet` V2 binary file.
|
||||
pub use self::persistence::create_wallet_file_v2;
|
||||
/// Reads and verifies a locked native Wallet using V1/V2 auto-detection.
|
||||
pub use self::persistence::inspect_locked_wallet_file;
|
||||
/// Reads and verifies a locked native `.kspwallet` V1 file.
|
||||
pub use self::persistence::inspect_locked_wallet_file_v1;
|
||||
/// Reads and verifies a locked native `.kspwallet` V2 binary file.
|
||||
pub use self::persistence::inspect_locked_wallet_file_v2;
|
||||
/// Opens a native Wallet with OWNER capability using V1/V2 auto-detection.
|
||||
pub use self::persistence::open_wallet_owner_file;
|
||||
/// Opens a native `.kspwallet` V1 file with OWNER capability.
|
||||
pub use self::persistence::open_wallet_owner_file_v1;
|
||||
/// Opens a native `.kspwallet` V2 binary file with OWNER capability.
|
||||
pub use self::persistence::open_wallet_owner_file_v2;
|
||||
/// Opens a native Wallet with VIEW capability using V1/V2 auto-detection.
|
||||
pub use self::persistence::open_wallet_view_file;
|
||||
/// Opens a native `.kspwallet` V1 file with VIEW capability.
|
||||
pub use self::persistence::open_wallet_view_file_v1;
|
||||
/// Opens a native `.kspwallet` V2 binary file with VIEW capability.
|
||||
pub use self::persistence::open_wallet_view_file_v2;
|
||||
/// Explicit secret-transfer format supported by Wallet.
|
||||
pub use self::transfer::WalletTransferFormat;
|
||||
/// Safe public identity projection of one validated secret-transfer source.
|
||||
pub use self::transfer::WalletTransferInspection;
|
||||
/// Imports one in-memory transfer payload into a new no-clobber native Wallet using the explicit default format.
|
||||
pub use self::transfer::import_wallet_transfer;
|
||||
/// Imports one bounded external transfer file into a new no-clobber native Wallet using the explicit default format.
|
||||
pub use self::transfer::import_wallet_transfer_file;
|
||||
/// Imports one bounded external transfer file into a new no-clobber native Wallet V1.
|
||||
pub use self::transfer::import_wallet_transfer_file_v1;
|
||||
/// Imports one bounded external transfer file into a new no-clobber native Wallet V2.
|
||||
pub use self::transfer::import_wallet_transfer_file_v2;
|
||||
/// Imports one in-memory transfer payload into a new no-clobber native Wallet V1.
|
||||
pub use self::transfer::import_wallet_transfer_v1;
|
||||
/// Imports one in-memory transfer payload into a new no-clobber native Wallet V2.
|
||||
pub use self::transfer::import_wallet_transfer_v2;
|
||||
/// Validates one in-memory transfer payload and exposes only its derived public identity.
|
||||
pub use self::transfer::inspect_wallet_transfer;
|
||||
/// Validates one bounded external transfer file and exposes only its derived public identity.
|
||||
@@ -249,6 +305,14 @@ pub use self::wallet::inspect_locked_wallet_v1;
|
||||
pub use self::wallet::open_wallet_owner_v1;
|
||||
/// Opens the VIEW capability from a native Wallet V1 document.
|
||||
pub use self::wallet::open_wallet_view_v1;
|
||||
/// Creates a new in-memory native Wallet V2.
|
||||
pub use self::wallet_v2::create_wallet_v2;
|
||||
/// Parses and verifies locked native Wallet V2 state without unlocking protected metadata.
|
||||
pub use self::wallet_v2::inspect_locked_wallet_v2;
|
||||
/// Opens the OWNER capability from a native Wallet V2 document.
|
||||
pub use self::wallet_v2::open_wallet_owner_v2;
|
||||
/// Opens the VIEW capability from a native Wallet V2 document.
|
||||
pub use self::wallet_v2::open_wallet_view_v2;
|
||||
/// Strict semantic representation of one parsed native `.kspwallet` V1 envelope.
|
||||
pub use self::wire::KspWalletEnvelopeV1;
|
||||
/// Authenticated-encryption algorithm fixed by native Wallet V1.
|
||||
@@ -308,6 +372,8 @@ pub(crate) use self::crypto::SecretKeyV1;
|
||||
pub(crate) use self::crypto::decrypt_bytes;
|
||||
/// Derives one V1 password wrapping key from serialized Argon2id parameters.
|
||||
pub(crate) use self::crypto::derive_password_key;
|
||||
/// Derives one V2 password wrapping key from serialized Argon2id parameters.
|
||||
pub(crate) use self::crypto::derive_password_key_v2;
|
||||
/// Encrypts bounded plaintext bytes with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
|
||||
pub(crate) use self::crypto::encrypt_bytes;
|
||||
/// Generates a fresh fixed-size byte array from the operating-system CSPRNG.
|
||||
@@ -328,7 +394,9 @@ pub(crate) use self::payload::decode_owner_control_payload;
|
||||
pub(crate) use self::payload::encode_initial_metadata_payload;
|
||||
/// Encodes owner control payload.
|
||||
pub(crate) use self::payload::encode_owner_control_payload;
|
||||
/// Internal no-clobber native persistence path shared by transfer adapters.
|
||||
/// Internal no-clobber native persistence path shared by version-neutral transfer adapters.
|
||||
pub(crate) use self::persistence::persist_new_wallet_content;
|
||||
/// Internal V1 no-clobber native persistence path retained for explicit compatibility adapters.
|
||||
pub(crate) use self::persistence::persist_new_wallet_content_v1;
|
||||
/// Persists new wallet fault before publish.
|
||||
#[cfg(test)]
|
||||
@@ -341,9 +409,15 @@ pub(crate) use self::persistence::persist_new_wallet_for_test;
|
||||
pub(crate) use self::persistence::replace_wallet_fault_before_publish;
|
||||
/// Replaces wallet file v1.
|
||||
pub(crate) use self::persistence::replace_wallet_file_v1;
|
||||
/// Replaces wallet file v2.
|
||||
pub(crate) use self::persistence::replace_wallet_file_v2;
|
||||
/// Replaces wallet for test.
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::persistence::replace_wallet_for_test;
|
||||
/// Version-neutral OWNER runtime state shared by public Wallet handles.
|
||||
pub(crate) use self::runtime::OwnerState;
|
||||
/// Version-neutral VIEW runtime state shared by public Wallet handles.
|
||||
pub(crate) use self::runtime::ViewState;
|
||||
/// Internal deterministic compartment-AAD codec shared by Wallet crypto layers.
|
||||
pub(crate) use self::transcript::compartment_aad;
|
||||
/// Internal deterministic key-slot-AAD codec shared by Wallet crypto layers.
|
||||
@@ -357,12 +431,20 @@ pub(crate) use self::transcript_v2::slot_aad_v2;
|
||||
/// Internal deterministic V2 OWNER-state transcript codec shared by Wallet crypto layers.
|
||||
pub(crate) use self::transcript_v2::state_transcript_v2;
|
||||
/// Internal no-clobber transfer-file writer used only by OWNER export.
|
||||
pub(crate) use self::transfer::write_wallet_transfer_file_v1;
|
||||
pub(crate) use self::transfer::write_wallet_transfer_file;
|
||||
/// Crate-internal `OwnerStateV1` state shared across the owning crate.
|
||||
pub(crate) use self::wallet::OwnerStateV1;
|
||||
/// Crate-internal `ViewStateV1` state shared across the owning crate.
|
||||
pub(crate) use self::wallet::ViewStateV1;
|
||||
/// Internal imported-keypair creation path shared by transfer adapters.
|
||||
/// Internal imported-keypair V1 creation path shared by transfer adapters.
|
||||
pub(crate) use self::wallet::create_wallet_v1_from_keypair;
|
||||
/// Verifies state signature.
|
||||
/// Verifies V1 state signature.
|
||||
pub(crate) use self::wallet::verify_state_signature;
|
||||
/// Crate-internal `OwnerStateV2` state shared across the owning crate.
|
||||
pub(crate) use self::wallet_v2::OwnerStateV2;
|
||||
/// Crate-internal `ViewStateV2` state shared across the owning crate.
|
||||
pub(crate) use self::wallet_v2::ViewStateV2;
|
||||
/// Internal imported-keypair V2 creation path shared by transfer adapters.
|
||||
pub(crate) use self::wallet_v2::create_wallet_v2_from_keypair;
|
||||
/// Verifies V2 state signature.
|
||||
pub(crate) use self::wallet_v2::verify_state_signature_v2;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/metadata.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
/// Protected metadata requested when creating a new native Wallet.
|
||||
///
|
||||
@@ -78,12 +78,13 @@ pub struct WalletInfo {
|
||||
impl WalletInfo {
|
||||
/// Creates a new `WalletInfo` value.
|
||||
pub(crate) fn new(
|
||||
format_version: u32,
|
||||
capability: crate::WalletCapability,
|
||||
pubkey: ksp_core_lib::Pubkey,
|
||||
alias: std::option::Option<std::string::String>,
|
||||
notes: std::vec::Vec<crate::WalletNote>,
|
||||
) -> Self {
|
||||
return Self { format_version: crate::KSPWALLET_FORMAT_VERSION_V1, capability, pubkey, alias, notes };
|
||||
return Self { format_version, capability, pubkey, alias, notes };
|
||||
}
|
||||
|
||||
/// Returns the native Wallet format version parsed for this projection.
|
||||
@@ -139,8 +140,8 @@ pub struct LockedWalletInfo {
|
||||
|
||||
impl LockedWalletInfo {
|
||||
/// Creates a new `LockedWalletInfo` value.
|
||||
pub(crate) const fn new(view_enabled: bool) -> Self {
|
||||
return Self { format_version: crate::KSPWALLET_FORMAT_VERSION_V1, view_enabled };
|
||||
pub(crate) const fn new(format_version: u32, view_enabled: bool) -> Self {
|
||||
return Self { format_version, view_enabled };
|
||||
}
|
||||
|
||||
/// Returns the native Wallet format version.
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
// file: crates/ksp-wallet-lib/src/owner.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
/// Authorized OWNER capability handle.
|
||||
///
|
||||
/// OWNER exposes protected metadata, Solana message signing and the authenticated administration operations defined by native Wallet V1. Secret key
|
||||
/// OWNER exposes protected metadata, Solana message signing and the authenticated administration operations defined by the authenticated native Wallet format. Secret key
|
||||
/// material remains encapsulated and is never exposed through a general-purpose getter.
|
||||
pub struct WalletOwner {
|
||||
info: crate::WalletInfo,
|
||||
state: crate::OwnerStateV1,
|
||||
state: crate::OwnerState,
|
||||
}
|
||||
|
||||
impl WalletOwner {
|
||||
/// Builds `WalletOwner` from unlocked.
|
||||
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::OwnerStateV1) -> Self {
|
||||
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::OwnerState) -> Self {
|
||||
return Self { info, state };
|
||||
}
|
||||
|
||||
/// Returns the native Wallet format version backing this authorized handle.
|
||||
#[must_use]
|
||||
pub const fn format_version(&self) -> u32 {
|
||||
return self.info.format_version();
|
||||
}
|
||||
|
||||
/// Returns the authorization capability represented by this handle.
|
||||
#[must_use]
|
||||
pub const fn capability(&self) -> crate::WalletCapability {
|
||||
@@ -84,7 +90,7 @@ impl WalletOwner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let result = crate::write_wallet_transfer_file_v1(destination.as_ref().to_path_buf(), encoded, format).await;
|
||||
let result = crate::write_wallet_transfer_file(destination.as_ref().to_path_buf(), encoded, format).await;
|
||||
if result.is_ok() {
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
@@ -181,11 +187,13 @@ impl WalletOwner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
|
||||
let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await;
|
||||
if let std::result::Result::Err(error) = persist_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
self.state.apply_envelope(envelope);
|
||||
if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_rotate_owner_password",
|
||||
@@ -207,11 +215,13 @@ impl WalletOwner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
|
||||
let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await;
|
||||
if let std::result::Result::Err(error) = persist_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
self.state.apply_envelope(envelope);
|
||||
if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_rotate_view_password",
|
||||
@@ -229,11 +239,13 @@ impl WalletOwner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
|
||||
let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await;
|
||||
if let std::result::Result::Err(error) = persist_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
self.state.apply_strong_view_state(envelope, metadata_key);
|
||||
if let std::result::Result::Err(error) = self.state.apply_strong_view_state(envelope, metadata_key) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_disable_view",
|
||||
@@ -253,11 +265,13 @@ impl WalletOwner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
|
||||
let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await;
|
||||
if let std::result::Result::Err(error) = persist_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
self.state.apply_strong_view_state(envelope, metadata_key);
|
||||
if let std::result::Result::Err(error) = self.state.apply_strong_view_state(envelope, metadata_key) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_recreate_view",
|
||||
@@ -267,9 +281,16 @@ impl WalletOwner {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
/// Serializes the complete locked `.kspwallet` V1 document without exposing any unlocked secret material.
|
||||
/// Serializes the complete locked native Wallet in its current V1 or V2 wire format.
|
||||
pub fn to_native_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
return self.state.native_bytes();
|
||||
}
|
||||
|
||||
/// Serializes a V1 handle as its historical JSON document.
|
||||
///
|
||||
/// V2 handles return a format error instead of being converted implicitly.
|
||||
pub fn to_json_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
return self.state.envelope().to_json_bytes();
|
||||
return self.state.json_bytes_v1();
|
||||
}
|
||||
|
||||
async fn persist_metadata_payload(
|
||||
@@ -282,11 +303,13 @@ impl WalletOwner {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let persist_result = persist_staged(destination, self.state.envelope(), &envelope).await;
|
||||
let persist_result = self.state.persist_staged(destination, &envelope).await;
|
||||
if let std::result::Result::Err(error) = persist_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
self.state.apply_envelope(envelope);
|
||||
if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
self.info = info;
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, operation = operation, capability = "owner", "wallet protected metadata updated");
|
||||
return std::result::Result::Ok(());
|
||||
@@ -299,18 +322,6 @@ impl std::fmt::Debug for WalletOwner {
|
||||
}
|
||||
}
|
||||
|
||||
async fn persist_staged(
|
||||
destination: std::path::PathBuf,
|
||||
expected_current: &crate::KspWalletEnvelopeV1,
|
||||
envelope: &crate::KspWalletEnvelopeV1,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let serialized = match envelope.to_json_bytes() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::replace_wallet_file_v1(destination, expected_current.clone(), serialized).await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/administration.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-wallet-lib/src/payload.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Plaintext payload codecs protected inside native `.kspwallet` V1 compartments.
|
||||
//! Plaintext payload codecs shared by the initial native `.kspwallet` V1/V2 protected compartments.
|
||||
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
use std::str::FromStr; // rust-rules: trait-import
|
||||
@@ -119,8 +119,8 @@ impl crate::MetadataPayloadV1 {
|
||||
}
|
||||
|
||||
/// Consumes this value and returns info.
|
||||
pub(crate) fn into_info(self, capability: crate::WalletCapability) -> crate::WalletInfo {
|
||||
return crate::WalletInfo::new(capability, self.pubkey, self.alias, self.notes);
|
||||
pub(crate) fn into_info(self, format_version: u32, capability: crate::WalletCapability) -> crate::WalletInfo {
|
||||
return crate::WalletInfo::new(format_version, capability, self.pubkey, self.alias, self.notes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,54 @@
|
||||
// file: crates/ksp-wallet-lib/src/persistence.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! Async-first native Wallet V1 filesystem persistence.
|
||||
//! Async-first native Wallet V1/V2 filesystem persistence and version-neutral dispatch.
|
||||
|
||||
use std::io::Read; // rust-rules: trait-import
|
||||
use std::io::Write; // rust-rules: trait-import
|
||||
|
||||
/// Creates a new native `.kspwallet` using [`crate::DEFAULT_WALLET_FORMAT`].
|
||||
///
|
||||
/// The default is explicitly V2 in this release and does not track future `LATEST_SUPPORTED_WALLET_FORMAT` values automatically.
|
||||
pub async fn create_wallet_file(
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
owner_password: crate::OwnerPassword,
|
||||
view_password: std::option::Option<crate::ViewPassword>,
|
||||
metadata: crate::WalletCreateMetadata,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
return match crate::DEFAULT_WALLET_FORMAT {
|
||||
crate::WalletFormat::V1 => create_wallet_file_v1(destination, owner_password, view_password, metadata).await,
|
||||
crate::WalletFormat::V2 => create_wallet_file_v2(destination, owner_password, view_password, metadata).await,
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates and no-clobber persists a new native `.kspwallet` V2 binary file.
|
||||
pub async fn create_wallet_file_v2(
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
owner_password: crate::OwnerPassword,
|
||||
view_password: std::option::Option<crate::ViewPassword>,
|
||||
metadata: crate::WalletCreateMetadata,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
let destination = destination.as_ref().to_path_buf();
|
||||
let owner = match crate::create_wallet_v2(owner_password, view_password, metadata).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let serialized = match owner.to_native_bytes() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) = persist_new_wallet_async(destination, serialized).await {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_create_file",
|
||||
format_version = crate::KSPWALLET_FORMAT_VERSION_V2,
|
||||
"native wallet persisted with no-clobber semantics"
|
||||
);
|
||||
return std::result::Result::Ok(owner);
|
||||
}
|
||||
|
||||
/// Creates a new native `.kspwallet` V1 at `destination` without overwriting an existing path.
|
||||
///
|
||||
/// The complete encrypted document is created in memory first, written and synchronized through a temporary file in the destination directory, then
|
||||
@@ -40,6 +83,30 @@ pub async fn create_wallet_file_v1(
|
||||
return std::result::Result::Ok(owner);
|
||||
}
|
||||
|
||||
/// Opens a supported native `.kspwallet` file with VIEW capability after bounded V1/V2 detection.
|
||||
pub async fn open_wallet_view_file(
|
||||
source: impl std::convert::AsRef<std::path::Path>,
|
||||
password: crate::ViewPassword,
|
||||
) -> ksp_core_lib::Result<crate::WalletView> {
|
||||
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::open_wallet_view(bytes.as_slice(), password).await;
|
||||
}
|
||||
|
||||
/// Opens a native `.kspwallet` V2 binary file with VIEW capability.
|
||||
pub async fn open_wallet_view_file_v2(
|
||||
source: impl std::convert::AsRef<std::path::Path>,
|
||||
password: crate::ViewPassword,
|
||||
) -> ksp_core_lib::Result<crate::WalletView> {
|
||||
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::open_wallet_view_v2(bytes.as_slice(), password).await;
|
||||
}
|
||||
|
||||
/// Opens a native `.kspwallet` V1 from `source` with VIEW capability.
|
||||
///
|
||||
/// The file is read through the bounded async persistence boundary before the normal strict parser, OWNER state-signature verification and VIEW KDF flow.
|
||||
@@ -56,6 +123,30 @@ pub async fn open_wallet_view_file_v1(
|
||||
return crate::open_wallet_view_v1(bytes.as_slice(), password).await;
|
||||
}
|
||||
|
||||
/// Opens a supported native `.kspwallet` file with OWNER capability after bounded V1/V2 detection.
|
||||
pub async fn open_wallet_owner_file(
|
||||
source: impl std::convert::AsRef<std::path::Path>,
|
||||
password: crate::OwnerPassword,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::open_wallet_owner(bytes.as_slice(), password).await;
|
||||
}
|
||||
|
||||
/// Opens a native `.kspwallet` V2 binary file with OWNER capability.
|
||||
pub async fn open_wallet_owner_file_v2(
|
||||
source: impl std::convert::AsRef<std::path::Path>,
|
||||
password: crate::OwnerPassword,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::open_wallet_owner_v2(bytes.as_slice(), password).await;
|
||||
}
|
||||
|
||||
/// Opens a native `.kspwallet` V1 from `source` with OWNER capability.
|
||||
///
|
||||
/// The file is read through the bounded async persistence boundary before the normal strict parser, OWNER state-signature verification and OWNER KDF flow.
|
||||
@@ -72,6 +163,24 @@ pub async fn open_wallet_owner_file_v1(
|
||||
return crate::open_wallet_owner_v1(bytes.as_slice(), password).await;
|
||||
}
|
||||
|
||||
/// Reads and verifies the locked projection of a supported native `.kspwallet` file after bounded V1/V2 detection.
|
||||
pub async fn inspect_locked_wallet_file(source: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
|
||||
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::inspect_locked_wallet(bytes.as_slice());
|
||||
}
|
||||
|
||||
/// Reads and verifies the locked projection of a native `.kspwallet` V2 binary file without running a password KDF.
|
||||
pub async fn inspect_locked_wallet_file_v2(source: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
|
||||
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::inspect_locked_wallet_v2(bytes.as_slice());
|
||||
}
|
||||
|
||||
/// Reads and verifies the locked projection of a native `.kspwallet` V1 from `source` without running a password KDF.
|
||||
pub async fn inspect_locked_wallet_file_v1(source: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
|
||||
let source = source.as_ref().to_path_buf();
|
||||
@@ -88,6 +197,11 @@ pub(crate) async fn persist_new_wallet_content_v1(destination: std::path::PathBu
|
||||
return persist_new_wallet_async(destination, content).await;
|
||||
}
|
||||
|
||||
/// Persists one already serialized native Wallet document with no-clobber semantics.
|
||||
pub(crate) async fn persist_new_wallet_content(destination: std::path::PathBuf, content: std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
||||
return persist_new_wallet_async(destination, content).await;
|
||||
}
|
||||
|
||||
/// Replaces wallet file v1.
|
||||
pub(crate) async fn replace_wallet_file_v1(
|
||||
destination: std::path::PathBuf,
|
||||
@@ -103,6 +217,21 @@ pub(crate) async fn replace_wallet_file_v1(
|
||||
};
|
||||
}
|
||||
|
||||
/// Replaces one authenticated V2 Wallet file only if the current V2 state still matches the caller's expected state.
|
||||
pub(crate) async fn replace_wallet_file_v2(
|
||||
destination: std::path::PathBuf,
|
||||
expected_current: crate::KspWalletEnvelopeV2,
|
||||
content: std::vec::Vec<u8>,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let task = tokio::task::spawn_blocking(move || {
|
||||
return replace_wallet_file_v2_checked_blocking(destination.as_path(), &expected_current, content.as_slice());
|
||||
});
|
||||
return match task.await {
|
||||
std::result::Result::Ok(result) => result,
|
||||
std::result::Result::Err(error) => std::result::Result::Err(blocking_atomic_error("replace_task", error)),
|
||||
};
|
||||
}
|
||||
|
||||
async fn read_wallet_file_async(source: std::path::PathBuf) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
let task = tokio::task::spawn_blocking(move || return read_wallet_file_blocking(source.as_path()));
|
||||
return match task.await {
|
||||
@@ -162,6 +291,18 @@ fn replace_wallet_file_checked_blocking(
|
||||
return replace_wallet_file_with_hook(destination, content, || return verify_expected_wallet_state(destination, expected_current));
|
||||
}
|
||||
|
||||
fn replace_wallet_file_v2_checked_blocking(
|
||||
destination: &std::path::Path,
|
||||
expected_current: &crate::KspWalletEnvelopeV2,
|
||||
content: &[u8],
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let current_check = verify_expected_wallet_state_v2(destination, expected_current);
|
||||
if let std::result::Result::Err(error) = current_check {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return replace_wallet_file_with_hook(destination, content, || return verify_expected_wallet_state_v2(destination, expected_current));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn replace_wallet_file_blocking(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
||||
return replace_wallet_file_with_hook(destination, content, || return std::result::Result::Ok(()));
|
||||
@@ -186,6 +327,24 @@ fn verify_expected_wallet_state(destination: &std::path::Path, expected_current:
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn verify_expected_wallet_state_v2(destination: &std::path::Path, expected_current: &crate::KspWalletEnvelopeV2) -> ksp_core_lib::Result<()> {
|
||||
let current_bytes = match read_wallet_file_blocking(destination) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let current = match crate::KspWalletEnvelopeV2::parse_binary(current_bytes.as_slice()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) = crate::verify_state_signature_v2(¤t) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if ¤t != expected_current {
|
||||
return std::result::Result::Err(state_conflict_error());
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn replace_wallet_file_with_hook<F>(destination: &std::path::Path, content: &[u8], before_publish: F) -> ksp_core_lib::Result<()>
|
||||
where
|
||||
F: std::ops::FnOnce() -> ksp_core_lib::Result<()>,
|
||||
|
||||
233
crates/ksp-wallet-lib/src/runtime.rs
Normal file
233
crates/ksp-wallet-lib/src/runtime.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
// file: crates/ksp-wallet-lib/src/runtime.rs
|
||||
// version: 1
|
||||
|
||||
//! Version-neutral unlocked Wallet state dispatch used by stable OWNER/VIEW handles.
|
||||
|
||||
/// One staged native envelope produced by an authenticated administration operation.
|
||||
pub(crate) enum StagedEnvelope {
|
||||
V1(crate::KspWalletEnvelopeV1),
|
||||
V2(crate::KspWalletEnvelopeV2),
|
||||
}
|
||||
|
||||
/// Version-neutral OWNER runtime state.
|
||||
pub(crate) enum OwnerState {
|
||||
V1(crate::OwnerStateV1),
|
||||
V2(crate::OwnerStateV2),
|
||||
}
|
||||
|
||||
impl OwnerState {
|
||||
/// Returns the native format version owned by this OWNER state.
|
||||
pub(crate) const fn format_version(&self) -> u32 {
|
||||
return match self {
|
||||
Self::V1(_) => crate::KSPWALLET_FORMAT_VERSION_V1,
|
||||
Self::V2(_) => crate::KSPWALLET_FORMAT_VERSION_V2,
|
||||
};
|
||||
}
|
||||
|
||||
/// Exports the Solana identity through the requested external transfer format.
|
||||
pub(crate) fn export_transfer(&self, format: crate::WalletTransferFormat) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
return match self {
|
||||
Self::V1(state) => state.export_transfer(format),
|
||||
Self::V2(state) => state.export_transfer(format),
|
||||
};
|
||||
}
|
||||
|
||||
/// Signs one message with the OWNER-authorized Solana identity.
|
||||
pub(crate) fn sign_message(&self, message: &[u8]) -> ksp_core_lib::Result<[u8; crate::KSPWALLET_SOLANA_SIGNATURE_BYTES]> {
|
||||
return match self {
|
||||
Self::V1(state) => state.sign_message(message),
|
||||
Self::V2(state) => state.sign_message(message),
|
||||
};
|
||||
}
|
||||
|
||||
/// Stages an authenticated metadata replacement without publishing it.
|
||||
pub(crate) fn stage_metadata_payload(&self, payload: crate::MetadataPayloadV1) -> ksp_core_lib::Result<(StagedEnvelope, crate::WalletInfo)> {
|
||||
return match self {
|
||||
Self::V1(state) => state.stage_metadata_payload(payload).map(|(envelope, info)| (StagedEnvelope::V1(envelope), info)),
|
||||
Self::V2(state) => state.stage_metadata_payload(payload).map(|(envelope, info)| (StagedEnvelope::V2(envelope), info)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Stages an OWNER credential rotation in the state native format.
|
||||
pub(crate) async fn stage_owner_password_rotation(&self, password: crate::OwnerPassword) -> ksp_core_lib::Result<StagedEnvelope> {
|
||||
return match self {
|
||||
Self::V1(state) => state.stage_owner_password_rotation(password).await.map(StagedEnvelope::V1),
|
||||
Self::V2(state) => state.stage_owner_password_rotation(password).await.map(StagedEnvelope::V2),
|
||||
};
|
||||
}
|
||||
|
||||
/// Stages a VIEW credential rotation in the state native format.
|
||||
pub(crate) async fn stage_view_password_rotation(&self, password: crate::ViewPassword) -> ksp_core_lib::Result<StagedEnvelope> {
|
||||
return match self {
|
||||
Self::V1(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V1),
|
||||
Self::V2(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V2),
|
||||
};
|
||||
}
|
||||
|
||||
/// Stages strong VIEW disable while retaining OWNER authority.
|
||||
pub(crate) fn stage_disable_view(&self) -> ksp_core_lib::Result<(StagedEnvelope, crate::SecretKeyV1)> {
|
||||
return match self {
|
||||
Self::V1(state) => state.stage_disable_view().map(|(envelope, key)| (StagedEnvelope::V1(envelope), key)),
|
||||
Self::V2(state) => state.stage_disable_view().map(|(envelope, key)| (StagedEnvelope::V2(envelope), key)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Stages strong VIEW recreation under a new credential.
|
||||
pub(crate) async fn stage_recreate_view(&self, password: crate::ViewPassword) -> ksp_core_lib::Result<(StagedEnvelope, crate::SecretKeyV1)> {
|
||||
return match self {
|
||||
Self::V1(state) => state.stage_recreate_view(password).await.map(|(envelope, key)| (StagedEnvelope::V1(envelope), key)),
|
||||
Self::V2(state) => state.stage_recreate_view(password).await.map(|(envelope, key)| (StagedEnvelope::V2(envelope), key)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Atomically publishes a staged envelope after format-specific state-conflict verification.
|
||||
pub(crate) async fn persist_staged(&self, destination: std::path::PathBuf, envelope: &StagedEnvelope) -> ksp_core_lib::Result<()> {
|
||||
return match (self, envelope) {
|
||||
(Self::V1(state), StagedEnvelope::V1(staged)) => {
|
||||
let serialized = match staged.to_json_bytes() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
crate::replace_wallet_file_v1(destination, state.envelope().clone(), serialized).await
|
||||
},
|
||||
(Self::V2(state), StagedEnvelope::V2(staged)) => {
|
||||
let serialized = match staged.to_binary_bytes() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
crate::replace_wallet_file_v2(destination, state.envelope().clone(), serialized).await
|
||||
},
|
||||
_ => std::result::Result::Err(version_state_error()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Replaces the in-memory authenticated envelope after successful persistence.
|
||||
pub(crate) fn apply_envelope(&mut self, envelope: StagedEnvelope) -> ksp_core_lib::Result<()> {
|
||||
return match (self, envelope) {
|
||||
(Self::V1(state), StagedEnvelope::V1(value)) => {
|
||||
state.apply_envelope(value);
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
(Self::V2(state), StagedEnvelope::V2(value)) => {
|
||||
state.apply_envelope(value);
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
_ => std::result::Result::Err(version_state_error()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Replaces the envelope and metadata key after a strong VIEW administration operation.
|
||||
pub(crate) fn apply_strong_view_state(&mut self, envelope: StagedEnvelope, metadata_key: crate::SecretKeyV1) -> ksp_core_lib::Result<()> {
|
||||
return match (self, envelope) {
|
||||
(Self::V1(state), StagedEnvelope::V1(value)) => {
|
||||
state.apply_strong_view_state(value, metadata_key);
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
(Self::V2(state), StagedEnvelope::V2(value)) => {
|
||||
state.apply_strong_view_state(value, metadata_key);
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
_ => std::result::Result::Err(version_state_error()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Serializes the authenticated state in its native V1 or V2 wire format.
|
||||
pub(crate) fn native_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
return match self {
|
||||
Self::V1(state) => state.envelope().to_json_bytes(),
|
||||
Self::V2(state) => state.envelope().to_binary_bytes(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Serializes only V1 state as JSON and rejects V2 instead of converting formats.
|
||||
pub(crate) fn json_bytes_v1(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
return match self {
|
||||
Self::V1(state) => state.envelope().to_json_bytes(),
|
||||
Self::V2(_) => std::result::Result::Err(v1_serialization_error()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Version-neutral VIEW runtime state.
|
||||
pub(crate) enum ViewState {
|
||||
V1(crate::ViewStateV1),
|
||||
V2(crate::ViewStateV2),
|
||||
}
|
||||
|
||||
impl ViewState {
|
||||
/// Returns the native format version owned by this VIEW state.
|
||||
pub(crate) const fn format_version(&self) -> u32 {
|
||||
return match self {
|
||||
Self::V1(_) => crate::KSPWALLET_FORMAT_VERSION_V1,
|
||||
Self::V2(_) => crate::KSPWALLET_FORMAT_VERSION_V2,
|
||||
};
|
||||
}
|
||||
|
||||
/// Stages a VIEW credential rotation in the state native format.
|
||||
pub(crate) async fn stage_view_password_rotation(&self, password: crate::ViewPassword) -> ksp_core_lib::Result<StagedEnvelope> {
|
||||
return match self {
|
||||
Self::V1(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V1),
|
||||
Self::V2(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V2),
|
||||
};
|
||||
}
|
||||
|
||||
/// Atomically publishes a staged envelope after format-specific state-conflict verification.
|
||||
pub(crate) async fn persist_staged(&self, destination: std::path::PathBuf, envelope: &StagedEnvelope) -> ksp_core_lib::Result<()> {
|
||||
return match (self, envelope) {
|
||||
(Self::V1(state), StagedEnvelope::V1(staged)) => {
|
||||
let serialized = match staged.to_json_bytes() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
crate::replace_wallet_file_v1(destination, state.envelope().clone(), serialized).await
|
||||
},
|
||||
(Self::V2(state), StagedEnvelope::V2(staged)) => {
|
||||
let serialized = match staged.to_binary_bytes() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
crate::replace_wallet_file_v2(destination, state.envelope().clone(), serialized).await
|
||||
},
|
||||
_ => std::result::Result::Err(version_state_error()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Replaces the in-memory authenticated envelope after successful persistence.
|
||||
pub(crate) fn apply_envelope(&mut self, envelope: StagedEnvelope) -> ksp_core_lib::Result<()> {
|
||||
return match (self, envelope) {
|
||||
(Self::V1(state), StagedEnvelope::V1(value)) => {
|
||||
state.apply_envelope(value);
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
(Self::V2(state), StagedEnvelope::V2(value)) => {
|
||||
state.apply_envelope(value);
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
_ => std::result::Result::Err(version_state_error()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Serializes the authenticated state in its native V1 or V2 wire format.
|
||||
pub(crate) fn native_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
return match self {
|
||||
Self::V1(state) => state.envelope().to_json_bytes(),
|
||||
Self::V2(state) => state.envelope().to_binary_bytes(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Serializes only V1 state as JSON and rejects V2 instead of converting formats.
|
||||
pub(crate) fn json_bytes_v1(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
return match self {
|
||||
Self::V1(state) => state.envelope().to_json_bytes(),
|
||||
Self::V2(_) => std::result::Result::Err(v1_serialization_error()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn version_state_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "Wallet runtime state and staged format are inconsistent");
|
||||
}
|
||||
|
||||
fn v1_serialization_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "Wallet is not a V1 JSON document");
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/transfer.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Explicit OWNER-only Solana keypair import/export adapters.
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::io::Write; // rust-rules: trait-import
|
||||
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
|
||||
use zeroize::Zeroize; // rust-rules: trait-import
|
||||
|
||||
/// Explicit secret-transfer formats supported by Wallet `0.2.5`.
|
||||
/// Explicit secret-transfer formats supported by Wallet.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum WalletTransferFormat {
|
||||
@@ -106,6 +106,55 @@ pub async fn inspect_wallet_transfer_file(
|
||||
return inspect_wallet_transfer(bytes.as_slice(), format);
|
||||
}
|
||||
|
||||
/// Imports one in-memory Solana keypair transfer payload into a new native Wallet using [`crate::DEFAULT_WALLET_FORMAT`].
|
||||
pub async fn import_wallet_transfer(
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
source: &[u8],
|
||||
format: WalletTransferFormat,
|
||||
owner_password: crate::OwnerPassword,
|
||||
view_password: std::option::Option<crate::ViewPassword>,
|
||||
metadata: crate::WalletCreateMetadata,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
return match crate::DEFAULT_WALLET_FORMAT {
|
||||
crate::WalletFormat::V1 => import_wallet_transfer_v1(destination, source, format, owner_password, view_password, metadata).await,
|
||||
crate::WalletFormat::V2 => import_wallet_transfer_v2(destination, source, format, owner_password, view_password, metadata).await,
|
||||
};
|
||||
}
|
||||
|
||||
/// Imports one in-memory Solana keypair transfer payload into a new native `.kspwallet` V2 binary file.
|
||||
pub async fn import_wallet_transfer_v2(
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
source: &[u8],
|
||||
format: WalletTransferFormat,
|
||||
owner_password: crate::OwnerPassword,
|
||||
view_password: std::option::Option<crate::ViewPassword>,
|
||||
metadata: crate::WalletCreateMetadata,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
let keypair = match decode_transfer_keypair(source, format) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let owner = match crate::create_wallet_v2_from_keypair(owner_password, view_password, metadata, keypair).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let serialized = match owner.to_native_bytes() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) = crate::persist_new_wallet_content(destination.as_ref().to_path_buf(), serialized).await {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_import_transfer",
|
||||
format_version = crate::KSPWALLET_FORMAT_VERSION_V2,
|
||||
transfer_format = format.code(),
|
||||
"external Solana keypair imported into a new native wallet"
|
||||
);
|
||||
return std::result::Result::Ok(owner);
|
||||
}
|
||||
|
||||
/// Imports one in-memory Solana keypair transfer payload into a new native `.kspwallet` V1 file.
|
||||
///
|
||||
/// Import always creates a fresh native Wallet envelope around the validated immutable Solana keypair and publishes with the same no-clobber semantics as
|
||||
@@ -143,6 +192,38 @@ pub async fn import_wallet_transfer_v1(
|
||||
return std::result::Result::Ok(owner);
|
||||
}
|
||||
|
||||
/// Imports one bounded external transfer file into a new native Wallet using [`crate::DEFAULT_WALLET_FORMAT`].
|
||||
pub async fn import_wallet_transfer_file(
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
source: impl std::convert::AsRef<std::path::Path>,
|
||||
format: WalletTransferFormat,
|
||||
owner_password: crate::OwnerPassword,
|
||||
view_password: std::option::Option<crate::ViewPassword>,
|
||||
metadata: crate::WalletCreateMetadata,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
let bytes = match read_transfer_file_async(source.as_ref().to_path_buf(), format).await {
|
||||
std::result::Result::Ok(value) => zeroize::Zeroizing::new(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return import_wallet_transfer(destination, bytes.as_slice(), format, owner_password, view_password, metadata).await;
|
||||
}
|
||||
|
||||
/// Imports one bounded external transfer file into a new native `.kspwallet` V2 binary file.
|
||||
pub async fn import_wallet_transfer_file_v2(
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
source: impl std::convert::AsRef<std::path::Path>,
|
||||
format: WalletTransferFormat,
|
||||
owner_password: crate::OwnerPassword,
|
||||
view_password: std::option::Option<crate::ViewPassword>,
|
||||
metadata: crate::WalletCreateMetadata,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
let bytes = match read_transfer_file_async(source.as_ref().to_path_buf(), format).await {
|
||||
std::result::Result::Ok(value) => zeroize::Zeroizing::new(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return import_wallet_transfer_v2(destination, bytes.as_slice(), format, owner_password, view_password, metadata).await;
|
||||
}
|
||||
|
||||
/// Imports one bounded external transfer file into a new native `.kspwallet` V1 file.
|
||||
///
|
||||
/// The source file is never modified and the destination remains no-clobber.
|
||||
@@ -161,8 +242,8 @@ pub async fn import_wallet_transfer_file_v1(
|
||||
return import_wallet_transfer_v1(destination, bytes.as_slice(), format, owner_password, view_password, metadata).await;
|
||||
}
|
||||
|
||||
/// Executes the crate-internal write wallet transfer file v1 operation for the owning module.
|
||||
pub(crate) async fn write_wallet_transfer_file_v1(
|
||||
/// Writes one bounded OWNER export using no-clobber external transfer semantics.
|
||||
pub(crate) async fn write_wallet_transfer_file(
|
||||
destination: std::path::PathBuf,
|
||||
content: std::vec::Vec<u8>,
|
||||
format: WalletTransferFormat,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/view.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
/// Authorized VIEW capability handle.
|
||||
///
|
||||
@@ -7,15 +7,21 @@
|
||||
/// Solana secret, OWNER administration material or metadata-write authority.
|
||||
pub struct WalletView {
|
||||
info: crate::WalletInfo,
|
||||
state: crate::ViewStateV1,
|
||||
state: crate::ViewState,
|
||||
}
|
||||
|
||||
impl WalletView {
|
||||
/// Builds `WalletView` from unlocked.
|
||||
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::ViewStateV1) -> Self {
|
||||
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::ViewState) -> Self {
|
||||
return Self { info, state };
|
||||
}
|
||||
|
||||
/// Returns the native Wallet format version backing this authorized handle.
|
||||
#[must_use]
|
||||
pub const fn format_version(&self) -> u32 {
|
||||
return self.info.format_version();
|
||||
}
|
||||
|
||||
/// Returns the authorization capability represented by this handle.
|
||||
#[must_use]
|
||||
pub const fn capability(&self) -> crate::WalletCapability {
|
||||
@@ -65,15 +71,13 @@ impl WalletView {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let serialized = match envelope.to_json_bytes() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let persist_result = crate::replace_wallet_file_v1(destination.as_ref().to_path_buf(), self.state.envelope().clone(), serialized).await;
|
||||
let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await;
|
||||
if let std::result::Result::Err(error) = persist_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
self.state.apply_envelope(envelope);
|
||||
if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_rotate_view_password",
|
||||
@@ -83,9 +87,16 @@ impl WalletView {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
/// Serializes the unchanged locked `.kspwallet` V1 document without exposing the metadata content key.
|
||||
/// Serializes the unchanged locked native Wallet in its current V1 or V2 wire format.
|
||||
pub fn to_native_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
return self.state.native_bytes();
|
||||
}
|
||||
|
||||
/// Serializes a V1 handle as its historical JSON document.
|
||||
///
|
||||
/// V2 handles return a format error instead of being converted implicitly.
|
||||
pub fn to_json_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
return self.state.envelope().to_json_bytes();
|
||||
return self.state.json_bytes_v1();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/wallet.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
//! In-memory native Wallet V1 create/open orchestration.
|
||||
|
||||
@@ -143,7 +143,7 @@ impl OwnerStateV1 {
|
||||
if let std::result::Result::Err(error) = verify_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok((envelope, payload.into_info(crate::WalletCapability::Owner)));
|
||||
return std::result::Result::Ok((envelope, payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::Owner)));
|
||||
}
|
||||
|
||||
/// Executes the crate-internal stage owner password rotation operation for `OwnerStateV1`.
|
||||
@@ -497,7 +497,7 @@ pub async fn open_wallet_view_v1(source: &[u8], password: crate::ViewPassword) -
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let info = metadata_payload.into_info(crate::WalletCapability::View);
|
||||
let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::View);
|
||||
let state = ViewStateV1::new(envelope, metadata_key);
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
@@ -506,7 +506,7 @@ pub async fn open_wallet_view_v1(source: &[u8], password: crate::ViewPassword) -
|
||||
capability = "view",
|
||||
"native wallet VIEW capability opened"
|
||||
);
|
||||
return std::result::Result::Ok(crate::WalletView::from_unlocked(info, state));
|
||||
return std::result::Result::Ok(crate::WalletView::from_unlocked(info, crate::ViewState::V1(state)));
|
||||
}
|
||||
|
||||
/// Opens the OWNER capability from a native `.kspwallet` V1 JSON document.
|
||||
@@ -602,7 +602,7 @@ pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword)
|
||||
if &secret_pubkey != metadata_payload.pubkey() {
|
||||
return std::result::Result::Err(key_material_error());
|
||||
}
|
||||
let info = metadata_payload.into_info(crate::WalletCapability::Owner);
|
||||
let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::Owner);
|
||||
let state = OwnerStateV1::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair);
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
@@ -611,7 +611,7 @@ pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword)
|
||||
capability = "owner",
|
||||
"native wallet OWNER capability opened"
|
||||
);
|
||||
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
|
||||
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, crate::OwnerState::V1(state)));
|
||||
}
|
||||
|
||||
/// Parses and verifies the OWNER-authenticated locked state without unlocking metadata or secret material.
|
||||
@@ -624,7 +624,7 @@ pub fn inspect_locked_wallet_v1(source: &[u8]) -> ksp_core_lib::Result<crate::Lo
|
||||
if let std::result::Result::Err(error) = verify_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(crate::LockedWalletInfo::new(envelope.view_descriptor().enabled()));
|
||||
return std::result::Result::Ok(crate::LockedWalletInfo::new(crate::KSPWALLET_FORMAT_VERSION_V1, envelope.view_descriptor().enabled()));
|
||||
}
|
||||
|
||||
/// Executes the crate-internal create wallet v1 from keypair operation for the owning module.
|
||||
@@ -821,7 +821,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
|
||||
if let std::result::Result::Err(error) = verify_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let info = metadata_payload.into_info(crate::WalletCapability::Owner);
|
||||
let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::Owner);
|
||||
let state = OwnerStateV1::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair);
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
@@ -830,7 +830,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
|
||||
view_enabled = state.envelope().view_descriptor().enabled(),
|
||||
"native wallet created in memory"
|
||||
);
|
||||
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
|
||||
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, crate::OwnerState::V1(state)));
|
||||
}
|
||||
|
||||
/// Verifies state signature.
|
||||
|
||||
1147
crates/ksp-wallet-lib/src/wallet_v2.rs
Normal file
1147
crates/ksp-wallet-lib/src/wallet_v2.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/wire_v2.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Strict native `.kspwallet` V2 binary wire envelope.
|
||||
|
||||
@@ -120,6 +120,17 @@ impl WalletKdfParametersV2 {
|
||||
return Self { algorithm: WalletKdfAlgorithmV2::Argon2id, version, memory_kib, iterations, parallelism, salt };
|
||||
}
|
||||
|
||||
/// Creates one V2 KDF value using the release-calibrated default creation profile.
|
||||
pub(crate) fn new_creation(salt: std::vec::Vec<u8>) -> Self {
|
||||
return Self::new_internal(
|
||||
crate::KSPWALLET_V2_ARGON2_VERSION,
|
||||
crate::KSPWALLET_V2_DEFAULT_ARGON2_MEMORY_KIB,
|
||||
crate::KSPWALLET_V2_DEFAULT_ARGON2_ITERATIONS,
|
||||
crate::KSPWALLET_V2_DEFAULT_ARGON2_PARALLELISM,
|
||||
salt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the KDF algorithm.
|
||||
#[must_use]
|
||||
pub const fn algorithm(&self) -> WalletKdfAlgorithmV2 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/tests/public_api.rs
|
||||
// version: 10
|
||||
// version: 11
|
||||
|
||||
//! Public API canaries for the Wallet foundation.
|
||||
|
||||
@@ -177,3 +177,31 @@ fn public_pre_008_transfer_adapters_are_available_from_crate_root() {
|
||||
);
|
||||
drop(import_file_future);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_pre_016_version_neutral_and_explicit_v2_surfaces_are_available_from_crate_root() {
|
||||
assert_eq!(ksp_wallet_lib::DEFAULT_WALLET_FORMAT, ksp_wallet_lib::WalletFormat::V2);
|
||||
assert_eq!(ksp_wallet_lib::LATEST_SUPPORTED_WALLET_FORMAT, ksp_wallet_lib::WalletFormat::V2);
|
||||
assert_eq!(ksp_wallet_lib::WalletFormat::V1.version(), ksp_wallet_lib::KSPWALLET_FORMAT_VERSION_V1);
|
||||
assert_eq!(ksp_wallet_lib::WalletFormat::V2.version(), ksp_wallet_lib::KSPWALLET_FORMAT_VERSION_V2);
|
||||
let _ = ksp_wallet_lib::create_wallet;
|
||||
let _ = ksp_wallet_lib::create_wallet_v2;
|
||||
let _ = ksp_wallet_lib::open_wallet_view;
|
||||
let _ = ksp_wallet_lib::open_wallet_view_v2;
|
||||
let _ = ksp_wallet_lib::open_wallet_owner;
|
||||
let _ = ksp_wallet_lib::open_wallet_owner_v2;
|
||||
let _ = ksp_wallet_lib::inspect_locked_wallet;
|
||||
let _ = ksp_wallet_lib::inspect_locked_wallet_v2;
|
||||
let path = std::path::Path::new("not-polled-pre016.kspwallet");
|
||||
let owner_password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("public-pre016-owner-password"));
|
||||
let create_future = ksp_wallet_lib::create_wallet_file(path, owner_password, std::option::Option::None, ksp_wallet_lib::WalletCreateMetadata::default());
|
||||
drop(create_future);
|
||||
let owner_password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("public-pre016-owner-password"));
|
||||
let create_v2_future =
|
||||
ksp_wallet_lib::create_wallet_file_v2(path, owner_password, std::option::Option::None, ksp_wallet_lib::WalletCreateMetadata::default());
|
||||
drop(create_v2_future);
|
||||
let inspect_future = ksp_wallet_lib::inspect_locked_wallet_file(path);
|
||||
drop(inspect_future);
|
||||
let inspect_v2_future = ksp_wallet_lib::inspect_locked_wallet_file_v2(path);
|
||||
drop(inspect_v2_future);
|
||||
}
|
||||
|
||||
18
crates/ksp-wallet-lib/unit_tests/format.rs
Normal file
18
crates/ksp-wallet-lib/unit_tests/format.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/format.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn default_and_latest_are_explicit_and_currently_v2() {
|
||||
assert_eq!(crate::DEFAULT_WALLET_FORMAT, crate::WalletFormat::V2);
|
||||
assert_eq!(crate::LATEST_SUPPORTED_WALLET_FORMAT, crate::WalletFormat::V2);
|
||||
assert_eq!(crate::DEFAULT_WALLET_FORMAT.version(), crate::KSPWALLET_FORMAT_VERSION_V2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detector_distinguishes_v1_json_and_v2_binary_without_crypto() -> ksp_core_lib::Result<()> {
|
||||
let v1 = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json");
|
||||
let v2 = include_bytes!("../tests/fixtures/kspwallet_v2_wire_only.bin");
|
||||
assert_eq!(crate::detect_wallet_format(v1)?, crate::WalletFormat::V1);
|
||||
assert_eq!(crate::detect_wallet_format(v2)?, crate::WalletFormat::V2);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/transfer.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
|
||||
@@ -144,6 +144,41 @@ fn cli_json_import_creates_new_no_clobber_wallet_with_imported_identity_and_meta
|
||||
source.zeroize();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_transfer_import_creates_v2_without_removing_explicit_v1_import() {
|
||||
let directory = tempfile::tempdir().expect("Wallet transfer test directory must be creatable");
|
||||
let default_destination = directory.path().join("default-v2.kspwallet");
|
||||
let explicit_v1_destination = directory.path().join("explicit-v1.kspwallet");
|
||||
let keypair = test_keypair();
|
||||
let mut source = cli_json(&keypair);
|
||||
let runtime = runtime();
|
||||
let default_owner = runtime
|
||||
.block_on(crate::import_wallet_transfer(
|
||||
default_destination.as_path(),
|
||||
source.as_slice(),
|
||||
crate::WalletTransferFormat::SolanaCliJson,
|
||||
crate::OwnerPassword::new(std::string::String::from("pre016-default-import-owner")),
|
||||
std::option::Option::None,
|
||||
crate::WalletCreateMetadata::default(),
|
||||
))
|
||||
.expect("default transfer import must succeed");
|
||||
assert_eq!(default_owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
|
||||
let default_bytes = std::fs::read(default_destination).expect("default imported Wallet must be readable");
|
||||
assert_eq!(crate::detect_wallet_format(default_bytes.as_slice()).expect("default imported Wallet must detect"), crate::WalletFormat::V2);
|
||||
let explicit_owner = runtime
|
||||
.block_on(crate::import_wallet_transfer_v1(
|
||||
explicit_v1_destination.as_path(),
|
||||
source.as_slice(),
|
||||
crate::WalletTransferFormat::SolanaCliJson,
|
||||
crate::OwnerPassword::new(std::string::String::from("pre016-explicit-v1-import-owner")),
|
||||
std::option::Option::None,
|
||||
crate::WalletCreateMetadataV1::default(),
|
||||
))
|
||||
.expect("explicit V1 transfer import must remain available");
|
||||
assert_eq!(explicit_owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V1);
|
||||
source.zeroize();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_file_import_is_non_destructive_and_bounded() {
|
||||
let directory = tempfile::tempdir().expect("Wallet transfer test directory must be creatable");
|
||||
|
||||
126
crates/ksp-wallet-lib/unit_tests/wallet_v2.rs
Normal file
126
crates/ksp-wallet-lib/unit_tests/wallet_v2.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/wallet_v2.rs
|
||||
// version: 1
|
||||
|
||||
const V1_FULL_VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
|
||||
|
||||
fn runtime() -> tokio::runtime::Runtime {
|
||||
return tokio::runtime::Builder::new_current_thread().build().expect("Wallet V2 test runtime must build");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_create_open_and_generic_dispatch_preserve_authorized_identity() {
|
||||
let runtime = runtime();
|
||||
let owner_password_text = std::string::String::from("pre016-v2-owner-password");
|
||||
let view_password_text = std::string::String::from("pre016-v2-view-password");
|
||||
let owner = runtime
|
||||
.block_on(crate::create_wallet_v2(
|
||||
crate::OwnerPassword::new(owner_password_text.clone()),
|
||||
std::option::Option::Some(crate::ViewPassword::new(view_password_text.clone())),
|
||||
crate::WalletCreateMetadataV1::new(std::option::Option::Some(std::string::String::from("pre016-v2")), std::vec::Vec::new()),
|
||||
))
|
||||
.expect("V2 wallet creation must succeed");
|
||||
assert_eq!(owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
|
||||
let pubkey = *owner.pubkey();
|
||||
let bytes = owner.to_native_bytes().expect("V2 owner must serialize native bytes");
|
||||
assert_eq!(crate::detect_wallet_format(bytes.as_slice()).expect("V2 framing must detect"), crate::WalletFormat::V2);
|
||||
assert!(owner.to_json_bytes().is_err());
|
||||
let view = runtime
|
||||
.block_on(crate::open_wallet_view(bytes.as_slice(), crate::ViewPassword::new(view_password_text)))
|
||||
.expect("generic VIEW open must dispatch V2");
|
||||
assert_eq!(view.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
|
||||
assert_eq!(*view.pubkey(), pubkey);
|
||||
assert_eq!(view.alias(), std::option::Option::Some("pre016-v2"));
|
||||
let reopened = runtime
|
||||
.block_on(crate::open_wallet_owner(bytes.as_slice(), crate::OwnerPassword::new(owner_password_text)))
|
||||
.expect("generic OWNER open must dispatch V2");
|
||||
assert_eq!(reopened.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
|
||||
assert_eq!(*reopened.pubkey(), pubkey);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_dispatch_keeps_v1_read_compatibility() {
|
||||
let runtime = runtime();
|
||||
assert_eq!(crate::detect_wallet_format(V1_FULL_VECTOR).expect("V1 framing must detect"), crate::WalletFormat::V1);
|
||||
let locked = crate::inspect_locked_wallet(V1_FULL_VECTOR).expect("generic inspect must keep V1 compatibility");
|
||||
assert_eq!(locked.format_version(), crate::KSPWALLET_FORMAT_VERSION_V1);
|
||||
let owner = runtime
|
||||
.block_on(crate::open_wallet_owner(V1_FULL_VECTOR, crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
|
||||
.expect("generic OWNER open must dispatch V1");
|
||||
assert_eq!(owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_file_creation_is_v2_and_v2_administration_stays_v2() {
|
||||
let directory = tempfile::Builder::new().prefix("ksp-pre016-v2-").tempdir().expect("V2 test directory must be creatable");
|
||||
let path = directory.path().join("default.kspwallet");
|
||||
let runtime = runtime();
|
||||
let mut owner = runtime
|
||||
.block_on(crate::create_wallet_file(
|
||||
path.as_path(),
|
||||
crate::OwnerPassword::new(std::string::String::from("pre016-default-owner")),
|
||||
std::option::Option::Some(crate::ViewPassword::new(std::string::String::from("pre016-default-view"))),
|
||||
crate::WalletCreateMetadataV1::default(),
|
||||
))
|
||||
.expect("default file creation must succeed");
|
||||
assert_eq!(owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
|
||||
let locked = runtime.block_on(crate::inspect_locked_wallet_file(path.as_path())).expect("generic inspect must read default file");
|
||||
assert_eq!(locked.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
|
||||
runtime
|
||||
.block_on(owner.update_alias(path.as_path(), std::option::Option::Some(std::string::String::from("pre016-updated"))))
|
||||
.expect("V2 metadata update must persist in V2");
|
||||
runtime
|
||||
.block_on(owner.rotate_owner_password(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre016-owner-rotated"))))
|
||||
.expect("V2 OWNER rotation must persist in V2");
|
||||
runtime
|
||||
.block_on(owner.rotate_view_password(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-rotated"))))
|
||||
.expect("V2 VIEW rotation by OWNER must persist in V2");
|
||||
let reopened_owner = runtime
|
||||
.block_on(crate::open_wallet_owner_file(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre016-owner-rotated"))))
|
||||
.expect("rotated V2 OWNER must reopen through generic dispatch");
|
||||
assert_eq!(reopened_owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
|
||||
assert_eq!(reopened_owner.alias(), std::option::Option::Some("pre016-updated"));
|
||||
let reopened_view = runtime
|
||||
.block_on(crate::open_wallet_view_file(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-rotated"))))
|
||||
.expect("rotated V2 VIEW must reopen through generic dispatch");
|
||||
assert_eq!(reopened_view.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
|
||||
assert_eq!(reopened_view.alias(), std::option::Option::Some("pre016-updated"));
|
||||
runtime.block_on(owner.disable_view(path.as_path())).expect("V2 strong VIEW disable must persist");
|
||||
let disabled = runtime.block_on(crate::inspect_locked_wallet_file(path.as_path())).expect("disabled V2 must remain inspectable");
|
||||
assert!(!disabled.view_enabled());
|
||||
let old_view = runtime.block_on(crate::open_wallet_view_file(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-rotated"))));
|
||||
assert_eq!(old_view.expect_err("disabled V2 VIEW must not reopen").code(), crate::ERROR_CODE_CAPABILITY_INSUFFICIENT);
|
||||
runtime
|
||||
.block_on(owner.recreate_view(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-recreated"))))
|
||||
.expect("V2 strong VIEW recreation must persist");
|
||||
let mut recreated_view = runtime
|
||||
.block_on(crate::open_wallet_view_file(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-recreated"))))
|
||||
.expect("recreated V2 VIEW must open");
|
||||
runtime
|
||||
.block_on(recreated_view.rotate_view_password(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-self-rotated"))))
|
||||
.expect("V2 VIEW self-rotation must persist");
|
||||
let final_view = runtime
|
||||
.block_on(crate::open_wallet_view_file(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-self-rotated"))))
|
||||
.expect("self-rotated V2 VIEW must reopen");
|
||||
assert_eq!(final_view.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
|
||||
let bytes = std::fs::read(path).expect("V2 file must remain readable for framing canary");
|
||||
assert_eq!(crate::detect_wallet_format(bytes.as_slice()).expect("administered file must still be V2"), crate::WalletFormat::V2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_versioned_readers_reject_the_other_native_wire_format() {
|
||||
let runtime = runtime();
|
||||
let v1_as_v2 =
|
||||
runtime.block_on(crate::open_wallet_owner_v2(V1_FULL_VECTOR, crate::OwnerPassword::new(std::string::String::from("irrelevant-before-v2-parse"))));
|
||||
assert_eq!(v1_as_v2.expect_err("explicit V2 reader must reject V1 framing").code(), crate::ERROR_CODE_FORMAT_INVALID);
|
||||
let owner = runtime
|
||||
.block_on(crate::create_wallet_v2(
|
||||
crate::OwnerPassword::new(std::string::String::from("pre016-strict-v2-owner")),
|
||||
std::option::Option::None,
|
||||
crate::WalletCreateMetadata::default(),
|
||||
))
|
||||
.expect("strict-reader V2 fixture creation must succeed");
|
||||
let bytes = owner.to_native_bytes().expect("strict-reader V2 fixture must serialize");
|
||||
let v2_as_v1 =
|
||||
runtime.block_on(crate::open_wallet_owner_v1(bytes.as_slice(), crate::OwnerPassword::new(std::string::String::from("irrelevant-before-v1-parse"))));
|
||||
assert_eq!(v2_as_v1.expect_err("explicit V1 reader must reject V2 framing").code(), crate::ERROR_CODE_FORMAT_INVALID);
|
||||
}
|
||||
Reference in New Issue
Block a user