diora-web/static/js/app/32-crypto.js

71 lines
2.5 KiB
JavaScript
Raw Permalink Normal View History

'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);
}