Auf der Login-Seite gibt es „Mit Authelia anmelden“, wenn der Server dafür eingerichtet ist. Das eigene Konto wird einmal in den Einstellungen verknüpft, die Anmeldung mit Passwort bleibt. Fremde Authelia-Konten kommen nicht hinein, neue Konten entstehen darüber nicht. Fehlt einem Gerät der Bücher-Schlüssel (nach der Authelia-Anmeldung oder nach gelöschtem Speicher), erzeugt diora keinen neuen mehr, der die Bücher unlesbar machen würde. Stattdessen fragt es: Ein schon entsperrtes Gerät zeigt denselben Code und gibt den Schlüssel verschlüsselt frei, oder man gibt einmal das diora-Passwort ein. Der Server sieht den Schlüssel dabei nie. Details: docs/accounts.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
70 lines
2.5 KiB
JavaScript
70 lines
2.5 KiB
JavaScript
'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 */ }
|
|
}
|
|
// 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']);
|
|
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);
|
|
}
|