+
+
+ Passwort-Manager-Erweiterung für OpenNIT – + Autofill für Benutzer-, Passwort- und 2FA-Felder direkt im Browser. +
+ +--- + +## Was ist das? + +**OpenNIT Vault** ist die Browser-Erweiterung zum Passwort-Tresor von OpenNIT. Sie verbindet sich mit +deiner selbst gehosteten OpenNIT-Instanz und bietet: + +- 🔎 **Sofortsuche** über alle persönlichen und Team-Tresore +- ⌨️ **Autofill** von Benutzername, Passwort und **2FA/TOTP-Codes** – auch auf mehrstufigen Login-Seiten +- 💡 **Vorschläge direkt im Eingabefeld** (passend zur aufgerufenen Website) +- 👁️ **Detailansicht** mit Anzeigen/Kopieren von Zugangsdaten und 2FA-Code mit Countdown +- ➕ **Neue Einträge anlegen** inkl. **Passwort-Generator** +- 🔒 **PIN-Sperre** mit demselben PIN wie der Web-Tresor (Dauer frei wählbar, bis „bis Browser schließt") +- 🧹 **Zwischenablage-Auto-Clear** nach dem Kopieren von Geheimnissen +- 🖼️ **Favicons** der hinterlegten Seiten (serverseitig gecacht – keine externen Aufrufe) +- 🌙 **Heller & dunkler Modus** (folgt dem System) + +
+
+
+
l?EON1vB-~x2)UW(t@n0IPj&``-8x4c`{u1D&uPC=hmXH`qp#4B
zo8%;3n;9Pw5AQMWaIMnaoCfi6O0vf0sL+_YG&$P*Server-Verbindung
+ Erweitert: manueller Token
+ Sicherheit
+ Einrichtungsschritte
+
+
+
Einstellungen prüfen.';
+ }
+ });
+ });
+}
+
+// ── Detailansicht ─────────────────────────────────────────────────────────
+function closeDetailTimers() {
+ if (detailState && detailState.totpInterval) {
+ clearInterval(detailState.totpInterval);
+ detailState.totpInterval = null;
+ }
+}
+
+function openDetail(id) {
+ const e = entryIndex[String(id)];
+ if (!e) return;
+ closeDetailTimers();
+ detailState = { id: String(id), password: null, revealUser: true, revealPass: false, totpInterval: null };
+
+ $('listWrap').style.display = 'none';
+ $('search').closest('.search-wrap').style.display = 'none';
+ $('newEntryPanel').style.display = 'none';
+ $('detailPanel').style.display = 'block';
+
+ $('detailHdTitle').textContent = e.title || 'Eintrag';
+ $('detailName').textContent = e.title || '';
+
+ // Icon: Favicon (gecacht) oder Monogramm
+ const icon = $('detailIcon');
+ const m = monogram(e.title);
+ icon.style.background = `hsl(${m.hue},52%,90%)`;
+ icon.innerHTML = `${m.ch}`;
+ if (e.favicon_domain) {
+ chrome.runtime.sendMessage({ type: 'GET_FAVICON', id }, resp => {
+ if (resp?.dataUrl && detailState && detailState.id === String(id)) {
+ icon.style.background = '#eef0f7';
+ icon.innerHTML = '
';
+ }
+ });
+ }
+
+ // URL
+ const urlLink = $('detailUrlLink');
+ const firstUrl = String(e.url || '').split('\n')[0].trim();
+ if (firstUrl) {
+ urlLink.textContent = firstUrl;
+ urlLink.href = /^https?:\/\//i.test(firstUrl) ? firstUrl : 'https://' + firstUrl;
+ urlLink.style.display = '';
+ } else {
+ urlLink.style.display = 'none';
+ }
+
+ // Benutzername (standardmäßig sichtbar; Auge schaltet Maskierung)
+ const uval = e.username || '';
+ const uEl = $('detailUser');
+ uEl.dataset.value = uval;
+ detailState.revealUser = true;
+ if (uval) {
+ uEl.classList.remove('empty');
+ uEl.textContent = uval;
+ $('btnRevealUser').style.display = '';
+ $('btnCopyUser').style.display = '';
+ } else {
+ uEl.classList.add('empty');
+ uEl.textContent = 'Kein Benutzername';
+ $('btnRevealUser').style.display = 'none';
+ $('btnCopyUser').style.display = 'none';
+ }
+
+ // Passwort (standardmäßig maskiert)
+ detailState.revealPass = false;
+ $('detailPass').textContent = '••••••••••';
+
+ // Notizen
+ const notes = (e.notes || '').trim();
+ if (notes) {
+ $('detailNotes').textContent = notes;
+ $('detailNotes').dataset.value = notes;
+ $('fieldNotes').style.display = '';
+ } else {
+ $('fieldNotes').style.display = 'none';
+ }
+
+ // TOTP
+ if (e.has_totp) {
+ $('fieldTotp').style.display = '';
+ loadDetailTotp(String(id));
+ } else {
+ $('fieldTotp').style.display = 'none';
+ }
+}
+
+function closeDetail() {
+ closeDetailTimers();
+ detailState = null;
+ $('detailPanel').style.display = 'none';
+ $('listWrap').style.display = '';
+ $('search').closest('.search-wrap').style.display = '';
+}
+
+function toggleRevealUser() {
+ const uEl = $('detailUser');
+ const val = uEl.dataset.value || '';
+ if (!val) return;
+ detailState.revealUser = !detailState.revealUser;
+ uEl.textContent = detailState.revealUser ? val : '•'.repeat(Math.min(val.length, 14));
+}
+
+async function ensurePassword(id) {
+ if (detailState && detailState.password !== null) return detailState.password;
+ const pw = await new Promise(resolve => {
+ chrome.runtime.sendMessage({ type: 'GET_PASSWORD', id }, resp => resolve(resp?.password ?? null));
+ });
+ if (detailState && detailState.id === String(id)) detailState.password = pw || '';
+ return pw || '';
+}
+
+async function toggleRevealPass() {
+ const pEl = $('detailPass');
+ if (detailState.revealPass) {
+ detailState.revealPass = false;
+ pEl.textContent = '••••••••••';
+ return;
+ }
+ pEl.textContent = '…';
+ const pw = await ensurePassword(detailState.id);
+ if (!detailState) return;
+ detailState.revealPass = true;
+ pEl.textContent = pw || '(leer)';
+}
+
+async function copyDetailPassword() {
+ const pw = await ensurePassword(detailState.id);
+ if (pw) copySecret(pw, 'Passwort kopiert');
+ else showToast('Kein Passwort');
+}
+
+function loadDetailTotp(id) {
+ const codeEl = $('detailTotp');
+ const secsEl = $('detailTotpSecs');
+ const barEl = $('detailTotpBar');
+ codeEl.textContent = '…';
+ 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; }
+
+ 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) + '%';
+ };
+ 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';
+ }
+ }, 1000);
+ });
+}
+
+async function fillActiveTab() {
+ if (!detailState) return;
+ const e = entryIndex[detailState.id];
+ if (!e) return;
+ const btn = $('btnDetailFill');
+ btn.disabled = true;
+ const pw = await ensurePassword(detailState.id);
+
+ chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
+ const tab = tabs[0];
+ if (!tab || !tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://')) {
+ showToast('Auf dieser Seite nicht möglich');
+ btn.disabled = false;
+ return;
+ }
+ // Sicherheit: Warnen, wenn die aktive Seite NICHT zur URL des Eintrags
+ // passt (verhindert versehentliches Ausfüllen auf einer fremden Domain).
+ if (!fillDomainMatches(e.url, tab.url)) {
+ const host = hostOf(tab.url);
+ if (!window.confirm('Diese Seite (' + host + ') passt nicht zur hinterlegten Adresse des Eintrags. Zugangsdaten trotzdem hier ausfüllen?')) {
+ btn.disabled = false;
+ return;
+ }
+ }
+ chrome.tabs.sendMessage(tab.id, { type: 'VAULT_FILL', id: detailState.id, username: e.username || '', password: pw || '', has_totp: !!e.has_totp }, () => {
+ if (chrome.runtime.lastError) {
+ showToast('Seite nicht bereit – neu laden');
+ btn.disabled = false;
+ } else {
+ showToast('Ausgefüllt');
+ setTimeout(() => window.close(), 350);
+ }
+ });
+ });
+}
+
+// ── Helfer ────────────────────────────────────────────────────────────────
+function hostOf(u) {
+ try {
+ const s = String(u || '');
+ return new URL(s.includes('://') ? s : 'https://' + s).hostname.replace(/^www\./, '').toLowerCase();
+ } catch { return ''; }
+}
+// True, wenn eine der (mehrzeiligen) Eintrags-URLs zur Seiten-Domain passt –
+// oder wenn im Eintrag gar keine URL hinterlegt ist (dann keine Warnung).
+function fillDomainMatches(entryUrls, pageUrl) {
+ const pageHost = hostOf(pageUrl);
+ const list = String(entryUrls || '').split('\n').map(s => s.trim()).filter(Boolean);
+ if (!list.length) return true;
+ if (!pageHost) return false;
+ return list.some(u => {
+ let eh = hostOf(u);
+ if (eh.startsWith('*.')) eh = eh.slice(2);
+ return eh && (pageHost === eh || pageHost.endsWith('.' + eh) || eh.endsWith('.' + pageHost));
+ });
+}
+function copyToClipboard(text, msg) {
+ navigator.clipboard.writeText(text).then(() => showToast(msg)).catch(() => showToast('Fehler'));
+}
+// Wie copyToClipboard, plant aber zusätzlich das automatische Leeren der
+// Zwischenablage (für Zugangsdaten/2FA).
+function copySecret(text, msg) {
+ navigator.clipboard.writeText(text).then(() => {
+ showToast(msg);
+ chrome.runtime.sendMessage({ type: 'SCHEDULE_CLIP_CLEAR', text });
+ }).catch(() => showToast('Fehler'));
+}
+function showToast(msg) {
+ const t = $('toast');
+ t.textContent = msg;
+ t.classList.add('show');
+ setTimeout(() => t.classList.remove('show'), 1800);
+}
+function esc(s) { return String(s||'').replace(/&/g,'&').replace(//g,'>'); }
+function escAttr(s) { return String(s||'').replace(/"/g,'"'); }
+
+init();
\ No newline at end of file
diff --git a/store/listing.md b/store/listing.md
new file mode 100644
index 0000000..8de2f66
--- /dev/null
+++ b/store/listing.md
@@ -0,0 +1,108 @@
+# Chrome Web Store – Eintrag
+
+Vorlagen für den Store-Eintrag. Vor Veröffentlichung Kontaktadresse/Datenschutz-URL ergänzen.
+
+---
+
+## Name
+
+```
+OpenNIT Vault
+```
+
+## Kurzbeschreibung (max. 132 Zeichen)
+
+**DE**
+```
+Passwort-Manager für OpenNIT: Autofill für Login- und 2FA-Felder, Suche, PIN-Sperre – sicher aus deiner eigenen Instanz.
+```
+
+**EN**
+```
+Password manager for OpenNIT: autofill for login and 2FA fields, search and PIN lock — served from your own instance.
+```
+
+## Ausführliche Beschreibung
+
+**DE**
+```
+OpenNIT Vault ist die offizielle Browser-Erweiterung zum Passwort-Tresor von OpenNIT – deiner
+selbst gehosteten IT-Dokumentation und Asset-Verwaltung.
+
+FUNKTIONEN
+• Autofill von Benutzername, Passwort und 2FA-/TOTP-Codes – auch bei mehrstufigen Logins
+• Vorschläge direkt im Eingabefeld, passend zur aufgerufenen Website
+• Sofortsuche über persönliche und Team-Tresore
+• Detailansicht: Zugangsdaten anzeigen/kopieren, 2FA-Code mit Countdown
+• Neue Einträge anlegen inkl. Passwort-Generator
+• PIN-Sperre mit demselben PIN wie dein Web-Tresor (Dauer frei wählbar)
+• Zwischenablage wird nach dem Kopieren automatisch geleert
+• Favicons der Seiten, heller & dunkler Modus
+
+SICHERHEIT & DATENSCHUTZ
+• Kein Remote-Code, keine Telemetrie, keine Drittanbieter-Server
+• Datenfluss ausschließlich zwischen Browser und deiner OpenNIT-Instanz
+• 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.
+
+OpenNIT Vault benötigt eine laufende OpenNIT-Instanz.
+```
+
+**EN**
+```
+OpenNIT Vault is the official browser extension for the OpenNIT password vault — your self-hosted
+IT documentation and asset management platform.
+
+FEATURES
+• Autofill for username, password and 2FA/TOTP codes, incl. multi-step logins
+• In-field suggestions matching the current website
+• Instant search across personal and team vaults
+• Detail view: reveal/copy credentials, 2FA code with countdown
+• Create new entries incl. password generator
+• PIN lock using the same PIN as your web vault (configurable duration)
+• Clipboard is cleared automatically after copying
+• Site favicons, light & dark mode
+
+SECURITY & PRIVACY
+• No remote code, no telemetry, no third-party servers
+• Data flows only between your browser and your OpenNIT instance
+• 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.
+
+OpenNIT Vault requires a running OpenNIT instance.
+```
+
+## Kategorie
+
+`Produktivität` (Productivity)
+
+## Sprache(n)
+
+Deutsch (primär), Englisch
+
+## Assets
+
+| Asset | Größe | Datei |
+|-------|-------|-------|
+| Store-Icon | 128×128 | `../extension/icon128.png` |
+| Screenshots | 1280×800 | `screenshots/01–05*.png` |
+
+## Datenschutz-Angaben (Data usage im Dashboard)
+
+- Erhobene Datentypen: **Authentifizierungsinformationen** (API-Token), **Website-Inhalte** nur für
+ Autofill (verlassen den Browser nicht).
+- Nicht verkauft/weitergegeben. Nicht für Werbung/Bonität genutzt.
+- Verschlüsselte Übertragung (HTTPS).
+- **Datenschutzerklärung-URL:** _(vor Veröffentlichung eintragen — z. B. Rohlink zu PRIVACY.md)_
+
+## Pflichtangaben vor Einreichung
+
+- [ ] Datenschutzerklärung-URL hinterlegt
+- [ ] Kontakt-E-Mail im Developer-Konto verifiziert
+- [ ] Berechtigungen begründet (siehe `../docs/PERMISSIONS.md`)
+- [ ] Single-Purpose-Beschreibung angegeben
+- [ ] Screenshots hochgeladen
diff --git a/store/screenshots/01-uebersicht.png b/store/screenshots/01-uebersicht.png
new file mode 100644
index 0000000000000000000000000000000000000000..c70cde2ea787e79cfcc3efdd29254272aadd4c1c
GIT binary patch
literal 355309
zcmb@uWmuGJ`~451f`Wudmx2n?C7qH2(k(r7N_Qv
XYrASp{MOvu>(InXasLU~svl^lCRako6cWe7{BG}Fh@Pz0`t)O2g3S=v
z7tihFo<)Wy;=PS`g6ui4O})uw#C_xp-O_UG?(kzZun_2i6dErtuiF$Be6Z7%38}YH
z?vm*Vd{ysu#5XWrbOQr}<03_CgR9TiI2oxB7$J*^KY
z3Blf7Sd&{#%o9vX5Lecv;-h1;%~QO|A$1NVfLmB-Dm55!_^{286mrK}gft|)Bc#NO
zr|%^=*4{WGM$b+rNu_irW0L$!A9K$G-&>{RZ?CJ8Zxoo%IqEcdVU4pH#{J=m%*^J+
z)tyXJj#<{RSHCJze)4zSL2tL$bEV}5E6C&RAh*Khd7O7!B2v_jXd`oZQAxQoF8KZ<
zb}0_F>n6~8464y@l~#Z()iBg_q3n(VMh09v2Ps}#=bI_g6yI5_@|RBQpV5sP;OKrL
z)LaR_PFo)#n7}}VQL?*j8QbF)2L~maQh*0kYLs7=%}fnc?MWvj7?ue--c84>-{92X
zti|FV#;h?IdTbd>IRkqcqG5|INGVSAr3bEYCujS}{SW4Jg;*0-{fYnc2C?4P-aDCh
z82Vr=-ZwU6(|gvYNc$G*0rep3M>aX1P@?zU?S-SUz*Pw4WFgxgjG1rOCy#{bQ9c@%
zBdeOw9xmCniGq8qW^Tmp6Zv#l1aZ$1Ov;=5V@T6%YZ{mZ
bNN+Ez_glzf7sw!Ox<4s_|~_H>;5rP2Osgp{;8A#ph*9~J^HvUS#lS5lRw1I
z`)}RaH9sx