v0.2.6-pre.015
This commit is contained in:
@@ -914,23 +914,23 @@ impl AppState {
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()),
|
||||
};
|
||||
let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection());
|
||||
match previous {
|
||||
return match previous {
|
||||
crate::WalletSession::ViewOperation { wallet_id: reserved_wallet_id, path: reserved_path, pubkey: reserved_pubkey }
|
||||
if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey =>
|
||||
{
|
||||
*session = crate::WalletSession::View { wallet_id: wallet_id.clone(), path, wallet: view };
|
||||
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet VIEW privileged operation completed");
|
||||
return std::result::Result::Ok(dto);
|
||||
std::result::Result::Ok(dto)
|
||||
},
|
||||
other => {
|
||||
*session = other;
|
||||
drop(view);
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_WALLET_SESSION_INVALID,
|
||||
"Wallet VIEW operation completion no longer owns the selected session",
|
||||
));
|
||||
))
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn restore_view_after_operation_failure(&self, context: ViewOperationContext) {
|
||||
@@ -1039,7 +1039,7 @@ impl AppState {
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()),
|
||||
};
|
||||
let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection());
|
||||
match previous {
|
||||
return match previous {
|
||||
crate::WalletSession::OwnerOperation {
|
||||
wallet_id: reserved_wallet_id,
|
||||
path: reserved_path,
|
||||
@@ -1048,17 +1048,17 @@ impl AppState {
|
||||
} if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey && reserved_view_enabled == view_enabled => {
|
||||
*session = crate::WalletSession::Owner { wallet_id: wallet_id.clone(), path, view_enabled: result_view_enabled, wallet: owner };
|
||||
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet OWNER privileged operation completed");
|
||||
return std::result::Result::Ok(dto);
|
||||
std::result::Result::Ok(dto)
|
||||
},
|
||||
other => {
|
||||
*session = other;
|
||||
drop(owner);
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_WALLET_SESSION_INVALID,
|
||||
"Wallet OWNER operation completion no longer owns the selected session",
|
||||
));
|
||||
))
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn restore_owner_after_operation_failure(&self, context: OwnerOperationContext) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: crates/ksp-wallet-lib/README.md -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 3 -->
|
||||
|
||||
# `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, 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, 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.
|
||||
|
||||
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,6 +14,7 @@ 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 ;
|
||||
- les key slots OWNER/VIEW indépendants ;
|
||||
- Argon2id pour les KDF de passwords ;
|
||||
- XChaCha20-Poly1305 pour le wrapping et les compartiments ;
|
||||
@@ -192,3 +193,7 @@ Elles couvrent le wire, Argon2id/XChaCha20-Poly1305, l'ouverture VIEW/OWNER, la
|
||||
- [`../../docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](../../docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md) — plan historique et threat model de `0.2.5` ;
|
||||
- [`../../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`
|
||||
|
||||
`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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-wallet-lib/USAGE.md -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 3 -->
|
||||
|
||||
# Utilisation de `ksp-wallet-lib`
|
||||
|
||||
@@ -277,3 +277,14 @@ 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`)
|
||||
|
||||
Le codec structurel V2 peut être utilisé pour analyser une fixture/document V2 déjà produit :
|
||||
|
||||
```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.
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// file: crates/ksp-wallet-lib/src/constants.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! Wallet-owned constants.
|
||||
|
||||
/// Native Wallet format version implemented by the V1 codec.
|
||||
pub const KSPWALLET_FORMAT_VERSION_V1: u32 = 1;
|
||||
/// Native Wallet format version implemented by the V2 binary codec.
|
||||
pub const KSPWALLET_FORMAT_VERSION_V2: u32 = 2;
|
||||
/// Exact magic string required by every native `.kspwallet` document.
|
||||
pub const KSPWALLET_MAGIC: &str = "KSPWALLET";
|
||||
/// Maximum accepted `.kspwallet` document size before JSON parsing.
|
||||
@@ -85,6 +87,50 @@ pub const KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN: &[u8] = b"KSPWALLET-V1-STATE";
|
||||
pub const KSPWALLET_V1_VIEW_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-VIEW-SLOT";
|
||||
/// Byte length of an XChaCha20-Poly1305 nonce.
|
||||
pub const KSPWALLET_V1_XCHACHA_NONCE_BYTES: usize = 24;
|
||||
/// Byte length of the Poly1305 authentication tag appended to each V2 ciphertext.
|
||||
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;
|
||||
/// 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.
|
||||
pub const KSPWALLET_V2_ED25519_SIGNATURE_BYTES: usize = KSPWALLET_V1_ED25519_SIGNATURE_BYTES;
|
||||
/// Initial protected payload version used independently by V2 control, metadata and secret compartments.
|
||||
pub const KSPWALLET_V2_INITIAL_PAYLOAD_VERSION: u32 = KSPWALLET_V1_INITIAL_PAYLOAD_VERSION;
|
||||
/// Structural V2 ceiling for serialized Argon2 iteration cost.
|
||||
pub const KSPWALLET_V2_MAX_ARGON2_ITERATIONS: u32 = KSPWALLET_V1_MAX_ARGON2_ITERATIONS;
|
||||
/// Structural V2 ceiling for serialized Argon2 memory cost.
|
||||
pub const KSPWALLET_V2_MAX_ARGON2_MEMORY_KIB: u32 = KSPWALLET_V1_MAX_ARGON2_MEMORY_KIB;
|
||||
/// Structural V2 ceiling for serialized Argon2 parallelism.
|
||||
pub const KSPWALLET_V2_MAX_ARGON2_PARALLELISM: u32 = KSPWALLET_V1_MAX_ARGON2_PARALLELISM;
|
||||
/// Maximum Argon2 salt size accepted by V2.
|
||||
pub const KSPWALLET_V2_MAX_KDF_SALT_BYTES: usize = KSPWALLET_V1_MAX_KDF_SALT_BYTES;
|
||||
/// Maximum wrapped-key ciphertext size accepted by V2.
|
||||
pub const KSPWALLET_V2_MAX_KEY_WRAP_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_KEY_WRAP_CIPHERTEXT_BYTES;
|
||||
/// Maximum protected metadata ciphertext size accepted by V2.
|
||||
pub const KSPWALLET_V2_MAX_METADATA_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_METADATA_CIPHERTEXT_BYTES;
|
||||
/// Maximum OWNER-control ciphertext size accepted by V2.
|
||||
pub const KSPWALLET_V2_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES;
|
||||
/// Maximum OWNER-only secret ciphertext size accepted by V2.
|
||||
pub const KSPWALLET_V2_MAX_SECRET_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_SECRET_CIPHERTEXT_BYTES;
|
||||
/// Domain separator for V2 metadata-compartment AEAD AAD.
|
||||
pub const KSPWALLET_V2_METADATA_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-METADATA";
|
||||
/// Minimum Argon2 salt size accepted by V2.
|
||||
pub const KSPWALLET_V2_MIN_KDF_SALT_BYTES: usize = KSPWALLET_V1_MIN_KDF_SALT_BYTES;
|
||||
/// Domain separator for V2 OWNER-control compartment AEAD AAD.
|
||||
pub const KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-OWNER-CONTROL";
|
||||
/// Domain separator for V2 OWNER key-slot wrapping AAD.
|
||||
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";
|
||||
/// 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.
|
||||
pub const KSPWALLET_V2_STATE_TRANSCRIPT_DOMAIN: &[u8] = b"KSPWALLET-V2-STATE";
|
||||
/// Domain separator for V2 VIEW key-slot wrapping AAD.
|
||||
pub const KSPWALLET_V2_VIEW_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-VIEW-SLOT";
|
||||
/// Byte length of an XChaCha20-Poly1305 nonce in V2.
|
||||
pub const KSPWALLET_V2_XCHACHA_NONCE_BYTES: usize = KSPWALLET_V1_XCHACHA_NONCE_BYTES;
|
||||
|
||||
/// Owning tracing target for events emitted by the Wallet crate.
|
||||
pub(crate) const TRACING_TARGET: &str = "ksp-wallet-lib";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/lib.rs
|
||||
// version: 11
|
||||
// version: 12
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -15,7 +15,8 @@
|
||||
//! adds Solana message signing, protected metadata administration, password rotation and strong VIEW disable/recreate with capability-bound atomic
|
||||
//! 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. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral
|
||||
//! 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
|
||||
//! observability uses only
|
||||
//! `ksp-logging-lib` with the explicit crate target defined in `src/constants.rs`.
|
||||
|
||||
@@ -29,10 +30,12 @@ mod password;
|
||||
mod payload;
|
||||
mod persistence;
|
||||
mod transcript;
|
||||
mod transcript_v2;
|
||||
mod transfer;
|
||||
mod view;
|
||||
mod wallet;
|
||||
mod wire;
|
||||
mod wire_v2;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/security.rs"]
|
||||
@@ -42,6 +45,8 @@ mod security_tests;
|
||||
pub use self::capability::WalletCapability;
|
||||
/// Native `.kspwallet` V1 format version.
|
||||
pub use self::constants::KSPWALLET_FORMAT_VERSION_V1;
|
||||
/// Native `.kspwallet` V2 binary format version.
|
||||
pub use self::constants::KSPWALLET_FORMAT_VERSION_V2;
|
||||
/// Exact magic string required by every native `.kspwallet` document.
|
||||
pub use self::constants::KSPWALLET_MAGIC;
|
||||
/// Maximum accepted `.kspwallet` document size before parsing.
|
||||
@@ -122,6 +127,50 @@ pub use self::constants::KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN;
|
||||
pub use self::constants::KSPWALLET_V1_VIEW_SLOT_AAD_DOMAIN;
|
||||
/// Byte length of an XChaCha20-Poly1305 nonce.
|
||||
pub use self::constants::KSPWALLET_V1_XCHACHA_NONCE_BYTES;
|
||||
/// Byte length of the AEAD authentication tag appended to V2 ciphertexts.
|
||||
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;
|
||||
/// 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.
|
||||
pub use self::constants::KSPWALLET_V2_ED25519_SIGNATURE_BYTES;
|
||||
/// Initial per-compartment protected payload version for V2.
|
||||
pub use self::constants::KSPWALLET_V2_INITIAL_PAYLOAD_VERSION;
|
||||
/// Maximum structural Argon2 iteration count accepted by the V2 parser.
|
||||
pub use self::constants::KSPWALLET_V2_MAX_ARGON2_ITERATIONS;
|
||||
/// Maximum structural Argon2 memory cost accepted by the V2 parser.
|
||||
pub use self::constants::KSPWALLET_V2_MAX_ARGON2_MEMORY_KIB;
|
||||
/// Maximum structural Argon2 parallelism accepted by the V2 parser.
|
||||
pub use self::constants::KSPWALLET_V2_MAX_ARGON2_PARALLELISM;
|
||||
/// Maximum Argon2 salt size accepted by V2.
|
||||
pub use self::constants::KSPWALLET_V2_MAX_KDF_SALT_BYTES;
|
||||
/// Maximum wrapped-key ciphertext size accepted by V2.
|
||||
pub use self::constants::KSPWALLET_V2_MAX_KEY_WRAP_CIPHERTEXT_BYTES;
|
||||
/// Maximum protected metadata ciphertext size accepted by V2.
|
||||
pub use self::constants::KSPWALLET_V2_MAX_METADATA_CIPHERTEXT_BYTES;
|
||||
/// Maximum OWNER-control ciphertext size accepted by V2.
|
||||
pub use self::constants::KSPWALLET_V2_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES;
|
||||
/// Maximum OWNER-only secret ciphertext size accepted by V2.
|
||||
pub use self::constants::KSPWALLET_V2_MAX_SECRET_CIPHERTEXT_BYTES;
|
||||
/// Domain separator for V2 metadata-compartment AEAD AAD.
|
||||
pub use self::constants::KSPWALLET_V2_METADATA_AAD_DOMAIN;
|
||||
/// Minimum Argon2 salt size accepted by V2.
|
||||
pub use self::constants::KSPWALLET_V2_MIN_KDF_SALT_BYTES;
|
||||
/// Domain separator for V2 OWNER-control compartment AEAD AAD.
|
||||
pub use self::constants::KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN;
|
||||
/// Domain separator for V2 OWNER key-slot wrapping AAD.
|
||||
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;
|
||||
/// 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.
|
||||
pub use self::constants::KSPWALLET_V2_STATE_TRANSCRIPT_DOMAIN;
|
||||
/// Domain separator for V2 VIEW key-slot wrapping AAD.
|
||||
pub use self::constants::KSPWALLET_V2_VIEW_SLOT_AAD_DOMAIN;
|
||||
/// Byte length of an XChaCha20-Poly1305 nonce in V2.
|
||||
pub use self::constants::KSPWALLET_V2_XCHACHA_NONCE_BYTES;
|
||||
/// Error code used when an atomic Wallet persistence operation cannot publish a valid replacement.
|
||||
pub use self::error::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED;
|
||||
/// Error code used when an authenticated Wallet structure cannot be verified.
|
||||
@@ -224,6 +273,30 @@ pub use self::wire::WalletStateSignatureAlgorithmV1;
|
||||
pub use self::wire::WalletStateSignatureV1;
|
||||
/// OWNER-signed stable descriptor of the optional VIEW slot.
|
||||
pub use self::wire::WalletViewDescriptorV1;
|
||||
/// Strict semantic representation of one parsed native `.kspwallet` V2 binary envelope.
|
||||
pub use self::wire_v2::KspWalletEnvelopeV2;
|
||||
/// Authenticated-encryption algorithm fixed by native Wallet V2.
|
||||
pub use self::wire_v2::WalletAeadAlgorithmV2;
|
||||
/// Protected compartment kind fixed by native Wallet V2.
|
||||
pub use self::wire_v2::WalletCompartmentKindV2;
|
||||
/// One validated encrypted V2 compartment.
|
||||
pub use self::wire_v2::WalletEncryptedCompartmentV2;
|
||||
/// Password KDF fixed by native Wallet V2.
|
||||
pub use self::wire_v2::WalletKdfAlgorithmV2;
|
||||
/// Parsed Argon2id parameters from one V2 key slot.
|
||||
pub use self::wire_v2::WalletKdfParametersV2;
|
||||
/// Role of one native V2 key slot.
|
||||
pub use self::wire_v2::WalletKeySlotRoleV2;
|
||||
/// One validated OWNER or VIEW V2 key slot.
|
||||
pub use self::wire_v2::WalletKeySlotV2;
|
||||
/// Parsed AEAD wrapped-key payload from one V2 key slot.
|
||||
pub use self::wire_v2::WalletKeyWrapV2;
|
||||
/// State-signature algorithm fixed by native Wallet V2.
|
||||
pub use self::wire_v2::WalletStateSignatureAlgorithmV2;
|
||||
/// Detached OWNER state signature embedded in a V2 envelope.
|
||||
pub use self::wire_v2::WalletStateSignatureV2;
|
||||
/// OWNER-signed stable descriptor of the optional VIEW slot in V2.
|
||||
pub use self::wire_v2::WalletViewDescriptorV2;
|
||||
|
||||
/// Wallet-owned tracing target used by the KSP logging facade.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
@@ -277,6 +350,12 @@ pub(crate) use self::transcript::compartment_aad;
|
||||
pub(crate) use self::transcript::slot_aad;
|
||||
/// Internal deterministic OWNER-state transcript codec shared by Wallet crypto layers.
|
||||
pub(crate) use self::transcript::state_transcript;
|
||||
/// Internal deterministic V2 compartment-AAD codec shared by Wallet crypto layers.
|
||||
pub(crate) use self::transcript_v2::compartment_aad_v2;
|
||||
/// Internal deterministic V2 key-slot-AAD codec shared by Wallet crypto layers.
|
||||
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;
|
||||
/// Crate-internal `OwnerStateV1` state shared across the owning crate.
|
||||
|
||||
141
crates/ksp-wallet-lib/src/transcript_v2.rs
Normal file
141
crates/ksp-wallet-lib/src/transcript_v2.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
// file: crates/ksp-wallet-lib/src/transcript_v2.rs
|
||||
// version: 1
|
||||
|
||||
//! Deterministic `.kspwallet` V2 state-transcript and AEAD-AAD encoding.
|
||||
|
||||
const TAG_COMPARTMENT_ALGORITHM: u16 = 0x0202;
|
||||
const TAG_COMPARTMENT_CIPHERTEXT: u16 = 0x0204;
|
||||
const TAG_COMPARTMENT_KIND: u16 = 0x0200;
|
||||
const TAG_COMPARTMENT_NONCE: u16 = 0x0203;
|
||||
const TAG_COMPARTMENT_VERSION: u16 = 0x0201;
|
||||
const TAG_FORMAT_VERSION: u16 = 0x0002;
|
||||
const TAG_KDF_ALGORITHM: u16 = 0x0102;
|
||||
const TAG_KDF_ITERATIONS: u16 = 0x0105;
|
||||
const TAG_KDF_MEMORY_KIB: u16 = 0x0104;
|
||||
const TAG_KDF_PARALLELISM: u16 = 0x0106;
|
||||
const TAG_KDF_SALT: u16 = 0x0107;
|
||||
const TAG_KDF_VERSION: u16 = 0x0103;
|
||||
const TAG_MAGIC: u16 = 0x0001;
|
||||
const TAG_OWNER_AUTH_PUBLIC_KEY: u16 = 0x0003;
|
||||
const TAG_SLOT_ID: u16 = 0x0100;
|
||||
const TAG_SLOT_ROLE: u16 = 0x0101;
|
||||
const TAG_STATE_SIGNATURE_ALGORITHM: u16 = 0x0500;
|
||||
const TAG_VIEW_ENABLED: u16 = 0x0010;
|
||||
const TAG_VIEW_ROLE: u16 = 0x0011;
|
||||
const TAG_VIEW_SLOT_ID: u16 = 0x0012;
|
||||
const TAG_WRAP_ALGORITHM: u16 = 0x0108;
|
||||
const TAG_WRAP_CIPHERTEXT: u16 = 0x010A;
|
||||
const TAG_WRAP_NONCE: u16 = 0x0109;
|
||||
|
||||
/// Builds the normative OWNER state-signature transcript for one validated V2 envelope.
|
||||
pub(crate) fn state_transcript_v2(envelope: &crate::KspWalletEnvelopeV2) -> std::vec::Vec<u8> {
|
||||
let mut output = start(crate::KSPWALLET_V2_STATE_TRANSCRIPT_DOMAIN);
|
||||
push_common(&mut output, envelope);
|
||||
push_bool(&mut output, TAG_VIEW_ENABLED, envelope.view_descriptor().enabled());
|
||||
push_u8(&mut output, TAG_VIEW_ROLE, crate::WalletKeySlotRoleV2::View.wire_id());
|
||||
match envelope.view_descriptor().slot_id() {
|
||||
std::option::Option::Some(slot_id) => push_bytes(&mut output, TAG_VIEW_SLOT_ID, slot_id),
|
||||
std::option::Option::None => push_bytes(&mut output, TAG_VIEW_SLOT_ID, &[]),
|
||||
}
|
||||
push_slot(&mut output, envelope.owner_slot(), true);
|
||||
push_compartment(&mut output, envelope.owner_control(), true);
|
||||
push_compartment(&mut output, envelope.metadata(), true);
|
||||
push_compartment(&mut output, envelope.secret(), true);
|
||||
push_u8(&mut output, TAG_STATE_SIGNATURE_ALGORITHM, envelope.state_signature().algorithm().wire_id());
|
||||
return output;
|
||||
}
|
||||
|
||||
/// Builds the normative wrapping AAD for one validated V2 OWNER or VIEW key slot.
|
||||
pub(crate) fn slot_aad_v2(envelope: &crate::KspWalletEnvelopeV2, slot: &crate::WalletKeySlotV2) -> std::vec::Vec<u8> {
|
||||
let domain = match slot.role() {
|
||||
crate::WalletKeySlotRoleV2::Owner => crate::KSPWALLET_V2_OWNER_SLOT_AAD_DOMAIN,
|
||||
crate::WalletKeySlotRoleV2::View => crate::KSPWALLET_V2_VIEW_SLOT_AAD_DOMAIN,
|
||||
};
|
||||
let mut output = start(domain);
|
||||
push_common(&mut output, envelope);
|
||||
push_slot(&mut output, slot, false);
|
||||
return output;
|
||||
}
|
||||
|
||||
/// Builds the normative AEAD AAD for one validated V2 encrypted compartment.
|
||||
pub(crate) fn compartment_aad_v2(envelope: &crate::KspWalletEnvelopeV2, compartment: &crate::WalletEncryptedCompartmentV2) -> std::vec::Vec<u8> {
|
||||
let domain = match compartment.kind() {
|
||||
crate::WalletCompartmentKindV2::OwnerControl => crate::KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN,
|
||||
crate::WalletCompartmentKindV2::Metadata => crate::KSPWALLET_V2_METADATA_AAD_DOMAIN,
|
||||
crate::WalletCompartmentKindV2::Secret => crate::KSPWALLET_V2_SECRET_AAD_DOMAIN,
|
||||
};
|
||||
let mut output = start(domain);
|
||||
push_common(&mut output, envelope);
|
||||
push_compartment(&mut output, compartment, false);
|
||||
return output;
|
||||
}
|
||||
|
||||
fn start(domain: &[u8]) -> std::vec::Vec<u8> {
|
||||
let mut output = std::vec::Vec::with_capacity(512);
|
||||
output.extend_from_slice(domain);
|
||||
output.push(0);
|
||||
return output;
|
||||
}
|
||||
|
||||
fn push_bool(output: &mut std::vec::Vec<u8>, tag: u16, value: bool) {
|
||||
let byte = if value { 1_u8 } else { 0_u8 };
|
||||
push_bytes(output, tag, &[byte]);
|
||||
return;
|
||||
}
|
||||
|
||||
fn push_bytes(output: &mut std::vec::Vec<u8>, tag: u16, value: &[u8]) {
|
||||
output.extend_from_slice(tag.to_be_bytes().as_slice());
|
||||
let length = value.len() as u64;
|
||||
output.extend_from_slice(length.to_be_bytes().as_slice());
|
||||
output.extend_from_slice(value);
|
||||
return;
|
||||
}
|
||||
|
||||
fn push_common(output: &mut std::vec::Vec<u8>, envelope: &crate::KspWalletEnvelopeV2) {
|
||||
push_bytes(output, TAG_MAGIC, crate::KSPWALLET_MAGIC.as_bytes());
|
||||
push_u32(output, TAG_FORMAT_VERSION, envelope.format_version());
|
||||
push_bytes(output, TAG_OWNER_AUTH_PUBLIC_KEY, envelope.owner_auth_public_key());
|
||||
return;
|
||||
}
|
||||
|
||||
fn push_compartment(output: &mut std::vec::Vec<u8>, compartment: &crate::WalletEncryptedCompartmentV2, include_ciphertext: bool) {
|
||||
push_u8(output, TAG_COMPARTMENT_KIND, compartment.kind().wire_id());
|
||||
push_u32(output, TAG_COMPARTMENT_VERSION, compartment.payload_version());
|
||||
push_u8(output, TAG_COMPARTMENT_ALGORITHM, compartment.algorithm().wire_id());
|
||||
if include_ciphertext {
|
||||
push_bytes(output, TAG_COMPARTMENT_NONCE, compartment.nonce());
|
||||
push_bytes(output, TAG_COMPARTMENT_CIPHERTEXT, compartment.ciphertext());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fn push_slot(output: &mut std::vec::Vec<u8>, slot: &crate::WalletKeySlotV2, include_wrap_payload: bool) {
|
||||
push_bytes(output, TAG_SLOT_ID, slot.slot_id());
|
||||
push_u8(output, TAG_SLOT_ROLE, slot.role().wire_id());
|
||||
push_u8(output, TAG_KDF_ALGORITHM, slot.kdf().algorithm().wire_id());
|
||||
push_u32(output, TAG_KDF_VERSION, slot.kdf().version());
|
||||
push_u32(output, TAG_KDF_MEMORY_KIB, slot.kdf().memory_kib());
|
||||
push_u32(output, TAG_KDF_ITERATIONS, slot.kdf().iterations());
|
||||
push_u32(output, TAG_KDF_PARALLELISM, slot.kdf().parallelism());
|
||||
push_bytes(output, TAG_KDF_SALT, slot.kdf().salt());
|
||||
push_u8(output, TAG_WRAP_ALGORITHM, slot.wrap().algorithm().wire_id());
|
||||
if include_wrap_payload {
|
||||
push_bytes(output, TAG_WRAP_NONCE, slot.wrap().nonce());
|
||||
push_bytes(output, TAG_WRAP_CIPHERTEXT, slot.wrap().ciphertext());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fn push_u32(output: &mut std::vec::Vec<u8>, tag: u16, value: u32) {
|
||||
push_bytes(output, tag, value.to_be_bytes().as_slice());
|
||||
return;
|
||||
}
|
||||
|
||||
fn push_u8(output: &mut std::vec::Vec<u8>, tag: u16, value: u8) {
|
||||
push_bytes(output, tag, &[value]);
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/transcript_v2.rs"]
|
||||
mod tests;
|
||||
1037
crates/ksp-wallet-lib/src/wire_v2.rs
Normal file
1037
crates/ksp-wallet-lib/src/wire_v2.rs
Normal file
File diff suppressed because it is too large
Load Diff
BIN
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v2_wire_only.bin
vendored
Normal file
BIN
crates/ksp-wallet-lib/tests/fixtures/kspwallet_v2_wire_only.bin
vendored
Normal file
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/tests/public_api.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
//! Public API canaries for the Wallet foundation.
|
||||
|
||||
@@ -81,6 +81,25 @@ fn strict_v1_envelope_and_transcript_are_available_from_crate_root() -> ksp_core
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_pre_015_v2_binary_wire_codec_is_available_from_crate_root() -> ksp_core_lib::Result<()> {
|
||||
let source = include_bytes!("fixtures/kspwallet_v2_wire_only.bin");
|
||||
let envelope = match ksp_wallet_lib::KspWalletEnvelopeV2::parse_binary(source) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(envelope.format_version(), ksp_wallet_lib::KSPWALLET_FORMAT_VERSION_V2);
|
||||
assert_eq!(envelope.owner_slot().role(), ksp_wallet_lib::WalletKeySlotRoleV2::Owner);
|
||||
assert!(envelope.view_descriptor().enabled());
|
||||
assert_eq!(envelope.metadata().kind(), ksp_wallet_lib::WalletCompartmentKindV2::Metadata);
|
||||
let encoded = match envelope.to_binary_bytes() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(encoded.as_slice(), source);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_pre_005_create_open_and_calibrated_defaults_are_available_from_crate_root() {
|
||||
let _ = ksp_wallet_lib::create_wallet_v1;
|
||||
|
||||
55
crates/ksp-wallet-lib/unit_tests/transcript_v2.rs
Normal file
55
crates/ksp-wallet-lib/unit_tests/transcript_v2.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/transcript_v2.rs
|
||||
// version: 1
|
||||
|
||||
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v2_wire_only.bin");
|
||||
|
||||
#[test]
|
||||
fn v2_state_transcript_is_domain_separated_and_deterministic() -> ksp_core_lib::Result<()> {
|
||||
let envelope = match crate::KspWalletEnvelopeV2::parse_binary(FIXTURE) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let first = envelope.state_transcript();
|
||||
let second = envelope.state_transcript();
|
||||
assert_eq!(first, second);
|
||||
assert!(first.starts_with(crate::KSPWALLET_V2_STATE_TRANSCRIPT_DOMAIN));
|
||||
assert!(!first.starts_with(crate::KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN));
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_owner_and_view_slot_aad_are_role_domain_separated() -> ksp_core_lib::Result<()> {
|
||||
let envelope = match crate::KspWalletEnvelopeV2::parse_binary(FIXTURE) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let owner = envelope.owner_slot_aad();
|
||||
let view = match envelope.view_slot_aad() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "fixture VIEW slot is missing"));
|
||||
},
|
||||
};
|
||||
assert!(owner.starts_with(crate::KSPWALLET_V2_OWNER_SLOT_AAD_DOMAIN));
|
||||
assert!(view.starts_with(crate::KSPWALLET_V2_VIEW_SLOT_AAD_DOMAIN));
|
||||
assert_ne!(owner, view);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_compartment_aad_is_domain_separated_by_kind() -> ksp_core_lib::Result<()> {
|
||||
let envelope = match crate::KspWalletEnvelopeV2::parse_binary(FIXTURE) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let owner_control = envelope.compartment_aad(crate::WalletCompartmentKindV2::OwnerControl);
|
||||
let metadata = envelope.compartment_aad(crate::WalletCompartmentKindV2::Metadata);
|
||||
let secret = envelope.compartment_aad(crate::WalletCompartmentKindV2::Secret);
|
||||
assert!(owner_control.starts_with(crate::KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN));
|
||||
assert!(metadata.starts_with(crate::KSPWALLET_V2_METADATA_AAD_DOMAIN));
|
||||
assert!(secret.starts_with(crate::KSPWALLET_V2_SECRET_AAD_DOMAIN));
|
||||
assert_ne!(owner_control, metadata);
|
||||
assert_ne!(metadata, secret);
|
||||
assert_ne!(owner_control, secret);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
136
crates/ksp-wallet-lib/unit_tests/wire_v2.rs
Normal file
136
crates/ksp-wallet-lib/unit_tests/wire_v2.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/wire_v2.rs
|
||||
// version: 1
|
||||
|
||||
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v2_wire_only.bin");
|
||||
const V1_JSON_FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json");
|
||||
|
||||
#[test]
|
||||
fn strict_v2_fixture_parses_and_round_trips_byte_exact() -> ksp_core_lib::Result<()> {
|
||||
let parsed = match crate::KspWalletEnvelopeV2::parse_binary(FIXTURE) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(parsed.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
|
||||
assert!(parsed.view_descriptor().enabled());
|
||||
assert_eq!(parsed.owner_slot().role(), crate::WalletKeySlotRoleV2::Owner);
|
||||
assert_eq!(parsed.view_slot().map(crate::WalletKeySlotV2::role), std::option::Option::Some(crate::WalletKeySlotRoleV2::View));
|
||||
assert_eq!(parsed.owner_control().kind(), crate::WalletCompartmentKindV2::OwnerControl);
|
||||
assert_eq!(parsed.metadata().kind(), crate::WalletCompartmentKindV2::Metadata);
|
||||
assert_eq!(parsed.secret().kind(), crate::WalletCompartmentKindV2::Secret);
|
||||
let encoded = match parsed.to_binary_bytes() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(encoded.as_slice(), FIXTURE);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_wire_fixture_is_materially_smaller_than_equivalent_v1_json_fixture() {
|
||||
assert!(FIXTURE.len() < V1_JSON_FIXTURE.len());
|
||||
assert_eq!(FIXTURE.len(), 628);
|
||||
assert_eq!(V1_JSON_FIXTURE.len(), 2037);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_v2_format_version_is_rejected_before_body_parsing() {
|
||||
let mut source = FIXTURE.to_vec();
|
||||
source[9..11].copy_from_slice(3_u16.to_be_bytes().as_slice());
|
||||
let result = crate::KspWalletEnvelopeV2::parse_binary(source.as_slice());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_VERSION_UNSUPPORTED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_length_mismatch_and_trailing_bytes_are_rejected() {
|
||||
let mut mismatched = FIXTURE.to_vec();
|
||||
mismatched[11..15].copy_from_slice(627_u32.to_be_bytes().as_slice());
|
||||
let mismatch_result = crate::KspWalletEnvelopeV2::parse_binary(mismatched.as_slice());
|
||||
assert!(mismatch_result.is_err());
|
||||
let mut trailing = FIXTURE.to_vec();
|
||||
trailing.push(0);
|
||||
trailing[11..15].copy_from_slice(629_u32.to_be_bytes().as_slice());
|
||||
let trailing_result = crate::KspWalletEnvelopeV2::parse_binary(trailing.as_slice());
|
||||
assert!(trailing_result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_header_flags_are_rejected() {
|
||||
let mut source = FIXTURE.to_vec();
|
||||
source[15..17].copy_from_slice(2_u16.to_be_bytes().as_slice());
|
||||
let result = crate::KspWalletEnvelopeV2::parse_binary(source.as_slice());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enabled_view_descriptor_must_match_the_v2_view_slot() {
|
||||
let mut source = FIXTURE.to_vec();
|
||||
source[49] ^= 0x01;
|
||||
let result = crate::KspWalletEnvelopeV2::parse_binary(source.as_slice());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_key_slots_require_canonical_owner_then_view_order() {
|
||||
let mut source = FIXTURE.to_vec();
|
||||
source[65] = 2;
|
||||
let result = crate::KspWalletEnvelopeV2::parse_binary(source.as_slice());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_pathological_kdf_parameters_are_rejected_before_crypto() {
|
||||
let mut source = FIXTURE.to_vec();
|
||||
source[87..91].copy_from_slice(0_u32.to_be_bytes().as_slice());
|
||||
let result = crate::KspWalletEnvelopeV2::parse_binary(source.as_slice());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_CRYPTO_PARAMETERS_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_oversized_document_is_rejected_before_body_allocation() {
|
||||
let oversized = std::vec![0_u8; crate::KSPWALLET_MAX_FILE_BYTES + 1];
|
||||
let result = crate::KspWalletEnvelopeV2::parse_binary(oversized.as_slice());
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Err(error) => error,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_envelope_debug_does_not_render_ciphertext_contents() -> ksp_core_lib::Result<()> {
|
||||
let parsed = match crate::KspWalletEnvelopeV2::parse_binary(FIXTURE) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let rendered = format!("{parsed:?}");
|
||||
assert!(rendered.contains("ciphertext_bytes"));
|
||||
assert!(!rendered.contains("128, 129, 130, 131"));
|
||||
assert!(!rendered.contains("224, 225, 226, 227"));
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
Reference in New Issue
Block a user