Watch
1
0
Fork
You've already forked souveraine
0

break-glass: one-time emergency grant with journaling

StepUpAuth.breakGlass(actionFamily, reason):
- Requires a non-empty reason (logged prominently)
- 60-second TTL (not the normal 5 minutes)
- One-time: consumed on use via isBreakGlass()
- Cannot be issued while session is locked
- Cleared on lock (fail-closed)
- Tracked separately from normal grants for audit

Souveraine.send() now checks break-glass before blocking on step-up.
Break-glass bypasses normal PAM auth — the user declared an emergency.
This commit is contained in:
Fimeg 2026-07-14 21:24:57 -04:00
commit 377b82999d
2 changed files with 90 additions and 4 deletions

View file

@ -285,9 +285,12 @@ Singleton {
if (text.length === 0) return false;
// Step-up gate: if enabled and no valid send grant exists, block
// the send and emit a signal so the UI can trigger auth + retry.
// Break-glass grants bypass normal step-up — they are one-time,
// short-lived, and journaled.
if (Config.options?.lock?.stepUp?.enabled
&& typeof StepUpAuth !== "undefined"
&& !StepUpAuth.isGranted("send")) {
&& !StepUpAuth.isGranted("send")
&& !StepUpAuth.isBreakGlass("send")) {
root.stepUpRequired("send", text);
return "step-up";
}

View file

@ -206,10 +206,13 @@ Singleton {
// per-family, all grants die.
function revokeAll() {
const families = Object.keys(root.grants);
if (families.length === 0) return;
const hadBreakGlass = root._breakGlassGrant !== null;
if (families.length === 0 && !hadBreakGlass) return;
root.grants = ({});
root._clearBreakGlass();
families.forEach(family => root.grantRevoked(family));
console.log(`[step-up] all grants revoked (${families.length} families)`);
console.log(`[step-up] all grants revoked (${families.length} families`
+ (hadBreakGlass ? ", break-glass cleared" : "") + ")");
}
// state — return the current grant state as a plain object for IPC
@ -234,10 +237,90 @@ Singleton {
grantTtlMs: root.grantTtlMs,
activeGrants: active,
authInProgress: authProc.running,
pendingFamily: root._pendingFamily
pendingFamily: root._pendingFamily,
breakGlassActive: root._breakGlassGrant !== null
};
}
// --- Break-glass grant -------------------------------------------------
// A one-time, short-lived, journaled override for emergency operations.
// This is NOT a config toggle. It is a per-decision, reasoned, logged
// bypass that exists because real emergencies happen and the user must
// be able to act.
//
// Properties:
// - Requires a non-empty reason string (the "why")
// - Short TTL (60 seconds by default, not the normal 5 minutes)
// - One-time: consumed on use, cannot be reused
// - Prominently logged with the reason
// - Cannot be issued while the session is locked
// - Cleared on lock, like all other grants
//
// The break-glass grant is tracked separately from normal grants so
// that audit tools can distinguish "user authenticated normally" from
// "user declared an emergency override".
property var _breakGlassGrant: null
property int breakGlassTtlMs: 60000 // 60 seconds
signal breakGlassIssued(string reason, int expiresAt)
signal breakGlassConsumed(string reason)
signal breakGlassExpired(string reason)
// breakGlass — issue a one-time emergency grant for the given action
// family. Returns { ok: true, expiresAt } or { ok: false, reason }.
//
// The reason is mandatory and logged. If the caller cannot explain why
// they need break-glass, they should use normal step-up auth instead.
function breakGlass(actionFamily, reason) {
const family = String(actionFamily || "").trim();
const why = String(reason || "").trim();
if (!family) return { ok: false, reason: "empty action family" };
if (!why) return { ok: false, reason: "break-glass requires a reason" };
if (GlobalStates.screenLocked)
return { ok: false, reason: "break-glass unavailable while locked" };
if (root._breakGlassGrant)
return { ok: false, reason: "break-glass already active" };
const now = Date.now();
root._breakGlassGrant = {
family: family,
reason: why,
granted: now,
expires: now + root.breakGlassTtlMs
};
console.log(`[step-up] BREAK-GLASS issued for ${family}: "${why}" `
+ `(expires in ${root.breakGlassTtlMs / 1000}s)`);
root.breakGlassIssued(why, now + root.breakGlassTtlMs);
return { ok: true, expiresAt: now + root.breakGlassTtlMs };
}
// isBreakGlass — check if a valid break-glass grant exists for the
// given action family. Unlike normal grants, break-glass is one-time:
// calling this function consumes it. Returns true if the grant was
// valid and consumed, false otherwise.
function isBreakGlass(actionFamily) {
const family = String(actionFamily || "").trim();
const bg = root._breakGlassGrant;
if (!bg) return false;
if (bg.family !== family) return false;
if (Date.now() >= bg.expires) {
root._breakGlassGrant = null;
root.breakGlassExpired(bg.reason);
return false;
}
// Consume the grant — one-time use.
root._breakGlassGrant = null;
root.breakGlassConsumed(bg.reason);
console.log(`[step-up] BREAK-GLASS consumed for ${family}: "${bg.reason}"`);
return true;
}
// Revoke break-glass on lock (fail-closed).
function _clearBreakGlass() {
if (!root._breakGlassGrant) return;
root._breakGlassGrant = null;
}
// --- Internal -----------------------------------------------------------
function _mintGrant(family) {