Add password prompt in Books tab to derive encryption key on-device
All checks were successful
Build and push Docker image / build (push) Successful in 14s
Test / test (push) Successful in 16s

Bypasses unreliable login-form interception; user enters password once
per device to derive the same PBKDF2 key cross-device.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
marwin 2026-03-19 21:29:51 +01:00
parent bbd920d75e
commit b9c5f835f4
3 changed files with 55 additions and 6 deletions

View file

@ -2198,14 +2198,22 @@ function bookFileSelected(input) {
}
function initBookDropZone() {
const zone = $('book-drop-zone');
// Prevent Firefox from opening dragged files when dropped outside the zone
document.addEventListener('dragover', e => e.preventDefault());
document.addEventListener('drop', e => {
const zone = $('book-drop-zone');
if (!zone || !zone.contains(e.target)) e.preventDefault();
});
const storageKey = `diora_enc_key_${window.USER_ID || 'anon'}`;
const hasKey = !!localStorage.getItem(storageKey);
const prompt = $('enc-key-prompt');
const uploadArea = $('book-upload-area');
if (prompt) prompt.style.display = hasKey ? 'none' : '';
if (uploadArea) uploadArea.style.display = hasKey ? '' : 'none';
const zone = $('book-drop-zone');
if (!zone) return;
zone.addEventListener('dragover', e => {
@ -2221,6 +2229,36 @@ function initBookDropZone() {
});
}
async function deriveAndStoreKey() {
const pwInput = document.getElementById('enc-key-password');
const statusEl = $('enc-key-status');
const pw = pwInput ? pwInput.value : '';
if (!pw) { if (statusEl) statusEl.textContent = 'Please enter your password.'; return; }
if (statusEl) statusEl.textContent = 'Deriving key…';
try {
const enc = new TextEncoder();
const username = document.querySelector('meta[name="username"]')?.content || '';
const mat = await crypto.subtle.importKey('raw', enc.encode(pw), 'PBKDF2', false, ['deriveKey']);
const key = await crypto.subtle.deriveKey(
{name: 'PBKDF2', salt: enc.encode('diora:' + username), iterations: 200000, hash: 'SHA-256'},
mat, {name: 'AES-GCM', length: 256}, true, ['encrypt', 'decrypt']
);
const raw = await crypto.subtle.exportKey('raw', key);
const storageKey = `diora_enc_key_${window.USER_ID || 'anon'}`;
localStorage.setItem(storageKey, bytesToBase64(new Uint8Array(raw)));
_encKey = null; // reset cached key
if (statusEl) statusEl.textContent = '✓ Unlocked';
const prompt = $('enc-key-prompt');
const uploadArea = $('book-upload-area');
if (prompt) prompt.style.display = 'none';
if (uploadArea) uploadArea.style.display = '';
loadBookList();
} catch (err) {
if (statusEl) statusEl.textContent = 'Error: ' + err.message;
}
}
async function uploadEbook(file) {
const statusEl = $('book-upload-status');
const isPdf = /\.pdf$/i.test(file.name);

View file

@ -8,6 +8,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="diora">
<meta name="description" content="Internet radio player">
{% if user.is_authenticated %}<meta name="username" content="{{ user.username }}">{% endif %}
<link rel="manifest" href="/static/manifest.json">
<link rel="apple-touch-icon" href="/static/icon-192.png">
<link rel="stylesheet" href="/static/css/app.css">

View file

@ -278,10 +278,20 @@
<!-- ===== BOOKS TAB ===== -->
<section class="tab-panel" id="tab-books" style="display:none;">
{% if user.is_authenticated %}
<div class="book-drop-zone" id="book-drop-zone">
<span>Drop .epub or .pdf here or <label for="book-file-input" style="cursor:pointer;text-decoration:underline;">browse</label></span>
<input type="file" id="book-file-input" accept=".epub,.pdf" style="display:none;" onchange="bookFileSelected(this)">
<span id="book-upload-status" class="muted"></span>
<div id="enc-key-prompt" style="display:none;" class="enc-key-prompt">
<p class="muted">Enter your password to unlock encrypted storage on this device.</p>
<div class="search-bar">
<input type="password" id="enc-key-password" class="search-input" placeholder="Your password…">
<button class="btn" onclick="deriveAndStoreKey()">Unlock</button>
</div>
<span id="enc-key-status" class="muted"></span>
</div>
<div id="book-upload-area" style="display:none;">
<div class="book-drop-zone" id="book-drop-zone">
<span>Drop .epub or .pdf here or <label for="book-file-input" style="cursor:pointer;text-decoration:underline;">browse</label></span>
<input type="file" id="book-file-input" accept=".epub,.pdf" style="display:none;" onchange="bookFileSelected(this)">
<span id="book-upload-status" class="muted"></span>
</div>
</div>
<div id="book-list" class="book-list"></div>
{% else %}