v0.2.6-pre.010
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 178
|
# version: 179
|
||||||
|
|
||||||
[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.9.fix.1"
|
version = "0.2.6-pre.10"
|
||||||
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"
|
||||||
|
|||||||
@@ -326,6 +326,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header fw-semibold">Rotation credentials OWNER</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>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<label class="form-label" for="rotateOwnerPassword">Nouveau OWNER password</label>
|
||||||
|
<input id="rotateOwnerPassword" class="form-control mb-2" type="password" autocomplete="new-password" data-owner-rotation-control disabled>
|
||||||
|
<label class="form-label" for="rotateOwnerPasswordConfirm">Confirmer OWNER password</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input id="rotateOwnerPasswordConfirm" class="form-control" type="password" autocomplete="new-password" data-owner-rotation-control disabled>
|
||||||
|
<button id="rotateOwnerPasswordSubmit" class="btn btn-outline-danger" type="button" data-owner-rotation-control disabled>Rotate OWNER</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<label class="form-label" for="rotateViewPassword">Nouveau VIEW password</label>
|
||||||
|
<input id="rotateViewPassword" class="form-control mb-2" type="password" autocomplete="new-password" data-view-rotation-control disabled>
|
||||||
|
<label class="form-label" for="rotateViewPasswordConfirm">Confirmer VIEW password</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input id="rotateViewPasswordConfirm" class="form-control" type="password" autocomplete="new-password" data-view-rotation-control disabled>
|
||||||
|
<button id="rotateViewPasswordSubmit" class="btn btn-outline-primary" type="button" data-view-rotation-control disabled>Rotate VIEW</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="rotationStatus" class="alert alert-secondary mt-3 mb-0" role="status">Session OWNER requise.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div id="unlockStatus" class="alert alert-secondary mb-0" role="status">Sélectionner un wallet verrouillé.</div>
|
<div id="unlockStatus" class="alert alert-secondary mb-0" role="status">Sélectionner un wallet verrouillé.</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -360,7 +389,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4">
|
<footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4">
|
||||||
<span>KSP Wallet Desk · OWNER metadata pre.009</span>
|
<span>KSP Wallet Desk · credential rotation pre.010</span>
|
||||||
</footer>
|
</footer>
|
||||||
<script type="module" src="./ts/main.ts"></script>
|
<script type="module" src="./ts/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -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: 9
|
// version: 10
|
||||||
|
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
import DataTable from "datatables.net-bs5";
|
import DataTable from "datatables.net-bs5";
|
||||||
@@ -21,6 +21,7 @@ import type { WalletAliasUpdateRequestDto } from "./bindings/ksp_app_wallet_desk
|
|||||||
import type { WalletNoteAddRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteAddRequestDto.ts";
|
import type { WalletNoteAddRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteAddRequestDto.ts";
|
||||||
import type { WalletNoteDeleteRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteDeleteRequestDto.ts";
|
import type { WalletNoteDeleteRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteDeleteRequestDto.ts";
|
||||||
import type { WalletNoteUpdateRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteUpdateRequestDto.ts";
|
import type { WalletNoteUpdateRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteUpdateRequestDto.ts";
|
||||||
|
import type { WalletPasswordRotationRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_security/WalletPasswordRotationRequestDto.ts";
|
||||||
import type { WalletAuthorizedDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletAuthorizedDto.ts";
|
import type { WalletAuthorizedDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletAuthorizedDto.ts";
|
||||||
import type { WalletCreateRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletCreateRequestDto.ts";
|
import type { WalletCreateRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletCreateRequestDto.ts";
|
||||||
import type { WalletSessionDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletSessionDto.ts";
|
import type { WalletSessionDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletSessionDto.ts";
|
||||||
@@ -183,7 +184,7 @@ function initializeWalletTable(): void {
|
|||||||
zeroRecords: "Aucun wallet correspondant.",
|
zeroRecords: "Aucun wallet correspondant.",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.008-wallet-import" });
|
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.010-credential-rotation" });
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
|
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
|
||||||
@@ -221,6 +222,7 @@ function updateSessionActions(): void {
|
|||||||
}
|
}
|
||||||
updateUnlockActions();
|
updateUnlockActions();
|
||||||
updateOwnerMetadataActions();
|
updateOwnerMetadataActions();
|
||||||
|
updateSecurityRotationActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateOwnerMetadataActions(): void {
|
function updateOwnerMetadataActions(): void {
|
||||||
@@ -230,6 +232,17 @@ function updateOwnerMetadataActions(): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateSecurityRotationActions(): void {
|
||||||
|
const ownerOpen = activeSessionState === "owner_open" && authorizedWalletProjection?.capability === "owner";
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateUnlockActions(): void {
|
function updateUnlockActions(): void {
|
||||||
const locked = activeSessionState === "locked";
|
const locked = activeSessionState === "locked";
|
||||||
const viewManual = document.querySelector<HTMLButtonElement>("#unlockViewManual");
|
const viewManual = document.querySelector<HTMLButtonElement>("#unlockViewManual");
|
||||||
@@ -319,6 +332,15 @@ function clearUnlockSensitiveInputs(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearRotationSensitiveInputs(): void {
|
||||||
|
for (const selector of ["#rotateOwnerPassword", "#rotateOwnerPasswordConfirm", "#rotateViewPassword", "#rotateViewPasswordConfirm"]) {
|
||||||
|
const input = document.querySelector<HTMLInputElement>(selector);
|
||||||
|
if (input) {
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function clearBalanceProjection(): void {
|
function clearBalanceProjection(): void {
|
||||||
setText("#currentWalletBalanceSol", "—");
|
setText("#currentWalletBalanceSol", "—");
|
||||||
setText("#detailsBalanceLamports", "—");
|
setText("#detailsBalanceLamports", "—");
|
||||||
@@ -340,7 +362,9 @@ function clearAuthorizedProjection(): void {
|
|||||||
setText("#detailsWalletNotes", "—");
|
setText("#detailsWalletNotes", "—");
|
||||||
clearBalanceProjection();
|
clearBalanceProjection();
|
||||||
clearCreateFormSensitiveInputs();
|
clearCreateFormSensitiveInputs();
|
||||||
|
clearRotationSensitiveInputs();
|
||||||
clearUnlockSensitiveInputs();
|
clearUnlockSensitiveInputs();
|
||||||
|
setText("#rotationStatus", "Session OWNER requise.");
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSelectedWallet(): void {
|
function clearSelectedWallet(): void {
|
||||||
@@ -415,6 +439,12 @@ function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void {
|
|||||||
clearBalanceProjection();
|
clearBalanceProjection();
|
||||||
setText("#balanceStatus", "Session autorisée. Refresh balance appelle getBalance avec la Pubkey détenue par Rust.");
|
setText("#balanceStatus", "Session autorisée. Refresh balance appelle getBalance avec la Pubkey détenue par Rust.");
|
||||||
setText("#unlockStatus", `${capabilityLabel} ouvert. Lock pour purger le handle autorisé.`);
|
setText("#unlockStatus", `${capabilityLabel} ouvert. Lock pour purger le handle autorisé.`);
|
||||||
|
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.",
|
||||||
|
);
|
||||||
updateSessionActions();
|
updateSessionActions();
|
||||||
frontendDebug("main", "Authorized Wallet session rendered", {
|
frontendDebug("main", "Authorized Wallet session rendered", {
|
||||||
capability: wallet.capability,
|
capability: wallet.capability,
|
||||||
@@ -510,7 +540,7 @@ function isWalletStateConflict(caughtError: unknown): boolean {
|
|||||||
return error.domain === "wallet" && error.code === "state_conflict";
|
return error.domain === "wallet" && error.code === "state_conflict";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function recoverOwnerMetadataStateConflict(walletId: string): Promise<void> {
|
async function recoverOwnerStateConflict(walletId: string): Promise<void> {
|
||||||
clearAuthorizedProjection();
|
clearAuthorizedProjection();
|
||||||
clearBalanceProjection();
|
clearBalanceProjection();
|
||||||
const request: WalletSelectionRequestDto = { walletId };
|
const request: WalletSelectionRequestDto = { walletId };
|
||||||
@@ -518,6 +548,7 @@ async function recoverOwnerMetadataStateConflict(walletId: string): Promise<void
|
|||||||
const wallet = await invokeKsp<LockedWalletDto>("main", "select_wallet", { request });
|
const wallet = await invokeKsp<LockedWalletDto>("main", "select_wallet", { request });
|
||||||
renderLockedWallet(wallet);
|
renderLockedWallet(wallet);
|
||||||
setText("#ownerMetadataStatus", "Le fichier wallet a changé hors de cette session. Le handle OWNER stale a été purgé ; unlock OWNER requis avant toute nouvelle mutation.");
|
setText("#ownerMetadataStatus", "Le fichier wallet a changé hors de cette session. Le handle OWNER stale a été purgé ; unlock OWNER requis avant toute nouvelle mutation.");
|
||||||
|
setText("#rotationStatus", "Conflit d’état : rotation interrompue, wallet reverrouillé et réautorisation OWNER obligatoire.");
|
||||||
setText("#unlockStatus", "Conflit d’état détecté : wallet réinspecté et reverrouillé. Réautoriser OWNER explicitement.");
|
setText("#unlockStatus", "Conflit d’état détecté : wallet réinspecté et reverrouillé. Réautoriser OWNER explicitement.");
|
||||||
await loadWalletInventory("list_wallets", false);
|
await loadWalletInventory("list_wallets", false);
|
||||||
activateView("security", "user");
|
activateView("security", "user");
|
||||||
@@ -525,6 +556,7 @@ async function recoverOwnerMetadataStateConflict(walletId: string): Promise<void
|
|||||||
} catch {
|
} catch {
|
||||||
clearSelectedWallet();
|
clearSelectedWallet();
|
||||||
setText("#ownerMetadataStatus", "Conflit d’état détecté et réinspection impossible. Resélectionner le wallet depuis l’inventaire.");
|
setText("#ownerMetadataStatus", "Conflit d’état détecté et réinspection impossible. Resélectionner le wallet depuis l’inventaire.");
|
||||||
|
setText("#rotationStatus", "Conflit d’état détecté ; la réinspection du wallet a échoué.");
|
||||||
frontendWarn("main", "Wallet state conflict recovery failed", { walletId });
|
frontendWarn("main", "Wallet state conflict recovery failed", { walletId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,7 +589,7 @@ async function runOwnerMetadataMutation(
|
|||||||
return true;
|
return true;
|
||||||
} catch (caughtError) {
|
} catch (caughtError) {
|
||||||
if (isWalletStateConflict(caughtError)) {
|
if (isWalletStateConflict(caughtError)) {
|
||||||
await recoverOwnerMetadataStateConflict(walletId);
|
await recoverOwnerStateConflict(walletId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (activeWalletId === walletId) {
|
if (activeWalletId === walletId) {
|
||||||
@@ -930,6 +962,79 @@ async function unlockConfigured(capability: UnlockCapability): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function rotateWalletPassword(capability: UnlockCapability): Promise<void> {
|
||||||
|
if (activeSessionState !== "owner_open" || !activeWalletId || authorizedWalletProjection?.capability !== "owner") {
|
||||||
|
clearRotationSensitiveInputs();
|
||||||
|
setText("#rotationStatus", "Session OWNER requise.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (capability === "view" && !activeViewEnabled) {
|
||||||
|
clearRotationSensitiveInputs();
|
||||||
|
setText("#rotationStatus", "VIEW est désactivé ; aucune rotation VIEW n’est possible.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const passwordSelector = capability === "owner" ? "#rotateOwnerPassword" : "#rotateViewPassword";
|
||||||
|
const confirmationSelector = capability === "owner" ? "#rotateOwnerPasswordConfirm" : "#rotateViewPasswordConfirm";
|
||||||
|
const password = document.querySelector<HTMLInputElement>(passwordSelector);
|
||||||
|
const confirmation = document.querySelector<HTMLInputElement>(confirmationSelector);
|
||||||
|
if (!password || !confirmation || password.value.length === 0) {
|
||||||
|
clearRotationSensitiveInputs();
|
||||||
|
setText("#rotationStatus", "Nouveau password et confirmation requis.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (password.value !== confirmation.value) {
|
||||||
|
clearRotationSensitiveInputs();
|
||||||
|
setText("#rotationStatus", "La confirmation ne correspond pas au nouveau password.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const walletId = activeWalletId;
|
||||||
|
const request: WalletPasswordRotationRequestDto = { password: password.value };
|
||||||
|
const command = capability === "owner" ? "rotate_owner_password" : "rotate_view_password";
|
||||||
|
activeSessionState = "privileged_operation";
|
||||||
|
setText("#currentWalletState", `Rotation ${capability.toUpperCase()}…`);
|
||||||
|
setText("#rotationStatus", "Rotation Argon2 en cours…");
|
||||||
|
updateSessionActions();
|
||||||
|
frontendDebug("main", "Wallet credential rotation requested", { capability, walletId });
|
||||||
|
try {
|
||||||
|
const wallet = await invokeKsp<WalletAuthorizedDto>("main", command, { request });
|
||||||
|
clearRotationSensitiveInputs();
|
||||||
|
if (activeWalletId !== walletId) {
|
||||||
|
frontendWarn("main", "Stale Wallet credential rotation response ignored", { capability, walletId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderAuthorizedWallet(wallet);
|
||||||
|
setText(
|
||||||
|
"#rotationStatus",
|
||||||
|
`${capability.toUpperCase()} password rotated. Les secrets Config (${configuredSecretCandidateCount} candidat(s)) ne sont pas modifiés automatiquement.`,
|
||||||
|
);
|
||||||
|
await loadWalletInventory("list_wallets", false);
|
||||||
|
frontendInfo("main", "Wallet credential rotation completed", { capability, walletId });
|
||||||
|
} catch (caughtError) {
|
||||||
|
clearRotationSensitiveInputs();
|
||||||
|
if (isWalletStateConflict(caughtError)) {
|
||||||
|
await recoverOwnerStateConflict(walletId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeWalletId === walletId) {
|
||||||
|
activeSessionState = "owner_open";
|
||||||
|
setText("#currentWalletState", "OWNER open");
|
||||||
|
setText("#rotationStatus", "Rotation refusée ; la session OWNER reste ouverte.");
|
||||||
|
updateSessionActions();
|
||||||
|
}
|
||||||
|
frontendWarn("main", "Wallet credential rotation failed", { capability, walletId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindRotationActions(): void {
|
||||||
|
document.querySelector<HTMLButtonElement>("#rotateOwnerPasswordSubmit")?.addEventListener("click", () => {
|
||||||
|
void rotateWalletPassword("owner");
|
||||||
|
});
|
||||||
|
document.querySelector<HTMLButtonElement>("#rotateViewPasswordSubmit")?.addEventListener("click", () => {
|
||||||
|
void rotateWalletPassword("view");
|
||||||
|
});
|
||||||
|
frontendTrace("main", "Wallet credential rotation handlers installed");
|
||||||
|
}
|
||||||
|
|
||||||
function bindUnlockActions(): void {
|
function bindUnlockActions(): void {
|
||||||
document.querySelector<HTMLButtonElement>("#unlockViewManual")?.addEventListener("click", () => {
|
document.querySelector<HTMLButtonElement>("#unlockViewManual")?.addEventListener("click", () => {
|
||||||
void unlockManual("view");
|
void unlockManual("view");
|
||||||
@@ -1033,7 +1138,7 @@ function renderRuntimeStatus(status: RuntimeStatusDto): void {
|
|||||||
setText("#runtimeEffectiveWalletsDirectory", status.effectiveWalletsDirectory);
|
setText("#runtimeEffectiveWalletsDirectory", status.effectiveWalletsDirectory);
|
||||||
setText("#runtimeWalletDirectoryCreated", status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent");
|
setText("#runtimeWalletDirectoryCreated", status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent");
|
||||||
setText("#runtimeShellPhase", status.shellPhase);
|
setText("#runtimeShellPhase", status.shellPhase);
|
||||||
setText("#shellStatus", "Config/Transport résolus ; import, VIEW/OWNER, metadata OWNER et getBalance prêts.");
|
setText("#shellStatus", "Config/Transport résolus ; import, VIEW/OWNER, metadata, rotations et getBalance prêts.");
|
||||||
frontendTrace("main", "Wallet Desk runtime status rendered", {
|
frontendTrace("main", "Wallet Desk runtime status rendered", {
|
||||||
compositeProfile: status.activeCompositeProfile,
|
compositeProfile: status.activeCompositeProfile,
|
||||||
fallbackLoggingActive: status.fallbackLoggingActive,
|
fallbackLoggingActive: status.fallbackLoggingActive,
|
||||||
@@ -1085,6 +1190,7 @@ async function initializeMain(): Promise<void> {
|
|||||||
bindUnlockActions();
|
bindUnlockActions();
|
||||||
bindBalanceActions();
|
bindBalanceActions();
|
||||||
bindOwnerMetadataActions();
|
bindOwnerMetadataActions();
|
||||||
|
bindRotationActions();
|
||||||
bindShellActions();
|
bindShellActions();
|
||||||
clearSelectedWallet();
|
clearSelectedWallet();
|
||||||
activateView("dashboard", "startup");
|
activateView("dashboard", "startup");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ksp-app-wallet-desk",
|
"name": "ksp-app-wallet-desk",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.6-pre.9.fix.1",
|
"version": "0.2.6-pre.10",
|
||||||
"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: 12
|
// version: 13
|
||||||
|
|
||||||
//! Shared backend state owned by the Wallet Desk Tauri application.
|
//! Shared backend state owned by the Wallet Desk Tauri application.
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ impl AppState {
|
|||||||
effective_wallets_directory_created_on_startup: self.wallet_config_startup.effective_directory_created_on_startup(),
|
effective_wallets_directory_created_on_startup: self.wallet_config_startup.effective_directory_created_on_startup(),
|
||||||
fallback_logging_active: runtime.fallback_active,
|
fallback_logging_active: runtime.fallback_active,
|
||||||
root_wallets_directory_created_on_startup: self.wallet_config_startup.root_directory_created_on_startup(),
|
root_wallets_directory_created_on_startup: self.wallet_config_startup.root_directory_created_on_startup(),
|
||||||
shell_phase: "pre.009-owner-metadata".to_owned(),
|
shell_phase: "pre.010-credential-rotation".to_owned(),
|
||||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||||
transport_available_endpoint_count,
|
transport_available_endpoint_count,
|
||||||
transport_clusters: self.transport_runtime.clusters(),
|
transport_clusters: self.transport_runtime.clusters(),
|
||||||
@@ -492,7 +492,7 @@ impl AppState {
|
|||||||
*session = crate::WalletSession::OwnerOperation { wallet_id, path, pubkey, view_enabled };
|
*session = crate::WalletSession::OwnerOperation { wallet_id, path, pubkey, view_enabled };
|
||||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
crate::ERROR_CODE_WALLET_SESSION_INVALID,
|
crate::ERROR_CODE_WALLET_SESSION_INVALID,
|
||||||
"Wallet OWNER metadata operation is already in progress",
|
"Wallet OWNER privileged operation is already in progress",
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
crate::WalletSession::View { wallet_id, path, wallet, .. } => {
|
crate::WalletSession::View { wallet_id, path, wallet, .. } => {
|
||||||
@@ -663,19 +663,19 @@ impl AppState {
|
|||||||
|
|
||||||
/// Updates or clears the protected alias through the current OWNER handle.
|
/// Updates or clears the protected alias through the current OWNER handle.
|
||||||
pub(crate) async fn update_wallet_alias(&self, request: crate::WalletAliasUpdateRequestDto) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
pub(crate) async fn update_wallet_alias(&self, request: crate::WalletAliasUpdateRequestDto) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
||||||
let context = self.begin_owner_metadata_operation("alias_update");
|
let context = self.begin_owner_operation("alias_update");
|
||||||
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),
|
||||||
};
|
};
|
||||||
let alias = normalize_optional_text(request.alias);
|
let alias = normalize_optional_text(request.alias);
|
||||||
let result = context.owner.update_alias(context.path.as_path(), alias).await;
|
let result = context.owner.update_alias(context.path.as_path(), alias).await;
|
||||||
return self.finish_owner_metadata_operation(context, result).await;
|
return self.finish_owner_operation(context, result).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Appends one protected note through the current OWNER handle.
|
/// Appends one protected note through the current OWNER handle.
|
||||||
pub(crate) async fn add_wallet_note(&self, request: crate::WalletNoteAddRequestDto) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
pub(crate) async fn add_wallet_note(&self, request: crate::WalletNoteAddRequestDto) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
||||||
let context = self.begin_owner_metadata_operation("note_add");
|
let context = self.begin_owner_operation("note_add");
|
||||||
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),
|
||||||
@@ -685,32 +685,63 @@ impl AppState {
|
|||||||
std::result::Result::Ok(_) => std::result::Result::Ok(()),
|
std::result::Result::Ok(_) => std::result::Result::Ok(()),
|
||||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
return self.finish_owner_metadata_operation(context, result).await;
|
return self.finish_owner_operation(context, result).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates one protected note through the current OWNER handle.
|
/// Updates one protected note through the current OWNER handle.
|
||||||
pub(crate) async fn update_wallet_note(&self, request: crate::WalletNoteUpdateRequestDto) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
pub(crate) async fn update_wallet_note(&self, request: crate::WalletNoteUpdateRequestDto) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
||||||
let context = self.begin_owner_metadata_operation("note_update");
|
let context = self.begin_owner_operation("note_update");
|
||||||
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),
|
||||||
};
|
};
|
||||||
let result = context.owner.update_note(context.path.as_path(), request.note_id.as_str(), request.text).await;
|
let result = context.owner.update_note(context.path.as_path(), request.note_id.as_str(), request.text).await;
|
||||||
return self.finish_owner_metadata_operation(context, result).await;
|
return self.finish_owner_operation(context, result).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes one protected note through the current OWNER handle.
|
/// Deletes one protected note through the current OWNER handle.
|
||||||
pub(crate) async fn delete_wallet_note(&self, request: crate::WalletNoteDeleteRequestDto) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
pub(crate) async fn delete_wallet_note(&self, request: crate::WalletNoteDeleteRequestDto) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
||||||
let context = self.begin_owner_metadata_operation("note_delete");
|
let context = self.begin_owner_operation("note_delete");
|
||||||
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),
|
||||||
};
|
};
|
||||||
let result = context.owner.delete_note(context.path.as_path(), request.note_id.as_str()).await;
|
let result = context.owner.delete_note(context.path.as_path(), request.note_id.as_str()).await;
|
||||||
return self.finish_owner_metadata_operation(context, result).await;
|
return self.finish_owner_operation(context, result).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn begin_owner_metadata_operation(&self, operation: &'static str) -> ksp_core_lib::Result<OwnerMetadataContext> {
|
/// Rotates the OWNER password while preserving the current authorized identity and metadata projection.
|
||||||
|
pub(crate) async fn rotate_owner_password(&self, request: crate::WalletPasswordRotationRequestDto) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
||||||
|
let context = self.begin_owner_operation("owner_password_rotate");
|
||||||
|
let mut context = match context {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let password = ksp_wallet_lib::OwnerPassword::new(request.password);
|
||||||
|
let result = context.owner.rotate_owner_password(context.path.as_path(), password).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.
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin_owner_operation(&self, operation: &'static str) -> ksp_core_lib::Result<OwnerOperationContext> {
|
||||||
let session = self.wallet_session.lock();
|
let session = self.wallet_session.lock();
|
||||||
let mut session = match session {
|
let mut session = match session {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
@@ -723,35 +754,35 @@ impl AppState {
|
|||||||
*session = other;
|
*session = other;
|
||||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
crate::ERROR_CODE_WALLET_AUTHORIZATION_REQUIRED,
|
crate::ERROR_CODE_WALLET_AUTHORIZATION_REQUIRED,
|
||||||
"Wallet metadata administration requires an authorized OWNER session",
|
"Wallet OWNER operation requires an authorized OWNER session",
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let pubkey = owner.pubkey().to_owned();
|
let pubkey = owner.pubkey().to_owned();
|
||||||
let context = OwnerMetadataContext { operation, owner, path: path.clone(), pubkey, view_enabled, wallet_id: wallet_id.clone() };
|
let context = OwnerOperationContext { operation, owner, path: path.clone(), pubkey, view_enabled, wallet_id: wallet_id.clone() };
|
||||||
*session = crate::WalletSession::OwnerOperation { wallet_id, path, pubkey, view_enabled };
|
*session = crate::WalletSession::OwnerOperation { wallet_id, path, pubkey, view_enabled };
|
||||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = context.wallet_id.as_str(), operation, "Wallet OWNER metadata operation started");
|
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = context.wallet_id.as_str(), operation, "Wallet OWNER privileged operation started");
|
||||||
return std::result::Result::Ok(context);
|
return std::result::Result::Ok(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn finish_owner_metadata_operation(
|
async fn finish_owner_operation(
|
||||||
&self,
|
&self,
|
||||||
context: OwnerMetadataContext,
|
context: OwnerOperationContext,
|
||||||
operation_result: ksp_core_lib::Result<()>,
|
operation_result: ksp_core_lib::Result<()>,
|
||||||
) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
||||||
if let std::result::Result::Err(error) = operation_result {
|
if let std::result::Result::Err(error) = operation_result {
|
||||||
if error.code() == ksp_wallet_lib::ERROR_CODE_STATE_CONFLICT {
|
if error.code() == ksp_wallet_lib::ERROR_CODE_STATE_CONFLICT {
|
||||||
self.recover_owner_metadata_state_conflict(context).await;
|
self.recover_owner_state_conflict(context).await;
|
||||||
return std::result::Result::Err(error);
|
return std::result::Result::Err(error);
|
||||||
}
|
}
|
||||||
self.restore_owner_after_metadata_failure(context);
|
self.restore_owner_after_operation_failure(context);
|
||||||
return std::result::Result::Err(error);
|
return std::result::Result::Err(error);
|
||||||
}
|
}
|
||||||
return self.install_owner_after_metadata_success(context);
|
return self.install_owner_after_operation_success(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn install_owner_after_metadata_success(&self, context: OwnerMetadataContext) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
fn install_owner_after_operation_success(&self, context: OwnerOperationContext) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
||||||
let OwnerMetadataContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context;
|
let OwnerOperationContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context;
|
||||||
let configured_secret_candidate_count = self.secret_candidate_count_or_zero(wallet_id.as_str());
|
let configured_secret_candidate_count = self.secret_candidate_count_or_zero(wallet_id.as_str());
|
||||||
let dto = crate::owner_projection(wallet_id.as_str(), view_enabled, configured_secret_candidate_count, &owner);
|
let dto = crate::owner_projection(wallet_id.as_str(), view_enabled, configured_secret_candidate_count, &owner);
|
||||||
let session = self.wallet_session.lock();
|
let session = self.wallet_session.lock();
|
||||||
@@ -768,7 +799,7 @@ impl AppState {
|
|||||||
view_enabled: reserved_view_enabled,
|
view_enabled: reserved_view_enabled,
|
||||||
} if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey && reserved_view_enabled == view_enabled => {
|
} if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey && reserved_view_enabled == view_enabled => {
|
||||||
*session = crate::WalletSession::Owner { wallet_id: wallet_id.clone(), path, view_enabled, wallet: owner };
|
*session = crate::WalletSession::Owner { wallet_id: wallet_id.clone(), path, view_enabled, wallet: owner };
|
||||||
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet OWNER metadata operation completed");
|
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet OWNER privileged operation completed");
|
||||||
return std::result::Result::Ok(dto);
|
return std::result::Result::Ok(dto);
|
||||||
},
|
},
|
||||||
other => {
|
other => {
|
||||||
@@ -776,14 +807,14 @@ impl AppState {
|
|||||||
drop(owner);
|
drop(owner);
|
||||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
crate::ERROR_CODE_WALLET_SESSION_INVALID,
|
crate::ERROR_CODE_WALLET_SESSION_INVALID,
|
||||||
"Wallet metadata completion no longer owns the selected OWNER session",
|
"Wallet OWNER operation completion no longer owns the selected session",
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restore_owner_after_metadata_failure(&self, context: OwnerMetadataContext) {
|
fn restore_owner_after_operation_failure(&self, context: OwnerOperationContext) {
|
||||||
let OwnerMetadataContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context;
|
let OwnerOperationContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context;
|
||||||
let session = self.wallet_session.lock();
|
let session = self.wallet_session.lock();
|
||||||
let mut session = match session {
|
let mut session = match session {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
@@ -798,7 +829,7 @@ impl AppState {
|
|||||||
view_enabled: reserved_view_enabled,
|
view_enabled: reserved_view_enabled,
|
||||||
} if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey && reserved_view_enabled == view_enabled => {
|
} if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey && reserved_view_enabled == view_enabled => {
|
||||||
*session = crate::WalletSession::Owner { wallet_id: wallet_id.clone(), path, view_enabled, wallet: owner };
|
*session = crate::WalletSession::Owner { wallet_id: wallet_id.clone(), path, view_enabled, wallet: owner };
|
||||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet OWNER metadata operation failed without invalidating the authenticated handle");
|
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet OWNER privileged operation failed without invalidating the authenticated handle");
|
||||||
},
|
},
|
||||||
other => {
|
other => {
|
||||||
*session = other;
|
*session = other;
|
||||||
@@ -807,8 +838,8 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn recover_owner_metadata_state_conflict(&self, context: OwnerMetadataContext) {
|
async fn recover_owner_state_conflict(&self, context: OwnerOperationContext) {
|
||||||
let OwnerMetadataContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context;
|
let OwnerOperationContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context;
|
||||||
drop(owner);
|
drop(owner);
|
||||||
let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await;
|
let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await;
|
||||||
let session = self.wallet_session.lock();
|
let session = self.wallet_session.lock();
|
||||||
@@ -827,7 +858,7 @@ impl AppState {
|
|||||||
if let std::result::Result::Ok(locked_info) = inspected {
|
if let std::result::Result::Ok(locked_info) = inspected {
|
||||||
*session = crate::WalletSession::Locked { wallet_id: wallet_id.clone(), path, locked_info };
|
*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 OWNER metadata state conflict purged the stale handle and forced reauthorization");
|
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet OWNER state conflict purged the stale handle and forced reauthorization");
|
||||||
},
|
},
|
||||||
other => {
|
other => {
|
||||||
*session = other;
|
*session = other;
|
||||||
@@ -964,7 +995,7 @@ 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 OwnerMetadataContext {
|
struct OwnerOperationContext {
|
||||||
operation: &'static str,
|
operation: &'static str,
|
||||||
owner: std::boxed::Box<ksp_wallet_lib::WalletOwner>,
|
owner: std::boxed::Box<ksp_wallet_lib::WalletOwner>,
|
||||||
path: std::path::PathBuf,
|
path: std::path::PathBuf,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-wallet-desk/src/lib.rs
|
// file: crates/ksp-app-wallet-desk/src/lib.rs
|
||||||
// version: 9
|
// version: 10
|
||||||
|
|
||||||
//! Tauri desktop application shell for KSP Wallet management and inspection.
|
//! Tauri desktop application shell for KSP Wallet management and inspection.
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ mod wallet_import;
|
|||||||
mod wallet_inventory;
|
mod wallet_inventory;
|
||||||
mod wallet_metadata;
|
mod wallet_metadata;
|
||||||
mod wallet_secrets;
|
mod wallet_secrets;
|
||||||
|
mod wallet_security;
|
||||||
mod wallet_session;
|
mod wallet_session;
|
||||||
|
|
||||||
/// Runs the KSP wallet desktop application.
|
/// Runs the KSP wallet desktop application.
|
||||||
@@ -199,6 +200,8 @@ 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.
|
||||||
|
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;
|
||||||
/// Create request whose password strings move frontend -> Rust only.
|
/// Create request whose password strings move frontend -> Rust only.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-wallet-desk/src/tauri.rs
|
// file: crates/ksp-app-wallet-desk/src/tauri.rs
|
||||||
// version: 7
|
// version: 8
|
||||||
|
|
||||||
//! Tauri runtime assembly for the KSP wallet desktop application.
|
//! Tauri runtime assembly for the KSP wallet desktop application.
|
||||||
|
|
||||||
@@ -53,6 +53,8 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
|
|||||||
lock_wallet,
|
lock_wallet,
|
||||||
refresh_wallet_balance,
|
refresh_wallet_balance,
|
||||||
refresh_wallets,
|
refresh_wallets,
|
||||||
|
rotate_owner_password,
|
||||||
|
rotate_view_password,
|
||||||
select_wallet,
|
select_wallet,
|
||||||
splash_frontend_ready,
|
splash_frontend_ready,
|
||||||
unlock_wallet_owner_configured,
|
unlock_wallet_owner_configured,
|
||||||
@@ -248,6 +250,30 @@ async fn refresh_wallets(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn rotate_owner_password(
|
||||||
|
request: crate::WalletPasswordRotationRequestDto,
|
||||||
|
state: tauri::State<'_, crate::AppState>,
|
||||||
|
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
|
||||||
|
let result = state.rotate_owner_password(request).await;
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn rotate_view_password(
|
||||||
|
request: crate::WalletPasswordRotationRequestDto,
|
||||||
|
state: tauri::State<'_, crate::AppState>,
|
||||||
|
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
|
||||||
|
let result = state.rotate_view_password(request).await;
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn select_wallet(
|
async fn select_wallet(
|
||||||
request: crate::WalletSelectionRequestDto,
|
request: crate::WalletSelectionRequestDto,
|
||||||
|
|||||||
19
crates/ksp-app-wallet-desk/src/wallet_security.rs
Normal file
19
crates/ksp-app-wallet-desk/src/wallet_security.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/src/wallet_security.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! OWNER-only credential rotation contracts for Wallet Desk.
|
||||||
|
|
||||||
|
use ts_rs::TS; // rust-rules: trait-import
|
||||||
|
|
||||||
|
/// New credential moved frontend -> Rust for one explicit Wallet password rotation.
|
||||||
|
#[derive(serde::Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_security/WalletPasswordRotationRequestDto.ts")]
|
||||||
|
pub(crate) struct WalletPasswordRotationRequestDto {
|
||||||
|
/// New password transported only for the explicit rotation call.
|
||||||
|
pub(crate) password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/wallet_security.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -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: 5
|
// version: 6
|
||||||
|
|
||||||
//! Durable root-scoped Wallet session lifecycle for Wallet Desk.
|
//! Durable root-scoped Wallet session lifecycle for Wallet Desk.
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ pub(crate) enum WalletSession {
|
|||||||
/// Whether the authenticated Wallet currently exposes a VIEW slot.
|
/// Whether the authenticated Wallet currently exposes a VIEW slot.
|
||||||
view_enabled: bool,
|
view_enabled: bool,
|
||||||
},
|
},
|
||||||
/// One OWNER metadata mutation temporarily owns the authorized handle outside the session mutex.
|
/// One OWNER-authorized privileged mutation temporarily owns the handle outside the session mutex.
|
||||||
OwnerOperation {
|
OwnerOperation {
|
||||||
/// Native Wallet filename / root-scoped identifier.
|
/// Native Wallet filename / root-scoped identifier.
|
||||||
wallet_id: String,
|
wallet_id: String,
|
||||||
|
|||||||
@@ -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.9.fix.1",
|
"version": "0.2.6-pre.10",
|
||||||
"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: 9
|
// version: 10
|
||||||
|
|
||||||
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
|
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
|
||||||
|
|
||||||
@@ -251,5 +251,27 @@ fn pre_009_owner_metadata_administration_uses_wallet_owner_and_refreshes_project
|
|||||||
assert!(html.contains("id=\"ownerNotesList\""));
|
assert!(html.contains("id=\"ownerNotesList\""));
|
||||||
assert!(html.contains("id=\"deleteWalletNoteModal\""));
|
assert!(html.contains("id=\"deleteWalletNoteModal\""));
|
||||||
assert!(main.contains("runOwnerMetadataMutation"));
|
assert!(main.contains("runOwnerMetadataMutation"));
|
||||||
assert!(main.contains("recoverOwnerMetadataStateConflict"));
|
assert!(main.contains("recoverOwnerStateConflict"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_010_owner_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());
|
||||||
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||||
|
let html = read_text(root.join("frontend/main.html").as_path());
|
||||||
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
assert!(security.contains("WalletPasswordRotationRequestDto"));
|
||||||
|
assert!(state.contains("owner.rotate_owner_password"));
|
||||||
|
assert!(state.contains("owner.rotate_view_password"));
|
||||||
|
assert!(state.contains("begin_owner_operation"));
|
||||||
|
assert!(state.contains("finish_owner_operation"));
|
||||||
|
assert!(tauri.contains("rotate_owner_password"));
|
||||||
|
assert!(tauri.contains("rotate_view_password"));
|
||||||
|
assert!(html.contains("id=\"rotateOwnerPasswordSubmit\""));
|
||||||
|
assert!(html.contains("id=\"rotateViewPasswordSubmit\""));
|
||||||
|
assert!(main.contains("rotateWalletPassword"));
|
||||||
|
assert!(main.contains("bindRotationActions"));
|
||||||
|
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: 9
|
// version: 10
|
||||||
|
|
||||||
//! Static desktop security contracts for the Wallet Desk pre.002 shell.
|
//! Static desktop security contracts for the Wallet Desk pre.002 shell.
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ fn owner_metadata_values_are_request_only_and_state_conflict_forces_reauthorizat
|
|||||||
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
assert!(!metadata.contains("derive(Clone"));
|
assert!(!metadata.contains("derive(Clone"));
|
||||||
assert!(!metadata.contains("derive(Debug"));
|
assert!(!metadata.contains("derive(Debug"));
|
||||||
assert!(state.contains("recover_owner_metadata_state_conflict"));
|
assert!(state.contains("recover_owner_state_conflict"));
|
||||||
assert!(state.contains("inspect_locked_wallet_file_v1"));
|
assert!(state.contains("inspect_locked_wallet_file_v1"));
|
||||||
assert!(state.contains("ERROR_CODE_STATE_CONFLICT"));
|
assert!(state.contains("ERROR_CODE_STATE_CONFLICT"));
|
||||||
assert!(main.contains("isWalletStateConflict"));
|
assert!(main.contains("isWalletStateConflict"));
|
||||||
@@ -183,3 +183,33 @@ fn owner_metadata_values_are_request_only_and_state_conflict_forces_reauthorizat
|
|||||||
assert!(!main.contains("window.confirm"));
|
assert!(!main.contains("window.confirm"));
|
||||||
assert!(main.contains("new Modal(modalElement)"));
|
assert!(main.contains("new Modal(modalElement)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rotation_passwords_are_request_only_confirmed_frontend_side_and_never_logged() {
|
||||||
|
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());
|
||||||
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
assert!(!security.contains("derive(Clone"));
|
||||||
|
assert!(!security.contains("derive(Debug"));
|
||||||
|
let request = security.split("pub(crate) struct WalletPasswordRotationRequestDto").nth(1);
|
||||||
|
assert!(request.is_some());
|
||||||
|
let fields = request
|
||||||
|
.unwrap_or_default()
|
||||||
|
.split("}\n")
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.lines()
|
||||||
|
.filter(|line| return line.trim_start().starts_with("pub(crate) "))
|
||||||
|
.collect::<std::vec::Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
assert_eq!(fields.matches("password").count(), 1);
|
||||||
|
assert!(!fields.contains("confirm"));
|
||||||
|
assert!(state.contains("OwnerPassword::new(request.password)"));
|
||||||
|
assert!(state.contains("ViewPassword::new(request.password)"));
|
||||||
|
assert!(main.contains("password.value !== confirmation.value"));
|
||||||
|
assert!(main.contains("clearRotationSensitiveInputs"));
|
||||||
|
assert!(!main.contains("frontendDebug(\"main\", request.password"));
|
||||||
|
assert!(!main.contains("frontendTrace(\"main\", request.password"));
|
||||||
|
assert!(!main.contains("KSP_SECRET_WALLET_PASS_"));
|
||||||
|
}
|
||||||
|
|||||||
13
crates/ksp-app-wallet-desk/unit_tests/wallet_security.rs
Normal file
13
crates/ksp-app-wallet-desk/unit_tests/wallet_security.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/unit_tests/wallet_security.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn password_rotation_request_is_deserializable_without_copy_contracts() {
|
||||||
|
fn assert_deserializable<T>(_marker: std::marker::PhantomData<T>)
|
||||||
|
where
|
||||||
|
T: serde::de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert_deserializable(std::marker::PhantomData::<crate::WalletPasswordRotationRequestDto>);
|
||||||
|
}
|
||||||
42
deltas/0.2.6/pre.010.md
Normal file
42
deltas/0.2.6/pre.010.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<!-- file: deltas/0.2.6/pre.010.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.6-pre.010` — rotations credentials OWNER / VIEW
|
||||||
|
|
||||||
|
## Objet
|
||||||
|
|
||||||
|
Cette tranche ajoute les rotations de credentials déjà supportées par `ksp-wallet-lib` à Wallet Desk, sans modifier le format `.kspwallet` ni les secrets Config.
|
||||||
|
|
||||||
|
## Changements
|
||||||
|
|
||||||
|
- ajoute `WalletPasswordRotationRequestDto`, request-only et sans `Debug`/`Clone` ;
|
||||||
|
- ajoute `rotate_owner_password` et `rotate_view_password` au bridge Tauri ;
|
||||||
|
- délègue à `WalletOwner::rotate_owner_password` et `WalletOwner::rotate_view_password` ;
|
||||||
|
- généralise le lifecycle `OwnerOperation` de `pre.009` pour les mutations OWNER privilégiées ;
|
||||||
|
- libère le mutex de session pendant Argon2/persistence ;
|
||||||
|
- conserve la session OWNER et la projection autorisée après succès ;
|
||||||
|
- restaure la session OWNER après une erreur non conflictuelle ;
|
||||||
|
- traite `wallet.state_conflict` par destruction du handle stale, réinspection Locked et réautorisation obligatoire ;
|
||||||
|
- refuse une rotation VIEW quand VIEW est désactivé ;
|
||||||
|
- ajoute double saisie de confirmation côté frontend uniquement ;
|
||||||
|
- vide les champs de rotation après succès, échec, Lock, Deselect ou changement de wallet ;
|
||||||
|
- indique explicitement que les secrets Config ne sont jamais mis à jour automatiquement ;
|
||||||
|
- ajoute les canaris contract/security dédiés.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md` détaille le contrat `pre.010`.
|
||||||
|
- `ROADMAP.md`, `CHANGELOG.md` et `docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md` restent inchangés dans cette tranche.
|
||||||
|
|
||||||
|
## Validation opérateur attendue
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis `cargo tauri dev` pour vérifier les rotations réelles OWNER/VIEW, la conservation de la Pubkey et le comportement des anciens/nouveaux passwords.
|
||||||
@@ -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: 23 -->
|
<!-- version: 24 -->
|
||||||
|
|
||||||
# Plan `0.2.6` — Wallet Desk
|
# Plan `0.2.6` — Wallet Desk
|
||||||
|
|
||||||
@@ -1510,17 +1510,28 @@ Le correctif `pre.009-fix.001` est strictement hygiénique : suppression du cham
|
|||||||
|
|
||||||
### `pre.010` — rotations credentials
|
### `pre.010` — rotations credentials
|
||||||
|
|
||||||
Objectifs :
|
Objectifs réalisés :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
rotate OWNER password
|
rotate OWNER password depuis une session OWNER
|
||||||
rotate VIEW password
|
rotate VIEW password depuis une session OWNER lorsque VIEW est activé
|
||||||
password confirmation UX
|
double saisie/confirmation uniquement dans le frontend
|
||||||
configured-secret interaction après rotation
|
un seul nouveau password traverse IPC vers Rust
|
||||||
session/projection coherence
|
conversion immédiate en OwnerPassword / ViewPassword
|
||||||
redaction tests
|
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
|
||||||
|
Pubkey, alias, notes et identité Solana inchangés
|
||||||
|
VIEW rotation sans ancien password VIEW conformément au contrat WalletOwner
|
||||||
|
secrets Config volontairement inchangés après rotation
|
||||||
|
message UI explicite avant Lock/unlock configuré
|
||||||
|
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.
|
||||||
|
|
||||||
### `pre.011` — strong VIEW disable/recreate
|
### `pre.011` — strong VIEW disable/recreate
|
||||||
|
|
||||||
Objectifs :
|
Objectifs :
|
||||||
|
|||||||
Reference in New Issue
Block a user