Add password prompt in Books tab to derive encryption key on-device
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:
parent
bbd920d75e
commit
b9c5f835f4
3 changed files with 55 additions and 6 deletions
|
|
@ -2198,14 +2198,22 @@ function bookFileSelected(input) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function initBookDropZone() {
|
function initBookDropZone() {
|
||||||
const zone = $('book-drop-zone');
|
|
||||||
|
|
||||||
// Prevent Firefox from opening dragged files when dropped outside the zone
|
// Prevent Firefox from opening dragged files when dropped outside the zone
|
||||||
document.addEventListener('dragover', e => e.preventDefault());
|
document.addEventListener('dragover', e => e.preventDefault());
|
||||||
document.addEventListener('drop', e => {
|
document.addEventListener('drop', e => {
|
||||||
|
const zone = $('book-drop-zone');
|
||||||
if (!zone || !zone.contains(e.target)) e.preventDefault();
|
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;
|
if (!zone) return;
|
||||||
|
|
||||||
zone.addEventListener('dragover', e => {
|
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) {
|
async function uploadEbook(file) {
|
||||||
const statusEl = $('book-upload-status');
|
const statusEl = $('book-upload-status');
|
||||||
const isPdf = /\.pdf$/i.test(file.name);
|
const isPdf = /\.pdf$/i.test(file.name);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
<meta name="apple-mobile-web-app-title" content="diora">
|
<meta name="apple-mobile-web-app-title" content="diora">
|
||||||
<meta name="description" content="Internet radio player">
|
<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="manifest" href="/static/manifest.json">
|
||||||
<link rel="apple-touch-icon" href="/static/icon-192.png">
|
<link rel="apple-touch-icon" href="/static/icon-192.png">
|
||||||
<link rel="stylesheet" href="/static/css/app.css">
|
<link rel="stylesheet" href="/static/css/app.css">
|
||||||
|
|
|
||||||
|
|
@ -278,11 +278,21 @@
|
||||||
<!-- ===== BOOKS TAB ===== -->
|
<!-- ===== BOOKS TAB ===== -->
|
||||||
<section class="tab-panel" id="tab-books" style="display:none;">
|
<section class="tab-panel" id="tab-books" style="display:none;">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
|
<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">
|
<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>
|
<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)">
|
<input type="file" id="book-file-input" accept=".epub,.pdf" style="display:none;" onchange="bookFileSelected(this)">
|
||||||
<span id="book-upload-status" class="muted"></span>
|
<span id="book-upload-status" class="muted"></span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div id="book-list" class="book-list"></div>
|
<div id="book-list" class="book-list"></div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="auth-prompt">
|
<p class="auth-prompt">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue