v0.2.6-pre.010-fix.001
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 179
|
# version: 180
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
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"]
|
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]
|
[workspace.package]
|
||||||
version = "0.2.6-pre.10"
|
version = "0.2.6-pre.10.fix.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
@@ -328,9 +328,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card shadow-sm">
|
<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">
|
<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="row g-3">
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<label class="form-label" for="rotateOwnerPassword">Nouveau OWNER password</label>
|
<label class="form-label" for="rotateOwnerPassword">Nouveau OWNER password</label>
|
||||||
@@ -351,7 +351,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
|
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
|
||||||
// version: 10
|
// version: 11
|
||||||
|
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
import DataTable from "datatables.net-bs5";
|
import DataTable from "datatables.net-bs5";
|
||||||
@@ -234,12 +234,13 @@ function updateOwnerMetadataActions(): void {
|
|||||||
|
|
||||||
function updateSecurityRotationActions(): void {
|
function updateSecurityRotationActions(): void {
|
||||||
const ownerOpen = activeSessionState === "owner_open" && authorizedWalletProjection?.capability === "owner";
|
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 => {
|
document.querySelectorAll<HTMLInputElement | HTMLButtonElement>("[data-owner-rotation-control]").forEach(control => {
|
||||||
control.disabled = !ownerOpen;
|
control.disabled = !ownerOpen;
|
||||||
});
|
});
|
||||||
const viewControls = document.querySelectorAll<HTMLInputElement | HTMLButtonElement>("[data-view-rotation-control]");
|
const viewControls = document.querySelectorAll<HTMLInputElement | HTMLButtonElement>("[data-view-rotation-control]");
|
||||||
viewControls.forEach(control => {
|
viewControls.forEach(control => {
|
||||||
control.disabled = !ownerOpen || !activeViewEnabled;
|
control.disabled = (!ownerOpen && !viewOpen) || !activeViewEnabled;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,8 +443,8 @@ function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void {
|
|||||||
setText(
|
setText(
|
||||||
"#rotationStatus",
|
"#rotationStatus",
|
||||||
wallet.capability === "owner"
|
wallet.capability === "owner"
|
||||||
? "OWNER ouvert. Une rotation modifie le fichier Wallet uniquement ; les secrets Config restent inchangés."
|
? "OWNER ouvert. Rotation OWNER et administration VIEW disponibles ; les secrets Config restent inchangés."
|
||||||
: "Session OWNER requise pour effectuer une rotation de credential.",
|
: "VIEW ouvert. Self-rotation VIEW disponible ; OWNER reste inaccessible.",
|
||||||
);
|
);
|
||||||
updateSessionActions();
|
updateSessionActions();
|
||||||
frontendDebug("main", "Authorized Wallet session rendered", {
|
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(
|
async function runOwnerMetadataMutation(
|
||||||
command: "add_wallet_note" | "delete_wallet_note" | "update_wallet_alias" | "update_wallet_note",
|
command: "add_wallet_note" | "delete_wallet_note" | "update_wallet_alias" | "update_wallet_note",
|
||||||
args: Record<string, unknown>,
|
args: Record<string, unknown>,
|
||||||
@@ -963,9 +983,12 @@ async function unlockConfigured(capability: UnlockCapability): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function rotateWalletPassword(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();
|
clearRotationSensitiveInputs();
|
||||||
setText("#rotationStatus", "Session OWNER requise.");
|
setText("#rotationStatus", capability === "owner" ? "Session OWNER requise." : "Session VIEW ou OWNER requise.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (capability === "view" && !activeViewEnabled) {
|
if (capability === "view" && !activeViewEnabled) {
|
||||||
@@ -1012,13 +1035,13 @@ async function rotateWalletPassword(capability: UnlockCapability): Promise<void>
|
|||||||
} catch (caughtError) {
|
} catch (caughtError) {
|
||||||
clearRotationSensitiveInputs();
|
clearRotationSensitiveInputs();
|
||||||
if (isWalletStateConflict(caughtError)) {
|
if (isWalletStateConflict(caughtError)) {
|
||||||
await recoverOwnerStateConflict(walletId);
|
await recoverRotationStateConflict(walletId, sessionCapability === "view" ? "view" : "owner");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (activeWalletId === walletId) {
|
if (activeWalletId === walletId && sessionCapability) {
|
||||||
activeSessionState = "owner_open";
|
activeSessionState = sessionCapability === "view" ? "view_open" : "owner_open";
|
||||||
setText("#currentWalletState", "OWNER open");
|
setText("#currentWalletState", `${sessionCapability.toUpperCase()} open`);
|
||||||
setText("#rotationStatus", "Rotation refusée ; la session OWNER reste ouverte.");
|
setText("#rotationStatus", `Rotation refusée ; la session ${sessionCapability.toUpperCase()} reste ouverte.`);
|
||||||
updateSessionActions();
|
updateSessionActions();
|
||||||
}
|
}
|
||||||
frontendWarn("main", "Wallet credential rotation failed", { capability, walletId });
|
frontendWarn("main", "Wallet credential rotation failed", { capability, walletId });
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ksp-app-wallet-desk",
|
"name": "ksp-app-wallet-desk",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.6-pre.10",
|
"version": "0.2.6-pre.10.fix.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-wallet-desk/src/app_state.rs
|
// file: crates/ksp-app-wallet-desk/src/app_state.rs
|
||||||
// version: 13
|
// version: 14
|
||||||
|
|
||||||
//! Shared backend state owned by the Wallet Desk Tauri application.
|
//! Shared backend state owned by the Wallet Desk Tauri application.
|
||||||
|
|
||||||
@@ -483,6 +483,18 @@ impl AppState {
|
|||||||
"Wallet unlock operation is already in progress",
|
"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 } => {
|
crate::WalletSession::OwnerOperation { wallet_id, path, pubkey, view_enabled } => {
|
||||||
let session = self.wallet_session.lock();
|
let session = self.wallet_session.lock();
|
||||||
let mut session = match session {
|
let mut session = match session {
|
||||||
@@ -722,23 +734,146 @@ impl AppState {
|
|||||||
return self.finish_owner_operation(context, result).await;
|
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> {
|
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 {
|
let mut context = match context {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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 password = ksp_wallet_lib::ViewPassword::new(request.password);
|
||||||
let result = context.owner.rotate_view_password(context.path.as_path(), password).await;
|
let result = context.view.rotate_view_password(context.path.as_path(), password).await;
|
||||||
return self.finish_owner_operation(context, result).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> {
|
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, .. } => {
|
crate::WalletSession::Owner { wallet_id, wallet, .. } => {
|
||||||
std::result::Result::Ok(WalletBalanceContext { capability: "owner", pubkey: wallet.pubkey().to_owned(), wallet_id: wallet_id.clone() })
|
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(
|
std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
crate::ERROR_CODE_WALLET_SESSION_INVALID,
|
crate::ERROR_CODE_WALLET_SESSION_INVALID,
|
||||||
"Wallet balance is unavailable while a privileged Wallet operation is running",
|
"Wallet balance is unavailable while a privileged Wallet operation is running",
|
||||||
@@ -948,6 +1083,7 @@ impl AppState {
|
|||||||
},
|
},
|
||||||
crate::WalletSession::NoSelection
|
crate::WalletSession::NoSelection
|
||||||
| crate::WalletSession::Locked { .. }
|
| crate::WalletSession::Locked { .. }
|
||||||
|
| crate::WalletSession::ViewOperation { .. }
|
||||||
| crate::WalletSession::OwnerOperation { .. }
|
| crate::WalletSession::OwnerOperation { .. }
|
||||||
| crate::WalletSession::PrivilegedOperation { .. } => false,
|
| 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");
|
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 {
|
struct OwnerOperationContext {
|
||||||
operation: &'static str,
|
operation: &'static str,
|
||||||
owner: std::boxed::Box<ksp_wallet_lib::WalletOwner>,
|
owner: std::boxed::Box<ksp_wallet_lib::WalletOwner>,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-wallet-desk/src/lib.rs
|
// file: crates/ksp-app-wallet-desk/src/lib.rs
|
||||||
// version: 10
|
// version: 11
|
||||||
|
|
||||||
//! Tauri desktop application shell for KSP Wallet management and inspection.
|
//! 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;
|
pub(crate) use self::wallet_secrets::discover_wallet_secret_candidates;
|
||||||
/// Returns the configured Wallet password candidate count without exposing their names or values.
|
/// Returns the configured Wallet password candidate count without exposing their names or values.
|
||||||
pub(crate) use self::wallet_secrets::wallet_secret_candidate_count;
|
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;
|
pub(crate) use self::wallet_security::WalletPasswordRotationRequestDto;
|
||||||
/// Authorized OWNER projection exposed only after successful creation.
|
/// Authorized OWNER projection exposed only after successful creation.
|
||||||
pub(crate) use self::wallet_session::WalletAuthorizedDto;
|
pub(crate) use self::wallet_session::WalletAuthorizedDto;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// file: crates/ksp-app-wallet-desk/src/wallet_security.rs
|
// 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
|
use ts_rs::TS; // rust-rules: trait-import
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-wallet-desk/src/wallet_session.rs
|
// file: crates/ksp-app-wallet-desk/src/wallet_session.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
//! Durable root-scoped Wallet session lifecycle for Wallet Desk.
|
//! Durable root-scoped Wallet session lifecycle for Wallet Desk.
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ pub(crate) enum WalletSessionStateDto {
|
|||||||
NoSelection,
|
NoSelection,
|
||||||
/// One Wallet is selected but remains locked.
|
/// One Wallet is selected but remains locked.
|
||||||
Locked,
|
Locked,
|
||||||
/// One explicit unlock operation is running in Rust.
|
/// One explicit privileged Wallet operation is running in Rust.
|
||||||
PrivilegedOperation,
|
PrivilegedOperation,
|
||||||
/// One Wallet is open with VIEW capability.
|
/// One Wallet is open with VIEW capability.
|
||||||
ViewOpen,
|
ViewOpen,
|
||||||
@@ -150,6 +150,15 @@ pub(crate) enum WalletSession {
|
|||||||
/// Authorized VIEW handle retaining metadata capability only in Rust.
|
/// Authorized VIEW handle retaining metadata capability only in Rust.
|
||||||
wallet: std::boxed::Box<ksp_wallet_lib::WalletView>,
|
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.
|
/// One Wallet kept open with OWNER capability.
|
||||||
Owner {
|
Owner {
|
||||||
/// Native Wallet filename / root-scoped identifier.
|
/// Native Wallet filename / root-scoped identifier.
|
||||||
@@ -208,6 +217,11 @@ impl WalletSession {
|
|||||||
state: WalletSessionStateDto::ViewOpen,
|
state: WalletSessionStateDto::ViewOpen,
|
||||||
wallet_id: std::option::Option::Some(wallet_id.clone()),
|
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 {
|
Self::Owner { wallet_id, .. } => WalletSessionDto {
|
||||||
filename: std::option::Option::Some(wallet_id.clone()),
|
filename: std::option::Option::Some(wallet_id.clone()),
|
||||||
state: WalletSessionStateDto::OwnerOpen,
|
state: WalletSessionStateDto::OwnerOpen,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "KSP Wallet Desk",
|
"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",
|
"identifier": "com.sasedev.ksp-app-wallet-desk",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
|
// 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.
|
//! 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]
|
#[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 root = app_root();
|
||||||
let security = read_text(root.join("src/wallet_security.rs").as_path());
|
let security = read_text(root.join("src/wallet_security.rs").as_path());
|
||||||
let state = read_text(root.join("src/app_state.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!(security.contains("WalletPasswordRotationRequestDto"));
|
||||||
assert!(state.contains("owner.rotate_owner_password"));
|
assert!(state.contains("owner.rotate_owner_password"));
|
||||||
assert!(state.contains("owner.rotate_view_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("begin_owner_operation"));
|
||||||
assert!(state.contains("finish_owner_operation"));
|
assert!(state.contains("finish_owner_operation"));
|
||||||
assert!(tauri.contains("rotate_owner_password"));
|
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!(html.contains("id=\"rotateViewPasswordSubmit\""));
|
||||||
assert!(main.contains("rotateWalletPassword"));
|
assert!(main.contains("rotateWalletPassword"));
|
||||||
assert!(main.contains("bindRotationActions"));
|
assert!(main.contains("bindRotationActions"));
|
||||||
|
assert!(main.contains("viewAuthorized"));
|
||||||
assert!(main.contains("Les secrets Config"));
|
assert!(main.contains("Les secrets Config"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-wallet-desk/tests/desktop_security.rs
|
// 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.
|
//! 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!(!fields.contains("confirm"));
|
||||||
assert!(state.contains("OwnerPassword::new(request.password)"));
|
assert!(state.contains("OwnerPassword::new(request.password)"));
|
||||||
assert!(state.contains("ViewPassword::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("password.value !== confirmation.value"));
|
||||||
assert!(main.contains("clearRotationSensitiveInputs"));
|
assert!(main.contains("clearRotationSensitiveInputs"));
|
||||||
|
assert!(main.contains("viewAuthorized"));
|
||||||
|
assert!(main.contains("recoverRotationStateConflict"));
|
||||||
assert!(!main.contains("frontendDebug(\"main\", request.password"));
|
assert!(!main.contains("frontendDebug(\"main\", request.password"));
|
||||||
assert!(!main.contains("frontendTrace(\"main\", request.password"));
|
assert!(!main.contains("frontendTrace(\"main\", request.password"));
|
||||||
assert!(!main.contains("KSP_SECRET_WALLET_PASS_"));
|
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 -->
|
<!-- file: docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md -->
|
||||||
<!-- version: 24 -->
|
<!-- version: 25 -->
|
||||||
|
|
||||||
# Plan `0.2.6` — Wallet Desk
|
# Plan `0.2.6` — Wallet Desk
|
||||||
|
|
||||||
@@ -1515,14 +1515,16 @@ Objectifs réalisés :
|
|||||||
```text
|
```text
|
||||||
rotate OWNER password depuis une session OWNER
|
rotate OWNER password depuis une session OWNER
|
||||||
rotate VIEW password depuis une session OWNER lorsque VIEW est activé
|
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
|
double saisie/confirmation uniquement dans le frontend
|
||||||
un seul nouveau password traverse IPC vers Rust
|
un seul nouveau password traverse IPC vers Rust
|
||||||
conversion immédiate en OwnerPassword / ViewPassword
|
conversion immédiate en OwnerPassword / ViewPassword
|
||||||
réutilisation du lifecycle OwnerOperation hors mutex pendant Argon2 + persistence
|
réutilisation du lifecycle OwnerOperation hors mutex pendant Argon2 + persistence
|
||||||
wallet.state_conflict => purge du handle stale + retour Locked + re-unlock OWNER
|
wallet.state_conflict => purge du handle stale + retour Locked + re-unlock de la capacité initiatrice
|
||||||
session OWNER conservée après succès ou échec non conflictuel
|
session OWNER ou VIEW conservée après succès ou échec non conflictuel
|
||||||
Pubkey, alias, notes et identité Solana inchangés
|
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
|
secrets Config volontairement inchangés après rotation
|
||||||
message UI explicite avant Lock/unlock configuré
|
message UI explicite avant Lock/unlock configuré
|
||||||
redaction/canaries request-only
|
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.
|
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
|
### `pre.011` — strong VIEW disable/recreate
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user