Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Here’s what you can do with IttyBitz:
- **Client-side encryption/decryption**: all cryptographic operations happen in your browser. Your files and secrets are never sent to a server.
- **Password & key file protection**: secure your data with a strong password, an optional key file, or both for an added layer of security. You can use any existing file or generate a new, cryptographically secure key file directly within the app.
- **File & text support**: encrypt and decrypt both files and text snippets.
- **QR code sharing**: easily share encrypted text snippets via a downloadable QR code.
- **QR code sharing**: easily share encrypted text snippets via a downloadable QR code — or **print a one-page emergency card**: the QR, the encrypted text in full, and three steps to decrypt it (open ittybitz.app or the recovery file kept beside the card, scan or type, enter the password). The password is never on the card, so the card can be kept in a safe, a drawer or with a notary and lost or copied without harm. Only encrypted text can be printed this way; decrypted text and seed phrases never reach paper from this page.
- **Hardware-wallet SeedQR export**: when decrypted text is a valid BIP-39 seed phrase, IttyBitz auto-detects it and can display a **Standard SeedQR** for direct import into hardware wallets (Coldcard, SeedSigner, Sparrow, Specter, Krux, Keystone, Jade). Any other decrypted text can be shown as a plain QR. Both are blurred until you deliberately reveal them.
- **One self-contained file**: the entire app is a single HTML file — no dependencies, no build, no service worker. Save it (or *File → Save Page As…* from [ittybitz.app](https://ittybitz.app)) and it runs offline, on an air-gapped machine, from a USB stick, in twenty years.
- **Privacy-focused UI**: the secret text field offers a show/hide blur toggle to prevent shoulder-surfing during input, and decrypted output is blurred by default until you tap to reveal.
Expand Down
4 changes: 2 additions & 2 deletions SHA256SUMS.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
783f720327251aa6b85db63e358b1d1460b0c975d5b9fbb39c9b0de8898e8068 ittybitz.html
40dc1cf811d66ecae145c95a651831983c10fc8efa9f01efb7de2acd856fd81d ittybitz-recovery.html
e0db40cb9d1fcb49bf36032fbad9e2dff522b85ea79dfdc2bb0c94be7bb69193 ittybitz.html
7f4546addd544be5e5a6120ed9bee14cf9e1dbb6b3e4c7318a2df479975adf20 ittybitz-recovery.html
31 changes: 31 additions & 0 deletions scripts/build/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -495,9 +495,40 @@
$('qr-caption').style.display = 'none';
$('qr-fp').style.display = 'none';
}
// The card is for ENCRYPTED text only: a ciphertext is made to be kept on
// paper, a decrypted text or a seed never is. The button simply does not
// exist for those.
$('qr-print').style.display = (qrState.kind === 'plain' && mode === 'encrypt') ? '' : 'none';
$('qr-overlay').classList.add('show');
}
function closeQr() { $('qr-overlay').classList.remove('show'); }

// ---- Emergency card ----
// Fill the print-only card, print, then empty it again so the ciphertext
// does not stay in the document (a copy saved later would carry it).
function fillCard() {
var text = qrState.getValue();
drawQR($('card-canvas'), text, false, 1024, 2);
$('card-text').textContent = text;
$('card-when').textContent = 'Made on ' + new Date().toISOString().slice(0, 10) + ' · ' + text.length + ' characters of Base64 · format IBTZ v1 · AES-256-GCM, PBKDF2 1,000,000';
$('card-kf').textContent = useKeyFile && keyFile ? ' and the key file (' + keyFile.name + ', kept separately as well)' : '';
}
function clearCard() {
$('card-text').textContent = ''; $('card-when').textContent = ''; $('card-kf').textContent = '';
var c = $('card-canvas'); c.width = 1; c.height = 1;
document.body.classList.remove('print-card');
}
$('qr-print').onclick = function () {
if (!qrState || qrState.kind !== 'plain' || mode !== 'encrypt') return;
fillCard();
document.body.classList.add('print-card');
var done = function () { window.removeEventListener('afterprint', done); clearCard(); };
window.addEventListener('afterprint', done);
window.print();
// Browsers that never fire afterprint (or a cancelled dialog in some) still
// get the card cleared, a moment after the dialog has had its chance.
setTimeout(done, 1500);
};
$('qr-close').onclick = closeQr;
$('qr-overlay').addEventListener('click', function (e) { if (e.target === this) closeQr(); });
$('qr-reveal').onclick = function () {
Expand Down
46 changes: 43 additions & 3 deletions scripts/build/head.html
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,29 @@
#pwa-x, #offline-x { background: none; border: 0; color: var(--muted-foreground); font-size: 20px; line-height: 1; cursor: pointer; padding: 0 .25rem; }
#pwa-x:hover, #offline-x:hover { color: var(--foreground); }
@media (max-width: 640px) { .pwa-inner { flex-wrap: wrap; justify-content: center; text-align: center; } .pwa-inner span { flex-basis: 100%; } }
/* Printing (or "Save as PDF") writes whatever is on screen to paper or a
file — a revealed secret, the password while shown, a QR. None of that
belongs in a print: blank every field that can hold a secret. */
@media print {
#t, #out, #p, #qr-canvas, .qrbox, #enc-fp-code, #dec-fp-code, #qr-fp-code { visibility: hidden !important; }
.overlay { display: none !important; }
/* Card mode: the page is replaced by the one-page emergency card. */
body.print-card > :not(#card) { display: none !important; }
body.print-card { background: #fff !important; color: #000 !important; padding: 0 !important; }
body.print-card #card { display: block !important; }
}
#card { display: none; max-width: 17cm; margin: 0 auto; padding: 1.5cm 0; font: 12pt/1.5 Georgia, "Times New Roman", serif; color: #000; }
#card h1 { font: 700 20pt/1.2 system-ui, sans-serif; margin: 0 0 .25rem; color: #000; -webkit-text-fill-color: #000; background: none; }
#card .card-when { margin: 0 0 1rem; color: #444; font-size: 10pt; }
#card .card-qr { text-align: center; margin: 0 0 1rem; }
#card .card-qr canvas { width: 8cm; height: 8cm; image-rendering: pixelated; }
#card pre { font: 8.5pt/1.35 ui-monospace, Menlo, Consolas, monospace; white-space: pre-wrap; word-break: break-all; border: 1px solid #000; padding: .6rem; margin: 0 0 1rem; }
#card ol { padding-left: 1.3rem; margin: 0 0 1rem; }
#card li { margin: 0 0 .4rem; }
#card .card-foot { font-size: 10pt; color: #444; margin: 0; }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
}
/* Center the footer on narrow screens (it wraps to two stacked rows). */
@media (max-width: 640px) {
footer { flex-direction: column; align-items: center; text-align: center; }
Expand Down Expand Up @@ -359,7 +382,7 @@ <h3>Drop a file here</h3>
<div class="row" id="pane-text" style="display:none">
<label for="t" id="text-label">Secret text</label>
<div class="field">
<textarea id="t" placeholder="Enter text to encrypt&#8230;" spellcheck="false"></textarea>
<textarea id="t" placeholder="Enter text to encrypt&#8230;" spellcheck="false" autocomplete="off" autocapitalize="off" autocorrect="off"></textarea>
<div class="out-actions" id="t-actions">
<button type="button" class="toggle-btn icon" id="t-toggle" aria-label="Hide secret text" title="Hide secret text"></button>
<button type="button" class="toggle-btn icon" id="t-copy" title="Copy"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button>
Expand All @@ -375,7 +398,7 @@ <h3>Drop a file here</h3>
<div class="row">
<label for="p">Password <span class="muted" id="pw-hint" style="color:var(--faint);font-weight:400">· min 24 chars, upper, lower, number, symbol</span></label>
<div class="field">
<input type="password" id="p" class="pw" autocomplete="off" spellcheck="false" placeholder="Enter a strong password">
<input type="password" id="p" class="pw" autocomplete="new-password" spellcheck="false" placeholder="Enter a strong password">
<button type="button" class="toggle-btn" id="p-toggle">Show</button>
</div>
<div class="btnrow">
Expand Down Expand Up @@ -423,7 +446,7 @@ <h3 style="font-size:14px">Select key file</h3>
</div>
</div>

<div class="status" id="status"></div>
<div class="status" id="status" role="status" aria-live="polite"></div>

<button class="go" id="go">
<span id="go-icon"></span><span id="go-label">Encrypt</span>
Expand Down Expand Up @@ -482,12 +505,29 @@ <h2 id="qr-title">QR Code</h2>
<div class="modal-actions">
<button class="btn" id="qr-reveal"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>Reveal</button>
<button class="btn" id="qr-download" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3"/><path d="m7 10 5 5 5-5"/><path d="M21 21H3"/></svg>Download PNG</button>
<button class="btn" id="qr-print" style="display:none" title="Print a one-page card: this QR, the encrypted text, and how to decrypt it — for a safe, a drawer or a notary. The password is not on it."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9V3h12v6"/><rect x="4" y="9" width="16" height="8" rx="2"/><path d="M6 14h12v7H6z"/></svg>Print card</button>
</div>
</div>
<button class="closebtn" id="qr-close">Close</button>
</div>
</div>

<!-- Emergency card: print-only, filled when "Print card" is pressed and emptied
right after, so nothing sits in the document otherwise. It carries the
ENCRYPTED text and how to open it — never the password, never a plaintext. -->
<section id="card" aria-hidden="true">
<h1>Encrypted with IttyBitz</h1>
<p class="card-when" id="card-when"></p>
<div class="card-qr"><canvas id="card-canvas" width="1" height="1"></canvas></div>
<pre id="card-text"></pre>
<ol>
<li>Open <b>https://ittybitz.app</b> — or, if that site is gone, the file <b>ittybitz-recovery.html</b> kept with this card (also at github.com/seQRets/ittybitz).</li>
<li>Choose <b>Decrypt → Text</b>, then scan the code above with the phone, or type the text exactly as printed.</li>
<li>Enter the password<span id="card-kf"></span>. <b>The password is not on this card</b>; it was kept separately by the person who made this.</li>
</ol>
<p class="card-foot">Everything happens in the browser; nothing is sent anywhere. This card can be lost or copied without harm — without the password it opens nothing.</p>
</section>

<!-- Donate overlay -->
<div class="overlay" id="donate-overlay">
<div class="modal">
Expand Down
Loading