Built on a 1.8" AMOLED touchscreen ESP32‑S3. No cloud. No app account. Your vault never leaves the device.
SecureKey is a DIY hardware password manager — a tiny touchscreen "keyboard emulator." Pick an entry, tap a field, and the device types it into whatever it's plugged into (PC over USB‑C) or paired with (phone/laptop over Bluetooth). Because it presents itself as a HID keyboard, it works on any OS with zero drivers, zero browser extensions, and zero software installed. The secrets live in flash on the device, behind a PIN — never synced, never uploaded.
Illustrative renders of the on‑device UI · 368 × 448 AMOLED
flowchart LR
V["Your vault<br/>lives on the device"] --> P["Pick an entry,<br/>tap a field"]
P --> Q{"Connected<br/>how?"}
Q -->|USB-C cable| U["Types into your<br/>PC / Mac as a keyboard"]
Q -->|Bluetooth| W["Types into your phone<br/>or laptop, wirelessly"]
To the computer it just looks like someone typing — no app, no browser extension, no account.
| SecureKey | Cloud manager | Browser autofill | |
|---|---|---|---|
| Where the secrets live | 🔒 on the device | ☁️ a company server | 💻 your browser profile |
| Needs an account | ❌ | ✅ | ✅ |
| Needs software on the host | ❌ | ✅ | ✅ |
| Works on any device, no install | ✅ | ❌ | ❌ |
| Usable fully offline | ✅ | ❌ | ❌ |
| Open hardware + firmware | ✅ | ❌ | ❌ |
| Feature | What it does | |
|---|---|---|
| ⌨️ | Dual‑transport typing | Types passwords as real keystrokes over USB‑C HID and Bluetooth LE HID — both can be live at once. |
| 🔢 | PIN lock + brute‑force backoff | 4‑digit unlock with escalating lockout (30 s → 60 s → 2 m → 5 m) that survives reboots. |
| 📶 | On‑device BLE pairing gate | A connecting phone is blocked until you tap Accept on the device. Reject, or Block for 5 min. Shows the peer's Bluetooth address. |
| 🗂️ | 30,000‑entry vault | Fixed‑size binary records on FFat, with a PSRAM index so the list scrolls instantly. |
| 🔎 | Instant search & favorites | Live filtering with an on‑screen keyboard, plus a ❤️ favorites view. |
| 🌐 | Wi‑Fi import/export portal | Spin up a captive‑portal web app to bulk‑import a CSV (Chrome/Google export auto‑detected) or export your vault — code‑protected. |
| ✋ | Drag‑to‑rearrange home | Long‑press a home tile and drag it, just like arranging apps on a phone. Order persists. |
Android @ fix |
One‑tap toggle that fixes the classic US↔UK keycode swap (where @ types as ") on Android hosts. |
|
| 🔒 | Auto‑lock, sleep, factory reset | Idle auto‑lock, double‑tap‑to‑sleep, PIN‑gated factory wipe, change‑PIN flow. |
| 🎨 | Hand‑drawn AMOLED UI | A custom, double‑buffered graphics UI (no LVGL) tuned for pure‑black AMOLED — glow effects, color avatars, pill toggles. |
flowchart LR
L["Lock"] --> P["PIN"]
P --> H["Home"]
H --> List["Passwords"]
H --> Add["Add new"]
H --> Fav["Favorites"]
H --> Set["Settings"]
List --> D["Detail<br/>tap a field → it types"]
The renders at the top show the Lock, Home, and Detail screens. Real device photos are welcome — drop them in
docs/img/and link them here.
| Part | Detail |
|---|---|
| Board | MaTouch ESP32‑S3 1.8" AMOLED (FT3168) — ESP32‑S3 R8 |
| MCU | ESP32‑S3, dual‑core, 8 MB OPI PSRAM, 16 MB flash |
| Display | 1.8" AMOLED, 368 × 448, SH8601 controller over QSPI |
| Touch | FT3168 capacitive, I²C |
| LED | WS2812 (single RGB pixel) — status feedback |
| USB | USB‑C, native ESP32‑S3 (USB‑OTG / TinyUSB) |
| Power | USB‑C, optional 3.7 V LiPo |
| Tool | Version | Where |
|---|---|---|
| Arduino IDE | 2.3.x | arduino.cc |
esp32 boards core |
2.0.16 | Boards Manager — do not use 3.x |
| GFX Library for Arduino | 1.3.7 | Library Manager |
| NimBLE‑Arduino | 1.4.3 | Library Manager — do not use 2.x |
| ESP32 BLE Keyboard (T‑vK) | latest | Library Manager (+ 2 small patches, below) |
| Adafruit NeoPixel | latest | Library Manager |
Add this Boards Manager URL first (File → Preferences → Additional URLs):
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
The stock ESP32 BLE Keyboard ships with the heavyweight Bluedroid backend and MITM‑required pairing, which break USB+BLE coexistence and silent‑fail on Windows/Android. In your …/Arduino/libraries/ESP32_BLE_Keyboard/ copy:
// BleKeyboard.h
- // #define USE_NIMBLE
+ #define USE_NIMBLE // half the RAM; coexists with native USB HID
// BleKeyboard.cpp (inside begin(), the USE_NIMBLE branch)
- BLEDevice::setSecurityAuth(true, true, true);
+ BLEDevice::setSecurityAuth(true, false, true); // SC "Just Works" pairingBoard: ESP32S3 Dev Module
PSRAM: OPI PSRAM ← REQUIRED (canvas lives here)
Flash Size: 16MB (128Mb)
Partition Scheme: 16M Flash (3MB APP/9.9MB FATFS)
USB CDC On Boot: Enabled
USB Mode: USB-OTG (TinyUSB) ← REQUIRED for USB HID typing
Upload Mode: UART0 / Hardware CDC
CPU Frequency: 240MHz
Flash Mode: QIO 80MHz
Open 06_PasswordManager/06_PasswordManager.ino and Upload. First boot seeds a few demo entries so you can see the UI immediately.
Prefer the command line? (arduino-cli)
arduino-cli compile \
--fqbn "esp32:esp32:esp32s3:PartitionScheme=app3M_fat9M_16MB,PSRAM=opi,FlashSize=16M,USBMode=default,CDCOnBoot=cdc" \
06_PasswordManager(USBMode=default is USB‑OTG/TinyUSB.) This is exactly what the CI workflow runs on every push.
Default PIN is 1234 — change it in Settings → Change PIN.
- Tap to wake → enter PIN → Home.
- Open Passwords, tap an entry.
- On the detail screen, tap any field (Title / User / Pass / URL / Note) — SecureKey types it into the focused field on your computer/phone.
- Password is masked; first tap reveals it, second tap types it.
- Bottom bar:
EDIT· ❤️ ·DELETE.
To type over USB: plug the USB‑C into a PC data port → tap a field. To type over Bluetooth: Settings → turn on Bluetooth → pair "SecureKey" from your phone → unlock the device → tap Accept on the request → tap a field.
💡 If
@comes out as"on Android, enable Settings → Android @ Fix.
A short tour of the interesting engineering. For the full deep dive see docs/ARCHITECTURE.md.
Both USB and Bluetooth expose a standard HID keyboard. The OS sees a normal keyboard, so typing works everywhere with no software. The firmware sends scancodes, and the host applies its own keyboard layout — which is why the Android @→" swap exists (see below).
USB HID uses TinyUSB; Bluetooth HID uses NimBLE (chosen over Bluedroid because it uses ~half the RAM and coexists with native USB). The ESP32‑S3 has two cores and NimBLE runs its own radio task, so both transports can be live simultaneously — typeViaHID() sends to every ready transport instead of picking one.
A BLE central (your phone) can connect to the device any time, but SecureKey types nothing until you physically accept it on‑screen — and the prompt only appears after you've entered your PIN. You can Accept, Reject (snoozes the prompt so an auto‑reconnecting phone doesn't nag), or Block 5 min (drops the radio). The prompt shows the peer's Bluetooth MAC. (A BLE central doesn't transmit a friendly name to a keyboard, so the address is what's identifiable.)
sequenceDiagram
participant Phone
participant SecureKey
participant You
Phone->>SecureKey: BLE connect
Note over SecureKey: typing stays BLOCKED
SecureKey->>You: "A device wants to connect" (after PIN unlock)
You-->>SecureKey: Accept / Reject / Block 5 min
Note over SecureKey: only Accept unlocks typing
SecureKey->>Phone: now types your password
HID sends key positions, not characters. On a host set to a UK/Android layout, Shift+2 is " (not @), and @ lives on Shift+'. The Android @ Fix toggle swaps those two keycodes on the BLE path so symbols land correctly — the classic US↔UK keyboard difference, solved in firmware.
The vault is fixed‑size 256‑byte records appended to a single FFat file (db.bin). At boot a compact 64‑byte index per entry is loaded into PSRAM, so searching, sorting, and scrolling never touch flash. Deletes are tombstoned; edits rewrite in place. This scales to 30,000 entries (≈1.9 MB index in PSRAM, ≈7.3 MB on the 9 MB FAT partition).
Wrong PINs trigger an escalating, persisted lockout (30 s → 60 s → 2 m → 5 m). The fail count lives in NVS, so power‑cycling doesn't reset the penalty — a brute‑force attacker can't just yank power to retry instantly.
Bulk data entry on a touchscreen is painful, so Settings can launch a SoftAP + captive‑portal web app. You join its Wi‑Fi, open the page, enter a one‑time code, and paste a CSV (Chrome/Google password exports auto‑detected) or export your vault. Every entry requires title + username + password — enforced both in the browser and server‑side.
The FT3168 driver feeds a tap/drag/swipe state machine. A long‑press on a home tile lifts it; it follows your finger; on release it drops into the nearest slot and the others shuffle to make room. The arrangement is saved to NVS and validated as a permutation on load.
The UI is hand‑drawn into a double‑buffered canvas in PSRAM and flushed over QSPI — no LVGL, no flicker. Because AMOLED black is truly off, the theme leans into glow halos (banded radial gradients), color letter‑avatars, and a blue/red accent system on black.
06_PasswordManager/
├── 06_PasswordManager.ino Main: setup/loop, nav stack, HID dispatch, BLE gate, auto-lock
├── theme.h Palette, layout constants, PassRecord + ListItem structs, capacity
├── pin_config.h Board pin map (display, touch, LED, USB)
│
├── hid_usb.cpp USB-HID keyboard (TinyUSB) ← isolated translation unit
├── hid_ble.cpp BLE-HID keyboard (NimBLE) + @ fix ← isolated translation unit
│
├── storage.ino FFat binary DB: append/load/delete/update/seed
├── touch_input.ino FT3168 reader + tap/drag/swipe/long-press state machine
├── gfx_lib.ino Drawing helpers: text, icons, glow, avatars, status/nav bars
├── keyboard.ino On-screen keyboard (search + add/edit)
│
├── screen_lock.ino Branded lock screen
├── screen_pin.ino 4-digit keypad + lockout countdown
├── screen_home.ino Home grid + drag-to-reorder
├── screen_list.ino Virtual-scroll list + inline search + favorites
├── screen_detail.ino Entry detail + Edit/Fav/Delete + tap-to-type
├── screen_add.ino Multi-step add / edit form
├── screen_settings.ino Settings, HID test, About, factory reset
├── screen_chgpin.ino Change-PIN flow
├── screen_wifi.ino Wi-Fi import status screen
├── wifi_portal.ino SoftAP + HTTP server (import/export)
└── portal_html.h The captive-portal single-page web app
⚠️ USB & BLE keyboard libraries each#define KEY_*macros, so they cannot share a translation unit. That's whyhid_usb.cppandhid_ble.cppare separate.cppfiles exposing a tinyextern "C"surface to the.inocode.
What it does today
- Vault is offline and PIN‑gated; the screen locks on idle and on the physical button.
- Brute‑force PIN backoff persists across reboots.
- Bluetooth typing requires explicit on‑device approval per connection.
What it does not do yet (PRs very welcome 🙏)
- 🔓 The vault on flash is not yet encrypted at rest. A determined attacker with physical access and a flash reader could dump it. Roadmap: AES‑256 with a key derived from the PIN, plus ESP32 Flash Encryption + Secure Boot v2.
- The default PIN is
1234— change it.
Treat this as a strong DIY / learning project, not a certified security product. See the roadmap.
- Touch UI, vault, USB + BLE HID, Wi‑Fi import, drag‑reorder
- AES‑256 encrypted vault (key derived from PIN)
- Flash Encryption + Secure Boot v2 (one‑way fuses, release mode)
- TOTP / 2FA code generator (HMAC‑SHA1)
- FIDO2 / WebAuthn passkey (ECC P‑256)
- Custom PCB + 3D‑printed enclosure
Contributions, bug reports, and hardware ports are welcome — see CONTRIBUTING.md. The CI compiles the firmware on every push, so make sure your branch builds green.
MIT © Shubh Jaiswal.
- Makerfabs — MaTouch ESP32‑S3 AMOLED hardware
- moononournation/Arduino_GFX — display driver
- h2zero/NimBLE‑Arduino — lightweight BLE stack
- T‑vK/ESP32‑BLE‑Keyboard — BLE HID keyboard
- adafruit/Adafruit_NeoPixel — WS2812 LED
If this project helped you, drop a ⭐ — it genuinely helps.