repl: explain PBKDF2 + AES-GCM on the lock-screen modal

This commit is contained in:
russell@unturf.com 2026-06-14 13:12:05 -04:00
parent 656f206c40
commit b3bcff5f08
No known key found for this signature in database
6 changed files with 183 additions and 6 deletions

View file

@ -44,8 +44,37 @@
<div id="lock-screen" class="lock-screen">
<div class="lock-card">
<h2>unlock</h2>
<p>enter a password — transcripts are encrypted with it locally.
a fresh password starts a fresh vault.</p>
<p>type a password to unlock or create a vault. every keystroke and
transcript line stays inside your browser &mdash; nothing leaves
this tab.</p>
<details class="lock-explain">
<summary>how the encryption works</summary>
<ul>
<li>your password is run through <strong>PBKDF2</strong> (200,000
iterations, SHA-256) with a random 32-byte salt generated
once per device and stored in <code>localStorage</code>.</li>
<li>that derives an <strong>AES-GCM 256-bit key</strong>. all
transcripts are encrypted with this key before any
<code>localStorage.setItem()</code> call.</li>
<li>the <strong>vault id</strong> is
<code>SHA-256(password || salt)</code> &mdash; different
passwords on the same device address completely independent
vaults. wrong password = decryption fails, you don't see
other vaults' contents.</li>
<li>same protocol as <a href="https://unsandbox.com/blog/vault-encryption-design"
target="_blank">unsandbox's vault design</a>, implemented
on the native <code>crypto.subtle</code> API instead of a
shipped CryptoJS bundle. source:
<a href="crypto.js" target="_blank"><code>crypto.js</code></a>.</li>
<li>passwords are never sent anywhere &mdash; not to lumbda.com,
not to a backend, not to a worker. nothing to leak in
transit because nothing transits.</li>
<li><strong>ephemeral</strong> skips the vault entirely &mdash;
nothing is written to disk; closing the tab loses everything.
use this on a shared machine when persistence would be a
footgun.</li>
</ul>
</details>
<input id="password" type="password" autocomplete="off" autofocus placeholder="password">
<div class="lock-row">
<button id="unlock" class="primary">unlock vault</button>

View file

@ -78,6 +78,36 @@ body.repl {
font-size: 0.8rem;
min-height: 1.2em;
}
.lock-explain {
margin: 0.4rem 0 0;
font-size: 0.78rem;
color: var(--muted);
}
.lock-explain summary {
cursor: pointer;
color: var(--green);
user-select: none;
}
.lock-explain summary:hover { text-decoration: underline; }
.lock-explain ul {
margin: 0.5rem 0 0;
padding-left: 1.1rem;
display: grid;
gap: 0.3rem;
}
.lock-explain li { line-height: 1.45; }
.lock-explain code {
background: var(--code-bg);
padding: 0 0.2em;
border-radius: 2px;
font-size: 0.92em;
}
.lock-explain a {
color: var(--green);
text-decoration: none;
}
.lock-explain a:hover { text-decoration: underline; }
.lock-explain strong { color: var(--fg); }
/* ─── Tab bar ──────────────────────────────────────────────────── */

View file

@ -44,8 +44,37 @@
<div id="lock-screen" class="lock-screen">
<div class="lock-card">
<h2>unlock</h2>
<p>enter a password — transcripts are encrypted with it locally.
a fresh password starts a fresh vault.</p>
<p>type a password to unlock or create a vault. every keystroke and
transcript line stays inside your browser &mdash; nothing leaves
this tab.</p>
<details class="lock-explain">
<summary>how the encryption works</summary>
<ul>
<li>your password is run through <strong>PBKDF2</strong> (200,000
iterations, SHA-256) with a random 32-byte salt generated
once per device and stored in <code>localStorage</code>.</li>
<li>that derives an <strong>AES-GCM 256-bit key</strong>. all
transcripts are encrypted with this key before any
<code>localStorage.setItem()</code> call.</li>
<li>the <strong>vault id</strong> is
<code>SHA-256(password || salt)</code> &mdash; different
passwords on the same device address completely independent
vaults. wrong password = decryption fails, you don't see
other vaults' contents.</li>
<li>same protocol as <a href="https://unsandbox.com/blog/vault-encryption-design"
target="_blank">unsandbox's vault design</a>, implemented
on the native <code>crypto.subtle</code> API instead of a
shipped CryptoJS bundle. source:
<a href="crypto.js" target="_blank"><code>crypto.js</code></a>.</li>
<li>passwords are never sent anywhere &mdash; not to lumbda.com,
not to a backend, not to a worker. nothing to leak in
transit because nothing transits.</li>
<li><strong>ephemeral</strong> skips the vault entirely &mdash;
nothing is written to disk; closing the tab loses everything.
use this on a shared machine when persistence would be a
footgun.</li>
</ul>
</details>
<input id="password" type="password" autocomplete="off" autofocus placeholder="password">
<div class="lock-row">
<button id="unlock" class="primary">unlock vault</button>

View file

@ -78,6 +78,36 @@ body.repl {
font-size: 0.8rem;
min-height: 1.2em;
}
.lock-explain {
margin: 0.4rem 0 0;
font-size: 0.78rem;
color: var(--muted);
}
.lock-explain summary {
cursor: pointer;
color: var(--green);
user-select: none;
}
.lock-explain summary:hover { text-decoration: underline; }
.lock-explain ul {
margin: 0.5rem 0 0;
padding-left: 1.1rem;
display: grid;
gap: 0.3rem;
}
.lock-explain li { line-height: 1.45; }
.lock-explain code {
background: var(--code-bg);
padding: 0 0.2em;
border-radius: 2px;
font-size: 0.92em;
}
.lock-explain a {
color: var(--green);
text-decoration: none;
}
.lock-explain a:hover { text-decoration: underline; }
.lock-explain strong { color: var(--fg); }
/* ─── Tab bar ──────────────────────────────────────────────────── */

View file

@ -44,8 +44,37 @@
<div id="lock-screen" class="lock-screen">
<div class="lock-card">
<h2>unlock</h2>
<p>enter a password — transcripts are encrypted with it locally.
a fresh password starts a fresh vault.</p>
<p>type a password to unlock or create a vault. every keystroke and
transcript line stays inside your browser &mdash; nothing leaves
this tab.</p>
<details class="lock-explain">
<summary>how the encryption works</summary>
<ul>
<li>your password is run through <strong>PBKDF2</strong> (200,000
iterations, SHA-256) with a random 32-byte salt generated
once per device and stored in <code>localStorage</code>.</li>
<li>that derives an <strong>AES-GCM 256-bit key</strong>. all
transcripts are encrypted with this key before any
<code>localStorage.setItem()</code> call.</li>
<li>the <strong>vault id</strong> is
<code>SHA-256(password || salt)</code> &mdash; different
passwords on the same device address completely independent
vaults. wrong password = decryption fails, you don't see
other vaults' contents.</li>
<li>same protocol as <a href="https://unsandbox.com/blog/vault-encryption-design"
target="_blank">unsandbox's vault design</a>, implemented
on the native <code>crypto.subtle</code> API instead of a
shipped CryptoJS bundle. source:
<a href="crypto.js" target="_blank"><code>crypto.js</code></a>.</li>
<li>passwords are never sent anywhere &mdash; not to lumbda.com,
not to a backend, not to a worker. nothing to leak in
transit because nothing transits.</li>
<li><strong>ephemeral</strong> skips the vault entirely &mdash;
nothing is written to disk; closing the tab loses everything.
use this on a shared machine when persistence would be a
footgun.</li>
</ul>
</details>
<input id="password" type="password" autocomplete="off" autofocus placeholder="password">
<div class="lock-row">
<button id="unlock" class="primary">unlock vault</button>

View file

@ -78,6 +78,36 @@ body.repl {
font-size: 0.8rem;
min-height: 1.2em;
}
.lock-explain {
margin: 0.4rem 0 0;
font-size: 0.78rem;
color: var(--muted);
}
.lock-explain summary {
cursor: pointer;
color: var(--green);
user-select: none;
}
.lock-explain summary:hover { text-decoration: underline; }
.lock-explain ul {
margin: 0.5rem 0 0;
padding-left: 1.1rem;
display: grid;
gap: 0.3rem;
}
.lock-explain li { line-height: 1.45; }
.lock-explain code {
background: var(--code-bg);
padding: 0 0.2em;
border-radius: 2px;
font-size: 0.92em;
}
.lock-explain a {
color: var(--green);
text-decoration: none;
}
.lock-explain a:hover { text-decoration: underline; }
.lock-explain strong { color: var(--fg); }
/* ─── Tab bar ──────────────────────────────────────────────────── */