<!-- The markdown twin of https://kleidos.dev/security/, converted from that page at build time. The page is the original; this is the same words with the markup taken off. The site was last changed on 2026-10-25. -->

# Security

A password manager should be judged on what it is attacked by, so here is the list: twenty threats with an identifier each, forty-five controls that answer them, and five of the risks accepted and not fixed.

I cannot ask you to take any of it on trust. The repository is private while I prepare the first release, so you can read these claims and not the code behind them, and nobody outside the project has reviewed either. The last section on this page is about that, and it is the one I would read first.

**20**

threats in scope

**6**

declared out of scope

**45**

controls, each with the file that implements it

**13**

register entries citing a published CVE or advisory

On this page

1. [Threats](https://kleidos.dev/#model)

2. [Questions](https://kleidos.dev/#threats)

3. [Controls](https://kleidos.dev/#controls)

4. [Accepted risks](https://kleidos.dev/#residual)

5. [Unverifiable](https://kleidos.dev/#verify)

## What it is attacked by

Every threat the design answers carries a stable identifier, T1 to T20, and each document that answers some of them names which ones. Six more are declared out of scope, each with its reason, and that matters as much: a threat model that covers everything covers nothing.

9 of the twenty

### Somebody has the device

The flash read out, the PIN guessed on the screen, the PIN guessed offline after copying the flash, records moved or re-labeled inside the flash, the whole vault rolled back to an older state, the debug port, a probe on the external memory bus, a retired PIN used against an old copy of the vault's metadata, and the vault key guessed from a weak random draw, then checked against that metadata with one HMAC.

docs/security/vault-encryption.md

5 of the twenty

### Somebody is within radio range

Listening to the keystrokes or the advertisements, standing in the middle of a pairing, overwriting an existing bond, typing into a host before pairing has finished, and draining the battery by abusing the connection parameters.

docs/security/ble.md

2 of the twenty

### Somebody joins the admin network

Getting onto the access point at all, and getting past the panel's session token once there.

docs/security/wifi-admin-portal.md

1 of the twenty

### An update boots badly

The one that has to be undone without a cable, because the device is in somebody's pocket and the bad image is the one that just started.

docs/security/ota-and-boot.md

1 of the twenty

### A secret reaches a log

The one a developer causes rather than an attacker: a password, a PIN or a key ending up in a log line, a Bluetooth advertisement or a response that needed no authentication.

docs/security/key-material-and-logging.md

2 of the twenty

### An update is tampered with, or a dependency has a known bug

A tampered firmware image accepted as genuine, and a published vulnerability in a pinned dependency reaching a build.

docs/security/cve-register.md

Out of scope, and said so rather than left out: a computer that is already compromised and captures the keystrokes Kleidos types, because typing them is the entire product; an attacker who decaps the chip or injects faults, since there is no secure element and no tamper-evident case; power and electromagnetic side channels, for which I claim nothing; a firmware image tampered with before you ever flash it; choosing the wrong credential, or typing the right one into the wrong field, which the device cannot see.

Written down in docs/security/threat-model.md.

## The six you arrive with

Each is a question in the words a reader actually uses, then the answer, and then, for five of the six, the part the answer does not cover. A control described without its edge is the kind of security claim this page exists not to make.

**Somebody takes the device. How long do they have?**

Four wrong PINs lock it for a while, seven lock it for longer, and the tenth erases the vault, every Bluetooth bond and every saved admin network, in that order, with the counter cleared last, once the vault is already gone. The count survives a power cut, because it is written before the PIN is checked rather than after: pulling the battery halfway through a guess still costs the guess.

What that does not cover

A four-digit PIN is ten thousand combinations, and four digits is the default. Six and eight digits are offered, and so is a passphrase on the boards with a keyboard or a touch screen. What makes a short PIN survivable is the lockout, not the PIN.

Threats: T2

docs/security/pin-and-bruteforce.md

**Somebody desolders the flash and reads it. What do they get?**

The vault, as ciphertext. Every vault file is AES-256-CBC with an HMAC-SHA256 tag over the ciphertext, and the tag is verified before anything is decrypted, so a wrong key and a tampered file get the same answer, which is refusal rather than garbage. The PIN goes through PBKDF2-SHA256 (175,000 iterations on the ESP32-S3 boards, 40,000 on the classic ones, each sized to about a two-second unlock) to a key that checks the unlock and unwraps the vault's own random key, and the keys the records are encrypted under come from that random key.

What that does not cover

On the open builds the settings store beside the vault is not encrypted, so the same copy also holds, in the clear, the Bluetooth bond keys (enough to decrypt a recording of what the device typed into a paired computer), the attempt counter, the audit log, the settings, and the admin network's password if you chose to keep one. And the vault's own encryption is all there is: somebody holding the flash image can try PINs offline as fast as their hardware allows, and the only things slowing them down are the iteration count and how long your PIN is. The hardened build encrypts the flash and the settings and mixes in a secret of the device's own, which makes an offline attempt need that exact chip: on the classic ESP32 boards a random value kept in the encrypted settings, which only that chip can decrypt, and on the ESP32-S3 boards a key inside the chip's fuses, burned by a step that exists now and that I have not run on a board yet.

Threats: T1, T3

docs/security/vault-encryption.md

**Somebody records the Bluetooth traffic. Can they read the password?**

The link is Bluetooth LE Secure Connections only (legacy pairing is refused outright). Every characteristic of the keyboard service requires an encrypted link with a sixteen-byte key before its handler is ever reached, and the reports, which carry the keystrokes, also require that link to be authenticated; with numeric comparison the only pairing accepted, an encrypted link is always an authenticated one. Each burst of keystrokes re-checks that the link is still encrypted, still authenticated and still bonded before it sends, and every notification inside the burst checks again. The device advertises under a private address that changes every fifteen minutes.

Threats: T6

docs/security/ble.md

**Can something else pretend to be the computer I paired with?**

Pairing is numeric comparison: six digits on the device's own screen, which you confirm by hand. A request to re-pair a bond that already exists is denied by default and needs confirming on the device, and a confirmation that times out counts as a refusal rather than as a yes.

What that does not cover

Evicting the oldest bond when the table is full asks the same way, and the table holds four. On the classic ESP32 boards the controller's accept list and the rotating private address cannot both be on: the stack refuses the list while the address is on, so those boards keep the address privacy and not the list.

Threats: T7, T8

docs/security/ble.md

**The admin panel is a web server on a password manager. Really?**

It runs only while you ask for it, on a WPA3-only access point with protected management frames and a maximum of one client. The access point's password is shown on your unlocked screen and, unless you choose to keep one, is drawn afresh for each session, surviving only the one restart an update causes so that your browser can rejoin. The page asks for a session token when it loads, from a route capped at thirty requests a minute from any one address. Every route it calls after that, except the one that uploads an update, carries the token, which is compared in constant time and expires after five idle minutes; the upload route takes the upload password instead, compared the same way. All of them share a cap of eighty requests a minute. Only the page itself, and the redirect that sends any other path to it, are served with no cap at all. Bluetooth and Wi-Fi never run at the same time: every Bluetooth session has one owner that stops it, and this page is reached only after that stop.

What that does not cover

Inside that network, the panel trusts whoever joined it. That is the design and not an oversight: one client, a network that exists for minutes, a password you read off the device. But it does mean you should scan the QR code on the device's screen yourself, and in private. And if you choose to keep one password for every session, whoever reads it once keeps it, and on the open builds it is stored unencrypted in the device's settings, where a copy of the flash gives it away.

Threats: T11, T12

docs/security/wifi-admin-portal.md

**What happens when something it links has a CVE?**

There is a register: thirteen of its entries cite a published CVE or advisory, and two answer a known flaw nobody has published one for. Each says whether Kleidos is affected and why, with a link to its public source. Of all those entries, eleven cite a line of this repository as the evidence; the other four rest on an argument instead: the compiler the fleet is built with, the cryptography the firmware uses, or what the flaw needs in order to fire. Three are marked accepted risk, all three in the Bluetooth stack the framework bundles. One is in the radio controller's link layer, below the host this code drives. The other two are in that host, in the code that reads what the controller sends it, and both need the controller to send something it should not: the controller is part of the chip, so an attacker over the radio would have to compromise it first. And one of the two can only crash the device, which restarts with no keystroke and no pairing at risk. All three close when the framework ships the fixed release of that stack.

What that does not cover

It is a register of what has been published and looked at, which is not a register of what is there. What the firmware links is named and pinned. ESP-IDF 6.0.3 (which brings the Bluetooth host and the Wi-Fi stack), mbedTLS 4.1.1 with TF-PSA-Crypto 1.1.1 and ArduinoJson 7.4.3 are pinned to the digit, and LittleFS is held at 1.22.1 by the committed lock files, since its manifest accepts any 1.x from 1.10.2. So a bump is a deliberate act rather than a drift, and that is the whole of the supply-chain story today.

Threats: T17

docs/security/cve-register.md

## What answers them

One table maps each of the forty-five controls to the threat it mitigates, any advisory it addresses, the file that implements it, any standard it follows, and how to check it. Here they are grouped by what they protect.

### The vault at rest

Encrypt-then-MAC with verify-before-decrypt; a per-record envelope that authenticates the record's own type, slot and generation, so a record cannot be moved, re-labeled or put back on its own; a key-encryption key from the PIN that only checks the unlock and unwraps a random vault key, and from that vault key separate keys, each under its own label, that encrypt and authenticate the records; and, on the hardened builds, a secret of the device's own mixed into the PIN's derivation: a key inside the chip's fuses on the ESP32-S3, a random value in the encrypted settings on the classic ESP32.

docs/security/vault-encryption.md

### The PIN

Constant-time comparison; the attempt counter incremented before the derivation rather than after; the escalating lockout; and one erase routine that every surface destroying a vault has to call: the screen, the panel, the serial console and the boot recovery alike, with the counter reset last.

docs/security/pin-and-bruteforce.md

### The Bluetooth link

Secure Connections only, numeric comparison, encryption required across the whole keyboard service and authentication on the reports that carry the keystrokes, a live re-check per keystroke and per notification, a rotating private address, confirmation before re-pairing or evicting a bond, and a connection-parameter policy that clamps what a host may ask for.

docs/security/ble.md

### The admin panel

WPA3 with protected management frames and one client; a session token drawn at random for each session, and an upload password derived for each session from that token and the vault key; per-address rate limits on every route but the page itself and its redirect; and one shared gate that verifies the unlock secret against the vault before any route that destroys something runs, wired to the same lockout the on-device unlock uses.

docs/security/wifi-admin-portal.md

### Boot and updates

On the hardened builds: verified boot with RSA-3072 signatures, flash encryption in the chip (XTS-AES-128 on the ESP32-S3, and on the classic ESP32 that chip's own AES-256 scheme), encrypted settings, the chip's serial download mode disabled in the fuses, a refusal to install a version lower than the one running, and the rollback the bootloader performs when a new image does not report itself healthy in time. Turning them on is permanent: the first boot of a hardened build writes the chip's fuses, and nothing undoes that.

docs/security/ota-and-boot.md

### Key material and logs

One logging macro with the prohibition on logging credentials written into it; key arrays zeroized and platform key slots destroyed; a zeroizing allocator for the JSON the panel builds; an audit log that takes an event identifier and cannot be handed a credential; and an allow-list for what may live in external RAM.

docs/security/key-material-and-logging.md

### Backups

A separate envelope at 600,000 iterations under a passphrase of your own; re-encryption under the receiving device's keys on restore, so a backup can be carried between boards; and one sealed-backup path shared by the panel and the card slot rather than two that could disagree.

docs/security/vault-encryption.md

The table is written for somebody auditing the device, which is why each row points at the code rather than describing it. It is the document I would hand to a reviewer, and it is the document nobody outside the project has read.

Written down in docs/security/audit-dossier.md.

## What is accepted and not fixed

Here are five of the risks the design acknowledges and does not close; the threat model and the audit dossier list the others. The first is in effect today, and it is the one I would want to be told about if I were reading somebody else's page.

### A retired PIN still opens the current vault

Changing the PIN re-wraps the vault key rather than minting a new one. So the old PIN, plus any copy of the vault's metadata file made before the change, recovers the key in use now, and with it every credential written since. That file can come off the flash. Or it can come out of a sealed backup made before the change, for someone who also has that backup's passphrase. The attack needs read access to the current ciphertext and no write access at all. The whole-vault re-encryption that does rotate the key is kept and offered as an explicit action, on the device and in the panel. Destroy or retake any backup made before you changed the PIN.

Threats: T19

docs/security/vault-encryption.md

### A whole-vault rollback cannot be detected on the open builds

Each record carries a generation counter, so no single record can be put back on its own. Restoring the whole vault (the metadata and every record together) to an older consistent state passes every check, because the counters go back with it. On the hardened builds, encrypted flash and verified boot stop an attacker from forging such a state, but not from replaying one captured from that same device.

Threats: T5

docs/security/audit-dossier.md

### The PIN is short by default

Four digits is ten thousand combinations. Six and eight are offered, and a passphrase on the boards where you can type one. What makes the short PIN survivable is the lockout and the erase, and both are online defenses: they do nothing against somebody who already has the flash.

Threats: T2

docs/security/pin-and-bruteforce.md

### The classic boards cannot have both kinds of Bluetooth privacy

On a classic ESP32 the host-managed rotating address and the controller's accept list are mutually exclusive: the stack returns not-supported for the second. Those boards keep the rotating address; the ESP32-S3 boards have both.

Threats: T6

docs/security/ble.md

### Three boards have no hardened build at all

The Plus 1.1, the Plus SE and the CoreInk carry 4 MB of flash, which does not hold two update slots and a signed bootloader. They rely on the vault encryption alone against a stolen flash, and they are updated by re-flashing them whole.

Threats: T1, T16

docs/security/flash-and-production.md

## What you cannot check

Read this part before deciding anything.

- The repository is private. Every claim above points at a file, a path and often a line, and you cannot open any of them. I am asking you to believe a description of code you cannot read, which is the weakest form a security claim comes in. Opening the repository is a step on the roadmap and it is not done.

- Nobody independent has reviewed it. I wrote the firmware, and I also wrote the threat model, the control matrix and the register that describe it. Somebody checking their own work finds a different class of problem from somebody checking someone else's.

- The hardened build (the one with verified boot, encrypted flash and the key tied to the chip) does not work end to end on a device yet. On the ESP32-S3 boards the chip key it needs is burned by a step that exists now and that I have not run on a board yet. Every sentence on this page that says “hardened build” is describing something I have not finished.

- It is version 0.1.0 and there is no release. Nothing here has been run by anybody but me.

- The primitives are standard, two of them come from a library, and the way they are assembled is not reviewed. AES-256-CBC and HMAC-SHA256 go through mbedTLS 4.1 and its PSA interface, the same version on the device and in the tests I run on a PC. The other two are mine. PBKDF2-SHA256, the derivation that guards the vault, drives the chip's SHA peripheral directly on the ESP32-S3, and on the classic ESP32 and the PC it runs a SHA-256 written for this project; HKDF-SHA256 I built by hand on that HMAC. So the PC tests never run the S3's derivation: the S3's and the PC's give the same bytes because both are checked against the same published test vector, not because they share code. Whether I have written and assembled them correctly is exactly the question a review would answer.

If you want one sentence: this is one person's serious attempt at a hardware password manager, documented in detail and reviewed by nobody.
