⚫ Try it now → https://rotor.naklitechie.com/ 📖 Guide & trust notes → https://rotor.naklitechie.com/guide/
A TOTP authenticator in one HTML file.
No install, no account, no server, no sync service. Your vault is a folder. Move the folder and your vault moves with it.
Rotor does one thing: rotating TOTP codes. No passwords, no cards, no notes, ever. If you want those, use its sibling Tijori. They share the same engine.
- Rotating 6/7/8-digit TOTP codes (RFC 6238) — SHA-1, SHA-256, SHA-512, 30s or 60s periods
- Add codes by pasting an
otpauth://URI or entering the Base32 secret manually - Glance-grid view with per-code countdown rings, tap-to-copy, next-code preview, pinning
- Every entry is individually encrypted: AES-256-GCM with a random 12-byte nonce
- Key derived via PBKDF2-SHA-256, 600,000 iterations (OWASP 2023 level)
- Vault format is an append-only event log — one
.jsonlfile per device, SHA-256 hash-chained - Air-gapped sync via QR flash — beam the encrypted vault to another device as a stream of animated QR codes. No cable, no Wi-Fi, no cloud, no account. Works for both first-time bootstrap (empty receiving vault) and ongoing updates (existing vault merges new events only — dedupes by
device_id:seq). Two phones in airplane mode can sync. - Multi-device sync by any file transport: cloud folder, Syncthing, Git, USB, encrypted archive, or QR flash — same deterministic merge, same file format
- Per-field last-writer-wins merge — deterministic, no conflicts, works with events arriving out of order
- Device revocation (soft — skips future events from the revoked device on merge)
- FSA primary + OPFS fallback — vault lives in a folder on desktop, in browser storage on iOS / mobile
- Clipboard auto-clears, idle lock, lock-on-tab-hide
- Self-test on boot — RFC 6238 Appendix B test vectors verified in DevTools console
- No passwords. No cards. No notes. No secure text. No file attachments. No password manager features of any kind. If you want those, use Tijori.
- No server. Vault files never leave the folder (or browser storage) you choose.
- No account. There is nothing to log in to.
- No recovery. Forget the master password and your vault is gone. Back up.
- No telemetry, no analytics, no network requests of any kind after page load.
- No framework, no build step. One HTML file plus a 25-line service worker. Open it in a text editor, read every line.
| Concern | Solution |
|---|---|
| TOTP | Pure Web Crypto HMAC-SHA-{1,256,512}, ~20 lines. Base32 inline, ~15 lines. |
| KDF | PBKDF2-SHA-256, 600,000 iterations |
| Per-event encryption | AES-256-GCM, random 12-byte nonce |
| Hash chain | SHA-256 over previous raw event-line string; genesis for first |
| Merge | Union all device streams, sort by (ts, device_id), per-field last-writer-wins |
| Storage | FileSystemDirectoryHandle (File System Access API) on desktop; navigator.storage.getDirectory() (OPFS) on iOS / mobile |
| Reconnect | FSA handle persisted in IndexedDB (permission re-requested on next visit); OPFS vault name in IndexedDB (no permission needed) |
| QR flash | Archive chunked into TJ1|total|index|b64 frames, Nayuki qrcodegen inlined, receiver via BarcodeDetector API. Out-of-order and duplicate frames are fine; receiver waits for all indices. |
| Offline | Service worker caches app shell (cache-first, no telemetry) |
| Dependencies | Zero |
| Build step | None |
vault-folder/
rotor-meta.json — plaintext: KDF params, device roster
rotor-events-<deviceId>.jsonl — one per device, append-only, hash-chained
Each event line:
{
"seq": 3,
"prev_hash": "<sha256-of-previous-line>",
"ts": "2026-04-24T10:22:31.000Z",
"device_id": "abc123…",
"event_type": "entry_created",
"payload_ct": "<base64-aes-gcm-ciphertext>",
"nonce": "<base64-12-byte-nonce>"
}payload_ct is AES-256-GCM ciphertext of the entry payload (JSON). prev_hash is SHA-256 of the preceding raw line string. Tampering any byte breaks the chain — verifiable from Settings → Vault → Verify log integrity.
Event types: device_registered, device_revoked, entry_created, entry_updated, entry_deleted.
Details in BACKUP-FORMAT.md.
- Open
index.htmlin Chrome, Edge, Firefox, or Safari 16.4+. - Create new vault → pick an empty folder (desktop) or name a browser vault (iOS / Android) → set a device name and master password.
- + Add code → paste an
otpauth://URI to auto-fill everything, or enter the Base32 secret manually. - Tap any tile to copy the current code. The ring counts down to the next rotation; the small number below is the next code.
- Long-press or right-click a tile to pin it to the top.
- Back up regularly. Settings → Data → Export encrypted archive → store the
.rotorfile somewhere safe (a cloud folder is fine — it is AES-encrypted with your master password). On mobile (OPFS) this is critical: clearing site data wipes the vault. - Add a second device — two ways:
- Share the folder (desktop only). Open the same vault folder from the new browser, enter the master password, device registers itself.
- QR flash (any device, including iPhone). On the source: Settings → Data → Send vault via QR. On the receiver: Import → QR sequence → point the camera at the screen. Works for both first-time setup and periodic updates.
Each device writes only its own .jsonl file. Sync is whatever moves files between devices — Rotor never implements a sync protocol, it just merges what it finds. On import, events are deduped by (device_id, seq) and appended, so the same archive is both a full bootstrap and an incremental update: an empty vault receives everything, an existing vault receives only what it's missing.
The transport worth calling out: Settings → Data → Send vault via QR. Rotor builds the full encrypted archive in memory, chunks it into TJ1|total|index|b64 frames, and loops an animated QR on screen. On the receiving device (Import → QR sequence), the camera picks up frames with BarcodeDetector, and a grid of dots fills in as each chunk arrives. Out-of-order and duplicate frames are normal — the receiver waits for all indices, reassembles, decrypts, and merges.
Use it for:
- First-time bootstrap — pair a fresh device with no other infrastructure. Two phones in airplane mode can sync your codes.
- Periodic updates — after adding or editing codes on device A, re-send. Device B's existing events are deduped; only the new ones are appended.
- Air-gapped environments — no cloud vendor, no P2P software, no cables. Just two screens and a camera.
Because TOTP secrets are the thing you most want to never leak, QR flash is the ideal transport for Rotor: the secrets never touch the internet, never sit in a cloud bucket encrypted-at-rest, never pass through a vendor's servers.
Supported where BarcodeDetector is available (Chrome, Safari 17+).
| Transport | Notes |
|---|---|
| Cloud folder (iCloud Drive / Dropbox / Google Drive) | Easiest for continuous multi-device use. Each device's browser points at its local copy. |
| Syncthing | P2P, no cloud vendor. |
| Git | Each device's log is a separate file — git merge never produces conflicts on event logs. |
| USB / manual | Export encrypted archive, import on other device. Same .rotor file, same dedup-on-import semantics as QR. |
Rotor and Tijori share the same vault engine — same event-log format, same crypto, same sync transports. They differ only in scope: Tijori stores logins, cards, notes, and codes under one master password; Rotor stores only codes.
Why both exist: TOTP is a second factor. The point of a second factor is to live on a different footing from the first. Combined storage is convenient but a breach of the master password loses both factors at once. Rotor exists for users who want the second factor to stay strictly separate — a different folder, a different master password, a different blast radius.
Format compatibility is a byproduct of engine reuse, not a feature. Do not point both tools at the same vault folder. Use separate folders with separate master passwords.
On every boot Rotor runs four RFC 6238 Appendix B test vectors (SHA-1, SHA-256, SHA-512 at T=59, plus SHA-1 at T=1111111109) against its own TOTP implementation and logs PASS / FAIL to the console. Open DevTools on first load to see Rotor TOTP self-test: 4/4 passed.
Rotor is a single HTML file in every meaningful sense — index.html contains all the UI, all the logic, the TOTP engine, the vault, the QR codec, everything. The one exception is sw.js, the service worker. It exists because browsers refuse to register a service worker from a blob: or data: URL, and a service worker is the only mechanism that lets Rotor run with the network fully disabled after the first load. sw.js is ~25 lines, has zero logic beyond cache-first routing, and you can read it end-to-end in thirty seconds.
- Desktop — Chrome, Edge, Firefox for the full folder-vault experience (File System Access API).
- Mobile / iOS — Safari 16.4+, Chrome on iOS. Vault lives in the browser's Origin Private File System instead of a user-visible folder. Export regularly to a desktop vault or via QR sync.
Coloured with japan-10 · 漆 URUSHI — Wajima lacquerware with kintsugi gold repair. Lacquer-black body, kintsugi-cream ink, cinnabar accent. Same palette as its sibling Hue & Cry; both reference Sanzo Wada and Wajima craft.
Palette pulled from Rangrez, the global colour-palette library that backs all NakliTechie projects.
MIT. See LICENSE.
Part of the NakliTechie series — single-file, browser-native, no-backend tools.