v0.2.6-pre.010-fix.001
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 179
|
||||
# version: 180
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.6-pre.10"
|
||||
version = "0.2.6-pre.10.fix.1"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -328,9 +328,9 @@
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header fw-semibold">Rotation credentials OWNER</div>
|
||||
<div class="card-header fw-semibold">Rotation credentials OWNER / VIEW</div>
|
||||
<div class="card-body">
|
||||
<p class="small text-body-secondary">Les rotations nécessitent une session OWNER. Elles modifient le fichier Wallet, mais ne changent jamais automatiquement les secrets Config.</p>
|
||||
<p class="small text-body-secondary">OWNER peut administrer OWNER et VIEW sans ancien password VIEW. Une session VIEW peut self-rotate son propre password VIEW. Les rotations modifient le fichier Wallet sans changer automatiquement les secrets Config.</p>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="rotateOwnerPassword">Nouveau OWNER password</label>
|
||||
@@ -351,7 +351,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="rotationStatus" class="alert alert-secondary mt-3 mb-0" role="status">Session OWNER requise.</div>
|
||||
<div id="rotationStatus" class="alert alert-secondary mt-3 mb-0" role="status">Session VIEW ou OWNER requise pour une rotation VIEW ; OWNER requis pour une rotation OWNER.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
|
||||
// version: 10
|
||||
// version: 11
|
||||
|
||||
import { Modal } from "bootstrap";
|
||||
import DataTable from "datatables.net-bs5";
|
||||
@@ -234,12 +234,13 @@ function updateOwnerMetadataActions(): void {
|
||||
|
||||
function updateSecurityRotationActions(): void {
|
||||
const ownerOpen = activeSessionState === "owner_open" && authorizedWalletProjection?.capability === "owner";
|
||||
const viewOpen = activeSessionState === "view_open" && authorizedWalletProjection?.capability === "view";
|
||||
document.querySelectorAll<HTMLInputElement | HTMLButtonElement>("[data-owner-rotation-control]").forEach(control => {
|
||||
control.disabled = !ownerOpen;
|
||||
});
|
||||
const viewControls = document.querySelectorAll<HTMLInputElement | HTMLButtonElement>("[data-view-rotation-control]");
|
||||
viewControls.forEach(control => {
|
||||
control.disabled = !ownerOpen || !activeViewEnabled;
|
||||
control.disabled = (!ownerOpen && !viewOpen) || !activeViewEnabled;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -442,8 +443,8 @@ function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void {
|
||||
setText(
|
||||
"#rotationStatus",
|
||||
wallet.capability === "owner"
|
||||
? "OWNER ouvert. Une rotation modifie le fichier Wallet uniquement ; les secrets Config restent inchangés."
|
||||
: "Session OWNER requise pour effectuer une rotation de credential.",
|
||||
? "OWNER ouvert. Rotation OWNER et administration VIEW disponibles ; les secrets Config restent inchangés."
|
||||
: "VIEW ouvert. Self-rotation VIEW disponible ; OWNER reste inaccessible.",
|
||||
);
|
||||
updateSessionActions();
|
||||
frontendDebug("main", "Authorized Wallet session rendered", {
|
||||
@@ -561,6 +562,25 @@ async function recoverOwnerStateConflict(walletId: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function recoverRotationStateConflict(walletId: string, capability: UnlockCapability): Promise<void> {
|
||||
clearAuthorizedProjection();
|
||||
clearBalanceProjection();
|
||||
const request: WalletSelectionRequestDto = { walletId };
|
||||
try {
|
||||
const wallet = await invokeKsp<LockedWalletDto>("main", "select_wallet", { request });
|
||||
renderLockedWallet(wallet);
|
||||
setText("#rotationStatus", `Conflit d’état : rotation ${capability.toUpperCase()} interrompue, wallet reverrouillé.`);
|
||||
setText("#unlockStatus", `Conflit d’état détecté : réautoriser ${capability.toUpperCase()} explicitement avant une nouvelle rotation.`);
|
||||
await loadWalletInventory("list_wallets", false);
|
||||
activateView("security", "user");
|
||||
frontendWarn("main", "Wallet state conflict forced credential reauthorization", { capability, walletId });
|
||||
} catch {
|
||||
clearSelectedWallet();
|
||||
setText("#rotationStatus", "Conflit d’état détecté ; la réinspection du wallet a échoué.");
|
||||
frontendWarn("main", "Wallet credential rotation state conflict recovery failed", { capability, walletId });
|
||||
}
|
||||
}
|
||||
|
||||
async function runOwnerMetadataMutation(
|
||||
command: "add_wallet_note" | "delete_wallet_note" | "update_wallet_alias" | "update_wallet_note",
|
||||
args: Record<string, unknown>,
|
||||
@@ -963,9 +983,12 @@ async function unlockConfigured(capability: UnlockCapability): Promise<void> {
|
||||
}
|
||||
|
||||
async function rotateWalletPassword(capability: UnlockCapability): Promise<void> {
|
||||
if (activeSessionState !== "owner_open" || !activeWalletId || authorizedWalletProjection?.capability !== "owner") {
|
||||
const sessionCapability = authorizedWalletProjection?.capability;
|
||||
const ownerAuthorized = activeSessionState === "owner_open" && sessionCapability === "owner";
|
||||
const viewAuthorized = activeSessionState === "view_open" && sessionCapability === "view";
|
||||
if (!activeWalletId || (capability === "owner" && !ownerAuthorized) || (capability === "view" && !ownerAuthorized && !viewAuthorized)) {
|
||||
clearRotationSensitiveInputs();
|
||||
setText("#rotationStatus", "Session OWNER requise.");
|
||||
setText("#rotationStatus", capability === "owner" ? "Session OWNER requise." : "Session VIEW ou OWNER requise.");
|
||||
return;
|
||||
}
|
||||
if (capability === "view" && !activeViewEnabled) {
|
||||
@@ -1012,13 +1035,13 @@ async function rotateWalletPassword(capability: UnlockCapability): Promise<void>
|
||||
} catch (caughtError) {
|
||||
clearRotationSensitiveInputs();
|
||||
if (isWalletStateConflict(caughtError)) {
|
||||
await recoverOwnerStateConflict(walletId);
|
||||
await recoverRotationStateConflict(walletId, sessionCapability === "view" ? "view" : "owner");
|
||||
return;
|
||||
}
|
||||
if (activeWalletId === walletId) {
|
||||
activeSessionState = "owner_open";
|
||||
setText("#currentWalletState", "OWNER open");
|
||||
setText("#rotationStatus", "Rotation refusée ; la session OWNER reste ouverte.");
|
||||
if (activeWalletId === walletId && sessionCapability) {
|
||||
activeSessionState = sessionCapability === "view" ? "view_open" : "owner_open";
|
||||
setText("#currentWalletState", `${sessionCapability.toUpperCase()} open`);
|
||||
setText("#rotationStatus", `Rotation refusée ; la session ${sessionCapability.toUpperCase()} reste ouverte.`);
|
||||
updateSessionActions();
|
||||
}
|
||||
frontendWarn("main", "Wallet credential rotation failed", { capability, walletId });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ksp-app-wallet-desk",
|
||||
"private": true,
|
||||
"version": "0.2.6-pre.10",
|
||||
"version": "0.2.6-pre.10.fix.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/app_state.rs
|
||||
// version: 13
|
||||
// version: 14
|
||||
|
||||
//! Shared backend state owned by the Wallet Desk Tauri application.
|
||||
|
||||
@@ -483,6 +483,18 @@ impl AppState {
|
||||
"Wallet unlock operation is already in progress",
|
||||
));
|
||||
},
|
||||
crate::WalletSession::ViewOperation { wallet_id, path, pubkey } => {
|
||||
let session = self.wallet_session.lock();
|
||||
let mut session = match session {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()),
|
||||
};
|
||||
*session = crate::WalletSession::ViewOperation { wallet_id, path, pubkey };
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_WALLET_SESSION_INVALID,
|
||||
"Wallet VIEW privileged operation is already in progress",
|
||||
));
|
||||
},
|
||||
crate::WalletSession::OwnerOperation { wallet_id, path, pubkey, view_enabled } => {
|
||||
let session = self.wallet_session.lock();
|
||||
let mut session = match session {
|
||||
@@ -722,23 +734,146 @@ impl AppState {
|
||||
return self.finish_owner_operation(context, result).await;
|
||||
}
|
||||
|
||||
/// Rotates the enabled VIEW password from the current OWNER session without requiring the previous VIEW password.
|
||||
/// Rotates the enabled VIEW password from either VIEW self-service or OWNER administration.
|
||||
pub(crate) async fn rotate_view_password(&self, request: crate::WalletPasswordRotationRequestDto) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
||||
let context = self.begin_owner_operation("view_password_rotate");
|
||||
let owner_context = self.begin_owner_operation("view_password_rotate");
|
||||
if let std::result::Result::Ok(mut context) = owner_context {
|
||||
if !context.view_enabled {
|
||||
self.restore_owner_after_operation_failure(context);
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_WALLET_VIEW_DISABLED,
|
||||
"Wallet VIEW capability is disabled and cannot rotate a VIEW password",
|
||||
));
|
||||
}
|
||||
let password = ksp_wallet_lib::ViewPassword::new(request.password);
|
||||
let result = context.owner.rotate_view_password(context.path.as_path(), password).await;
|
||||
return self.finish_owner_operation(context, result).await;
|
||||
}
|
||||
let context = self.begin_view_operation("view_password_self_rotate");
|
||||
let mut context = match context {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if !context.view_enabled {
|
||||
self.restore_owner_after_operation_failure(context);
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_WALLET_VIEW_DISABLED,
|
||||
"Wallet VIEW capability is disabled and cannot rotate a VIEW password",
|
||||
));
|
||||
}
|
||||
let password = ksp_wallet_lib::ViewPassword::new(request.password);
|
||||
let result = context.owner.rotate_view_password(context.path.as_path(), password).await;
|
||||
return self.finish_owner_operation(context, result).await;
|
||||
let result = context.view.rotate_view_password(context.path.as_path(), password).await;
|
||||
return self.finish_view_operation(context, result).await;
|
||||
}
|
||||
|
||||
fn begin_view_operation(&self, operation: &'static str) -> ksp_core_lib::Result<ViewOperationContext> {
|
||||
let session = self.wallet_session.lock();
|
||||
let mut session = match session {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()),
|
||||
};
|
||||
let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection());
|
||||
let (wallet_id, path, view) = match previous {
|
||||
crate::WalletSession::View { wallet_id, path, wallet } => (wallet_id, path, wallet),
|
||||
other => {
|
||||
*session = other;
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_WALLET_AUTHORIZATION_REQUIRED,
|
||||
"Wallet VIEW self-service operation requires an authorized VIEW session",
|
||||
));
|
||||
},
|
||||
};
|
||||
let pubkey = view.pubkey().to_owned();
|
||||
let context = ViewOperationContext { operation, path: path.clone(), pubkey, view, wallet_id: wallet_id.clone() };
|
||||
*session = crate::WalletSession::ViewOperation { wallet_id, path, pubkey };
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = context.wallet_id.as_str(), operation, "Wallet VIEW privileged operation started");
|
||||
return std::result::Result::Ok(context);
|
||||
}
|
||||
|
||||
async fn finish_view_operation(
|
||||
&self,
|
||||
context: ViewOperationContext,
|
||||
operation_result: ksp_core_lib::Result<()>,
|
||||
) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
||||
if let std::result::Result::Err(error) = operation_result {
|
||||
if error.code() == ksp_wallet_lib::ERROR_CODE_STATE_CONFLICT {
|
||||
self.recover_view_state_conflict(context).await;
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
self.restore_view_after_operation_failure(context);
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return self.install_view_after_operation_success(context);
|
||||
}
|
||||
|
||||
fn install_view_after_operation_success(&self, context: ViewOperationContext) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
||||
let ViewOperationContext { operation, path, pubkey, view, wallet_id } = context;
|
||||
let configured_secret_candidate_count = self.secret_candidate_count_or_zero(wallet_id.as_str());
|
||||
let dto = crate::view_projection(wallet_id.as_str(), true, configured_secret_candidate_count, &view);
|
||||
let session = self.wallet_session.lock();
|
||||
let mut session = match session {
|
||||
std::result::Result::Ok(value) => value,
|
||||
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 {
|
||||
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);
|
||||
},
|
||||
other => {
|
||||
*session = other;
|
||||
drop(view);
|
||||
return 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) {
|
||||
let ViewOperationContext { operation, path, pubkey, view, wallet_id } = context;
|
||||
let session = self.wallet_session.lock();
|
||||
let mut session = match session {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection());
|
||||
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::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet VIEW privileged operation failed without invalidating the authenticated handle");
|
||||
},
|
||||
other => {
|
||||
*session = other;
|
||||
drop(view);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn recover_view_state_conflict(&self, context: ViewOperationContext) {
|
||||
let ViewOperationContext { operation, path, pubkey, view, wallet_id } = context;
|
||||
drop(view);
|
||||
let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await;
|
||||
let session = self.wallet_session.lock();
|
||||
let mut session = match session {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection());
|
||||
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 =>
|
||||
{
|
||||
if let std::result::Result::Ok(locked_info) = inspected {
|
||||
*session = crate::WalletSession::Locked { wallet_id: wallet_id.clone(), path, locked_info };
|
||||
}
|
||||
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet VIEW state conflict purged the stale handle and forced reauthorization");
|
||||
},
|
||||
other => {
|
||||
*session = other;
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn begin_owner_operation(&self, operation: &'static str) -> ksp_core_lib::Result<OwnerOperationContext> {
|
||||
@@ -920,7 +1055,7 @@ impl AppState {
|
||||
crate::WalletSession::Owner { wallet_id, wallet, .. } => {
|
||||
std::result::Result::Ok(WalletBalanceContext { capability: "owner", pubkey: wallet.pubkey().to_owned(), wallet_id: wallet_id.clone() })
|
||||
},
|
||||
crate::WalletSession::OwnerOperation { .. } | crate::WalletSession::PrivilegedOperation { .. } => {
|
||||
crate::WalletSession::ViewOperation { .. } | crate::WalletSession::OwnerOperation { .. } | crate::WalletSession::PrivilegedOperation { .. } => {
|
||||
std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_WALLET_SESSION_INVALID,
|
||||
"Wallet balance is unavailable while a privileged Wallet operation is running",
|
||||
@@ -948,6 +1083,7 @@ impl AppState {
|
||||
},
|
||||
crate::WalletSession::NoSelection
|
||||
| crate::WalletSession::Locked { .. }
|
||||
| crate::WalletSession::ViewOperation { .. }
|
||||
| crate::WalletSession::OwnerOperation { .. }
|
||||
| crate::WalletSession::PrivilegedOperation { .. } => false,
|
||||
};
|
||||
@@ -995,6 +1131,14 @@ fn configured_secret_missing_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_SECRET_CANDIDATES_MISSING, "No effective Config-owned Wallet password candidate is available");
|
||||
}
|
||||
|
||||
struct ViewOperationContext {
|
||||
operation: &'static str,
|
||||
path: std::path::PathBuf,
|
||||
pubkey: ksp_core_lib::Pubkey,
|
||||
view: std::boxed::Box<ksp_wallet_lib::WalletView>,
|
||||
wallet_id: String,
|
||||
}
|
||||
|
||||
struct OwnerOperationContext {
|
||||
operation: &'static str,
|
||||
owner: std::boxed::Box<ksp_wallet_lib::WalletOwner>,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/lib.rs
|
||||
// version: 10
|
||||
// version: 11
|
||||
|
||||
//! Tauri desktop application shell for KSP Wallet management and inspection.
|
||||
|
||||
@@ -200,7 +200,7 @@ pub(crate) use self::wallet_metadata::WalletNoteUpdateRequestDto;
|
||||
pub(crate) use self::wallet_secrets::discover_wallet_secret_candidates;
|
||||
/// Returns the configured Wallet password candidate count without exposing their names or values.
|
||||
pub(crate) use self::wallet_secrets::wallet_secret_candidate_count;
|
||||
/// New credential request moved frontend -> Rust only for one explicit OWNER-authorized rotation.
|
||||
/// New credential request moved frontend -> Rust only for one explicit VIEW/OWNER-authorized rotation.
|
||||
pub(crate) use self::wallet_security::WalletPasswordRotationRequestDto;
|
||||
/// Authorized OWNER projection exposed only after successful creation.
|
||||
pub(crate) use self::wallet_session::WalletAuthorizedDto;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/wallet_security.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! OWNER-only credential rotation contracts for Wallet Desk.
|
||||
//! VIEW/OWNER credential rotation contracts for Wallet Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/wallet_session.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Durable root-scoped Wallet session lifecycle for Wallet Desk.
|
||||
|
||||
@@ -14,7 +14,7 @@ pub(crate) enum WalletSessionStateDto {
|
||||
NoSelection,
|
||||
/// One Wallet is selected but remains locked.
|
||||
Locked,
|
||||
/// One explicit unlock operation is running in Rust.
|
||||
/// One explicit privileged Wallet operation is running in Rust.
|
||||
PrivilegedOperation,
|
||||
/// One Wallet is open with VIEW capability.
|
||||
ViewOpen,
|
||||
@@ -150,6 +150,15 @@ pub(crate) enum WalletSession {
|
||||
/// Authorized VIEW handle retaining metadata capability only in Rust.
|
||||
wallet: std::boxed::Box<ksp_wallet_lib::WalletView>,
|
||||
},
|
||||
/// One VIEW-authorized self-service mutation temporarily owns the handle outside the session mutex.
|
||||
ViewOperation {
|
||||
/// Native Wallet filename / root-scoped identifier.
|
||||
wallet_id: String,
|
||||
/// Full root-scoped filesystem path retained only in Rust.
|
||||
path: std::path::PathBuf,
|
||||
/// Authorized Solana identity used only to reject stale completion.
|
||||
pubkey: ksp_core_lib::Pubkey,
|
||||
},
|
||||
/// One Wallet kept open with OWNER capability.
|
||||
Owner {
|
||||
/// Native Wallet filename / root-scoped identifier.
|
||||
@@ -208,6 +217,11 @@ impl WalletSession {
|
||||
state: WalletSessionStateDto::ViewOpen,
|
||||
wallet_id: std::option::Option::Some(wallet_id.clone()),
|
||||
},
|
||||
Self::ViewOperation { wallet_id, .. } => WalletSessionDto {
|
||||
filename: std::option::Option::Some(wallet_id.clone()),
|
||||
state: WalletSessionStateDto::PrivilegedOperation,
|
||||
wallet_id: std::option::Option::Some(wallet_id.clone()),
|
||||
},
|
||||
Self::Owner { wallet_id, .. } => WalletSessionDto {
|
||||
filename: std::option::Option::Some(wallet_id.clone()),
|
||||
state: WalletSessionStateDto::OwnerOpen,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "KSP Wallet Desk",
|
||||
"version": "0.2.6-pre.10",
|
||||
"version": "0.2.6-pre.10.fix.1",
|
||||
"identifier": "com.sasedev.ksp-app-wallet-desk",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
|
||||
// version: 10
|
||||
// version: 11
|
||||
|
||||
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
|
||||
|
||||
@@ -255,7 +255,7 @@ fn pre_009_owner_metadata_administration_uses_wallet_owner_and_refreshes_project
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_010_owner_authorized_credential_rotations_preserve_session_projection() {
|
||||
fn pre_010_owner_and_view_authorized_credential_rotations_preserve_session_projection() {
|
||||
let root = app_root();
|
||||
let security = read_text(root.join("src/wallet_security.rs").as_path());
|
||||
let state = read_text(root.join("src/app_state.rs").as_path());
|
||||
@@ -265,6 +265,9 @@ fn pre_010_owner_authorized_credential_rotations_preserve_session_projection() {
|
||||
assert!(security.contains("WalletPasswordRotationRequestDto"));
|
||||
assert!(state.contains("owner.rotate_owner_password"));
|
||||
assert!(state.contains("owner.rotate_view_password"));
|
||||
assert!(state.contains("view.rotate_view_password"));
|
||||
assert!(state.contains("begin_view_operation"));
|
||||
assert!(state.contains("finish_view_operation"));
|
||||
assert!(state.contains("begin_owner_operation"));
|
||||
assert!(state.contains("finish_owner_operation"));
|
||||
assert!(tauri.contains("rotate_owner_password"));
|
||||
@@ -273,5 +276,6 @@ fn pre_010_owner_authorized_credential_rotations_preserve_session_projection() {
|
||||
assert!(html.contains("id=\"rotateViewPasswordSubmit\""));
|
||||
assert!(main.contains("rotateWalletPassword"));
|
||||
assert!(main.contains("bindRotationActions"));
|
||||
assert!(main.contains("viewAuthorized"));
|
||||
assert!(main.contains("Les secrets Config"));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/desktop_security.rs
|
||||
// version: 10
|
||||
// version: 11
|
||||
|
||||
//! Static desktop security contracts for the Wallet Desk pre.002 shell.
|
||||
|
||||
@@ -207,8 +207,12 @@ fn rotation_passwords_are_request_only_confirmed_frontend_side_and_never_logged(
|
||||
assert!(!fields.contains("confirm"));
|
||||
assert!(state.contains("OwnerPassword::new(request.password)"));
|
||||
assert!(state.contains("ViewPassword::new(request.password)"));
|
||||
assert!(state.contains("view.rotate_view_password"));
|
||||
assert!(state.contains("owner.rotate_view_password"));
|
||||
assert!(main.contains("password.value !== confirmation.value"));
|
||||
assert!(main.contains("clearRotationSensitiveInputs"));
|
||||
assert!(main.contains("viewAuthorized"));
|
||||
assert!(main.contains("recoverRotationStateConflict"));
|
||||
assert!(!main.contains("frontendDebug(\"main\", request.password"));
|
||||
assert!(!main.contains("frontendTrace(\"main\", request.password"));
|
||||
assert!(!main.contains("KSP_SECRET_WALLET_PASS_"));
|
||||
|
||||
42
deltas/0.2.6/pre.010-fix.001.md
Normal file
42
deltas/0.2.6/pre.010-fix.001.md
Normal file
@@ -0,0 +1,42 @@
|
||||
<!-- file: deltas/0.2.6/pre.010-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.6-pre.010-fix.001` — self-rotation VIEW
|
||||
|
||||
## Objet
|
||||
|
||||
Compléter `pre.010` pour respecter le contrat de capacité de `ksp-wallet-lib` : un détenteur VIEW déjà autorisé doit pouvoir changer son propre password VIEW, tandis qu'OWNER conserve son pouvoir administratif de rotation VIEW sans ancien password VIEW.
|
||||
|
||||
## Changements
|
||||
|
||||
- `workspace.package.version` passe à `0.2.6-pre.10.fix.1` ;
|
||||
- ajout de l'état Rust-only `WalletSession::ViewOperation` pour déplacer un `WalletView` hors mutex pendant Argon2/persistence ;
|
||||
- `rotate_view_password` accepte désormais une session VIEW ou OWNER :
|
||||
- VIEW -> `WalletView::rotate_view_password` -> session VIEW conservée ;
|
||||
- OWNER -> `WalletOwner::rotate_view_password` -> session OWNER conservée ;
|
||||
- `wallet.state_conflict` côté VIEW détruit le handle stale, réinspecte le fichier et force une nouvelle autorisation VIEW ;
|
||||
- Lock/balance considèrent `ViewOperation` comme une opération privilégiée en cours ;
|
||||
- UI : contrôles Rotate VIEW actifs en VIEW comme en OWNER, Rotate OWNER reste strictement OWNER-only ;
|
||||
- la récupération frontend de conflit de rotation exige la réautorisation de la capacité initiatrice ;
|
||||
- canaries contract/security étendus aux deux chemins de rotation VIEW ;
|
||||
- aucun secret Config n'est modifié implicitement.
|
||||
|
||||
## Hors changement
|
||||
|
||||
- aucun changement du format `.kspwallet` ;
|
||||
- aucun changement dans `ksp-wallet-lib` ;
|
||||
- aucune nouvelle dépendance ;
|
||||
- `ROADMAP.md`, `CHANGELOG.md` et `docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md` restent inchangés.
|
||||
|
||||
## Validation opérateur
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-app-wallet-desk
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Runtime utile : ouvrir VIEW, Rotate VIEW, Lock, vérifier ancien VIEW refusé / nouveau VIEW accepté ; puis ouvrir OWNER et vérifier que Rotate VIEW et Rotate OWNER restent fonctionnels.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md -->
|
||||
<!-- version: 24 -->
|
||||
<!-- version: 25 -->
|
||||
|
||||
# Plan `0.2.6` — Wallet Desk
|
||||
|
||||
@@ -1515,14 +1515,16 @@ Objectifs réalisés :
|
||||
```text
|
||||
rotate OWNER password depuis une session OWNER
|
||||
rotate VIEW password depuis une session OWNER lorsque VIEW est activé
|
||||
self-rotate VIEW password depuis une session VIEW autorisée
|
||||
double saisie/confirmation uniquement dans le frontend
|
||||
un seul nouveau password traverse IPC vers Rust
|
||||
conversion immédiate en OwnerPassword / ViewPassword
|
||||
réutilisation du lifecycle OwnerOperation hors mutex pendant Argon2 + persistence
|
||||
wallet.state_conflict => purge du handle stale + retour Locked + re-unlock OWNER
|
||||
session OWNER conservée après succès ou échec non conflictuel
|
||||
wallet.state_conflict => purge du handle stale + retour Locked + re-unlock de la capacité initiatrice
|
||||
session OWNER ou VIEW conservée après succès ou échec non conflictuel
|
||||
Pubkey, alias, notes et identité Solana inchangés
|
||||
VIEW rotation sans ancien password VIEW conformément au contrat WalletOwner
|
||||
VIEW rotation administrative sans ancien password VIEW conformément au contrat WalletOwner
|
||||
VIEW self-rotation conformément au contrat WalletView
|
||||
secrets Config volontairement inchangés après rotation
|
||||
message UI explicite avant Lock/unlock configuré
|
||||
redaction/canaries request-only
|
||||
@@ -1530,7 +1532,9 @@ redaction/canaries request-only
|
||||
|
||||
La rotation ne choisit ni ne modifie implicitement un `KSP_SECRET_WALLET_PASS_*`. Plusieurs candidats Config peuvent viser le même filename et leur politique de maintenance appartient à Config/opérateur. Wallet Desk expose donc seulement le compteur sûr déjà existant et avertit qu'un candidat contenant l'ancien password cessera de fonctionner après Lock tant qu'il n'aura pas été mis à jour.
|
||||
|
||||
Les deux commandes retournent la projection OWNER autorisée après succès afin de conserver une session cohérente sans réexposer de credential. Le password de confirmation reste strictement local au frontend et n'est jamais envoyé à Rust.
|
||||
`rotate_owner_password` retourne une projection OWNER. `rotate_view_password` conserve la capacité initiatrice : projection OWNER pour l'administration OWNER, projection VIEW pour la self-rotation VIEW. Aucun credential n'est réexposé. Le password de confirmation reste strictement local au frontend et n'est jamais envoyé à Rust.
|
||||
|
||||
Le correctif `pre.010-fix.001` complète ce contrat : la personne déjà autorisée en VIEW peut changer son propre password VIEW via `WalletView::rotate_view_password`, tandis qu'OWNER conserve le pouvoir administratif de remplacer le password VIEW sans connaître l'ancien. Wallet Desk réserve `ViewOperation` pendant l'Argon2/persistence async, refuse les complétions stale et applique au chemin VIEW la même politique de recovery `wallet.state_conflict` que pour OWNER.
|
||||
|
||||
### `pre.011` — strong VIEW disable/recreate
|
||||
|
||||
|
||||
Reference in New Issue
Block a user