diff --git a/CHANGELOG.md b/CHANGELOG.md index fb53562..e9af616 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,18 @@ Die Erweiterungsversion (`manifest.json`) ist unabhängig von der OpenNIT-Server ## [Unreleased] +### Geändert +- **Anmeldung nur noch per SSO:** Das Feld für den manuell erzeugten API-Token samt „Verbindung testen" + ist aus den Einstellungen entfernt. Die Verbindung entsteht ausschließlich über „Mit OpenNIT anmelden". + Ein aus einer früheren Version übernommener Token wird beim Update aus dem lokalen Speicher gelöscht; + wer bisher nur damit verbunden war, meldet sich einmal per SSO an. + ### Behoben +- **Restlaufzeit des 2FA-Codes wird wieder korrekt angezeigt:** Der Countdown blieb am Ende eines + Zeitfensters stehen und lief mit der Zeit aus dem Takt. Er rechnet nun gegen den tatsächlichen + Ablaufzeitpunkt und holt den neuen Code beim Wechsel genau einmal nach. Lässt sich kein neuer Code + laden (Tresor gesperrt oder Verbindung weg), zeigt die Anzeige das an, statt auf einem abgelaufenen + Wert stehen zu bleiben. - **Neuen Eintrag anlegen funktioniert wieder nach SSO-Anmeldung:** Das Speichern eines im Popup erzeugten Passworts brach bisher mit „Nicht konfiguriert." ab, wenn die Erweiterung über „Mit OpenNIT anmelden" verbunden war – es wurde ausschließlich der manuelle Token akzeptiert. diff --git a/PRIVACY.md b/PRIVACY.md index 825b1e8..31a5a69 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -17,7 +17,7 @@ technisches Client-Werkzeug ohne eigenen Server. | Datum | Zweck | Speicherort | |-------|-------|-------------| | **Server-URL** | Adresse deiner OpenNIT-Instanz | lokal (`chrome.storage.local`) | -| **API-Token** | Authentifizierung gegenüber deiner Instanz | lokal (`chrome.storage.local`) | +| **Sitzungstoken** | Authentifizierung gegenüber deiner Instanz (per SSO ausgestellt) | lokal (`chrome.storage.local`) | | **Einstellungen** (PIN-Sperrdauer, Zwischenablage-Option) | Verhalten der Erweiterung | lokal (`chrome.storage.local`) | | **Entsperr-Status** | Merkt, ob die PIN-Sperre entsperrt ist | Sitzungsspeicher (`chrome.storage.session`, wird beim Schließen des Browsers gelöscht) | | **Tresor-Einträge** (Titel, Benutzername, URL, Notizen, ob 2FA vorhanden) | Anzeige & Suche | flüchtig im Arbeitsspeicher (max. 5 Min zwischengespeichert) | @@ -30,7 +30,7 @@ Die Erweiterung erstellt **keine** Nutzungsprofile und protokolliert **kein** Su - Es werden Daten **ausschließlich** an die von dir eingetragene **OpenNIT-Server-URL** übertragen (verschlüsselt via HTTPS, sofern deine Instanz HTTPS nutzt). -- Übertragen werden: der API-Token (zur Authentifizierung), die aktuelle Seiten-Adresse (zum Finden +- Übertragen werden: der Sitzungstoken (zur Authentifizierung), die aktuelle Seiten-Adresse (zum Finden passender Einträge), sowie beim Anlegen neuer Einträge die von dir eingegebenen Felder. - Es erfolgt **keine** Übermittlung an Anthropic, Google (außer der Chrome-Sync deines eigenen Browsers, falls du ihn aktiviert hast) oder sonstige Dritte. @@ -42,7 +42,7 @@ Siehe [`docs/PERMISSIONS.md`](docs/PERMISSIONS.md). Kurz: - **Zugriff auf alle Websites** (`host_permissions: `): nötig, um Login-Felder auf beliebigen Seiten zu erkennen und auf Wunsch auszufüllen. Es werden **keine** Seiteninhalte gelesen oder übertragen, außer den zum Ausfüllen nötigen Formularfeldern – und diese verlassen den Browser nicht. -- **storage**: lokale Speicherung von URL, Token und Einstellungen. +- **storage**: lokale Speicherung von URL, Sitzungstoken und Einstellungen. - **scripting / activeTab**: Einfügen der Zugangsdaten in das aktive Tab-Formular. - **alarms / offscreen**: automatisches Leeren der Zwischenablage nach dem Kopieren. @@ -53,7 +53,7 @@ Zwischenablage entfernt. ## 6. Speicherdauer -- Server-URL, Token und Einstellungen bleiben lokal gespeichert, bis du sie änderst oder die Erweiterung +- Server-URL, Sitzungstoken und Einstellungen bleiben lokal gespeichert, bis du sie änderst oder die Erweiterung entfernst. - Zwischengespeicherte Einträge werden spätestens nach 5 Minuten bzw. beim Sperren verworfen. - Passwörter/2FA-Codes werden nicht dauerhaft gespeichert. @@ -62,7 +62,7 @@ Zwischenablage entfernt. Da die eigentliche Datenverarbeitung in deiner OpenNIT-Instanz stattfindet, richten sich Auskunfts-, Lösch- und Berichtigungsrechte an deren Betreiber. Lokale Daten der Erweiterung entfernst du durch -Deinstallieren der Erweiterung oder Löschen des Tokens in den Einstellungen. +Deinstallieren der Erweiterung oder „Abmelden" in den Einstellungen. ## 8. Kontakt @@ -78,7 +78,7 @@ eine erreichbare Kontaktadresse ergänzen. **Controller.** The operator of the OpenNIT instance you connect to. The extension itself has no server. -**Data processed.** Server URL, API token and settings (stored locally); unlock state (session storage, +**Data processed.** Server URL, session tokens and settings (stored locally); unlock state (session storage, cleared when the browser closes); vault entry metadata (title, username, URL, notes, whether 2FA exists – cached in memory for up to 5 minutes); passwords/2FA codes (fetched **only** at the moment of use, never stored permanently); the active tab URL (to find matching entries). @@ -96,4 +96,4 @@ browser. See [`docs/PERMISSIONS.md`](docs/PERMISSIONS.md). within 5 minutes; secrets are never stored persistently. **Your rights.** Because processing happens in your OpenNIT instance, direct data-subject requests to its -operator. Remove local extension data by uninstalling the extension or clearing the token. +operator. Remove local extension data by uninstalling the extension or signing out in the settings. diff --git a/README.md b/README.md index a0a531c..29b6148 100644 --- a/README.md +++ b/README.md @@ -37,19 +37,18 @@ Die Erweiterung ist ein reiner **Client** zu deiner OpenNIT-Instanz. Sie enthäl und **keinen** Remote-Code – alle Skripte sind im Paket enthalten. Kommuniziert wird ausschließlich mit dem von dir konfigurierten OpenNIT-Server über dessen REST-API (`/api/vault/extension/...`) per **Bearer-Token**. -- Die Anmeldung erfolgt per **SSO** („Mit OpenNIT anmelden"). Die Erweiterung erhält dabei automatisch - ein kurzlebiges, rotierendes Zugriffstoken – ein manuelles Erzeugen von Tokens im Web-Tresor entfällt. +- Die Anmeldung erfolgt ausschließlich per **SSO** („Mit OpenNIT anmelden"). Die Erweiterung erhält dabei + ein kurzlebiges, rotierendes Zugriffstoken – ein manuelles Erzeugen von Tokens entfällt. - Passwörter werden **serverseitig** ver-/entschlüsselt; die Erweiterung fordert das Klartext-Passwort eines Eintrags erst **im Moment des Ausfüllens/Kopierens** an – nicht beim Laden der Liste. - Es findet **keine** Ende-zu-Ende-Entschlüsselung im Browser statt; die Erweiterung speichert keine - Passwörter dauerhaft (nur Server-URL, Token und Einstellungen in `chrome.storage`). + Passwörter dauerhaft (nur Server-URL, Sitzungstoken und Einstellungen in `chrome.storage`). Details: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) · Berechtigungen: [`docs/PERMISSIONS.md`](docs/PERMISSIONS.md) -> **Anmeldung per SSO (Standard):** „Mit OpenNIT anmelden" (OAuth 2.0 + PKCE) – Anmeldung wie an OpenNIT -> (lokal + 2FA / M365 / Keycloak), automatische Token-Erneuerung mit Rotation. Ein manuell erzeugter Token -> ist nur noch als „Erweitert"-Fallback in den Erweiterungs-Einstellungen vorgesehen; OpenNIT bietet dafür -> im Frontend keine Token-Erzeugung mehr an. Konzept/Details: [`docs/SSO-PLAN.md`](docs/SSO-PLAN.md). +> **Anmeldung per SSO:** „Mit OpenNIT anmelden" (OAuth 2.0 + PKCE) – Anmeldung wie an OpenNIT +> (lokal + 2FA / M365 / Keycloak), automatische Token-Erneuerung mit Rotation. Einen manuell eingetragenen +> API-Token unterstützt die Erweiterung nicht mehr. ## Installation @@ -80,7 +79,7 @@ Repository gedacht. | Einstellung | Beschreibung | |--------------------|--------------| | **Server-URL** | Adresse deiner OpenNIT-Instanz, ohne abschließenden Slash (`https://…`). | -| **Anmeldung** | Per **SSO** („Mit OpenNIT anmelden"). Ein manueller API-Token ist nur als „Erweitert"-Fallback vorgesehen. | +| **Anmeldung** | Ausschließlich per **SSO** („Mit OpenNIT anmelden"). | | **PIN-Sperre** | Aus / 5 Min / 15 Min / 1 Std / bis der Browser geschlossen wird. Nutzt den **Tresor-PIN**. | | **Zwischenablage** | Nach 30 s automatisch leeren (Standard: an). | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d2f2bfc..3c20238 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -27,7 +27,7 @@ Die Erweiterung ist ein **Manifest-V3-Client** ohne eigenen Server. Sie besteht | `background.js` | Zentrale Logik: API-Aufrufe, 5-Minuten-Cache, **Lock-Gate**, Favicon-Cache (Data-URLs), Zwischenablage-Timer. Alle Secrets fließen hier durch. | | `content.js` | Wird auf jeder Seite ausgeführt. Erkennt Benutzer-/Passwort-/OTP-Felder (inkl. Shadow-DOM, mehrstufige Logins, segmentierte OTP-Felder), zeigt das Vorschlags-Dropdown und füllt Felder framework-kompatibel (React/Vue/Angular). | | `popup.html` / `popup.js` | Toolbar-Popup: Liste, Suche, Detailansicht, Anlegen + Generator, PIN-Schirm. | -| `options.html` / `options.js` | Einstellungen: Server-URL, Token, PIN-Sperrdauer, Zwischenablage. | +| `options.html` / `options.js` | Einstellungen: Server-URL, SSO-Anmeldung, PIN-Sperrdauer, Zwischenablage. | | `offscreen.html` / `offscreen.js` | Minimaldokument, das ausschließlich die Zwischenablage leert (MV3-konform). | ## Nachrichten (Auszug) @@ -38,6 +38,7 @@ Die Erweiterung ist ein **Manifest-V3-Client** ohne eigenen Server. Sie besteht | `GET_LOCK` / `DO_UNLOCK` / `LOCK_NOW` | popup → bg | PIN-Sperre abfragen/entsperren/sperren | | `GET_ENTRIES` / `GET_MATCHING_ENTRIES` | popup/content → bg | Einträge (alle / passend zur URL) | | `GET_PASSWORD` / `GET_TOTP` | popup/content → bg | Secret **on demand** | +| `CREATE_ENTRY` | popup → bg | Neuen Eintrag anlegen | | `GET_FAVICON` | popup/content → bg | Favicon als Data-URL (serverseitig gecacht) | | `VAULT_FILL` | popup → content | Aktives Tab-Formular ausfüllen | | `SCHEDULE_CLIP_CLEAR` | popup/content → bg | Zwischenablage-Leerung planen | @@ -74,5 +75,5 @@ Fensterdauer fest; „Bis der Browser geschlossen wird" nutzt ein langes Serverf - **Kein Remote-Code** – alle Skripte im Paket (MV3-CSP-konform, keine Inline-Skripte). - **Secrets on demand** – Passwörter/TOTP erst bei Nutzung, nie in der Liste. -- **Kein persistentes Secret** – nur URL, Token, Einstellungen in `chrome.storage`. +- **Kein persistentes Secret** – nur URL, Sitzungstoken, Einstellungen in `chrome.storage`. - **Server-seitige Krypto** – Ver-/Entschlüsselung im OpenNIT-Server, nicht im Browser. diff --git a/extension/background.js b/extension/background.js index 4291339..5d50de6 100644 --- a/extension/background.js +++ b/extension/background.js @@ -13,13 +13,13 @@ async function getServerUrl() { return c.serverUrl || null; } -// ── Zugangstoken beschaffen (SSO-Refresh oder manueller Token) ────────────── -// Reihenfolge: manueller Token (Erweitert) > SSO-Access-Token (mit Auto-Refresh). +// ── Zugangstoken beschaffen (SSO) ────────────────────────────────────────── +// Kurzlebiger Access-Token aus der Session; ist er abgelaufen, wird er über den +// rotierenden Refresh-Token erneuert. async function getAccessToken() { const cfg = await new Promise(r => chrome.storage.local.get( - ['serverUrl', 'apiToken', 'apiRefreshToken', 'apiRefreshExpiresAt'], r)); + ['serverUrl', 'apiRefreshToken', 'apiRefreshExpiresAt'], r)); if (!cfg.serverUrl) return null; - if (cfg.apiToken) return cfg.apiToken; // manueller Token if (!cfg.apiRefreshToken) return null; // nicht angemeldet const sess = await chrome.storage.session.get(['accessToken', 'accessExpiresAt']); if (sess.accessToken && sess.accessExpiresAt && Date.now() < sess.accessExpiresAt - 30000) { @@ -298,6 +298,12 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg.type === 'CLEAR_CACHE') { cachedEntries = null; cacheTime = 0; faviconCache.clear(); sendResponse({ ok: true }); return true; } }); +// Der manuell eingetragene API-Token wird nicht mehr unterstützt; ein aus einer +// früheren Version übernommener Wert wird beim Update aus dem Speicher entfernt. +chrome.runtime.onInstalled.addListener(() => { + chrome.storage.local.remove('apiToken'); +}); + // Cache alle 5 Minuten leeren; Zwischenablage-Clear nach Timeout. chrome.alarms.create('clearCache', { periodInMinutes: 5 }); chrome.alarms.onAlarm.addListener(async (alarm) => { diff --git a/extension/content.js b/extension/content.js index 667e6cf..c365cce 100644 --- a/extension/content.js +++ b/extension/content.js @@ -511,6 +511,8 @@ const _obs = new MutationObserver(() => { try { _obs.observe(document.documentElement, { childList: true, subtree: true }); } catch {} // ── TOTP-Benachrichtigung (unten rechts) ──────────────────────────────────── +const TOTP_PERIOD = 30; // Sekunden pro Code (RFC 6238, Serverseite nutzt denselben Wert) + function showTotpNotification(code, remaining) { const ID = '__vault_totp_notif__'; const old = document.getElementById(ID); @@ -531,18 +533,20 @@ function showTotpNotification(code, remaining) { + '2FA-Code kopiert' + '
' + esc(formatted) + '
' + '
' - + '
' + + '
' + '' + remaining + 's'; document.documentElement.appendChild(notif); - let secs = remaining; + // Gegen einen festen Ablaufzeitpunkt rechnen, damit die Anzeige auch nach + // gedrosselten oder ausgefallenen Timer-Ticks stimmt. + const deadline = Date.now() + (Number(remaining) > 0 ? Number(remaining) : TOTP_PERIOD) * 1000; const iv = setInterval(() => { - secs--; + const secs = Math.max(0, Math.ceil((deadline - Date.now()) / 1000)); const t = document.getElementById('__vault_totp_t'); const bar = document.getElementById('__vault_totp_bar'); - if (secs <= 0 || !t) { clearInterval(iv); notif.style.transition = 'opacity .4s'; notif.style.opacity = '0'; setTimeout(() => notif.remove(), 400); return; } + if (secs === 0 || !t) { clearInterval(iv); notif.style.transition = 'opacity .4s'; notif.style.opacity = '0'; setTimeout(() => notif.remove(), 400); return; } t.textContent = secs + 's'; - if (bar) { bar.style.width = (secs / 30 * 100) + '%'; if (secs < 10) bar.style.background = '#dc3545'; } + if (bar) { bar.style.width = Math.min(100, secs / TOTP_PERIOD * 100) + '%'; if (secs <= 10) bar.style.background = '#dc3545'; } }, 1000); notif.addEventListener('click', () => { clearInterval(iv); notif.remove(); }); } diff --git a/extension/options.html b/extension/options.html index 85b50a4..f92cb61 100644 --- a/extension/options.html +++ b/extension/options.html @@ -229,29 +229,13 @@ label.control-label { Mit OpenNIT anmelden + + - Empfohlen: Anmeldung wie an OpenNIT (lokal + 2FA / Microsoft 365 / Keycloak). Der Zugang wird automatisch erneuert. + Die Anmeldung erfolgt wie an OpenNIT (lokal + 2FA / Microsoft 365 / Keycloak). Der Zugang wird automatisch erneuert und lässt sich jederzeit im Vault widerrufen.
- - -
- Erweitert: manueller Token -
-
- - - Alternative ohne SSO (z. B. Kiosk/Headless): Vault → „Extension“ → Token generieren. -
-
- - - -
-
-
@@ -289,9 +273,9 @@ label.control-label {
diff --git a/extension/options.js b/extension/options.js index c0ebc76..8559555 100644 --- a/extension/options.js +++ b/extension/options.js @@ -3,9 +3,8 @@ function $(id) { return document.getElementById(id); } // Gespeicherte Werte laden (überschreibt das vorausgefüllte Feld nur wenn bereits gespeichert) -chrome.storage.local.get(['serverUrl', 'apiToken'], cfg => { +chrome.storage.local.get(['serverUrl'], cfg => { if (cfg.serverUrl) $('serverUrl').value = cfg.serverUrl; - if (cfg.apiToken) $('apiToken').value = '••••••••'; }); // Sicherheits-Einstellungen laden @@ -25,8 +24,8 @@ $('btnSaveSec').addEventListener('click', () => { }); }); -// HTTPS erzwingen (außer localhost) – sonst gingen Token und Passwörter im -// Klartext über die Leitung. +// HTTPS erzwingen (außer localhost) – sonst gingen Zugangsdaten und Passwörter +// im Klartext über die Leitung. function isSecureServerUrl(url) { try { const u = new URL(url); @@ -37,58 +36,20 @@ function isSecureServerUrl(url) { } $('btnSave').addEventListener('click', () => { - const url = $('serverUrl').value.trim().replace(/\/$/, ''); - const token = $('apiToken').value.trim(); + const url = $('serverUrl').value.trim().replace(/\/$/, ''); if (!url) { showStatus('Server-URL darf nicht leer sein.', false); return; } if (!isSecureServerUrl(url)) { - showStatus('Bitte eine https://-Adresse verwenden (nur localhost darf http:// sein). Sonst würden Token und Passwörter unverschlüsselt übertragen.', false); + showStatus('Bitte eine https://-Adresse verwenden (nur localhost darf http:// sein). Sonst würden Zugangsdaten und Passwörter unverschlüsselt übertragen.', false); return; } - const data = { serverUrl: url }; - if (token && !token.startsWith('•')) data.apiToken = token; - - chrome.storage.local.set(data, () => { + chrome.storage.local.set({ serverUrl: url }, () => { chrome.runtime.sendMessage({ type: 'CLEAR_CACHE' }); $('savedMsg').innerHTML = '✓ Gespeichert'; setTimeout(() => { $('savedMsg').textContent = ''; }, 2000); }); }); -$('btnTest').addEventListener('click', async () => { - const url = $('serverUrl').value.trim().replace(/\/$/, ''); - const token = $('apiToken').value.trim(); - - if (!url || !token || token.startsWith('•')) { - showStatus('Bitte zuerst URL und Token eingeben und speichern.', false); - return; - } - - $('btnTest').innerHTML = ' Teste…'; - $('btnTest').disabled = true; - - try { - const res = await fetch(`${url}/api/vault/extension/status`, { - headers: { 'Authorization': `Bearer ${token}` } - }); - const data = await res.json(); - if (data.ok) { - showStatus(`Verbunden als ${esc(data.user)}`, true); - if (data.user) { - $('headerUser').textContent = data.user; - $('headerStatus').style.display = ''; - } - } else { - showStatus('Ungültiger Token oder Server-Fehler.', false); - } - } catch (e) { - showStatus('Server nicht erreichbar: ' + esc(e.message), false); - } - - $('btnTest').innerHTML = 'Verbindung testen'; - $('btnTest').disabled = false; -}); - // ── SSO-Anmeldung (OAuth 2.0 + PKCE via chrome.identity) ──────────────────── function b64url(bytes) { let s = btoa(String.fromCharCode.apply(null, new Uint8Array(bytes))); @@ -149,7 +110,6 @@ async function loginWithSso() { const data = await res.json().catch(() => ({})); if (!res.ok || !data.access_token) { ssoMsg('Token konnte nicht ausgestellt werden.', false); return; } await ssoSet('local', { serverUrl: url, apiRefreshToken: data.refresh_token, apiRefreshExpiresAt: Date.now() + (data.refresh_expires_in || 0) * 1000 }); - await ssoRemove('local', ['apiToken']); await ssoSet('session', { accessToken: data.access_token, accessExpiresAt: Date.now() + (data.expires_in || 0) * 1000 }); chrome.runtime.sendMessage({ type: 'CLEAR_CACHE' }); ssoMsg('Angemeldet.', true); @@ -184,7 +144,7 @@ function reflectAuthState() { }); } -// Verbindungsstatus über den Background (nutzt SSO-Access-Token oder manuellen Token) +// Verbindungsstatus über den Background (nutzt den SSO-Access-Token) function loadConnStatus() { chrome.runtime.sendMessage({ type: 'CHECK_STATUS' }, data => { if (data && data.ok) { diff --git a/extension/popup.js b/extension/popup.js index 871d1ff..2f09f3a 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -262,7 +262,7 @@ function generatePassword() { } // Speichern läuft – wie alle anderen Aufrufe – über den Background-Service-Worker, -// der den gültigen Zugang (SSO oder manueller Token) beisteuert. +// der den gültigen Zugang beisteuert. function saveNewEntry() { const title = $('neTitle').value.trim(); if (!title) { $('newEntryMsg').textContent = 'Titel ist erforderlich.'; return; } @@ -475,6 +475,12 @@ async function copyDetailPassword() { else showToast('Kein Passwort'); } +const TOTP_PERIOD = 30; // Sekunden pro Code (RFC 6238, Serverseite nutzt denselben Wert) + +// Restlaufzeit des 2FA-Codes. Die Anzeige rechnet gegen einen festen Ablaufzeitpunkt +// statt blind herunterzuzählen – so bleibt sie korrekt, wenn der Timer gedrosselt wird +// oder ein Tick ausfällt. Nachgeladen wird erst, wenn der Code abgelaufen ist, und pro +// Ablauf nur einmal. function loadDetailTotp(id) { const codeEl = $('detailTotp'); const secsEl = $('detailTotpSecs'); @@ -483,33 +489,55 @@ function loadDetailTotp(id) { codeEl.dataset.code = ''; secsEl.textContent = ''; - chrome.runtime.sendMessage({ type: 'GET_TOTP', id }, resp => { - if (!detailState || detailState.id !== String(id)) return; - if (!resp?.code) { codeEl.textContent = '—'; return; } + let deadline = 0; + let refetching = false; - const apply = (code, remaining) => { - codeEl.textContent = code.slice(0, 3) + ' ' + code.slice(3); - codeEl.dataset.code = code; - secsEl.textContent = remaining + 's'; - if (barEl) barEl.style.width = Math.round(remaining / 30 * 100) + '%'; - }; + const stillOpen = () => detailState && detailState.id === String(id); + + const render = () => { + const secs = Math.max(0, Math.ceil((deadline - Date.now()) / 1000)); + secsEl.textContent = secs + 's'; + if (barEl) { + barEl.style.width = Math.min(100, Math.round(secs / TOTP_PERIOD * 100)) + '%'; + barEl.style.background = secs <= 10 ? '#dc3545' : '#34d399'; + } + return secs; + }; + + const apply = (code, remaining) => { + codeEl.textContent = code.slice(0, 3) + ' ' + code.slice(3); + codeEl.dataset.code = code; + // Der Server liefert die Restsekunden des laufenden Zeitfensters; daraus wird + // ein Ablaufzeitpunkt, gegen den die Anzeige lokal rechnet. + deadline = Date.now() + (Number(remaining) > 0 ? Number(remaining) : TOTP_PERIOD) * 1000; + render(); + }; + + const refetch = () => { + if (refetching) return; + refetching = true; + chrome.runtime.sendMessage({ type: 'GET_TOTP', id }, r => { + refetching = false; + if (!stillOpen()) return; + if (r?.code) { apply(r.code, r.remaining); return; } + // Kein Code mehr (gesperrt oder Verbindung weg) – Anzeige zurücksetzen + // statt weiter auf einem abgelaufenen Wert stehen zu bleiben. + closeDetailTimers(); + codeEl.textContent = '—'; + codeEl.dataset.code = ''; + secsEl.textContent = ''; + if (barEl) barEl.style.width = '0%'; + }); + }; + + chrome.runtime.sendMessage({ type: 'GET_TOTP', id }, resp => { + if (!stillOpen()) return; + if (!resp?.code) { codeEl.textContent = '—'; return; } apply(resp.code, resp.remaining); - let secs = resp.remaining; detailState.totpInterval = setInterval(() => { - secs--; - if (secs <= 0) { - chrome.runtime.sendMessage({ type: 'GET_TOTP', id }, r2 => { - if (!detailState || detailState.id !== String(id)) return; - if (r2?.code) { secs = r2.remaining; apply(r2.code, r2.remaining); } - }); - return; - } - secsEl.textContent = secs + 's'; - if (barEl) { - barEl.style.width = Math.round(secs / 30 * 100) + '%'; - barEl.style.background = secs < 10 ? '#dc3545' : '#34d399'; - } + if (!stillOpen()) return; + if (render() === 0) refetch(); }, 1000); }); } diff --git a/store/listing.md b/store/listing.md index 8de2f66..a61bb15 100644 --- a/store/listing.md +++ b/store/listing.md @@ -45,7 +45,7 @@ SICHERHEIT & DATENSCHUTZ • Passwörter werden erst im Moment der Nutzung abgerufen und nicht dauerhaft gespeichert EINRICHTUNG -Server-URL eintragen, im OpenNIT-Tresor einen API-Token erzeugen und einfügen – fertig. +Server-URL eintragen, auf „Mit OpenNIT anmelden" klicken und den Zugriff bestätigen – fertig. OpenNIT Vault benötigt eine laufende OpenNIT-Instanz. ``` @@ -71,7 +71,7 @@ SECURITY & PRIVACY • Passwords are fetched only at the moment of use and never stored permanently SETUP -Enter your server URL, generate an API token in the OpenNIT vault, paste it — done. +Enter your server URL, click "Sign in with OpenNIT" and approve access — done. OpenNIT Vault requires a running OpenNIT instance. ``` @@ -93,7 +93,7 @@ Deutsch (primär), Englisch ## Datenschutz-Angaben (Data usage im Dashboard) -- Erhobene Datentypen: **Authentifizierungsinformationen** (API-Token), **Website-Inhalte** nur für +- Erhobene Datentypen: **Authentifizierungsinformationen** (Sitzungstoken), **Website-Inhalte** nur für Autofill (verlassen den Browser nicht). - Nicht verkauft/weitergegeben. Nicht für Werbung/Bonität genutzt. - Verschlüsselte Übertragung (HTTPS).