v0.2.6-pre.011
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 180
|
||||
# version: 181
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.6-pre.10.fix.1"
|
||||
version = "0.2.6-pre.11"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -355,6 +355,31 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-warning">
|
||||
<div class="card-header fw-semibold">Administration forte VIEW — OWNER uniquement</div>
|
||||
<div class="card-body">
|
||||
<p class="small text-body-secondary">Ces opérations changent l’autorité VIEW courante, pas seulement son password. Disable retire VIEW et rekey les metadata ; Recreate génère une nouvelle metadata key, un nouveau slot VIEW et un nouveau password. La keypair Solana, l’OWNER, l’alias et les notes restent inchangés.</p>
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-lg-4">
|
||||
<button id="disableWalletView" class="btn btn-outline-danger w-100" type="button" disabled>Strong disable VIEW</button>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label" for="recreateViewPassword">Nouveau VIEW password</label>
|
||||
<input id="recreateViewPassword" class="form-control" type="password" autocomplete="new-password" data-view-security-recreate-control disabled>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label" for="recreateViewPasswordConfirm">Confirmer VIEW password</label>
|
||||
<div class="input-group">
|
||||
<input id="recreateViewPasswordConfirm" class="form-control" type="password" autocomplete="new-password" data-view-security-recreate-control disabled>
|
||||
<button id="recreateWalletView" class="btn btn-outline-warning" type="button" data-view-security-recreate-control disabled>Strong recreate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="viewSecurityStatus" class="alert alert-secondary mt-3 mb-0" role="status">Session OWNER requise pour administrer fortement VIEW.</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>
|
||||
@@ -388,8 +413,38 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="disableWalletViewModal" tabindex="-1" aria-labelledby="disableWalletViewModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5" id="disableWalletViewModalLabel">Strong disable VIEW ?</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">Le slot VIEW courant sera supprimé et les metadata seront rekeyées. Le password VIEW actuel ne pourra plus ouvrir l’état courant. Les copies historiques du fichier restent hors de la garantie de révocation.</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button id="confirmDisableWalletView" type="button" class="btn btn-danger">Disable VIEW</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="recreateWalletViewModal" tabindex="-1" aria-labelledby="recreateWalletViewModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5" id="recreateWalletViewModalLabel">Strong recreate VIEW ?</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">Une nouvelle metadata key et un nouveau slot VIEW seront créés. Tout credential VIEW courant sera remplacé. Le nouveau password n’est jamais recopié dans cette confirmation.</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button id="confirmRecreateWalletView" type="button" class="btn btn-warning">Recreate VIEW</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4">
|
||||
<span>KSP Wallet Desk · credential rotation pre.010</span>
|
||||
<span>KSP Wallet Desk · strong VIEW administration pre.011</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: 11
|
||||
// version: 12
|
||||
|
||||
import { Modal } from "bootstrap";
|
||||
import DataTable from "datatables.net-bs5";
|
||||
@@ -22,6 +22,7 @@ import type { WalletNoteAddRequestDto } from "./bindings/ksp_app_wallet_desk/wal
|
||||
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 { WalletViewSecurityStatusDto } from "./bindings/ksp_app_wallet_desk/wallet_security/WalletViewSecurityStatusDto.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";
|
||||
@@ -52,6 +53,8 @@ let activeViewEnabled = false;
|
||||
let authorizedWalletProjection: WalletAuthorizedDto | null = null;
|
||||
let configuredSecretCandidateCount = 0;
|
||||
let deleteNoteModal: Modal | null = null;
|
||||
let disableViewModal: Modal | null = null;
|
||||
let recreateViewModal: Modal | null = null;
|
||||
let pendingDeleteNoteId: string | null = null;
|
||||
let stagedImportSource: WalletTransferInspectionDto | null = null;
|
||||
|
||||
@@ -184,7 +187,7 @@ function initializeWalletTable(): void {
|
||||
zeroRecords: "Aucun wallet correspondant.",
|
||||
},
|
||||
});
|
||||
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.010-credential-rotation" });
|
||||
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.011-strong-view-administration" });
|
||||
}
|
||||
|
||||
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
|
||||
@@ -223,6 +226,7 @@ function updateSessionActions(): void {
|
||||
updateUnlockActions();
|
||||
updateOwnerMetadataActions();
|
||||
updateSecurityRotationActions();
|
||||
updateStrongViewSecurityActions();
|
||||
}
|
||||
|
||||
function updateOwnerMetadataActions(): void {
|
||||
@@ -244,6 +248,17 @@ function updateSecurityRotationActions(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function updateStrongViewSecurityActions(): void {
|
||||
const ownerOpen = activeSessionState === "owner_open" && authorizedWalletProjection?.capability === "owner";
|
||||
const disable = document.querySelector<HTMLButtonElement>("#disableWalletView");
|
||||
if (disable) {
|
||||
disable.disabled = !ownerOpen || !activeViewEnabled;
|
||||
}
|
||||
document.querySelectorAll<HTMLInputElement | HTMLButtonElement>("[data-view-security-recreate-control]").forEach(control => {
|
||||
control.disabled = !ownerOpen;
|
||||
});
|
||||
}
|
||||
|
||||
function updateUnlockActions(): void {
|
||||
const locked = activeSessionState === "locked";
|
||||
const viewManual = document.querySelector<HTMLButtonElement>("#unlockViewManual");
|
||||
@@ -342,6 +357,15 @@ function clearRotationSensitiveInputs(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function clearStrongViewSensitiveInputs(): void {
|
||||
for (const selector of ["#recreateViewPassword", "#recreateViewPasswordConfirm"]) {
|
||||
const input = document.querySelector<HTMLInputElement>(selector);
|
||||
if (input) {
|
||||
input.value = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearBalanceProjection(): void {
|
||||
setText("#currentWalletBalanceSol", "—");
|
||||
setText("#detailsBalanceLamports", "—");
|
||||
@@ -364,8 +388,10 @@ function clearAuthorizedProjection(): void {
|
||||
clearBalanceProjection();
|
||||
clearCreateFormSensitiveInputs();
|
||||
clearRotationSensitiveInputs();
|
||||
clearStrongViewSensitiveInputs();
|
||||
clearUnlockSensitiveInputs();
|
||||
setText("#rotationStatus", "Session OWNER requise.");
|
||||
setText("#viewSecurityStatus", "Session OWNER requise pour administrer fortement VIEW.");
|
||||
}
|
||||
|
||||
function clearSelectedWallet(): void {
|
||||
@@ -403,6 +429,7 @@ function renderLockedWallet(wallet: LockedWalletDto): void {
|
||||
setText("#currentWalletView", wallet.viewEnabled ? "enabled" : "disabled");
|
||||
setText("#currentWalletSecretCandidates", configuredSecretCandidateCount.toString());
|
||||
setText("#unlockStatus", "Prêt pour une tentative explicite. Argon2 peut prendre plusieurs secondes.");
|
||||
setText("#viewSecurityStatus", "Unlock OWNER requis pour administrer fortement VIEW.");
|
||||
updateSessionActions();
|
||||
frontendDebug("main", "Locked Wallet selection rendered", {
|
||||
configuredSecretCandidateCount,
|
||||
@@ -446,6 +473,14 @@ function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void {
|
||||
? "OWNER ouvert. Rotation OWNER et administration VIEW disponibles ; les secrets Config restent inchangés."
|
||||
: "VIEW ouvert. Self-rotation VIEW disponible ; OWNER reste inaccessible.",
|
||||
);
|
||||
setText(
|
||||
"#viewSecurityStatus",
|
||||
wallet.capability === "owner"
|
||||
? wallet.viewEnabled
|
||||
? "OWNER ouvert : VIEW enabled. Strong disable ou strong recreate disponibles."
|
||||
: "OWNER ouvert : VIEW disabled. Strong recreate disponible."
|
||||
: "VIEW ouvert : l’administration forte VIEW est réservée à OWNER.",
|
||||
);
|
||||
updateSessionActions();
|
||||
frontendDebug("main", "Authorized Wallet session rendered", {
|
||||
capability: wallet.capability,
|
||||
@@ -550,6 +585,7 @@ async function recoverOwnerStateConflict(walletId: string): Promise<void> {
|
||||
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("#viewSecurityStatus", "Conflit d’état : administration VIEW interrompue, handle OWNER purgé 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");
|
||||
@@ -558,6 +594,7 @@ async function recoverOwnerStateConflict(walletId: string): Promise<void> {
|
||||
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é.");
|
||||
setText("#viewSecurityStatus", "Conflit d’état détecté ; la réinspection du wallet a échoué.");
|
||||
frontendWarn("main", "Wallet state conflict recovery failed", { walletId });
|
||||
}
|
||||
}
|
||||
@@ -1048,6 +1085,165 @@ async function rotateWalletPassword(capability: UnlockCapability): Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
function applyViewSecurityStatus(status: WalletViewSecurityStatusDto): boolean {
|
||||
const projection = authorizedWalletProjection;
|
||||
if (
|
||||
!activeWalletId
|
||||
|| status.walletId !== activeWalletId
|
||||
|| status.capability !== "owner"
|
||||
|| activeSessionState !== "privileged_operation"
|
||||
|| projection?.capability !== "owner"
|
||||
) {
|
||||
frontendWarn("main", "Stale strong VIEW administration response ignored", { walletId: status.walletId });
|
||||
return false;
|
||||
}
|
||||
activeSessionState = "owner_open";
|
||||
activeViewEnabled = status.viewEnabled;
|
||||
configuredSecretCandidateCount = status.configuredSecretCandidateCount;
|
||||
authorizedWalletProjection = {
|
||||
...projection,
|
||||
configuredSecretCandidateCount: status.configuredSecretCandidateCount,
|
||||
viewEnabled: status.viewEnabled,
|
||||
};
|
||||
setText("#currentWalletState", "OWNER open");
|
||||
setText("#currentWalletView", status.viewEnabled ? "enabled" : "disabled");
|
||||
setText("#currentWalletSecretCandidates", configuredSecretCandidateCount.toString());
|
||||
setText("#unlockSecretCandidateCount", configuredSecretCandidateCount.toString());
|
||||
updateSessionActions();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function runStrongViewDisable(walletId: string): Promise<void> {
|
||||
activeSessionState = "privileged_operation";
|
||||
setText("#currentWalletState", "Strong disable VIEW…");
|
||||
setText("#viewSecurityStatus", "Strong disable VIEW en cours…");
|
||||
updateSessionActions();
|
||||
frontendDebug("main", "Strong VIEW disable requested", { walletId });
|
||||
try {
|
||||
const status = await invokeKsp<WalletViewSecurityStatusDto>("main", "disable_wallet_view");
|
||||
if (!applyViewSecurityStatus(status)) {
|
||||
return;
|
||||
}
|
||||
setText("#viewSecurityStatus", "VIEW fortement désactivé. L’ancien credential VIEW ne peut plus ouvrir l’état courant.");
|
||||
await loadWalletInventory("list_wallets", false);
|
||||
frontendInfo("main", "Strong VIEW disable completed", { walletId });
|
||||
} catch (caughtError) {
|
||||
if (isWalletStateConflict(caughtError)) {
|
||||
await recoverOwnerStateConflict(walletId);
|
||||
return;
|
||||
}
|
||||
if (activeWalletId === walletId) {
|
||||
activeSessionState = "owner_open";
|
||||
setText("#currentWalletState", "OWNER open");
|
||||
setText("#viewSecurityStatus", "Strong disable refusé ; la session OWNER reste ouverte.");
|
||||
updateSessionActions();
|
||||
}
|
||||
frontendWarn("main", "Strong VIEW disable failed", { walletId });
|
||||
}
|
||||
}
|
||||
|
||||
async function runStrongViewRecreate(walletId: string, request: WalletPasswordRotationRequestDto): Promise<void> {
|
||||
activeSessionState = "privileged_operation";
|
||||
setText("#currentWalletState", "Strong recreate VIEW…");
|
||||
setText("#viewSecurityStatus", "Strong recreate VIEW et Argon2 en cours…");
|
||||
updateSessionActions();
|
||||
frontendDebug("main", "Strong VIEW recreate requested", { walletId });
|
||||
try {
|
||||
const status = await invokeKsp<WalletViewSecurityStatusDto>("main", "recreate_wallet_view", { request });
|
||||
if (!applyViewSecurityStatus(status)) {
|
||||
return;
|
||||
}
|
||||
setText("#viewSecurityStatus", "VIEW fortement recréé avec une nouvelle autorité. Les secrets Config ne sont pas modifiés automatiquement.");
|
||||
await loadWalletInventory("list_wallets", false);
|
||||
frontendInfo("main", "Strong VIEW recreate completed", { walletId });
|
||||
} catch (caughtError) {
|
||||
if (isWalletStateConflict(caughtError)) {
|
||||
await recoverOwnerStateConflict(walletId);
|
||||
return;
|
||||
}
|
||||
if (activeWalletId === walletId) {
|
||||
activeSessionState = "owner_open";
|
||||
setText("#currentWalletState", "OWNER open");
|
||||
setText("#viewSecurityStatus", "Strong recreate refusé ; la session OWNER reste ouverte.");
|
||||
updateSessionActions();
|
||||
}
|
||||
frontendWarn("main", "Strong VIEW recreate failed", { walletId });
|
||||
}
|
||||
}
|
||||
|
||||
function bindStrongViewSecurityActions(): void {
|
||||
const disableModalElement = document.querySelector<HTMLElement>("#disableWalletViewModal");
|
||||
if (disableModalElement) {
|
||||
disableViewModal = new Modal(disableModalElement);
|
||||
}
|
||||
const recreateModalElement = document.querySelector<HTMLElement>("#recreateWalletViewModal");
|
||||
if (recreateModalElement) {
|
||||
recreateViewModal = new Modal(recreateModalElement);
|
||||
recreateModalElement.addEventListener("hidden.bs.modal", () => {
|
||||
clearStrongViewSensitiveInputs();
|
||||
});
|
||||
}
|
||||
document.querySelector<HTMLButtonElement>("#disableWalletView")?.addEventListener("click", () => {
|
||||
if (!activeWalletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner" || !activeViewEnabled) {
|
||||
setText("#viewSecurityStatus", "Session OWNER avec VIEW enabled requise.");
|
||||
return;
|
||||
}
|
||||
frontendDebug("main", "Strong VIEW disable confirmation opened", { walletId: activeWalletId });
|
||||
disableViewModal?.show();
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#confirmDisableWalletView")?.addEventListener("click", () => {
|
||||
const walletId = activeWalletId;
|
||||
disableViewModal?.hide();
|
||||
if (!walletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner" || !activeViewEnabled) {
|
||||
return;
|
||||
}
|
||||
void runStrongViewDisable(walletId);
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#recreateWalletView")?.addEventListener("click", () => {
|
||||
if (!activeWalletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner") {
|
||||
clearStrongViewSensitiveInputs();
|
||||
setText("#viewSecurityStatus", "Session OWNER requise.");
|
||||
return;
|
||||
}
|
||||
const password = document.querySelector<HTMLInputElement>("#recreateViewPassword");
|
||||
const confirmation = document.querySelector<HTMLInputElement>("#recreateViewPasswordConfirm");
|
||||
if (!password || !confirmation || password.value.length === 0) {
|
||||
clearStrongViewSensitiveInputs();
|
||||
setText("#viewSecurityStatus", "Nouveau VIEW password et confirmation requis.");
|
||||
return;
|
||||
}
|
||||
if (password.value !== confirmation.value) {
|
||||
clearStrongViewSensitiveInputs();
|
||||
setText("#viewSecurityStatus", "La confirmation ne correspond pas au nouveau VIEW password.");
|
||||
return;
|
||||
}
|
||||
frontendDebug("main", "Strong VIEW recreate confirmation opened", { walletId: activeWalletId });
|
||||
recreateViewModal?.show();
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#confirmRecreateWalletView")?.addEventListener("click", () => {
|
||||
const walletId = activeWalletId;
|
||||
const password = document.querySelector<HTMLInputElement>("#recreateViewPassword");
|
||||
const confirmation = document.querySelector<HTMLInputElement>("#recreateViewPasswordConfirm");
|
||||
recreateViewModal?.hide();
|
||||
if (
|
||||
!walletId
|
||||
|| activeSessionState !== "owner_open"
|
||||
|| authorizedWalletProjection?.capability !== "owner"
|
||||
|| !password
|
||||
|| !confirmation
|
||||
|| password.value.length === 0
|
||||
|| password.value !== confirmation.value
|
||||
) {
|
||||
clearStrongViewSensitiveInputs();
|
||||
return;
|
||||
}
|
||||
const request: WalletPasswordRotationRequestDto = { password: password.value };
|
||||
clearStrongViewSensitiveInputs();
|
||||
void runStrongViewRecreate(walletId, request);
|
||||
});
|
||||
frontendTrace("main", "Strong VIEW security action handlers installed");
|
||||
}
|
||||
|
||||
function bindRotationActions(): void {
|
||||
document.querySelector<HTMLButtonElement>("#rotateOwnerPasswordSubmit")?.addEventListener("click", () => {
|
||||
void rotateWalletPassword("owner");
|
||||
@@ -1161,7 +1357,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, rotations et getBalance prêts.");
|
||||
setText("#shellStatus", "Config/Transport résolus ; import, VIEW/OWNER, metadata, rotations, strong VIEW et getBalance prêts.");
|
||||
frontendTrace("main", "Wallet Desk runtime status rendered", {
|
||||
compositeProfile: status.activeCompositeProfile,
|
||||
fallbackLoggingActive: status.fallbackLoggingActive,
|
||||
@@ -1214,6 +1410,7 @@ async function initializeMain(): Promise<void> {
|
||||
bindBalanceActions();
|
||||
bindOwnerMetadataActions();
|
||||
bindRotationActions();
|
||||
bindStrongViewSecurityActions();
|
||||
bindShellActions();
|
||||
clearSelectedWallet();
|
||||
activateView("dashboard", "startup");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ksp-app-wallet-desk",
|
||||
"private": true,
|
||||
"version": "0.2.6-pre.10.fix.1",
|
||||
"version": "0.2.6-pre.11",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/app_state.rs
|
||||
// version: 14
|
||||
// version: 15
|
||||
|
||||
//! Shared backend state owned by the Wallet Desk Tauri application.
|
||||
|
||||
@@ -722,6 +722,46 @@ impl AppState {
|
||||
return self.finish_owner_operation(context, result).await;
|
||||
}
|
||||
|
||||
/// Strongly disables VIEW from the current OWNER session while preserving OWNER authorization.
|
||||
pub(crate) async fn disable_wallet_view(&self) -> ksp_core_lib::Result<crate::WalletViewSecurityStatusDto> {
|
||||
let context = self.begin_owner_operation("view_disable");
|
||||
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.disable_view(context.path.as_path()).await;
|
||||
if result.is_ok() {
|
||||
context.result_view_enabled = false;
|
||||
}
|
||||
let wallet = self.finish_owner_operation(context, result).await;
|
||||
return match wallet {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(crate::view_security_status_from_authorized(&value)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Strongly recreates VIEW from the current OWNER session with a fresh metadata key, slot and password.
|
||||
pub(crate) async fn recreate_wallet_view(
|
||||
&self,
|
||||
request: crate::WalletPasswordRotationRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::WalletViewSecurityStatusDto> {
|
||||
let context = self.begin_owner_operation("view_recreate");
|
||||
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::ViewPassword::new(request.password);
|
||||
let result = context.owner.recreate_view(context.path.as_path(), password).await;
|
||||
if result.is_ok() {
|
||||
context.result_view_enabled = true;
|
||||
}
|
||||
let wallet = self.finish_owner_operation(context, result).await;
|
||||
return match wallet {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(crate::view_security_status_from_authorized(&value)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// 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");
|
||||
@@ -894,7 +934,15 @@ impl AppState {
|
||||
},
|
||||
};
|
||||
let pubkey = owner.pubkey().to_owned();
|
||||
let context = OwnerOperationContext { operation, owner, path: path.clone(), pubkey, view_enabled, wallet_id: wallet_id.clone() };
|
||||
let context = OwnerOperationContext {
|
||||
operation,
|
||||
owner,
|
||||
path: path.clone(),
|
||||
pubkey,
|
||||
result_view_enabled: view_enabled,
|
||||
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 privileged operation started");
|
||||
return std::result::Result::Ok(context);
|
||||
@@ -917,9 +965,9 @@ impl AppState {
|
||||
}
|
||||
|
||||
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 OwnerOperationContext { operation, owner, path, pubkey, result_view_enabled, 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 dto = crate::owner_projection(wallet_id.as_str(), result_view_enabled, configured_secret_candidate_count, &owner);
|
||||
let session = self.wallet_session.lock();
|
||||
let mut session = match session {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -933,7 +981,7 @@ impl AppState {
|
||||
pubkey: reserved_pubkey,
|
||||
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 };
|
||||
*session = crate::WalletSession::Owner { wallet_id: wallet_id.clone(), path, view_enabled: result_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 privileged operation completed");
|
||||
return std::result::Result::Ok(dto);
|
||||
},
|
||||
@@ -949,7 +997,7 @@ impl AppState {
|
||||
}
|
||||
|
||||
fn restore_owner_after_operation_failure(&self, context: OwnerOperationContext) {
|
||||
let OwnerOperationContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context;
|
||||
let OwnerOperationContext { operation, owner, path, pubkey, result_view_enabled: _, view_enabled, wallet_id } = context;
|
||||
let session = self.wallet_session.lock();
|
||||
let mut session = match session {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -974,7 +1022,7 @@ impl AppState {
|
||||
}
|
||||
|
||||
async fn recover_owner_state_conflict(&self, context: OwnerOperationContext) {
|
||||
let OwnerOperationContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context;
|
||||
let OwnerOperationContext { operation, owner, path, pubkey, result_view_enabled: _, 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();
|
||||
@@ -1144,6 +1192,7 @@ struct OwnerOperationContext {
|
||||
owner: std::boxed::Box<ksp_wallet_lib::WalletOwner>,
|
||||
path: std::path::PathBuf,
|
||||
pubkey: ksp_core_lib::Pubkey,
|
||||
result_view_enabled: bool,
|
||||
view_enabled: bool,
|
||||
wallet_id: String,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/lib.rs
|
||||
// version: 11
|
||||
// version: 12
|
||||
|
||||
//! Tauri desktop application shell for KSP Wallet management and inspection.
|
||||
|
||||
@@ -202,6 +202,10 @@ pub(crate) use self::wallet_secrets::discover_wallet_secret_candidates;
|
||||
pub(crate) use self::wallet_secrets::wallet_secret_candidate_count;
|
||||
/// New credential request moved frontend -> Rust only for one explicit VIEW/OWNER-authorized rotation.
|
||||
pub(crate) use self::wallet_security::WalletPasswordRotationRequestDto;
|
||||
/// Safe VIEW security state returned after OWNER-authorized strong disable/recreate operations.
|
||||
pub(crate) use self::wallet_security::WalletViewSecurityStatusDto;
|
||||
/// Builds the safe strong VIEW administration status from the resulting OWNER projection.
|
||||
pub(crate) use self::wallet_security::view_security_status_from_authorized;
|
||||
/// 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: 8
|
||||
// version: 9
|
||||
|
||||
//! Tauri runtime assembly for the KSP wallet desktop application.
|
||||
|
||||
@@ -45,12 +45,14 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
|
||||
create_wallet,
|
||||
delete_wallet_note,
|
||||
deselect_wallet,
|
||||
disable_wallet_view,
|
||||
emit_frontend_log,
|
||||
get_runtime_status,
|
||||
import_wallet,
|
||||
inspect_import_source,
|
||||
list_wallets,
|
||||
lock_wallet,
|
||||
recreate_wallet_view,
|
||||
refresh_wallet_balance,
|
||||
refresh_wallets,
|
||||
rotate_owner_password,
|
||||
@@ -135,6 +137,15 @@ fn deselect_wallet(state: tauri::State<'_, crate::AppState>) -> std::result::Res
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn disable_wallet_view(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::WalletViewSecurityStatusDto, crate::CommandErrorDto> {
|
||||
let result = state.disable_wallet_view().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]
|
||||
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
|
||||
let result = crate::emit_frontend_log_event(payload);
|
||||
@@ -206,6 +217,18 @@ async fn inspect_import_source(
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn recreate_wallet_view(
|
||||
request: crate::WalletPasswordRotationRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::WalletViewSecurityStatusDto, crate::CommandErrorDto> {
|
||||
let result = state.recreate_wallet_view(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 refresh_wallet_balance(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::WalletBalanceDto, crate::CommandErrorDto> {
|
||||
let result = state.refresh_wallet_balance().await;
|
||||
|
||||
@@ -1,10 +1,35 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/wallet_security.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! VIEW/OWNER credential rotation contracts for Wallet Desk.
|
||||
//! VIEW/OWNER credential and strong VIEW security contracts for Wallet Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Safe VIEW security status returned after one OWNER-authorized strong VIEW administration operation.
|
||||
#[derive(serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_security/WalletViewSecurityStatusDto.ts")]
|
||||
pub(crate) struct WalletViewSecurityStatusDto {
|
||||
/// Capability that performed the strong VIEW administration operation.
|
||||
pub(crate) capability: String,
|
||||
/// Number of effective Config-owned Wallet password candidates without names or values.
|
||||
pub(crate) configured_secret_candidate_count: usize,
|
||||
/// Whether the current Wallet state exposes a VIEW slot after the operation.
|
||||
pub(crate) view_enabled: bool,
|
||||
/// Root-scoped Wallet identifier.
|
||||
pub(crate) wallet_id: String,
|
||||
}
|
||||
|
||||
/// Builds the safe strong VIEW administration status from an authorized OWNER projection.
|
||||
pub(crate) fn view_security_status_from_authorized(wallet: &crate::WalletAuthorizedDto) -> WalletViewSecurityStatusDto {
|
||||
return WalletViewSecurityStatusDto {
|
||||
capability: wallet.capability.clone(),
|
||||
configured_secret_candidate_count: wallet.configured_secret_candidate_count,
|
||||
view_enabled: wallet.view_enabled,
|
||||
wallet_id: wallet.wallet_id.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
/// New credential moved frontend -> Rust for one explicit Wallet password rotation.
|
||||
#[derive(serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "KSP Wallet Desk",
|
||||
"version": "0.2.6-pre.10.fix.1",
|
||||
"version": "0.2.6-pre.11",
|
||||
"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: 11
|
||||
// version: 12
|
||||
|
||||
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
|
||||
|
||||
@@ -279,3 +279,28 @@ fn pre_010_owner_and_view_authorized_credential_rotations_preserve_session_proje
|
||||
assert!(main.contains("viewAuthorized"));
|
||||
assert!(main.contains("Les secrets Config"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_011_strong_view_administration_is_owner_only_and_refreshes_safe_status() {
|
||||
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("WalletViewSecurityStatusDto"));
|
||||
assert!(state.contains("owner.disable_view"));
|
||||
assert!(state.contains("owner.recreate_view"));
|
||||
assert!(state.contains("result_view_enabled = false"));
|
||||
assert!(state.contains("result_view_enabled = true"));
|
||||
assert!(tauri.contains("disable_wallet_view"));
|
||||
assert!(tauri.contains("recreate_wallet_view"));
|
||||
assert!(html.contains("id=\"disableWalletViewModal\""));
|
||||
assert!(html.contains("id=\"recreateWalletViewModal\""));
|
||||
assert!(html.contains("id=\"disableWalletView\""));
|
||||
assert!(html.contains("id=\"recreateWalletView\""));
|
||||
assert!(main.contains("runStrongViewDisable"));
|
||||
assert!(main.contains("runStrongViewRecreate"));
|
||||
assert!(main.contains("applyViewSecurityStatus"));
|
||||
assert!(main.contains("pre.011-strong-view-administration"));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/desktop_security.rs
|
||||
// version: 11
|
||||
// version: 12
|
||||
|
||||
//! Static desktop security contracts for the Wallet Desk pre.002 shell.
|
||||
|
||||
@@ -217,3 +217,48 @@ fn rotation_passwords_are_request_only_confirmed_frontend_side_and_never_logged(
|
||||
assert!(!main.contains("frontendTrace(\"main\", request.password"));
|
||||
assert!(!main.contains("KSP_SECRET_WALLET_PASS_"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strong_view_administration_is_owner_only_uses_bootstrap_modals_and_returns_no_secret() {
|
||||
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 main = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||
let status = security.split("pub(crate) struct WalletViewSecurityStatusDto").nth(1);
|
||||
assert!(status.is_some());
|
||||
let fields = status
|
||||
.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!(!fields.contains("password"));
|
||||
assert!(!fields.contains("pubkey"));
|
||||
assert!(!fields.contains("alias"));
|
||||
assert!(!fields.contains("note"));
|
||||
assert!(state.contains("begin_owner_operation(\"view_disable\")"));
|
||||
assert!(state.contains("begin_owner_operation(\"view_recreate\")"));
|
||||
assert!(!state.contains("begin_view_operation(\"view_disable\")"));
|
||||
assert!(!state.contains("begin_view_operation(\"view_recreate\")"));
|
||||
let disable = tauri.find("async fn disable_wallet_view");
|
||||
assert!(disable.is_some());
|
||||
let disable = &tauri[disable.unwrap_or_default()..];
|
||||
let disable = disable.split("#[tauri::command]").next().unwrap_or_default();
|
||||
assert!(!disable.contains("request:"));
|
||||
let recreate = tauri.find("async fn recreate_wallet_view");
|
||||
assert!(recreate.is_some());
|
||||
let recreate = &tauri[recreate.unwrap_or_default()..];
|
||||
let recreate = recreate.split("#[tauri::command]").next().unwrap_or_default();
|
||||
assert!(recreate.contains("WalletPasswordRotationRequestDto"));
|
||||
assert!(main.contains("new Modal(disableModalElement)"));
|
||||
assert!(main.contains("new Modal(recreateModalElement)"));
|
||||
assert!(main.contains("clearStrongViewSensitiveInputs"));
|
||||
assert!(main.contains("hidden.bs.modal"));
|
||||
assert!(!main.contains("window.confirm"));
|
||||
assert!(!main.contains("frontendDebug(\"main\", request.password"));
|
||||
assert!(!main.contains("frontendTrace(\"main\", request.password"));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/unit_tests/wallet_security.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn password_rotation_request_is_deserializable_without_copy_contracts() {
|
||||
@@ -11,3 +11,18 @@ fn password_rotation_request_is_deserializable_without_copy_contracts() {
|
||||
}
|
||||
assert_deserializable(std::marker::PhantomData::<crate::WalletPasswordRotationRequestDto>);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_security_status_is_serializable_and_contains_no_password_field() {
|
||||
fn assert_serializable<T>(_marker: std::marker::PhantomData<T>)
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_serializable(std::marker::PhantomData::<crate::WalletViewSecurityStatusDto>);
|
||||
let source = include_str!("../src/wallet_security.rs");
|
||||
let status = source.split("pub(crate) struct WalletViewSecurityStatusDto").nth(1).unwrap_or_default().split("}\n").next().unwrap_or_default();
|
||||
let fields = status.lines().filter(|line| return line.trim_start().starts_with("pub(crate) ")).collect::<std::vec::Vec<_>>().join("\n");
|
||||
assert!(!fields.contains("password"));
|
||||
}
|
||||
|
||||
56
deltas/0.2.6/pre.011.md
Normal file
56
deltas/0.2.6/pre.011.md
Normal file
@@ -0,0 +1,56 @@
|
||||
<!-- file: deltas/0.2.6/pre.011.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.6-pre.011` — strong VIEW disable/recreate
|
||||
|
||||
## Objet
|
||||
|
||||
Exposer dans Wallet Desk l'administration forte de la capability VIEW déjà fournie par `ksp-wallet-lib` : suppression forte de VIEW et recréation avec une nouvelle autorité, exclusivement depuis une session OWNER.
|
||||
|
||||
## Changements
|
||||
|
||||
- `workspace.package.version` passe à `0.2.6-pre.11` ;
|
||||
- ajout de `disable_wallet_view` :
|
||||
- OWNER-only ;
|
||||
- délègue à `WalletOwner::disable_view` ;
|
||||
- retire le slot VIEW et rekey les metadata ;
|
||||
- conserve la session OWNER avec `view_enabled=false` ;
|
||||
- ajout de `recreate_wallet_view` :
|
||||
- OWNER-only ;
|
||||
- délègue à `WalletOwner::recreate_view` ;
|
||||
- génère une nouvelle metadata key, un nouveau slot VIEW et un nouveau credential ;
|
||||
- fonctionne depuis VIEW disabled ou enabled ;
|
||||
- conserve la session OWNER avec `view_enabled=true` ;
|
||||
- `OwnerOperationContext` distingue le `view_enabled` réservé avant mutation du `result_view_enabled` réinstallé après succès ;
|
||||
- ajout de `WalletViewSecurityStatusDto`, projection IPC minimale sans identité protégée ni secret ;
|
||||
- UI Security :
|
||||
- carte d'administration forte VIEW ;
|
||||
- modal Bootstrap pour disable ;
|
||||
- modal Bootstrap pour recreate ;
|
||||
- confirmation du nouveau password uniquement côté frontend ;
|
||||
- rafraîchissement immédiat du DataTable et de l'état `VIEW enabled/disabled` ;
|
||||
- `wallet.state_conflict` conserve la politique existante : purge du handle OWNER stale, réinspection Locked et réautorisation OWNER obligatoire ;
|
||||
- canaries contract/security étendus à la surface strong VIEW ;
|
||||
- `ROADMAP.md`, `CHANGELOG.md` et `docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md` restent inchangés.
|
||||
|
||||
## Sécurité
|
||||
|
||||
`disable_view` et `recreate_view` ne sont jamais accessibles depuis VIEW. Le nouveau password de recreate est converti en `ViewPassword` côté Rust et n'est ni projeté ni loggé. Les secrets Config restent inchangés.
|
||||
|
||||
La révocation forte porte sur l'état courant/futur : une copie historique déjà détenue reste explicitement hors garantie.
|
||||
|
||||
## Validation opérateur
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-app-wallet-desk
|
||||
cargo test --workspace
|
||||
|
||||
KSP_WALLETS_DIRECTORY=var/wallet-desk-pre011 \
|
||||
cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json
|
||||
```
|
||||
|
||||
Runtime : OWNER -> strong disable -> Lock -> VIEW impossible ; OWNER -> strong recreate avec nouveau password -> Lock -> ancien VIEW refusé / nouveau VIEW accepté ; vérifier Pubkey, alias, notes et OWNER inchangés.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md -->
|
||||
<!-- version: 25 -->
|
||||
<!-- version: 26 -->
|
||||
|
||||
# Plan `0.2.6` — Wallet Desk
|
||||
|
||||
@@ -1538,15 +1538,63 @@ Le correctif `pre.010-fix.001` complète ce contrat : la personne déjà autoris
|
||||
|
||||
### `pre.011` — strong VIEW disable/recreate
|
||||
|
||||
Objectifs :
|
||||
Cette tranche branche les primitives fortes déjà possédées par `ksp-wallet-lib` sans dupliquer leur cryptographie dans l'application.
|
||||
|
||||
Contrat retenu :
|
||||
|
||||
```text
|
||||
disable_view
|
||||
recreate_view
|
||||
security status DTO
|
||||
VIEW enabled/disabled dans DataTable
|
||||
modals Bootstrap privilégiés
|
||||
réouverture VIEW vérifiée
|
||||
OWNER uniquement -> disable_wallet_view
|
||||
-> WalletOwner::disable_view
|
||||
-> nouvelle metadata content key
|
||||
-> suppression du slot/descriptor VIEW
|
||||
-> session OWNER conservée avec view_enabled=false
|
||||
|
||||
OWNER uniquement -> recreate_wallet_view(new VIEW password)
|
||||
-> WalletOwner::recreate_view
|
||||
-> nouvelle metadata content key
|
||||
-> nouveau slot ID VIEW
|
||||
-> nouveau credential VIEW
|
||||
-> session OWNER conservée avec view_enabled=true
|
||||
```
|
||||
|
||||
`recreate_wallet_view` est volontairement disponible même lorsque VIEW est déjà enabled : dans ce cas l'opération remplace fortement l'autorité VIEW courante au lieu d'effectuer une simple rotation du password.
|
||||
|
||||
Le résultat IPC est un `WalletViewSecurityStatusDto` sûr et minimal :
|
||||
|
||||
```text
|
||||
wallet_id
|
||||
capability
|
||||
view_enabled
|
||||
configured_secret_candidate_count
|
||||
```
|
||||
|
||||
Il ne contient ni Pubkey, ni alias, ni notes, ni password. Les metadata autorisées déjà détenues par la session OWNER restent côté frontend dans leur projection existante et ne sont pas retransmises pour cette opération.
|
||||
|
||||
Le lifecycle réutilise `OwnerOperation` et libère le mutex avant persistence/Argon2. `OwnerOperationContext` distingue l'état VIEW réservé avant l'opération de l'état VIEW résultant afin que le contrôle anti-stale continue à comparer l'état original tout en réinstallant la session OWNER avec le nouveau `view_enabled`.
|
||||
|
||||
UX :
|
||||
|
||||
```text
|
||||
Strong disable VIEW -> modal Bootstrap destructif
|
||||
Strong recreate VIEW -> password + confirmation frontend-only -> modal Bootstrap privilégié
|
||||
aucun window.confirm / alert / prompt
|
||||
DataTable rafraîchie après succès
|
||||
VIEW enabled/disabled immédiatement synchronisé
|
||||
```
|
||||
|
||||
La confirmation du password de recreate reste strictement frontend ; un seul nouveau password traverse IPC. Les `KSP_SECRET_WALLET_PASS_*` ne sont jamais modifiés implicitement.
|
||||
|
||||
Garantie explicitement affichée : la révocation forte concerne l'état courant et futur du Wallet ; une copie historique déjà détenue reste hors de la garantie de révocation.
|
||||
|
||||
Validation runtime attendue :
|
||||
|
||||
```text
|
||||
OWNER -> strong disable VIEW
|
||||
Lock -> VIEW unlock impossible
|
||||
OWNER -> strong recreate VIEW avec nouveau password
|
||||
Lock -> ancien VIEW password refusé
|
||||
nouveau VIEW password accepté
|
||||
Pubkey / alias / notes / OWNER inchangés
|
||||
```
|
||||
|
||||
### `pre.012` — export OWNER fichier
|
||||
|
||||
Reference in New Issue
Block a user