mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
fix: show notification banner on TOFU first-use cert trust (BUG-133)
The cert-tofu event listener now handles "trusted_first_use" status and shows a visible notification banner with the server hostname and SHA-256 fingerprint. Adds onCertFirstTrust callback to the WS client API. First-use certificate trust is no longer silent.
This commit is contained in:
@@ -59,6 +59,7 @@ export function parseStoredFingerprint(message?: string): string | undefined {
|
||||
}
|
||||
|
||||
export type CertMismatchListener = (event: CertTofuEvent) => void;
|
||||
export type CertFirstTrustListener = (event: CertTofuEvent) => void;
|
||||
|
||||
export interface WsClientConfig {
|
||||
readonly host: string;
|
||||
@@ -103,6 +104,9 @@ export function createWsClient() {
|
||||
// TOFU cert mismatch listeners
|
||||
const certMismatchListeners = new Set<CertMismatchListener>();
|
||||
|
||||
// TOFU first-trust listeners (BUG-133)
|
||||
const certFirstTrustListeners = new Set<CertFirstTrustListener>();
|
||||
|
||||
function setState(newState: ConnectionState): void {
|
||||
if (state !== newState) {
|
||||
state = newState;
|
||||
@@ -325,7 +329,15 @@ export function createWsClient() {
|
||||
const raw = e.payload as CertTofuEvent;
|
||||
log.info("TOFU cert event", { host: raw.host, status: raw.status });
|
||||
|
||||
if (raw.status === "mismatch") {
|
||||
if (raw.status === "trusted_first_use") {
|
||||
log.warn("TOFU: first-use certificate trust", {
|
||||
host: raw.host,
|
||||
fingerprint: raw.fingerprint,
|
||||
});
|
||||
for (const listener of certFirstTrustListeners) {
|
||||
listener(raw);
|
||||
}
|
||||
} else if (raw.status === "mismatch") {
|
||||
const evt: CertTofuEvent = {
|
||||
...raw,
|
||||
storedFingerprint: parseStoredFingerprint(raw.message),
|
||||
@@ -473,6 +485,12 @@ export function createWsClient() {
|
||||
return () => stateListeners.delete(listener);
|
||||
},
|
||||
|
||||
/** Register a listener for TOFU first-trust events (BUG-133). */
|
||||
onCertFirstTrust(listener: CertFirstTrustListener): () => void {
|
||||
certFirstTrustListeners.add(listener);
|
||||
return () => certFirstTrustListeners.delete(listener);
|
||||
},
|
||||
|
||||
/** Register a listener for TOFU certificate mismatch events. */
|
||||
onCertMismatch(listener: CertMismatchListener): () => void {
|
||||
certMismatchListeners.add(listener);
|
||||
|
||||
@@ -96,6 +96,34 @@ let connectedOverlay: ConnectedOverlayControl | null = null;
|
||||
let lastConnectHost = "";
|
||||
let lastConnectToken = "";
|
||||
|
||||
// Certificate first-trust notification (BUG-133).
|
||||
// Show a brief banner so the user is aware a new server cert was pinned.
|
||||
ws.onCertFirstTrust((evt: CertTofuEvent) => {
|
||||
log.warn("TOFU: first-use certificate pinned", {
|
||||
host: evt.host,
|
||||
fingerprint: evt.fingerprint,
|
||||
});
|
||||
const banner = document.createElement("div");
|
||||
Object.assign(banner.style, {
|
||||
position: "fixed",
|
||||
top: "12px",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
background: "#2d5a27",
|
||||
color: "#e0e0e0",
|
||||
padding: "10px 20px",
|
||||
borderRadius: "8px",
|
||||
fontSize: "13px",
|
||||
zIndex: "10000",
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.5)",
|
||||
cursor: "default",
|
||||
});
|
||||
banner.textContent = `New server certificate trusted for ${evt.host}`;
|
||||
banner.title = `SHA-256: ${evt.fingerprint}`;
|
||||
document.body.appendChild(banner);
|
||||
setTimeout(() => banner.remove(), 8000);
|
||||
});
|
||||
|
||||
// Certificate mismatch modal handler
|
||||
let certModalActive = false;
|
||||
ws.onCertMismatch((evt: CertTofuEvent) => {
|
||||
|
||||
@@ -72,6 +72,10 @@ export function createMockWsClient() {
|
||||
return () => stateListeners.delete(listener);
|
||||
},
|
||||
|
||||
onCertFirstTrust(): () => void {
|
||||
return () => {};
|
||||
},
|
||||
|
||||
onCertMismatch(_listener: CertMismatchListener): () => void {
|
||||
return () => {};
|
||||
},
|
||||
|
||||
@@ -63,6 +63,10 @@ function createMockWsClient(): MockWsClient {
|
||||
return () => stateListeners.delete(listener);
|
||||
},
|
||||
|
||||
onCertFirstTrust(): () => void {
|
||||
return () => {};
|
||||
},
|
||||
|
||||
onCertMismatch(): () => void {
|
||||
return () => {};
|
||||
},
|
||||
|
||||
@@ -51,6 +51,7 @@ function createMockWs() {
|
||||
};
|
||||
},
|
||||
onStateChange: vi.fn(() => () => {}),
|
||||
onCertFirstTrust: vi.fn(() => () => {}),
|
||||
onCertMismatch: vi.fn(() => () => {}),
|
||||
acceptCertFingerprint: vi.fn(async () => {}),
|
||||
getState: vi.fn(() => "disconnected" as const),
|
||||
|
||||
@@ -31,6 +31,7 @@ function createMockWs(state: "connected" | "disconnected" = "connected"): WsClie
|
||||
stateListeners.add(listener);
|
||||
return () => stateListeners.delete(listener);
|
||||
}),
|
||||
onCertFirstTrust: vi.fn().mockReturnValue(() => {}),
|
||||
onCertMismatch: vi.fn().mockReturnValue(() => {}),
|
||||
acceptCertFingerprint: vi.fn(),
|
||||
getState: vi.fn(() => currentState),
|
||||
|
||||
Reference in New Issue
Block a user