v0.2.5-pre.005
This commit is contained in:
@@ -21,6 +21,8 @@ pub const KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES: usize = 64 * 1024;
|
||||
pub const KSPWALLET_V1_MAX_PASSWORD_BYTES: usize = 1024;
|
||||
/// Byte length of every V1 key-slot identifier.
|
||||
pub const KSPWALLET_V1_SLOT_ID_BYTES: usize = 16;
|
||||
/// Byte length of every protected metadata note identifier.
|
||||
pub const KSPWALLET_V1_NOTE_ID_BYTES: usize = 16;
|
||||
/// Byte length of an Ed25519 public key used as Wallet format authority.
|
||||
pub const KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES: usize = 32;
|
||||
/// Byte length of an Ed25519 detached state signature.
|
||||
@@ -31,6 +33,18 @@ pub const KSPWALLET_V1_XCHACHA_NONCE_BYTES: usize = 24;
|
||||
pub const KSPWALLET_V1_AEAD_TAG_BYTES: usize = 16;
|
||||
/// Argon2 version serialized by V1 key slots.
|
||||
pub const KSPWALLET_V1_ARGON2_VERSION: u32 = 19;
|
||||
/// Default Argon2id memory cost for newly created V1 key slots, calibrated on the 2026-08-19 operator benchmark.
|
||||
pub const KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB: u32 = 65_536;
|
||||
/// Default Argon2id iteration count for newly created V1 key slots.
|
||||
pub const KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS: u32 = 3;
|
||||
/// Default Argon2id parallelism for newly created V1 key slots.
|
||||
pub const KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM: u32 = 1;
|
||||
/// Salt size generated independently for every newly created V1 key slot.
|
||||
pub const KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES: usize = 32;
|
||||
/// Exact plaintext size of the V1 OWNER-control compartment.
|
||||
pub const KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES: usize = 96;
|
||||
/// Exact plaintext size of the V1 Solana secret compartment.
|
||||
pub const KSPWALLET_V1_SECRET_PLAINTEXT_BYTES: usize = 64;
|
||||
/// Minimum accepted Argon2 salt size in bytes.
|
||||
pub const KSPWALLET_V1_MIN_KDF_SALT_BYTES: usize = 16;
|
||||
/// Maximum accepted Argon2 salt size in bytes.
|
||||
@@ -53,6 +67,7 @@ pub const KSPWALLET_V1_MAX_METADATA_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_M
|
||||
pub const KSPWALLET_V1_MAX_SECRET_CIPHERTEXT_BYTES: usize = 4096;
|
||||
/// Initial protected payload version used independently by control, metadata and secret compartments.
|
||||
pub const KSPWALLET_V1_INITIAL_PAYLOAD_VERSION: u32 = 1;
|
||||
|
||||
/// Domain separator for the OWNER state-signature transcript.
|
||||
pub const KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN: &[u8] = b"KSPWALLET-V1-STATE";
|
||||
/// Domain separator for OWNER key-slot wrapping AAD.
|
||||
|
||||
@@ -1,42 +1,36 @@
|
||||
// file: crates/ksp-wallet-lib/src/crypto.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! In-memory cryptographic primitives for native `.kspwallet` V1.
|
||||
//!
|
||||
//! `pre.004` intentionally lands these crate-private primitives one tranche before `pre.005` wires them into create/open capability flows.
|
||||
|
||||
#![allow(dead_code, reason = "pre.004 stages private Wallet crypto primitives before their pre.005 production callers")]
|
||||
|
||||
use chacha20poly1305::KeyInit as _;
|
||||
use chacha20poly1305::aead::Aead as _;
|
||||
|
||||
/// Exact V1 content-key and password-derived-key size in bytes.
|
||||
const SECRET_KEY_BYTES: usize = 32;
|
||||
pub(crate) const SECRET_KEY_BYTES: usize = 32;
|
||||
|
||||
/// Owned 32-byte secret key with redacted diagnostics and drop-time zeroization.
|
||||
struct SecretKeyV1 {
|
||||
pub(crate) struct SecretKeyV1 {
|
||||
bytes: [u8; SECRET_KEY_BYTES],
|
||||
}
|
||||
|
||||
impl SecretKeyV1 {
|
||||
/// Takes ownership of exact 32-byte secret material.
|
||||
const fn from_bytes(bytes: [u8; SECRET_KEY_BYTES]) -> Self {
|
||||
pub(crate) const fn from_bytes(bytes: [u8; SECRET_KEY_BYTES]) -> Self {
|
||||
return Self { bytes };
|
||||
}
|
||||
|
||||
/// Generates a fresh secret key from the operating-system CSPRNG.
|
||||
fn random() -> ksp_core_lib::Result<Self> {
|
||||
let mut bytes = [0_u8; SECRET_KEY_BYTES];
|
||||
let fill_result = getrandom::fill(bytes.as_mut_slice());
|
||||
if fill_result.is_err() {
|
||||
zeroize::Zeroize::zeroize(&mut bytes);
|
||||
return std::result::Result::Err(randomness_error());
|
||||
}
|
||||
pub(crate) fn random() -> ksp_core_lib::Result<Self> {
|
||||
let bytes = match random_bytes::<SECRET_KEY_BYTES>() {
|
||||
std::result::Result::Ok(bytes) => bytes,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self { bytes });
|
||||
}
|
||||
|
||||
/// Borrows secret bytes internally without allocating or cloning.
|
||||
const fn as_bytes(&self) -> &[u8; SECRET_KEY_BYTES] {
|
||||
pub(crate) const fn as_bytes(&self) -> &[u8; SECRET_KEY_BYTES] {
|
||||
return &self.bytes;
|
||||
}
|
||||
}
|
||||
@@ -53,24 +47,29 @@ impl std::ops::Drop for SecretKeyV1 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a fresh XChaCha20-Poly1305 nonce from the operating-system CSPRNG.
|
||||
fn random_nonce() -> ksp_core_lib::Result<[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES]> {
|
||||
let mut nonce = [0_u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES];
|
||||
let fill_result = getrandom::fill(nonce.as_mut_slice());
|
||||
/// Generates a fresh fixed-size byte array from the operating-system CSPRNG.
|
||||
pub(crate) fn random_bytes<const LENGTH: usize>() -> ksp_core_lib::Result<[u8; LENGTH]> {
|
||||
let mut bytes = [0_u8; LENGTH];
|
||||
let fill_result = getrandom::fill(bytes.as_mut_slice());
|
||||
if fill_result.is_err() {
|
||||
zeroize::Zeroize::zeroize(&mut nonce);
|
||||
zeroize::Zeroize::zeroize(&mut bytes);
|
||||
return std::result::Result::Err(randomness_error());
|
||||
}
|
||||
return std::result::Result::Ok(nonce);
|
||||
return std::result::Result::Ok(bytes);
|
||||
}
|
||||
|
||||
/// Generates a fresh XChaCha20-Poly1305 nonce from the operating-system CSPRNG.
|
||||
pub(crate) fn random_nonce() -> ksp_core_lib::Result<[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES]> {
|
||||
return random_bytes::<{ crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES }>();
|
||||
}
|
||||
|
||||
/// Derives one V1 password wrapping key from serialized Argon2id parameters.
|
||||
fn derive_password_key(password: &[u8], kdf: &crate::WalletKdfParametersV1) -> ksp_core_lib::Result<SecretKeyV1> {
|
||||
pub(crate) fn derive_password_key(password: &[u8], kdf: &crate::WalletKdfParametersV1) -> ksp_core_lib::Result<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.
|
||||
fn wrap_key(
|
||||
pub(crate) fn wrap_key(
|
||||
wrapping_key: &SecretKeyV1,
|
||||
key_to_wrap: &SecretKeyV1,
|
||||
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
@@ -80,7 +79,7 @@ fn wrap_key(
|
||||
}
|
||||
|
||||
/// Unwraps one 32-byte content key and maps every AEAD authentication failure to the generic Wallet authentication error.
|
||||
fn unwrap_key(
|
||||
pub(crate) fn unwrap_key(
|
||||
wrapping_key: &SecretKeyV1,
|
||||
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
aad: &[u8],
|
||||
@@ -108,7 +107,7 @@ fn unwrap_key(
|
||||
}
|
||||
|
||||
/// Encrypts bounded plaintext bytes with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
|
||||
fn encrypt_bytes(
|
||||
pub(crate) fn encrypt_bytes(
|
||||
key: &SecretKeyV1,
|
||||
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
aad: &[u8],
|
||||
@@ -129,7 +128,7 @@ fn encrypt_bytes(
|
||||
}
|
||||
|
||||
/// Decrypts authenticated ciphertext bytes and returns a generic authentication error on tag failure.
|
||||
fn decrypt_bytes(
|
||||
pub(crate) fn decrypt_bytes(
|
||||
key: &SecretKeyV1,
|
||||
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
aad: &[u8],
|
||||
@@ -149,7 +148,7 @@ fn decrypt_bytes(
|
||||
};
|
||||
}
|
||||
|
||||
fn derive_argon2id(password: &[u8], salt: &[u8], memory_kib: u32, iterations: u32, parallelism: u32) -> ksp_core_lib::Result<SecretKeyV1> {
|
||||
pub(crate) fn derive_argon2id(password: &[u8], salt: &[u8], memory_kib: u32, iterations: u32, parallelism: u32) -> ksp_core_lib::Result<SecretKeyV1> {
|
||||
if password.is_empty() || password.len() > crate::KSPWALLET_V1_MAX_PASSWORD_BYTES {
|
||||
return std::result::Result::Err(crypto_parameter_error());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/error.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
/// Error code used when a native Wallet structure is invalid.
|
||||
pub const ERROR_CODE_FORMAT_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "format_invalid");
|
||||
@@ -11,6 +11,8 @@ pub const ERROR_CODE_CRYPTO_PARAMETERS_INVALID: ksp_core_lib::ErrorCode = ksp_co
|
||||
pub const ERROR_CODE_RANDOMNESS_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "randomness_failed");
|
||||
/// Error code used when an authenticated Wallet structure cannot be verified.
|
||||
pub const ERROR_CODE_AUTHENTICATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "authentication_failed");
|
||||
/// Error code used when an internal blocking cryptographic operation cannot complete.
|
||||
pub const ERROR_CODE_CRYPTO_OPERATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "crypto_operation_failed");
|
||||
/// Error code used when a VIEW unlock attempt fails without exposing a finer cryptographic oracle.
|
||||
pub const ERROR_CODE_VIEW_UNLOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "view_unlock_failed");
|
||||
/// Error code used when an OWNER unlock attempt fails without exposing a finer cryptographic oracle.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/lib.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -9,7 +9,8 @@
|
||||
//! `ksp-wallet-lib` owns the native `.kspwallet` domain, VIEW/OWNER capability model, protected metadata projection, password-secret wrappers and Wallet
|
||||
//! error contract. `0.2.5-pre.003` freezes the strict V1 JSON envelope, canonical Base64url decoding, structural limits and deterministic
|
||||
//! state-transcript/AEAD-AAD byte codecs. `0.2.5-pre.004` adds the in-memory Argon2id/XChaCha20-Poly1305/CSPRNG primitives and deterministic crypto
|
||||
//! vectors used by later create/open operations. It still performs no state-signature verification, Solana signing or filesystem persistence. Public
|
||||
//! vectors. `0.2.5-pre.005` adds exact protected payloads, OWNER Ed25519 state authentication and async in-memory create/open flows for VIEW and OWNER.
|
||||
//! Solana transaction signing and filesystem persistence remain outside this tranche. 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`.
|
||||
|
||||
@@ -20,8 +21,10 @@ mod error;
|
||||
mod metadata;
|
||||
mod owner;
|
||||
mod password;
|
||||
mod payload;
|
||||
mod transcript;
|
||||
mod view;
|
||||
mod wallet;
|
||||
mod wire;
|
||||
|
||||
/// Authorized capability represented by an unlocked Wallet handle.
|
||||
@@ -36,6 +39,14 @@ pub use self::constants::KSPWALLET_MAX_FILE_BYTES;
|
||||
pub use self::constants::KSPWALLET_V1_AEAD_TAG_BYTES;
|
||||
/// Argon2 version serialized by `.kspwallet` V1 key slots.
|
||||
pub use self::constants::KSPWALLET_V1_ARGON2_VERSION;
|
||||
/// Default Argon2id iteration count for newly created V1 slots.
|
||||
pub use self::constants::KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS;
|
||||
/// Default Argon2id memory cost for newly created V1 slots.
|
||||
pub use self::constants::KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB;
|
||||
/// Default Argon2id parallelism for newly created V1 slots.
|
||||
pub use self::constants::KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM;
|
||||
/// Default KDF salt size generated for newly created V1 slots.
|
||||
pub use self::constants::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES;
|
||||
/// Byte length of the Ed25519 format-authority public key.
|
||||
pub use self::constants::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES;
|
||||
/// Byte length of the Ed25519 detached state signature.
|
||||
@@ -74,12 +85,18 @@ pub use self::constants::KSPWALLET_V1_MAX_SECRET_CIPHERTEXT_BYTES;
|
||||
pub use self::constants::KSPWALLET_V1_METADATA_AAD_DOMAIN;
|
||||
/// Minimum Argon2 salt size accepted by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MIN_KDF_SALT_BYTES;
|
||||
/// Byte length of every protected metadata note identifier.
|
||||
pub use self::constants::KSPWALLET_V1_NOTE_ID_BYTES;
|
||||
/// Domain separator for OWNER-control compartment AEAD AAD.
|
||||
pub use self::constants::KSPWALLET_V1_OWNER_CONTROL_AAD_DOMAIN;
|
||||
/// Exact OWNER-control plaintext size fixed by V1.
|
||||
pub use self::constants::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES;
|
||||
/// Domain separator for OWNER key-slot wrapping AAD.
|
||||
pub use self::constants::KSPWALLET_V1_OWNER_SLOT_AAD_DOMAIN;
|
||||
/// Domain separator for OWNER-only secret compartment AEAD AAD.
|
||||
pub use self::constants::KSPWALLET_V1_SECRET_AAD_DOMAIN;
|
||||
/// Exact Solana keypair plaintext size fixed by V1.
|
||||
pub use self::constants::KSPWALLET_V1_SECRET_PLAINTEXT_BYTES;
|
||||
/// Byte length of every V1 key-slot identifier.
|
||||
pub use self::constants::KSPWALLET_V1_SLOT_ID_BYTES;
|
||||
/// Domain separator for the OWNER state-signature transcript.
|
||||
@@ -94,6 +111,8 @@ pub use self::error::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED;
|
||||
pub use self::error::ERROR_CODE_AUTHENTICATION_FAILED;
|
||||
/// Error code used when an operation requires a capability that the caller does not own.
|
||||
pub use self::error::ERROR_CODE_CAPABILITY_INSUFFICIENT;
|
||||
/// Error code used when a blocking cryptographic task cannot complete.
|
||||
pub use self::error::ERROR_CODE_CRYPTO_OPERATION_FAILED;
|
||||
/// Error code used when serialized cryptographic parameters are invalid or unsupported.
|
||||
pub use self::error::ERROR_CODE_CRYPTO_PARAMETERS_INVALID;
|
||||
/// Error code used when a no-clobber create or import destination already exists.
|
||||
@@ -118,6 +137,8 @@ pub use self::error::ERROR_CODE_TRANSFER_FORMAT_UNSUPPORTED;
|
||||
pub use self::error::ERROR_CODE_VIEW_UNLOCK_FAILED;
|
||||
/// Minimal non-secret information available while a native Wallet remains locked.
|
||||
pub use self::metadata::LockedWalletInfo;
|
||||
/// Protected initial metadata supplied to native Wallet creation.
|
||||
pub use self::metadata::WalletCreateMetadataV1;
|
||||
/// Safe metadata projection produced after VIEW or OWNER authorization.
|
||||
pub use self::metadata::WalletInfo;
|
||||
/// One protected Wallet note exposed only after authorization.
|
||||
@@ -130,6 +151,14 @@ pub use self::password::OwnerPassword;
|
||||
pub use self::password::ViewPassword;
|
||||
/// Authorized VIEW capability handle.
|
||||
pub use self::view::WalletView;
|
||||
/// Creates a new in-memory native Wallet V1.
|
||||
pub use self::wallet::create_wallet_v1;
|
||||
/// Parses and verifies locked native Wallet state without unlocking protected metadata.
|
||||
pub use self::wallet::inspect_locked_wallet_v1;
|
||||
/// Opens the OWNER capability from a native Wallet V1 document.
|
||||
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;
|
||||
/// Strict semantic representation of one parsed native `.kspwallet` V1 envelope.
|
||||
pub use self::wire::KspWalletEnvelopeV1;
|
||||
/// Authenticated-encryption algorithm fixed by native Wallet V1.
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
// file: crates/ksp-wallet-lib/src/metadata.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
/// Protected metadata requested when creating a new native Wallet.
|
||||
///
|
||||
/// Alias and note texts are never rendered by `Debug`. Wallet generates stable random note identifiers when the metadata is sealed into the V1 payload.
|
||||
#[derive(Default, Eq, PartialEq)]
|
||||
pub struct WalletCreateMetadataV1 {
|
||||
alias: std::option::Option<std::string::String>,
|
||||
note_texts: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
impl WalletCreateMetadataV1 {
|
||||
/// Creates protected initial Wallet metadata.
|
||||
#[must_use]
|
||||
pub fn new(alias: std::option::Option<std::string::String>, note_texts: std::vec::Vec<std::string::String>) -> Self {
|
||||
return Self { alias, note_texts };
|
||||
}
|
||||
|
||||
pub(crate) fn into_parts(self) -> (std::option::Option<std::string::String>, std::vec::Vec<std::string::String>) {
|
||||
return (self.alias, self.note_texts);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WalletCreateMetadataV1 {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("WalletCreateMetadataV1")
|
||||
.field("alias", &self.alias.as_ref().map(|_| return "<redacted>"))
|
||||
.field("note_count", &self.note_texts.len())
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// One protected Wallet note exposed only after VIEW or OWNER authorization.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
@@ -9,6 +40,10 @@ pub struct WalletNote {
|
||||
}
|
||||
|
||||
impl WalletNote {
|
||||
pub(crate) fn new(id: std::string::String, text: std::string::String) -> Self {
|
||||
return Self { id, text };
|
||||
}
|
||||
|
||||
/// Returns the stable note identifier used by Wallet administration operations.
|
||||
#[must_use]
|
||||
pub fn id(&self) -> &str {
|
||||
@@ -39,6 +74,15 @@ pub struct WalletInfo {
|
||||
}
|
||||
|
||||
impl WalletInfo {
|
||||
pub(crate) fn new(
|
||||
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 };
|
||||
}
|
||||
|
||||
/// Returns the native Wallet format version parsed for this projection.
|
||||
#[must_use]
|
||||
pub const fn format_version(&self) -> u32 {
|
||||
@@ -91,6 +135,10 @@ pub struct LockedWalletInfo {
|
||||
}
|
||||
|
||||
impl LockedWalletInfo {
|
||||
pub(crate) const fn new(view_enabled: bool) -> Self {
|
||||
return Self { format_version: crate::KSPWALLET_FORMAT_VERSION_V1, view_enabled };
|
||||
}
|
||||
|
||||
/// Returns the native Wallet format version.
|
||||
#[must_use]
|
||||
pub const fn format_version(&self) -> u32 {
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
// file: crates/ksp-wallet-lib/src/owner.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Authorized OWNER capability handle.
|
||||
///
|
||||
/// OWNER exposes all authorized metadata and will receive signing plus Wallet administration operations in later `0.2.5` tranches without exposing a
|
||||
/// general-purpose secret-key getter.
|
||||
/// OWNER exposes protected metadata and retains the authenticated in-memory state required by later signing and administration tranches without
|
||||
/// exposing a general-purpose secret-key getter.
|
||||
pub struct WalletOwner {
|
||||
info: crate::WalletInfo,
|
||||
state: crate::wallet::OwnerStateV1,
|
||||
}
|
||||
|
||||
impl WalletOwner {
|
||||
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::wallet::OwnerStateV1) -> Self {
|
||||
return Self { info, state };
|
||||
}
|
||||
|
||||
/// Returns the authorization capability represented by this handle.
|
||||
#[must_use]
|
||||
pub const fn capability(&self) -> crate::WalletCapability {
|
||||
@@ -45,10 +50,15 @@ impl WalletOwner {
|
||||
);
|
||||
return &self.info;
|
||||
}
|
||||
|
||||
/// Serializes the complete locked `.kspwallet` V1 document without exposing any unlocked secret material.
|
||||
pub fn to_json_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
return self.state.envelope().to_json_bytes();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WalletOwner {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.debug_struct("WalletOwner").field("info", &self.info).finish();
|
||||
return formatter.debug_struct("WalletOwner").field("info", &self.info).field("unlocked_state", &"<redacted>").finish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/password.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Owned VIEW password material.
|
||||
///
|
||||
@@ -21,6 +21,11 @@ impl ViewPassword {
|
||||
pub fn new(value: std::string::String) -> Self {
|
||||
return Self { value };
|
||||
}
|
||||
|
||||
/// Borrows exact UTF-8 password bytes inside Wallet cryptographic operations.
|
||||
pub(crate) fn as_bytes(&self) -> &[u8] {
|
||||
return self.value.as_bytes();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ViewPassword {
|
||||
@@ -55,6 +60,11 @@ impl OwnerPassword {
|
||||
pub fn new(value: std::string::String) -> Self {
|
||||
return Self { value };
|
||||
}
|
||||
|
||||
/// Borrows exact UTF-8 password bytes inside Wallet cryptographic operations.
|
||||
pub(crate) fn as_bytes(&self) -> &[u8] {
|
||||
return self.value.as_bytes();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for OwnerPassword {
|
||||
|
||||
207
crates/ksp-wallet-lib/src/payload.rs
Normal file
207
crates/ksp-wallet-lib/src/payload.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
// file: crates/ksp-wallet-lib/src/payload.rs
|
||||
// version: 2
|
||||
|
||||
//! Plaintext payload codecs protected inside native `.kspwallet` V1 compartments.
|
||||
|
||||
use base64::Engine as _;
|
||||
use std::str::FromStr as _;
|
||||
|
||||
pub(crate) struct MetadataPayloadV1 {
|
||||
pubkey: ksp_core_lib::Pubkey,
|
||||
alias: std::option::Option<std::string::String>,
|
||||
notes: std::vec::Vec<crate::WalletNote>,
|
||||
}
|
||||
|
||||
impl MetadataPayloadV1 {
|
||||
pub(crate) const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.pubkey;
|
||||
}
|
||||
|
||||
pub(crate) fn into_info(self, capability: crate::WalletCapability) -> crate::WalletInfo {
|
||||
return crate::WalletInfo::new(capability, self.pubkey, self.alias, self.notes);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct OwnerControlMaterialV1 {
|
||||
admin_signing_secret: [u8; crate::crypto::SECRET_KEY_BYTES],
|
||||
metadata_key: crate::crypto::SecretKeyV1,
|
||||
secret_key: crate::crypto::SecretKeyV1,
|
||||
}
|
||||
|
||||
impl OwnerControlMaterialV1 {
|
||||
pub(crate) fn into_parts(mut self) -> ([u8; crate::crypto::SECRET_KEY_BYTES], crate::crypto::SecretKeyV1, crate::crypto::SecretKeyV1) {
|
||||
let mut admin_signing_secret = [0_u8; crate::crypto::SECRET_KEY_BYTES];
|
||||
std::mem::swap(&mut admin_signing_secret, &mut self.admin_signing_secret);
|
||||
let metadata_key = std::mem::replace(&mut self.metadata_key, crate::crypto::SecretKeyV1::from_bytes([0_u8; crate::crypto::SECRET_KEY_BYTES]));
|
||||
let secret_key = std::mem::replace(&mut self.secret_key, crate::crypto::SecretKeyV1::from_bytes([0_u8; crate::crypto::SECRET_KEY_BYTES]));
|
||||
return (admin_signing_secret, metadata_key, secret_key);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Drop for OwnerControlMaterialV1 {
|
||||
fn drop(&mut self) {
|
||||
zeroize::Zeroize::zeroize(&mut self.admin_signing_secret);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawMetadataPayloadV1 {
|
||||
pubkey: std::string::String,
|
||||
alias: std::option::Option<std::string::String>,
|
||||
notes: std::vec::Vec<RawMetadataNoteV1>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawMetadataNoteV1 {
|
||||
id: std::string::String,
|
||||
text: std::string::String,
|
||||
}
|
||||
|
||||
pub(crate) fn encode_initial_metadata_payload(
|
||||
pubkey: ksp_core_lib::Pubkey,
|
||||
metadata: crate::WalletCreateMetadataV1,
|
||||
) -> ksp_core_lib::Result<(std::vec::Vec<u8>, MetadataPayloadV1)> {
|
||||
let (alias, note_texts) = metadata.into_parts();
|
||||
if let std::option::Option::Some(alias_value) = alias.as_ref()
|
||||
&& alias_value.len() > crate::KSPWALLET_V1_MAX_ALIAS_BYTES
|
||||
{
|
||||
return std::result::Result::Err(metadata_error("Wallet alias exceeds the V1 UTF-8 byte limit", "metadata.alias"));
|
||||
}
|
||||
if note_texts.len() > crate::KSPWALLET_V1_MAX_NOTES {
|
||||
return std::result::Result::Err(metadata_error("Wallet note count exceeds the V1 limit", "metadata.notes"));
|
||||
}
|
||||
|
||||
let mut raw_notes = std::vec::Vec::with_capacity(note_texts.len());
|
||||
let mut notes = std::vec::Vec::with_capacity(note_texts.len());
|
||||
let mut note_ids = std::collections::BTreeSet::new();
|
||||
for text in note_texts {
|
||||
if text.len() > crate::KSPWALLET_V1_MAX_NOTE_TEXT_BYTES {
|
||||
return std::result::Result::Err(metadata_error("Wallet note text exceeds the V1 UTF-8 byte limit", "metadata.notes.text"));
|
||||
}
|
||||
let note_id_bytes = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_NOTE_ID_BYTES }>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let note_id = encode_base64url(note_id_bytes.as_slice());
|
||||
if !note_ids.insert(note_id.clone()) {
|
||||
return std::result::Result::Err(random_note_identifier_error());
|
||||
}
|
||||
raw_notes.push(RawMetadataNoteV1 { id: note_id.clone(), text: text.clone() });
|
||||
notes.push(crate::WalletNote::new(note_id, text));
|
||||
}
|
||||
|
||||
let raw = RawMetadataPayloadV1 { pubkey: pubkey.to_string(), alias: alias.clone(), notes: raw_notes };
|
||||
let serialized_result = serde_json::to_vec(&raw);
|
||||
let serialized = match serialized_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(metadata_error("Wallet metadata payload cannot be serialized", "metadata")),
|
||||
};
|
||||
if serialized.len() > crate::KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES {
|
||||
return std::result::Result::Err(metadata_error("Wallet metadata payload exceeds the V1 plaintext limit", "metadata"));
|
||||
}
|
||||
return std::result::Result::Ok((serialized, MetadataPayloadV1 { pubkey, alias, notes }));
|
||||
}
|
||||
|
||||
pub(crate) fn decode_metadata_payload(source: &[u8]) -> ksp_core_lib::Result<MetadataPayloadV1> {
|
||||
if source.len() > crate::KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES {
|
||||
return std::result::Result::Err(metadata_error("Wallet metadata payload exceeds the V1 plaintext limit", "metadata"));
|
||||
}
|
||||
let raw_result = serde_json::from_slice::<RawMetadataPayloadV1>(source);
|
||||
let raw = match raw_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(metadata_error("Wallet metadata payload is invalid", "metadata")),
|
||||
};
|
||||
if let std::option::Option::Some(alias) = raw.alias.as_ref()
|
||||
&& alias.len() > crate::KSPWALLET_V1_MAX_ALIAS_BYTES
|
||||
{
|
||||
return std::result::Result::Err(metadata_error("Wallet alias exceeds the V1 UTF-8 byte limit", "metadata.alias"));
|
||||
}
|
||||
if raw.notes.len() > crate::KSPWALLET_V1_MAX_NOTES {
|
||||
return std::result::Result::Err(metadata_error("Wallet note count exceeds the V1 limit", "metadata.notes"));
|
||||
}
|
||||
|
||||
let pubkey_result = ksp_core_lib::Pubkey::from_str(raw.pubkey.as_str());
|
||||
let pubkey = match pubkey_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(metadata_error("Wallet metadata Pubkey is invalid", "metadata.pubkey")),
|
||||
};
|
||||
if pubkey.to_string() != raw.pubkey {
|
||||
return std::result::Result::Err(metadata_error("Wallet metadata Pubkey is not canonical Base58", "metadata.pubkey"));
|
||||
}
|
||||
|
||||
let mut notes = std::vec::Vec::with_capacity(raw.notes.len());
|
||||
let mut note_ids = std::collections::BTreeSet::new();
|
||||
for raw_note in raw.notes {
|
||||
if raw_note.text.len() > crate::KSPWALLET_V1_MAX_NOTE_TEXT_BYTES {
|
||||
return std::result::Result::Err(metadata_error("Wallet note text exceeds the V1 UTF-8 byte limit", "metadata.notes.text"));
|
||||
}
|
||||
let id_bytes = match decode_base64url(raw_note.id.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if id_bytes.len() != crate::KSPWALLET_V1_NOTE_ID_BYTES || encode_base64url(id_bytes.as_slice()) != raw_note.id {
|
||||
return std::result::Result::Err(metadata_error("Wallet note identifier is not canonical V1 Base64url", "metadata.notes.id"));
|
||||
}
|
||||
if !note_ids.insert(raw_note.id.clone()) {
|
||||
return std::result::Result::Err(metadata_error("Wallet note identifiers must be unique", "metadata.notes.id"));
|
||||
}
|
||||
notes.push(crate::WalletNote::new(raw_note.id, raw_note.text));
|
||||
}
|
||||
return std::result::Result::Ok(MetadataPayloadV1 { pubkey, alias: raw.alias, notes });
|
||||
}
|
||||
|
||||
pub(crate) fn encode_owner_control_payload(
|
||||
admin_signing_secret: &[u8; crate::crypto::SECRET_KEY_BYTES],
|
||||
metadata_key: &crate::crypto::SecretKeyV1,
|
||||
secret_key: &crate::crypto::SecretKeyV1,
|
||||
) -> [u8; crate::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES] {
|
||||
let mut output = [0_u8; crate::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES];
|
||||
output[0..32].copy_from_slice(admin_signing_secret);
|
||||
output[32..64].copy_from_slice(metadata_key.as_bytes());
|
||||
output[64..96].copy_from_slice(secret_key.as_bytes());
|
||||
return output;
|
||||
}
|
||||
|
||||
pub(crate) fn decode_owner_control_payload(source: &[u8]) -> ksp_core_lib::Result<OwnerControlMaterialV1> {
|
||||
if source.len() != crate::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES {
|
||||
return std::result::Result::Err(metadata_error("Wallet OWNER-control payload length is invalid", "owner_control"));
|
||||
}
|
||||
let mut admin_signing_secret = [0_u8; crate::crypto::SECRET_KEY_BYTES];
|
||||
admin_signing_secret.copy_from_slice(&source[0..32]);
|
||||
let mut metadata_key = [0_u8; crate::crypto::SECRET_KEY_BYTES];
|
||||
metadata_key.copy_from_slice(&source[32..64]);
|
||||
let mut secret_key = [0_u8; crate::crypto::SECRET_KEY_BYTES];
|
||||
secret_key.copy_from_slice(&source[64..96]);
|
||||
return std::result::Result::Ok(OwnerControlMaterialV1 {
|
||||
admin_signing_secret,
|
||||
metadata_key: crate::crypto::SecretKeyV1::from_bytes(metadata_key),
|
||||
secret_key: crate::crypto::SecretKeyV1::from_bytes(secret_key),
|
||||
});
|
||||
}
|
||||
|
||||
fn encode_base64url(bytes: &[u8]) -> std::string::String {
|
||||
return base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
|
||||
}
|
||||
|
||||
fn decode_base64url(encoded: &str) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(encoded.as_bytes());
|
||||
let value = match decoded {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(metadata_error("Wallet metadata Base64url field is invalid", "metadata.notes.id")),
|
||||
};
|
||||
return std::result::Result::Ok(value);
|
||||
}
|
||||
|
||||
fn random_note_identifier_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_RANDOMNESS_FAILED, "Wallet generated a duplicate protected note identifier");
|
||||
}
|
||||
|
||||
fn metadata_error(message: &'static str, field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, message).with_context("field", field);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/payload.rs"]
|
||||
mod tests;
|
||||
@@ -1,15 +1,20 @@
|
||||
// file: crates/ksp-wallet-lib/src/view.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Authorized VIEW capability handle.
|
||||
///
|
||||
/// VIEW exposes protected metadata and, in later `0.2.5` tranches, will expose only self-rotation of its VIEW password. It never owns the Solana secret or
|
||||
/// OWNER administration material.
|
||||
/// VIEW exposes protected metadata and retains only the authenticated metadata capability needed for its future self-service password rotation. It never
|
||||
/// owns the Solana secret or OWNER administration material.
|
||||
pub struct WalletView {
|
||||
info: crate::WalletInfo,
|
||||
state: crate::wallet::ViewStateV1,
|
||||
}
|
||||
|
||||
impl WalletView {
|
||||
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::wallet::ViewStateV1) -> Self {
|
||||
return Self { info, state };
|
||||
}
|
||||
|
||||
/// Returns the authorization capability represented by this handle.
|
||||
#[must_use]
|
||||
pub const fn capability(&self) -> crate::WalletCapability {
|
||||
@@ -45,10 +50,15 @@ impl WalletView {
|
||||
);
|
||||
return &self.info;
|
||||
}
|
||||
|
||||
/// Serializes the unchanged locked `.kspwallet` V1 document without exposing the metadata content key.
|
||||
pub fn to_json_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
return self.state.envelope().to_json_bytes();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WalletView {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.debug_struct("WalletView").field("info", &self.info).finish();
|
||||
return formatter.debug_struct("WalletView").field("info", &self.info).field("unlocked_state", &"<redacted>").finish();
|
||||
}
|
||||
}
|
||||
|
||||
682
crates/ksp-wallet-lib/src/wallet.rs
Normal file
682
crates/ksp-wallet-lib/src/wallet.rs
Normal file
@@ -0,0 +1,682 @@
|
||||
// file: crates/ksp-wallet-lib/src/wallet.rs
|
||||
// version: 2
|
||||
|
||||
//! In-memory native Wallet V1 create/open orchestration.
|
||||
|
||||
use ed25519_dalek::Signer as _;
|
||||
use zeroize::Zeroize as _;
|
||||
|
||||
pub(crate) struct OwnerStateV1 {
|
||||
envelope: crate::KspWalletEnvelopeV1,
|
||||
owner_root: std::option::Option<crate::crypto::SecretKeyV1>,
|
||||
metadata_key: std::option::Option<crate::crypto::SecretKeyV1>,
|
||||
secret_key: std::option::Option<crate::crypto::SecretKeyV1>,
|
||||
admin_signing_key: std::option::Option<ed25519_dalek::SigningKey>,
|
||||
solana_keypair: std::option::Option<solana_keypair::Keypair>,
|
||||
}
|
||||
|
||||
impl OwnerStateV1 {
|
||||
pub(crate) fn new(
|
||||
envelope: crate::KspWalletEnvelopeV1,
|
||||
owner_root: crate::crypto::SecretKeyV1,
|
||||
metadata_key: crate::crypto::SecretKeyV1,
|
||||
secret_key: crate::crypto::SecretKeyV1,
|
||||
admin_signing_key: ed25519_dalek::SigningKey,
|
||||
solana_keypair: solana_keypair::Keypair,
|
||||
) -> Self {
|
||||
return Self {
|
||||
envelope,
|
||||
owner_root: std::option::Option::Some(owner_root),
|
||||
metadata_key: std::option::Option::Some(metadata_key),
|
||||
secret_key: std::option::Option::Some(secret_key),
|
||||
admin_signing_key: std::option::Option::Some(admin_signing_key),
|
||||
solana_keypair: std::option::Option::Some(solana_keypair),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) const fn envelope(&self) -> &crate::KspWalletEnvelopeV1 {
|
||||
return &self.envelope;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Drop for OwnerStateV1 {
|
||||
fn drop(&mut self) {
|
||||
let owner_root = self.owner_root.take();
|
||||
let metadata_key = self.metadata_key.take();
|
||||
let secret_key = self.secret_key.take();
|
||||
let admin_signing_key = self.admin_signing_key.take();
|
||||
let solana_keypair = self.solana_keypair.take();
|
||||
drop(owner_root);
|
||||
drop(metadata_key);
|
||||
drop(secret_key);
|
||||
drop(admin_signing_key);
|
||||
drop(solana_keypair);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ViewStateV1 {
|
||||
envelope: crate::KspWalletEnvelopeV1,
|
||||
metadata_key: std::option::Option<crate::crypto::SecretKeyV1>,
|
||||
}
|
||||
|
||||
impl ViewStateV1 {
|
||||
pub(crate) fn new(envelope: crate::KspWalletEnvelopeV1, metadata_key: crate::crypto::SecretKeyV1) -> Self {
|
||||
return Self { envelope, metadata_key: std::option::Option::Some(metadata_key) };
|
||||
}
|
||||
|
||||
pub(crate) const fn envelope(&self) -> &crate::KspWalletEnvelopeV1 {
|
||||
return &self.envelope;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Drop for ViewStateV1 {
|
||||
fn drop(&mut self) {
|
||||
let metadata_key = self.metadata_key.take();
|
||||
drop(metadata_key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new in-memory native `.kspwallet` V1 with a fresh Solana keypair.
|
||||
///
|
||||
/// The optional VIEW password creates an independent VIEW slot. This function performs no filesystem I/O; use the OWNER handle's
|
||||
/// `to_json_bytes()` projection for the complete locked document until `pre.006` adds atomic persistence.
|
||||
pub async fn create_wallet_v1(
|
||||
owner_password: crate::OwnerPassword,
|
||||
view_password: std::option::Option<crate::ViewPassword>,
|
||||
metadata: crate::WalletCreateMetadataV1,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
let owner_root = match crate::crypto::SecretKeyV1::random() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let metadata_key = match crate::crypto::SecretKeyV1::random() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let secret_key = match crate::crypto::SecretKeyV1::random() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
let mut admin_secret = match crate::crypto::random_bytes::<{ crate::crypto::SECRET_KEY_BYTES }>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let admin_signing_key = ed25519_dalek::SigningKey::from_bytes(&admin_secret);
|
||||
let owner_auth_public_key = admin_signing_key.verifying_key().to_bytes();
|
||||
|
||||
let mut solana_secret = match crate::crypto::random_bytes::<32>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
admin_secret.zeroize();
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
let solana_keypair = solana_keypair::Keypair::new_from_array(solana_secret);
|
||||
solana_secret.zeroize();
|
||||
let mut solana_keypair_bytes = solana_keypair.to_bytes();
|
||||
let pubkey_result = pubkey_from_keypair_bytes(&solana_keypair_bytes);
|
||||
let pubkey = match pubkey_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
admin_secret.zeroize();
|
||||
solana_keypair_bytes.zeroize();
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
|
||||
let metadata_encoded_result = crate::payload::encode_initial_metadata_payload(pubkey, metadata);
|
||||
let (mut metadata_plaintext, metadata_payload) = match metadata_encoded_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
admin_secret.zeroize();
|
||||
solana_keypair_bytes.zeroize();
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
let mut owner_control_plaintext = crate::payload::encode_owner_control_payload(&admin_secret, &metadata_key, &secret_key);
|
||||
admin_secret.zeroize();
|
||||
|
||||
let owner_slot_id = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
let owner_salt = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
let owner_kdf = crate::WalletKdfParametersV1::new_creation(owner_salt.to_vec());
|
||||
let owner_wrap_nonce = match crate::crypto::random_nonce() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
|
||||
let view_material_result = prepare_view_creation(view_password);
|
||||
let view_material = match view_material_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
|
||||
let owner_control_nonce = match crate::crypto::random_nonce() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
let metadata_nonce = match crate::crypto::random_nonce() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
let secret_nonce = match crate::crypto::random_nonce() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
|
||||
let provisional = provisional_envelope(
|
||||
owner_auth_public_key,
|
||||
owner_slot_id,
|
||||
owner_kdf.clone(),
|
||||
owner_wrap_nonce,
|
||||
view_material.as_ref(),
|
||||
owner_control_nonce,
|
||||
metadata_nonce,
|
||||
secret_nonce,
|
||||
);
|
||||
|
||||
let owner_derived_result = derive_owner_password_key_async(owner_password, owner_kdf.clone()).await;
|
||||
let owner_derived = match owner_derived_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
let owner_wrapped_result = crate::crypto::wrap_key(&owner_derived, &owner_root, &owner_wrap_nonce, provisional.owner_slot_aad().as_slice());
|
||||
let owner_wrapped = match owner_wrapped_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
|
||||
let view_slot_result = seal_view_slot(&provisional, view_material, &metadata_key).await;
|
||||
let view_slot = match view_slot_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
|
||||
let owner_control_ciphertext_result = crate::crypto::encrypt_bytes(
|
||||
&owner_root,
|
||||
&owner_control_nonce,
|
||||
provisional.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(),
|
||||
owner_control_plaintext.as_slice(),
|
||||
);
|
||||
let owner_control_ciphertext = match owner_control_ciphertext_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
owner_control_plaintext.zeroize();
|
||||
|
||||
let metadata_ciphertext_result = crate::crypto::encrypt_bytes(
|
||||
&metadata_key,
|
||||
&metadata_nonce,
|
||||
provisional.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
|
||||
metadata_plaintext.as_slice(),
|
||||
);
|
||||
let metadata_ciphertext = match metadata_ciphertext_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
metadata_plaintext.zeroize();
|
||||
|
||||
let secret_ciphertext_result = crate::crypto::encrypt_bytes(
|
||||
&secret_key,
|
||||
&secret_nonce,
|
||||
provisional.compartment_aad(crate::WalletCompartmentKindV1::Secret).as_slice(),
|
||||
solana_keypair_bytes.as_slice(),
|
||||
);
|
||||
let secret_ciphertext = match secret_ciphertext_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
|
||||
},
|
||||
};
|
||||
solana_keypair_bytes.zeroize();
|
||||
|
||||
let owner_slot =
|
||||
crate::WalletKeySlotV1::new(owner_slot_id, crate::WalletKeySlotRoleV1::Owner, owner_kdf, crate::WalletKeyWrapV1::new(owner_wrap_nonce, owner_wrapped));
|
||||
let view_descriptor = match view_slot.as_ref() {
|
||||
std::option::Option::Some(slot) => crate::WalletViewDescriptorV1::enabled(*slot.slot_id()),
|
||||
std::option::Option::None => crate::WalletViewDescriptorV1::disabled(),
|
||||
};
|
||||
let owner_control = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::OwnerControl, owner_control_nonce, owner_control_ciphertext);
|
||||
let metadata_compartment = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Metadata, metadata_nonce, metadata_ciphertext);
|
||||
let secret_compartment = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Secret, secret_nonce, secret_ciphertext);
|
||||
let unsigned = crate::KspWalletEnvelopeV1::new_internal(
|
||||
owner_auth_public_key,
|
||||
view_descriptor,
|
||||
owner_slot.clone(),
|
||||
view_slot.clone(),
|
||||
owner_control.clone(),
|
||||
metadata_compartment.clone(),
|
||||
secret_compartment.clone(),
|
||||
crate::WalletStateSignatureV1::new([0_u8; crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES]),
|
||||
);
|
||||
let signature = admin_signing_key.sign(unsigned.state_transcript().as_slice()).to_bytes();
|
||||
let envelope = crate::KspWalletEnvelopeV1::new_internal(
|
||||
owner_auth_public_key,
|
||||
view_descriptor,
|
||||
owner_slot,
|
||||
view_slot,
|
||||
owner_control,
|
||||
metadata_compartment,
|
||||
secret_compartment,
|
||||
crate::WalletStateSignatureV1::new(signature),
|
||||
);
|
||||
let verify_result = verify_state_signature(&envelope);
|
||||
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 state = OwnerStateV1::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair);
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_create",
|
||||
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
|
||||
view_enabled = state.envelope().view_descriptor().enabled(),
|
||||
"native wallet created in memory"
|
||||
);
|
||||
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
|
||||
}
|
||||
|
||||
/// Opens the VIEW capability from a native `.kspwallet` V1 JSON document.
|
||||
pub async fn open_wallet_view_v1(source: &[u8], password: crate::ViewPassword) -> ksp_core_lib::Result<crate::WalletView> {
|
||||
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let verify_result = verify_state_signature(&envelope);
|
||||
if let std::result::Result::Err(error) = verify_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let view_slot = match envelope.view_slot() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")),
|
||||
};
|
||||
let derived_result = derive_view_password_key_async(password, view_slot.kdf().clone()).await;
|
||||
let derived = match derived_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let aad = envelope.view_slot_aad();
|
||||
let view_aad = match aad {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")),
|
||||
};
|
||||
let metadata_key_result = crate::crypto::unwrap_key(&derived, view_slot.wrap().nonce(), view_aad.as_slice(), view_slot.wrap().ciphertext());
|
||||
let metadata_key = match metadata_key_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()),
|
||||
};
|
||||
let metadata_plaintext_result = crate::crypto::decrypt_bytes(
|
||||
&metadata_key,
|
||||
envelope.metadata().nonce(),
|
||||
envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
|
||||
envelope.metadata().ciphertext(),
|
||||
);
|
||||
let mut metadata_plaintext = match metadata_plaintext_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()),
|
||||
};
|
||||
let metadata_payload_result = crate::payload::decode_metadata_payload(metadata_plaintext.as_slice());
|
||||
metadata_plaintext.zeroize();
|
||||
let metadata_payload = match metadata_payload_result {
|
||||
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 state = ViewStateV1::new(envelope, metadata_key);
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_open_view",
|
||||
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
|
||||
capability = "view",
|
||||
"native wallet VIEW capability opened"
|
||||
);
|
||||
return std::result::Result::Ok(crate::WalletView::from_unlocked(info, state));
|
||||
}
|
||||
|
||||
/// Opens the OWNER capability from a native `.kspwallet` V1 JSON document.
|
||||
pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let verify_result = verify_state_signature(&envelope);
|
||||
if let std::result::Result::Err(error) = verify_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let owner_slot = envelope.owner_slot();
|
||||
let derived_result = derive_owner_password_key_async(password, owner_slot.kdf().clone()).await;
|
||||
let derived = match derived_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let owner_root_result =
|
||||
crate::crypto::unwrap_key(&derived, owner_slot.wrap().nonce(), envelope.owner_slot_aad().as_slice(), owner_slot.wrap().ciphertext());
|
||||
let owner_root = match owner_root_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(owner_unlock_error()),
|
||||
};
|
||||
let owner_control_plaintext_result = crate::crypto::decrypt_bytes(
|
||||
&owner_root,
|
||||
envelope.owner_control().nonce(),
|
||||
envelope.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(),
|
||||
envelope.owner_control().ciphertext(),
|
||||
);
|
||||
let mut owner_control_plaintext = match owner_control_plaintext_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(owner_unlock_error()),
|
||||
};
|
||||
let control_result = crate::payload::decode_owner_control_payload(owner_control_plaintext.as_slice());
|
||||
owner_control_plaintext.zeroize();
|
||||
let control = match control_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let (mut admin_secret, metadata_key, secret_key) = control.into_parts();
|
||||
let admin_signing_key = ed25519_dalek::SigningKey::from_bytes(&admin_secret);
|
||||
admin_secret.zeroize();
|
||||
if admin_signing_key.verifying_key().to_bytes() != *envelope.owner_auth_public_key() {
|
||||
return std::result::Result::Err(authentication_error());
|
||||
}
|
||||
|
||||
let metadata_plaintext_result = crate::crypto::decrypt_bytes(
|
||||
&metadata_key,
|
||||
envelope.metadata().nonce(),
|
||||
envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
|
||||
envelope.metadata().ciphertext(),
|
||||
);
|
||||
let mut metadata_plaintext = match metadata_plaintext_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
|
||||
};
|
||||
let metadata_payload_result = crate::payload::decode_metadata_payload(metadata_plaintext.as_slice());
|
||||
metadata_plaintext.zeroize();
|
||||
let metadata_payload = match metadata_payload_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
let secret_plaintext_result = crate::crypto::decrypt_bytes(
|
||||
&secret_key,
|
||||
envelope.secret().nonce(),
|
||||
envelope.compartment_aad(crate::WalletCompartmentKindV1::Secret).as_slice(),
|
||||
envelope.secret().ciphertext(),
|
||||
);
|
||||
let mut secret_plaintext = match secret_plaintext_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
|
||||
};
|
||||
if secret_plaintext.len() != crate::KSPWALLET_V1_SECRET_PLAINTEXT_BYTES {
|
||||
secret_plaintext.zeroize();
|
||||
return std::result::Result::Err(key_material_error());
|
||||
}
|
||||
let keypair_result = solana_keypair::Keypair::try_from(secret_plaintext.as_slice());
|
||||
let solana_keypair = match keypair_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
secret_plaintext.zeroize();
|
||||
return std::result::Result::Err(key_material_error());
|
||||
},
|
||||
};
|
||||
let secret_pubkey_result = pubkey_from_keypair_bytes(secret_plaintext.as_slice());
|
||||
let secret_pubkey = match secret_pubkey_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
secret_plaintext.zeroize();
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
secret_plaintext.zeroize();
|
||||
if &secret_pubkey != metadata_payload.pubkey() {
|
||||
return std::result::Result::Err(key_material_error());
|
||||
}
|
||||
|
||||
let info = metadata_payload.into_info(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,
|
||||
operation = "wallet_open_owner",
|
||||
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
|
||||
capability = "owner",
|
||||
"native wallet OWNER capability opened"
|
||||
);
|
||||
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
|
||||
}
|
||||
|
||||
/// Parses and verifies the OWNER-authenticated locked state without unlocking metadata or secret material.
|
||||
pub fn inspect_locked_wallet_v1(source: &[u8]) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
|
||||
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let verify_result = verify_state_signature(&envelope);
|
||||
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()));
|
||||
}
|
||||
|
||||
pub(crate) fn verify_state_signature(envelope: &crate::KspWalletEnvelopeV1) -> ksp_core_lib::Result<()> {
|
||||
let verifying_key_result = ed25519_dalek::VerifyingKey::from_bytes(envelope.owner_auth_public_key());
|
||||
let verifying_key = match verifying_key_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
|
||||
};
|
||||
let signature = ed25519_dalek::Signature::from_bytes(envelope.state_signature().signature());
|
||||
let verify_result = verifying_key.verify_strict(envelope.state_transcript().as_slice(), &signature);
|
||||
return match verify_result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(authentication_error()),
|
||||
};
|
||||
}
|
||||
|
||||
struct ViewCreationMaterialV1 {
|
||||
slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES],
|
||||
kdf: crate::WalletKdfParametersV1,
|
||||
wrap_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
password: crate::ViewPassword,
|
||||
}
|
||||
|
||||
fn prepare_view_creation(password: std::option::Option<crate::ViewPassword>) -> ksp_core_lib::Result<std::option::Option<ViewCreationMaterialV1>> {
|
||||
let password = match password {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
|
||||
};
|
||||
let slot_id = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let salt = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let wrap_nonce = match crate::crypto::random_nonce() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(std::option::Option::Some(ViewCreationMaterialV1 {
|
||||
slot_id,
|
||||
kdf: crate::WalletKdfParametersV1::new_creation(salt.to_vec()),
|
||||
wrap_nonce,
|
||||
password,
|
||||
}));
|
||||
}
|
||||
|
||||
async fn seal_view_slot(
|
||||
envelope: &crate::KspWalletEnvelopeV1,
|
||||
material: std::option::Option<ViewCreationMaterialV1>,
|
||||
metadata_key: &crate::crypto::SecretKeyV1,
|
||||
) -> ksp_core_lib::Result<std::option::Option<crate::WalletKeySlotV1>> {
|
||||
let material = match material {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
|
||||
};
|
||||
let derived_result = derive_view_password_key_async(material.password, material.kdf.clone()).await;
|
||||
let derived = match derived_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let provisional_slot = crate::WalletKeySlotV1::new(
|
||||
material.slot_id,
|
||||
crate::WalletKeySlotRoleV1::View,
|
||||
material.kdf.clone(),
|
||||
crate::WalletKeyWrapV1::new(material.wrap_nonce, std::vec![0_u8; crate::crypto::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
|
||||
);
|
||||
let aad = crate::slot_aad(envelope, &provisional_slot);
|
||||
let wrapped_result = crate::crypto::wrap_key(&derived, metadata_key, &material.wrap_nonce, aad.as_slice());
|
||||
let wrapped = match wrapped_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(std::option::Option::Some(crate::WalletKeySlotV1::new(
|
||||
material.slot_id,
|
||||
crate::WalletKeySlotRoleV1::View,
|
||||
material.kdf,
|
||||
crate::WalletKeyWrapV1::new(material.wrap_nonce, wrapped),
|
||||
)));
|
||||
}
|
||||
|
||||
fn provisional_envelope(
|
||||
owner_auth_public_key: [u8; crate::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES],
|
||||
owner_slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES],
|
||||
owner_kdf: crate::WalletKdfParametersV1,
|
||||
owner_wrap_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
view_material: std::option::Option<&ViewCreationMaterialV1>,
|
||||
owner_control_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
metadata_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
secret_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
) -> crate::KspWalletEnvelopeV1 {
|
||||
let owner_slot = crate::WalletKeySlotV1::new(
|
||||
owner_slot_id,
|
||||
crate::WalletKeySlotRoleV1::Owner,
|
||||
owner_kdf,
|
||||
crate::WalletKeyWrapV1::new(owner_wrap_nonce, std::vec![0_u8; crate::crypto::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
|
||||
);
|
||||
let (view_descriptor, view_slot) = match view_material {
|
||||
std::option::Option::Some(material) => (
|
||||
crate::WalletViewDescriptorV1::enabled(material.slot_id),
|
||||
std::option::Option::Some(crate::WalletKeySlotV1::new(
|
||||
material.slot_id,
|
||||
crate::WalletKeySlotRoleV1::View,
|
||||
material.kdf.clone(),
|
||||
crate::WalletKeyWrapV1::new(material.wrap_nonce, std::vec![0_u8; crate::crypto::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
|
||||
)),
|
||||
),
|
||||
std::option::Option::None => (crate::WalletViewDescriptorV1::disabled(), std::option::Option::None),
|
||||
};
|
||||
return crate::KspWalletEnvelopeV1::new_internal(
|
||||
owner_auth_public_key,
|
||||
view_descriptor,
|
||||
owner_slot,
|
||||
view_slot,
|
||||
crate::WalletEncryptedCompartmentV1::new(
|
||||
crate::WalletCompartmentKindV1::OwnerControl,
|
||||
owner_control_nonce,
|
||||
std::vec![0_u8; crate::KSPWALLET_V1_AEAD_TAG_BYTES],
|
||||
),
|
||||
crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Metadata, metadata_nonce, std::vec![0_u8; crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
|
||||
crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Secret, secret_nonce, std::vec![0_u8; crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
|
||||
crate::WalletStateSignatureV1::new([0_u8; crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES]),
|
||||
);
|
||||
}
|
||||
|
||||
async fn derive_owner_password_key_async(
|
||||
password: crate::OwnerPassword,
|
||||
kdf: crate::WalletKdfParametersV1,
|
||||
) -> ksp_core_lib::Result<crate::crypto::SecretKeyV1> {
|
||||
let task = tokio::task::spawn_blocking(move || return crate::crypto::derive_password_key(password.as_bytes(), &kdf));
|
||||
return match task.await {
|
||||
std::result::Result::Ok(result) => result,
|
||||
std::result::Result::Err(_) => std::result::Result::Err(crypto_operation_error()),
|
||||
};
|
||||
}
|
||||
|
||||
async fn derive_view_password_key_async(password: crate::ViewPassword, kdf: crate::WalletKdfParametersV1) -> ksp_core_lib::Result<crate::crypto::SecretKeyV1> {
|
||||
let task = tokio::task::spawn_blocking(move || return crate::crypto::derive_password_key(password.as_bytes(), &kdf));
|
||||
return match task.await {
|
||||
std::result::Result::Ok(result) => result,
|
||||
std::result::Result::Err(_) => std::result::Result::Err(crypto_operation_error()),
|
||||
};
|
||||
}
|
||||
|
||||
fn pubkey_from_keypair_bytes(bytes: &[u8]) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
|
||||
let public_slice = match bytes.get(32..64) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(key_material_error()),
|
||||
};
|
||||
let public_array = match <[u8; 32]>::try_from(public_slice) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(key_material_error()),
|
||||
};
|
||||
return std::result::Result::Ok(ksp_core_lib::Pubkey::new_from_array(public_array));
|
||||
}
|
||||
|
||||
fn cleanup_create_error<T>(
|
||||
error: ksp_core_lib::Error,
|
||||
owner_control_plaintext: &mut [u8],
|
||||
metadata_plaintext: &mut [u8],
|
||||
solana_keypair_bytes: &mut [u8],
|
||||
) -> ksp_core_lib::Result<T> {
|
||||
owner_control_plaintext.zeroize();
|
||||
metadata_plaintext.zeroize();
|
||||
solana_keypair_bytes.zeroize();
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
|
||||
fn authentication_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_AUTHENTICATION_FAILED, "Wallet OWNER-authenticated state verification failed");
|
||||
}
|
||||
|
||||
fn crypto_operation_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_CRYPTO_OPERATION_FAILED, "Wallet blocking cryptographic operation could not complete");
|
||||
}
|
||||
|
||||
fn view_unlock_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_VIEW_UNLOCK_FAILED, "Wallet VIEW capability could not be unlocked");
|
||||
}
|
||||
|
||||
fn owner_unlock_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_OWNER_UNLOCK_FAILED, "Wallet OWNER capability could not be unlocked");
|
||||
}
|
||||
|
||||
fn capability_error(message: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_CAPABILITY_INSUFFICIENT, message);
|
||||
}
|
||||
|
||||
fn key_material_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_KEY_MATERIAL_INVALID, "Wallet Solana key material is invalid or inconsistent");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/wallet.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/wire.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Strict native `.kspwallet` V1 wire envelope.
|
||||
|
||||
@@ -105,6 +105,17 @@ pub struct WalletKdfParametersV1 {
|
||||
}
|
||||
|
||||
impl WalletKdfParametersV1 {
|
||||
pub(crate) fn new_creation(salt: std::vec::Vec<u8>) -> Self {
|
||||
return Self {
|
||||
algorithm: WalletKdfAlgorithmV1::Argon2id,
|
||||
version: crate::KSPWALLET_V1_ARGON2_VERSION,
|
||||
memory_kib: crate::KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB,
|
||||
iterations: crate::KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS,
|
||||
parallelism: crate::KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM,
|
||||
salt,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the KDF algorithm.
|
||||
#[must_use]
|
||||
pub const fn algorithm(&self) -> WalletKdfAlgorithmV1 {
|
||||
@@ -165,6 +176,10 @@ pub struct WalletKeyWrapV1 {
|
||||
}
|
||||
|
||||
impl WalletKeyWrapV1 {
|
||||
pub(crate) fn new(nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec<u8>) -> Self {
|
||||
return Self { algorithm: WalletAeadAlgorithmV1::XChaCha20Poly1305, nonce, ciphertext };
|
||||
}
|
||||
|
||||
/// Returns the wrapping AEAD algorithm.
|
||||
#[must_use]
|
||||
pub const fn algorithm(&self) -> WalletAeadAlgorithmV1 {
|
||||
@@ -205,6 +220,10 @@ pub struct WalletKeySlotV1 {
|
||||
}
|
||||
|
||||
impl WalletKeySlotV1 {
|
||||
pub(crate) fn new(slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES], role: WalletKeySlotRoleV1, kdf: WalletKdfParametersV1, wrap: WalletKeyWrapV1) -> Self {
|
||||
return Self { slot_id, role, kdf, wrap };
|
||||
}
|
||||
|
||||
/// Returns the stable 16-byte slot identifier.
|
||||
#[must_use]
|
||||
pub const fn slot_id(&self) -> &[u8; crate::KSPWALLET_V1_SLOT_ID_BYTES] {
|
||||
@@ -250,6 +269,14 @@ pub struct WalletViewDescriptorV1 {
|
||||
}
|
||||
|
||||
impl WalletViewDescriptorV1 {
|
||||
pub(crate) const fn disabled() -> Self {
|
||||
return Self { enabled: false, slot_id: std::option::Option::None };
|
||||
}
|
||||
|
||||
pub(crate) const fn enabled(slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES]) -> Self {
|
||||
return Self { enabled: true, slot_id: std::option::Option::Some(slot_id) };
|
||||
}
|
||||
|
||||
/// Reports whether a VIEW slot is enabled.
|
||||
#[must_use]
|
||||
pub const fn enabled(&self) -> bool {
|
||||
@@ -274,6 +301,16 @@ pub struct WalletEncryptedCompartmentV1 {
|
||||
}
|
||||
|
||||
impl WalletEncryptedCompartmentV1 {
|
||||
pub(crate) fn new(kind: WalletCompartmentKindV1, nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec<u8>) -> Self {
|
||||
return Self {
|
||||
kind,
|
||||
payload_version: crate::KSPWALLET_V1_INITIAL_PAYLOAD_VERSION,
|
||||
algorithm: WalletAeadAlgorithmV1::XChaCha20Poly1305,
|
||||
nonce,
|
||||
ciphertext,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the compartment kind.
|
||||
#[must_use]
|
||||
pub const fn kind(&self) -> WalletCompartmentKindV1 {
|
||||
@@ -326,6 +363,10 @@ pub struct WalletStateSignatureV1 {
|
||||
}
|
||||
|
||||
impl WalletStateSignatureV1 {
|
||||
pub(crate) const fn new(signature: [u8; crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES]) -> Self {
|
||||
return Self { algorithm: WalletStateSignatureAlgorithmV1::Ed25519, signature };
|
||||
}
|
||||
|
||||
/// Returns the state-signature algorithm.
|
||||
#[must_use]
|
||||
pub const fn algorithm(&self) -> WalletStateSignatureAlgorithmV1 {
|
||||
@@ -363,6 +404,28 @@ pub struct KspWalletEnvelopeV1 {
|
||||
}
|
||||
|
||||
impl KspWalletEnvelopeV1 {
|
||||
pub(crate) fn new_internal(
|
||||
owner_auth_public_key: [u8; crate::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES],
|
||||
view_descriptor: WalletViewDescriptorV1,
|
||||
owner_slot: WalletKeySlotV1,
|
||||
view_slot: std::option::Option<WalletKeySlotV1>,
|
||||
owner_control: WalletEncryptedCompartmentV1,
|
||||
metadata: WalletEncryptedCompartmentV1,
|
||||
secret: WalletEncryptedCompartmentV1,
|
||||
state_signature: WalletStateSignatureV1,
|
||||
) -> Self {
|
||||
return Self {
|
||||
owner_auth_public_key,
|
||||
view_descriptor,
|
||||
owner_slot,
|
||||
view_slot,
|
||||
owner_control,
|
||||
metadata,
|
||||
secret,
|
||||
state_signature,
|
||||
};
|
||||
}
|
||||
|
||||
/// Parses and semantically validates one complete `.kspwallet` V1 JSON document.
|
||||
///
|
||||
/// This function validates only the V1 wire grammar, identifiers, canonical Base64url representation, structural bounds and key-slot invariants.
|
||||
|
||||
Reference in New Issue
Block a user