2FA-Restlaufzeit korrigiert, manuellen API-Token entfernt
Countdown des 2FA-Codes: Die Anzeige zaehlte blind im Sekundentakt herunter und lief dadurch aus dem Takt. Am Ende eines Zeitfensters wurde zwar ein neuer Code angefordert, der Zaehler aber nicht angehalten - bei langsamer oder fehlschlagender Antwort lief er ins Negative und stiess jede Sekunde eine weitere Anfrage an (inkl. Audit-Eintrag je Abruf), waehrend die Anzeige auf dem alten Wert stehenblieb. Die Restlaufzeit wird jetzt aus einem festen Ablaufzeitpunkt berechnet, sodass gedrosselte oder ausgefallene Ticks sie nicht verschieben. Pro Ablauf wird genau einmal nachgeladen; liefert der Server keinen Code mehr (gesperrt/offline), stoppt der Timer und die Anzeige wird geleert. Dieselbe Rechnung gilt fuer die Benachrichtigung im Seiteninhalt; die Periode steckt dort wie im Popup in einer Konstanten statt als 30 im Ausdruck. Manueller API-Token: Eingabefeld, "Token speichern" und "Verbindung testen" sind aus den Optionen entfernt; die Server-URL laesst sich weiterhin separat speichern. getAccessToken() kennt nur noch den SSO-Weg, und ein aus einer frueheren Version uebernommener Token wird bei onInstalled aus chrome.storage.local geloescht. Doku und Store-Listing entsprechend angepasst. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G12DRMpe4UjYDwuRU1sjy1
This commit is contained in:
parent
ea5cd8d623
commit
19051bbacd
10 changed files with 115 additions and 122 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -5,7 +5,18 @@ Die Erweiterungsversion (`manifest.json`) ist unabhängig von der OpenNIT-Server
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
### 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
|
- **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
|
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.
|
„Mit OpenNIT anmelden" verbunden war – es wurde ausschließlich der manuelle Token akzeptiert.
|
||||||
|
|
|
||||||
14
PRIVACY.md
14
PRIVACY.md
|
|
@ -17,7 +17,7 @@ technisches Client-Werkzeug ohne eigenen Server.
|
||||||
| Datum | Zweck | Speicherort |
|
| Datum | Zweck | Speicherort |
|
||||||
|-------|-------|-------------|
|
|-------|-------|-------------|
|
||||||
| **Server-URL** | Adresse deiner OpenNIT-Instanz | lokal (`chrome.storage.local`) |
|
| **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`) |
|
| **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) |
|
| **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) |
|
| **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
|
- Es werden Daten **ausschließlich** an die von dir eingetragene **OpenNIT-Server-URL** übertragen
|
||||||
(verschlüsselt via HTTPS, sofern deine Instanz HTTPS nutzt).
|
(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.
|
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,
|
- Es erfolgt **keine** Übermittlung an Anthropic, Google (außer der Chrome-Sync deines eigenen Browsers,
|
||||||
falls du ihn aktiviert hast) oder sonstige Dritte.
|
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: <all_urls>`): nötig, um Login-Felder auf beliebigen
|
- **Zugriff auf alle Websites** (`host_permissions: <all_urls>`): nötig, um Login-Felder auf beliebigen
|
||||||
Seiten zu erkennen und auf Wunsch auszufüllen. Es werden **keine** Seiteninhalte gelesen oder
|
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.
|
ü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.
|
- **scripting / activeTab**: Einfügen der Zugangsdaten in das aktive Tab-Formular.
|
||||||
- **alarms / offscreen**: automatisches Leeren der Zwischenablage nach dem Kopieren.
|
- **alarms / offscreen**: automatisches Leeren der Zwischenablage nach dem Kopieren.
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ Zwischenablage entfernt.
|
||||||
|
|
||||||
## 6. Speicherdauer
|
## 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.
|
entfernst.
|
||||||
- Zwischengespeicherte Einträge werden spätestens nach 5 Minuten bzw. beim Sperren verworfen.
|
- Zwischengespeicherte Einträge werden spätestens nach 5 Minuten bzw. beim Sperren verworfen.
|
||||||
- Passwörter/2FA-Codes werden nicht dauerhaft gespeichert.
|
- 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-,
|
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
|
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
|
## 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.
|
**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 –
|
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
|
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).
|
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.
|
within 5 minutes; secrets are never stored persistently.
|
||||||
|
|
||||||
**Your rights.** Because processing happens in your OpenNIT instance, direct data-subject requests to its
|
**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.
|
||||||
|
|
|
||||||
15
README.md
15
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
|
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**.
|
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
|
- Die Anmeldung erfolgt ausschließlich per **SSO** („Mit OpenNIT anmelden"). Die Erweiterung erhält dabei
|
||||||
ein kurzlebiges, rotierendes Zugriffstoken – ein manuelles Erzeugen von Tokens im Web-Tresor entfällt.
|
ein kurzlebiges, rotierendes Zugriffstoken – ein manuelles Erzeugen von Tokens entfällt.
|
||||||
- Passwörter werden **serverseitig** ver-/entschlüsselt; die Erweiterung fordert das Klartext-Passwort
|
- 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.
|
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
|
- 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)
|
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
|
> **Anmeldung per SSO:** „Mit OpenNIT anmelden" (OAuth 2.0 + PKCE) – Anmeldung wie an OpenNIT
|
||||||
> (lokal + 2FA / M365 / Keycloak), automatische Token-Erneuerung mit Rotation. Ein manuell erzeugter Token
|
> (lokal + 2FA / M365 / Keycloak), automatische Token-Erneuerung mit Rotation. Einen manuell eingetragenen
|
||||||
> ist nur noch als „Erweitert"-Fallback in den Erweiterungs-Einstellungen vorgesehen; OpenNIT bietet dafür
|
> API-Token unterstützt die Erweiterung nicht mehr.
|
||||||
> im Frontend keine Token-Erzeugung mehr an. Konzept/Details: [`docs/SSO-PLAN.md`](docs/SSO-PLAN.md).
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
@ -80,7 +79,7 @@ Repository gedacht.
|
||||||
| Einstellung | Beschreibung |
|
| Einstellung | Beschreibung |
|
||||||
|--------------------|--------------|
|
|--------------------|--------------|
|
||||||
| **Server-URL** | Adresse deiner OpenNIT-Instanz, ohne abschließenden Slash (`https://…`). |
|
| **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**. |
|
| **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). |
|
| **Zwischenablage** | Nach 30 s automatisch leeren (Standard: an). |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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. |
|
| `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). |
|
| `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. |
|
| `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). |
|
| `offscreen.html` / `offscreen.js` | Minimaldokument, das ausschließlich die Zwischenablage leert (MV3-konform). |
|
||||||
|
|
||||||
## Nachrichten (Auszug)
|
## 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_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_ENTRIES` / `GET_MATCHING_ENTRIES` | popup/content → bg | Einträge (alle / passend zur URL) |
|
||||||
| `GET_PASSWORD` / `GET_TOTP` | popup/content → bg | Secret **on demand** |
|
| `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) |
|
| `GET_FAVICON` | popup/content → bg | Favicon als Data-URL (serverseitig gecacht) |
|
||||||
| `VAULT_FILL` | popup → content | Aktives Tab-Formular ausfüllen |
|
| `VAULT_FILL` | popup → content | Aktives Tab-Formular ausfüllen |
|
||||||
| `SCHEDULE_CLIP_CLEAR` | popup/content → bg | Zwischenablage-Leerung planen |
|
| `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).
|
- **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.
|
- **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.
|
- **Server-seitige Krypto** – Ver-/Entschlüsselung im OpenNIT-Server, nicht im Browser.
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,13 @@ async function getServerUrl() {
|
||||||
return c.serverUrl || null;
|
return c.serverUrl || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Zugangstoken beschaffen (SSO-Refresh oder manueller Token) ──────────────
|
// ── Zugangstoken beschaffen (SSO) ──────────────────────────────────────────
|
||||||
// Reihenfolge: manueller Token (Erweitert) > SSO-Access-Token (mit Auto-Refresh).
|
// Kurzlebiger Access-Token aus der Session; ist er abgelaufen, wird er über den
|
||||||
|
// rotierenden Refresh-Token erneuert.
|
||||||
async function getAccessToken() {
|
async function getAccessToken() {
|
||||||
const cfg = await new Promise(r => chrome.storage.local.get(
|
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.serverUrl) return null;
|
||||||
if (cfg.apiToken) return cfg.apiToken; // manueller Token
|
|
||||||
if (!cfg.apiRefreshToken) return null; // nicht angemeldet
|
if (!cfg.apiRefreshToken) return null; // nicht angemeldet
|
||||||
const sess = await chrome.storage.session.get(['accessToken', 'accessExpiresAt']);
|
const sess = await chrome.storage.session.get(['accessToken', 'accessExpiresAt']);
|
||||||
if (sess.accessToken && sess.accessExpiresAt && Date.now() < sess.accessExpiresAt - 30000) {
|
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; }
|
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.
|
// Cache alle 5 Minuten leeren; Zwischenablage-Clear nach Timeout.
|
||||||
chrome.alarms.create('clearCache', { periodInMinutes: 5 });
|
chrome.alarms.create('clearCache', { periodInMinutes: 5 });
|
||||||
chrome.alarms.onAlarm.addListener(async (alarm) => {
|
chrome.alarms.onAlarm.addListener(async (alarm) => {
|
||||||
|
|
|
||||||
|
|
@ -511,6 +511,8 @@ const _obs = new MutationObserver(() => {
|
||||||
try { _obs.observe(document.documentElement, { childList: true, subtree: true }); } catch {}
|
try { _obs.observe(document.documentElement, { childList: true, subtree: true }); } catch {}
|
||||||
|
|
||||||
// ── TOTP-Benachrichtigung (unten rechts) ────────────────────────────────────
|
// ── TOTP-Benachrichtigung (unten rechts) ────────────────────────────────────
|
||||||
|
const TOTP_PERIOD = 30; // Sekunden pro Code (RFC 6238, Serverseite nutzt denselben Wert)
|
||||||
|
|
||||||
function showTotpNotification(code, remaining) {
|
function showTotpNotification(code, remaining) {
|
||||||
const ID = '__vault_totp_notif__';
|
const ID = '__vault_totp_notif__';
|
||||||
const old = document.getElementById(ID);
|
const old = document.getElementById(ID);
|
||||||
|
|
@ -531,18 +533,20 @@ function showTotpNotification(code, remaining) {
|
||||||
+ '<span style="color:#adb5bd;font-size:10px;flex:1;">2FA-Code kopiert</span></div>'
|
+ '<span style="color:#adb5bd;font-size:10px;flex:1;">2FA-Code kopiert</span></div>'
|
||||||
+ '<div style="font-family:monospace;font-size:18px;font-weight:700;letter-spacing:.12em;">' + esc(formatted) + '</div>'
|
+ '<div style="font-family:monospace;font-size:18px;font-weight:700;letter-spacing:.12em;">' + esc(formatted) + '</div>'
|
||||||
+ '<div style="display:flex;align-items:center;gap:7px;"><div style="flex:1;height:3px;background:rgba(255,255,255,.15);border-radius:2px;overflow:hidden;">'
|
+ '<div style="display:flex;align-items:center;gap:7px;"><div style="flex:1;height:3px;background:rgba(255,255,255,.15);border-radius:2px;overflow:hidden;">'
|
||||||
+ '<div id="__vault_totp_bar" style="height:100%;background:#28a745;width:' + (remaining / 30 * 100) + '%;transition:width 1s linear;"></div></div>'
|
+ '<div id="__vault_totp_bar" style="height:100%;background:#28a745;width:' + Math.min(100, remaining / TOTP_PERIOD * 100) + '%;transition:width 1s linear;"></div></div>'
|
||||||
+ '<span id="__vault_totp_t" style="font-size:10px;color:#adb5bd;min-width:22px;text-align:right;">' + remaining + 's</span></div>';
|
+ '<span id="__vault_totp_t" style="font-size:10px;color:#adb5bd;min-width:22px;text-align:right;">' + remaining + 's</span></div>';
|
||||||
document.documentElement.appendChild(notif);
|
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(() => {
|
const iv = setInterval(() => {
|
||||||
secs--;
|
const secs = Math.max(0, Math.ceil((deadline - Date.now()) / 1000));
|
||||||
const t = document.getElementById('__vault_totp_t');
|
const t = document.getElementById('__vault_totp_t');
|
||||||
const bar = document.getElementById('__vault_totp_bar');
|
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';
|
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);
|
}, 1000);
|
||||||
notif.addEventListener('click', () => { clearInterval(iv); notif.remove(); });
|
notif.addEventListener('click', () => { clearInterval(iv); notif.remove(); });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -229,29 +229,13 @@ label.control-label {
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:2px;"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:2px;"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
|
||||||
Mit OpenNIT anmelden
|
Mit OpenNIT anmelden
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-default" id="btnSave">URL speichern</button>
|
||||||
<button class="btn btn-default" id="btnLogout" style="display:none;">Abmelden</button>
|
<button class="btn btn-default" id="btnLogout" style="display:none;">Abmelden</button>
|
||||||
<span id="ssoMsg"></span>
|
<span id="ssoMsg"></span>
|
||||||
|
<span id="savedMsg"></span>
|
||||||
</div>
|
</div>
|
||||||
<span class="form-text">Empfohlen: Anmeldung wie an OpenNIT (lokal + 2FA / Microsoft 365 / Keycloak). Der Zugang wird automatisch erneuert.</span>
|
<span class="form-text">Die Anmeldung erfolgt wie an OpenNIT (lokal + 2FA / Microsoft 365 / Keycloak). Der Zugang wird automatisch erneuert und lässt sich jederzeit im Vault widerrufen.</span>
|
||||||
<div id="statusMsg"></div>
|
<div id="statusMsg"></div>
|
||||||
|
|
||||||
<!-- Erweitert: manueller Token -->
|
|
||||||
<details style="margin-top:1rem;">
|
|
||||||
<summary style="cursor:pointer;font-size:.82rem;color:#6c757d;font-weight:600;">Erweitert: manueller Token</summary>
|
|
||||||
<div style="margin-top:.75rem;">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="control-label" for="apiToken">API-Token</label>
|
|
||||||
<input type="password" class="form-control" id="apiToken"
|
|
||||||
placeholder="Token aus dem Vault einfügen…">
|
|
||||||
<span class="form-text">Alternative ohne SSO (z. B. Kiosk/Headless): Vault → „Extension“ → Token generieren.</span>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group">
|
|
||||||
<button class="btn btn-default btn-sm" id="btnSave">Token speichern</button>
|
|
||||||
<button class="btn btn-default btn-sm" id="btnTest">Verbindung testen</button>
|
|
||||||
<span id="savedMsg"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -289,9 +273,9 @@ label.control-label {
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ul class="steps-list">
|
<ul class="steps-list">
|
||||||
<li><span class="step-badge">1</span><span>Öffnen Sie den <strong>Passwort-Vault</strong> in Ihrer OpenNIT-Anwendung.</span></li>
|
<li><span class="step-badge">1</span><span>Tragen Sie oben die <strong>Server-URL</strong> Ihrer OpenNIT-Instanz ein (nur <strong>https://</strong>).</span></li>
|
||||||
<li><span class="step-badge">2</span><span>Klicken Sie auf <strong>„Extension“</strong> in der Toolbar und generieren Sie einen API-Token.</span></li>
|
<li><span class="step-badge">2</span><span>Klicken Sie auf <strong>„Mit OpenNIT anmelden“</strong> und melden Sie sich wie gewohnt an.</span></li>
|
||||||
<li><span class="step-badge">3</span><span>Fügen Sie den Token oben in das Token-Feld ein und klicken Sie auf <strong>Speichern</strong>.</span></li>
|
<li><span class="step-badge">3</span><span>Bestätigen Sie den Zugriff auf den <strong>Passwort-Vault</strong> – die Erweiterung ist damit verbunden.</span></li>
|
||||||
<li><span class="step-badge">4</span><span>Fokussieren Sie ein Login-Feld auf einer Website – Vault-Vorschläge erscheinen automatisch.</span></li>
|
<li><span class="step-badge">4</span><span>Fokussieren Sie ein Login-Feld auf einer Website – Vault-Vorschläge erscheinen automatisch.</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,8 @@
|
||||||
function $(id) { return document.getElementById(id); }
|
function $(id) { return document.getElementById(id); }
|
||||||
|
|
||||||
// Gespeicherte Werte laden (überschreibt das vorausgefüllte Feld nur wenn bereits gespeichert)
|
// 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.serverUrl) $('serverUrl').value = cfg.serverUrl;
|
||||||
if (cfg.apiToken) $('apiToken').value = '••••••••';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sicherheits-Einstellungen laden
|
// Sicherheits-Einstellungen laden
|
||||||
|
|
@ -25,8 +24,8 @@ $('btnSaveSec').addEventListener('click', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// HTTPS erzwingen (außer localhost) – sonst gingen Token und Passwörter im
|
// HTTPS erzwingen (außer localhost) – sonst gingen Zugangsdaten und Passwörter
|
||||||
// Klartext über die Leitung.
|
// im Klartext über die Leitung.
|
||||||
function isSecureServerUrl(url) {
|
function isSecureServerUrl(url) {
|
||||||
try {
|
try {
|
||||||
const u = new URL(url);
|
const u = new URL(url);
|
||||||
|
|
@ -37,58 +36,20 @@ function isSecureServerUrl(url) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$('btnSave').addEventListener('click', () => {
|
$('btnSave').addEventListener('click', () => {
|
||||||
const url = $('serverUrl').value.trim().replace(/\/$/, '');
|
const url = $('serverUrl').value.trim().replace(/\/$/, '');
|
||||||
const token = $('apiToken').value.trim();
|
|
||||||
if (!url) { showStatus('Server-URL darf nicht leer sein.', false); return; }
|
if (!url) { showStatus('Server-URL darf nicht leer sein.', false); return; }
|
||||||
if (!isSecureServerUrl(url)) {
|
if (!isSecureServerUrl(url)) {
|
||||||
showStatus('Bitte eine <strong>https://</strong>-Adresse verwenden (nur localhost darf http:// sein). Sonst würden Token und Passwörter unverschlüsselt übertragen.', false);
|
showStatus('Bitte eine <strong>https://</strong>-Adresse verwenden (nur localhost darf http:// sein). Sonst würden Zugangsdaten und Passwörter unverschlüsselt übertragen.', false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = { serverUrl: url };
|
chrome.storage.local.set({ serverUrl: url }, () => {
|
||||||
if (token && !token.startsWith('•')) data.apiToken = token;
|
|
||||||
|
|
||||||
chrome.storage.local.set(data, () => {
|
|
||||||
chrome.runtime.sendMessage({ type: 'CLEAR_CACHE' });
|
chrome.runtime.sendMessage({ type: 'CLEAR_CACHE' });
|
||||||
$('savedMsg').innerHTML = '<span class="save-ok">✓ Gespeichert</span>';
|
$('savedMsg').innerHTML = '<span class="save-ok">✓ Gespeichert</span>';
|
||||||
setTimeout(() => { $('savedMsg').textContent = ''; }, 2000);
|
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 = '<span class="fa-spin-sm"></span> 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 <strong>${esc(data.user)}</strong>`, 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) ────────────────────
|
// ── SSO-Anmeldung (OAuth 2.0 + PKCE via chrome.identity) ────────────────────
|
||||||
function b64url(bytes) {
|
function b64url(bytes) {
|
||||||
let s = btoa(String.fromCharCode.apply(null, new Uint8Array(bytes)));
|
let s = btoa(String.fromCharCode.apply(null, new Uint8Array(bytes)));
|
||||||
|
|
@ -149,7 +110,6 @@ async function loginWithSso() {
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (!res.ok || !data.access_token) { ssoMsg('Token konnte nicht ausgestellt werden.', false); return; }
|
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 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 });
|
await ssoSet('session', { accessToken: data.access_token, accessExpiresAt: Date.now() + (data.expires_in || 0) * 1000 });
|
||||||
chrome.runtime.sendMessage({ type: 'CLEAR_CACHE' });
|
chrome.runtime.sendMessage({ type: 'CLEAR_CACHE' });
|
||||||
ssoMsg('Angemeldet.', true);
|
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() {
|
function loadConnStatus() {
|
||||||
chrome.runtime.sendMessage({ type: 'CHECK_STATUS' }, data => {
|
chrome.runtime.sendMessage({ type: 'CHECK_STATUS' }, data => {
|
||||||
if (data && data.ok) {
|
if (data && data.ok) {
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,7 @@ function generatePassword() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Speichern läuft – wie alle anderen Aufrufe – über den Background-Service-Worker,
|
// 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() {
|
function saveNewEntry() {
|
||||||
const title = $('neTitle').value.trim();
|
const title = $('neTitle').value.trim();
|
||||||
if (!title) { $('newEntryMsg').textContent = 'Titel ist erforderlich.'; return; }
|
if (!title) { $('newEntryMsg').textContent = 'Titel ist erforderlich.'; return; }
|
||||||
|
|
@ -475,6 +475,12 @@ async function copyDetailPassword() {
|
||||||
else showToast('Kein Passwort');
|
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) {
|
function loadDetailTotp(id) {
|
||||||
const codeEl = $('detailTotp');
|
const codeEl = $('detailTotp');
|
||||||
const secsEl = $('detailTotpSecs');
|
const secsEl = $('detailTotpSecs');
|
||||||
|
|
@ -483,33 +489,55 @@ function loadDetailTotp(id) {
|
||||||
codeEl.dataset.code = '';
|
codeEl.dataset.code = '';
|
||||||
secsEl.textContent = '';
|
secsEl.textContent = '';
|
||||||
|
|
||||||
chrome.runtime.sendMessage({ type: 'GET_TOTP', id }, resp => {
|
let deadline = 0;
|
||||||
if (!detailState || detailState.id !== String(id)) return;
|
let refetching = false;
|
||||||
if (!resp?.code) { codeEl.textContent = '—'; return; }
|
|
||||||
|
|
||||||
const apply = (code, remaining) => {
|
const stillOpen = () => detailState && detailState.id === String(id);
|
||||||
codeEl.textContent = code.slice(0, 3) + ' ' + code.slice(3);
|
|
||||||
codeEl.dataset.code = code;
|
const render = () => {
|
||||||
secsEl.textContent = remaining + 's';
|
const secs = Math.max(0, Math.ceil((deadline - Date.now()) / 1000));
|
||||||
if (barEl) barEl.style.width = Math.round(remaining / 30 * 100) + '%';
|
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);
|
apply(resp.code, resp.remaining);
|
||||||
|
|
||||||
let secs = resp.remaining;
|
|
||||||
detailState.totpInterval = setInterval(() => {
|
detailState.totpInterval = setInterval(() => {
|
||||||
secs--;
|
if (!stillOpen()) return;
|
||||||
if (secs <= 0) {
|
if (render() === 0) refetch();
|
||||||
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';
|
|
||||||
}
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ SICHERHEIT & DATENSCHUTZ
|
||||||
• Passwörter werden erst im Moment der Nutzung abgerufen und nicht dauerhaft gespeichert
|
• Passwörter werden erst im Moment der Nutzung abgerufen und nicht dauerhaft gespeichert
|
||||||
|
|
||||||
EINRICHTUNG
|
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.
|
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
|
• Passwords are fetched only at the moment of use and never stored permanently
|
||||||
|
|
||||||
SETUP
|
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.
|
OpenNIT Vault requires a running OpenNIT instance.
|
||||||
```
|
```
|
||||||
|
|
@ -93,7 +93,7 @@ Deutsch (primär), Englisch
|
||||||
|
|
||||||
## Datenschutz-Angaben (Data usage im Dashboard)
|
## 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).
|
Autofill (verlassen den Browser nicht).
|
||||||
- Nicht verkauft/weitergegeben. Nicht für Werbung/Bonität genutzt.
|
- Nicht verkauft/weitergegeben. Nicht für Werbung/Bonität genutzt.
|
||||||
- Verschlüsselte Übertragung (HTTPS).
|
- Verschlüsselte Übertragung (HTTPS).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue