v0.2.6-pre.010
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 178
|
||||
# version: 179
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.6-pre.9.fix.1"
|
||||
version = "0.2.6-pre.10"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -326,6 +326,35 @@
|
||||
</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 id="unlockStatus" class="alert alert-secondary mb-0" role="status">Sélectionner un wallet verrouillé.</div>
|
||||
</div>
|
||||
@@ -360,7 +389,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<script type="module" src="./ts/main.ts"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
import { Modal } from "bootstrap";
|
||||
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 { 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 { 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 { WalletCreateRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletCreateRequestDto.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.",
|
||||
},
|
||||
});
|
||||
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 {
|
||||
@@ -221,6 +222,7 @@ function updateSessionActions(): void {
|
||||
}
|
||||
updateUnlockActions();
|
||||
updateOwnerMetadataActions();
|
||||
updateSecurityRotationActions();
|
||||
}
|
||||
|
||||
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 {
|
||||
const locked = activeSessionState === "locked";
|
||||
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 {
|
||||
setText("#currentWalletBalanceSol", "—");
|
||||
setText("#detailsBalanceLamports", "—");
|
||||
@@ -340,7 +362,9 @@ function clearAuthorizedProjection(): void {
|
||||
setText("#detailsWalletNotes", "—");
|
||||
clearBalanceProjection();
|
||||
clearCreateFormSensitiveInputs();
|
||||
clearRotationSensitiveInputs();
|
||||
clearUnlockSensitiveInputs();
|
||||
setText("#rotationStatus", "Session OWNER requise.");
|
||||
}
|
||||
|
||||
function clearSelectedWallet(): void {
|
||||
@@ -415,6 +439,12 @@ function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void {
|
||||
clearBalanceProjection();
|
||||
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(
|
||||
"#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();
|
||||
frontendDebug("main", "Authorized Wallet session rendered", {
|
||||
capability: wallet.capability,
|
||||
@@ -510,7 +540,7 @@ function isWalletStateConflict(caughtError: unknown): boolean {
|
||||
return error.domain === "wallet" && error.code === "state_conflict";
|
||||
}
|
||||
|
||||
async function recoverOwnerMetadataStateConflict(walletId: string): Promise<void> {
|
||||
async function recoverOwnerStateConflict(walletId: string): Promise<void> {
|
||||
clearAuthorizedProjection();
|
||||
clearBalanceProjection();
|
||||
const request: WalletSelectionRequestDto = { walletId };
|
||||
@@ -518,6 +548,7 @@ async function recoverOwnerMetadataStateConflict(walletId: string): Promise<void
|
||||
const wallet = await invokeKsp<LockedWalletDto>("main", "select_wallet", { request });
|
||||
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("#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.");
|
||||
await loadWalletInventory("list_wallets", false);
|
||||
activateView("security", "user");
|
||||
@@ -525,6 +556,7 @@ async function recoverOwnerMetadataStateConflict(walletId: string): Promise<void
|
||||
} catch {
|
||||
clearSelectedWallet();
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -557,7 +589,7 @@ async function runOwnerMetadataMutation(
|
||||
return true;
|
||||
} catch (caughtError) {
|
||||
if (isWalletStateConflict(caughtError)) {
|
||||
await recoverOwnerMetadataStateConflict(walletId);
|
||||
await recoverOwnerStateConflict(walletId);
|
||||
return false;
|
||||
}
|
||||
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 {
|
||||
document.querySelector<HTMLButtonElement>("#unlockViewManual")?.addEventListener("click", () => {
|
||||
void unlockManual("view");
|
||||
@@ -1033,7 +1138,7 @@ function renderRuntimeStatus(status: RuntimeStatusDto): void {
|
||||
setText("#runtimeEffectiveWalletsDirectory", status.effectiveWalletsDirectory);
|
||||
setText("#runtimeWalletDirectoryCreated", status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent");
|
||||
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", {
|
||||
compositeProfile: status.activeCompositeProfile,
|
||||
fallbackLoggingActive: status.fallbackLoggingActive,
|
||||
@@ -1085,6 +1190,7 @@ async function initializeMain(): Promise<void> {
|
||||
bindUnlockActions();
|
||||
bindBalanceActions();
|
||||
bindOwnerMetadataActions();
|
||||
bindRotationActions();
|
||||
bindShellActions();
|
||||
clearSelectedWallet();
|
||||
activateView("dashboard", "startup");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ksp-app-wallet-desk",
|
||||
"private": true,
|
||||
"version": "0.2.6-pre.9.fix.1",
|
||||
"version": "0.2.6-pre.10",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/app_state.rs
|
||||
// version: 12
|
||||
// version: 13
|
||||
|
||||
//! 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(),
|
||||
fallback_logging_active: runtime.fallback_active,
|
||||
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(),
|
||||
transport_available_endpoint_count,
|
||||
transport_clusters: self.transport_runtime.clusters(),
|
||||
@@ -492,7 +492,7 @@ impl AppState {
|
||||
*session = crate::WalletSession::OwnerOperation { wallet_id, path, pubkey, view_enabled };
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
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, .. } => {
|
||||
@@ -663,19 +663,19 @@ impl AppState {
|
||||
|
||||
/// 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> {
|
||||
let context = self.begin_owner_metadata_operation("alias_update");
|
||||
let context = self.begin_owner_operation("alias_update");
|
||||
let mut context = match context {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let alias = normalize_optional_text(request.alias);
|
||||
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.
|
||||
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 {
|
||||
std::result::Result::Ok(value) => value,
|
||||
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::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.
|
||||
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 {
|
||||
std::result::Result::Ok(value) => value,
|
||||
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;
|
||||
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.
|
||||
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 {
|
||||
std::result::Result::Ok(value) => value,
|
||||
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;
|
||||
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 mut session = match session {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -723,35 +754,35 @@ impl AppState {
|
||||
*session = other;
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
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 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 };
|
||||
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);
|
||||
}
|
||||
|
||||
async fn finish_owner_metadata_operation(
|
||||
async fn finish_owner_operation(
|
||||
&self,
|
||||
context: OwnerMetadataContext,
|
||||
context: OwnerOperationContext,
|
||||
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_owner_metadata_state_conflict(context).await;
|
||||
self.recover_owner_state_conflict(context).await;
|
||||
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 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> {
|
||||
let OwnerMetadataContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context;
|
||||
fn install_owner_after_operation_success(&self, context: OwnerOperationContext) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
|
||||
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 dto = crate::owner_projection(wallet_id.as_str(), view_enabled, configured_secret_candidate_count, &owner);
|
||||
let session = self.wallet_session.lock();
|
||||
@@ -768,7 +799,7 @@ impl AppState {
|
||||
view_enabled: reserved_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 };
|
||||
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);
|
||||
},
|
||||
other => {
|
||||
@@ -776,14 +807,14 @@ impl AppState {
|
||||
drop(owner);
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
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) {
|
||||
let OwnerMetadataContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context;
|
||||
fn restore_owner_after_operation_failure(&self, context: OwnerOperationContext) {
|
||||
let OwnerOperationContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context;
|
||||
let session = self.wallet_session.lock();
|
||||
let mut session = match session {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -798,7 +829,7 @@ impl AppState {
|
||||
view_enabled: reserved_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 };
|
||||
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 => {
|
||||
*session = other;
|
||||
@@ -807,8 +838,8 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
|
||||
async fn recover_owner_metadata_state_conflict(&self, context: OwnerMetadataContext) {
|
||||
let OwnerMetadataContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context;
|
||||
async fn recover_owner_state_conflict(&self, context: OwnerOperationContext) {
|
||||
let OwnerOperationContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context;
|
||||
drop(owner);
|
||||
let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await;
|
||||
let session = self.wallet_session.lock();
|
||||
@@ -827,7 +858,7 @@ impl AppState {
|
||||
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 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 => {
|
||||
*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");
|
||||
}
|
||||
|
||||
struct OwnerMetadataContext {
|
||||
struct OwnerOperationContext {
|
||||
operation: &'static str,
|
||||
owner: std::boxed::Box<ksp_wallet_lib::WalletOwner>,
|
||||
path: std::path::PathBuf,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/lib.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
//! Tauri desktop application shell for KSP Wallet management and inspection.
|
||||
|
||||
@@ -25,6 +25,7 @@ mod wallet_import;
|
||||
mod wallet_inventory;
|
||||
mod wallet_metadata;
|
||||
mod wallet_secrets;
|
||||
mod wallet_security;
|
||||
mod wallet_session;
|
||||
|
||||
/// 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;
|
||||
/// Returns the configured Wallet password candidate count without exposing their names or values.
|
||||
pub(crate) use self::wallet_secrets::wallet_secret_candidate_count;
|
||||
/// New credential request moved frontend -> Rust only for one explicit OWNER-authorized rotation.
|
||||
pub(crate) use self::wallet_security::WalletPasswordRotationRequestDto;
|
||||
/// Authorized OWNER projection exposed only after successful creation.
|
||||
pub(crate) use self::wallet_session::WalletAuthorizedDto;
|
||||
/// Create request whose password strings move frontend -> Rust only.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/tauri.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! 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,
|
||||
refresh_wallet_balance,
|
||||
refresh_wallets,
|
||||
rotate_owner_password,
|
||||
rotate_view_password,
|
||||
select_wallet,
|
||||
splash_frontend_ready,
|
||||
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]
|
||||
async fn select_wallet(
|
||||
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
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! 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.
|
||||
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 {
|
||||
/// Native Wallet filename / root-scoped identifier.
|
||||
wallet_id: String,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"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",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// 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.
|
||||
|
||||
@@ -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=\"deleteWalletNoteModal\""));
|
||||
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
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
//! 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());
|
||||
assert!(!metadata.contains("derive(Clone"));
|
||||
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("ERROR_CODE_STATE_CONFLICT"));
|
||||
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("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 -->
|
||||
<!-- version: 23 -->
|
||||
<!-- version: 24 -->
|
||||
|
||||
# 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
|
||||
|
||||
Objectifs :
|
||||
Objectifs réalisés :
|
||||
|
||||
```text
|
||||
rotate OWNER password
|
||||
rotate VIEW password
|
||||
password confirmation UX
|
||||
configured-secret interaction après rotation
|
||||
session/projection coherence
|
||||
redaction tests
|
||||
rotate OWNER password depuis une session OWNER
|
||||
rotate VIEW password depuis une session OWNER lorsque VIEW est activé
|
||||
double saisie/confirmation uniquement dans le frontend
|
||||
un seul nouveau password traverse IPC vers Rust
|
||||
conversion immédiate en OwnerPassword / ViewPassword
|
||||
réutilisation du lifecycle OwnerOperation hors mutex pendant Argon2 + persistence
|
||||
wallet.state_conflict => purge du handle stale + retour Locked + re-unlock OWNER
|
||||
session OWNER conservée après succès ou échec non conflictuel
|
||||
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
|
||||
|
||||
Objectifs :
|
||||
|
||||
Reference in New Issue
Block a user