Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// E2E Encryption utilities (Web Crypto API)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
let _encKey = null;
|
|
|
|
|
|
|
|
|
|
function bytesToBase64(buf) {
|
|
|
|
|
const bytes = new Uint8Array(buf);
|
|
|
|
|
let str = '';
|
|
|
|
|
for (const b of bytes) str += String.fromCharCode(b);
|
|
|
|
|
return btoa(str);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function base64ToBytes(b64) {
|
|
|
|
|
const str = atob(b64);
|
|
|
|
|
const buf = new Uint8Array(str.length);
|
|
|
|
|
for (let i = 0; i < str.length; i++) buf[i] = str.charCodeAt(i);
|
|
|
|
|
return buf;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bytesToHex(buf) {
|
|
|
|
|
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hexToBytes(hex) {
|
|
|
|
|
const arr = new Uint8Array(hex.length / 2);
|
|
|
|
|
for (let i = 0; i < arr.length; i++) arr[i] = parseInt(hex.slice(i*2, i*2+2), 16);
|
|
|
|
|
return arr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function getOrCreateEncKey() {
|
|
|
|
|
if (_encKey) return _encKey;
|
|
|
|
|
const storageKey = `diora_enc_key_${window.USER_ID || 0}`;
|
|
|
|
|
const stored = localStorage.getItem(storageKey);
|
|
|
|
|
if (stored) {
|
|
|
|
|
try {
|
|
|
|
|
const raw = base64ToBytes(stored);
|
|
|
|
|
_encKey = await crypto.subtle.importKey('raw', raw, {name: 'AES-GCM'}, false, ['encrypt', 'decrypt']);
|
|
|
|
|
return _encKey;
|
|
|
|
|
} catch (e) { /* fall through, generate new */ }
|
|
|
|
|
}
|
2026-11-01 15:01:25 +01:00
|
|
|
// Just signed in with the password: the login form left the derived key (99-init.js
|
|
|
|
|
// moves it too, but this may run first).
|
|
|
|
|
const pending = window.USER_ID ? localStorage.getItem('diora_pending_enc_key') : null;
|
|
|
|
|
if (pending) {
|
|
|
|
|
localStorage.setItem(storageKey, pending);
|
|
|
|
|
localStorage.removeItem('diora_pending_enc_key');
|
|
|
|
|
return getOrCreateEncKey();
|
|
|
|
|
}
|
|
|
|
|
// No key on this device (signed in through Authelia, or the storage was cleared): never
|
|
|
|
|
// make a new one — that would lock the existing books away. Ask for the real one
|
|
|
|
|
// (33-unlock.js); throws DioraLocked when the user puts it off.
|
|
|
|
|
const raw = await dioraUnlock();
|
|
|
|
|
_encKey = await crypto.subtle.importKey('raw', raw, {name: 'AES-GCM'}, false, ['encrypt', 'decrypt']);
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
return _encKey;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function encryptBytes(key, plainBytes) {
|
|
|
|
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
|
|
|
const ct = await crypto.subtle.encrypt({name: 'AES-GCM', iv}, key, plainBytes);
|
|
|
|
|
return {iv: bytesToHex(iv), ciphertext: bytesToBase64(ct)};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function decryptBytes(key, ivHex, ctB64) {
|
|
|
|
|
const iv = hexToBytes(ivHex);
|
|
|
|
|
const ct = base64ToBytes(ctB64);
|
|
|
|
|
return crypto.subtle.decrypt({name: 'AES-GCM', iv}, key, ct);
|
|
|
|
|
}
|