Skip to content
Technology · Keystroke telemetry

Passwords can be stolen. Your rhythm cannot.

A credential vault that verifies how you type, not only what you type. Everything below is what the code does — the repository is public if you would rather read that.

2007 → 2026

It was written to tell a human from a bot.

Between 2007 and 2008, building a custom MVC framework called Fishtea at a large digital media company, I added a small telemetry script to its admin login: Keystroke Captcha. Visual CAPTCHAs were annoying people and scrapers were getting good. The idea was to listen to DOM key events — keydown and keyup, through YUI's YAHOO.util.Event — and decide whether a form was being filled in by a person or a machine.

It measured two things: flight, the gap between one key and the next, and dwell, how long a key stays down. Both worked for telling people from scripts. But the timings kept doing something else, which is the part that mattered: they separated one person from another. Everyone has an involuntary neuromuscular cadence, and it is about as distinctive as handwriting.

So I forked it into a biometric salt for the login itself, and demonstrated it the only way that is convincing. I wrote my password on the whiteboard in plain text and invited the room to log in as me. One engineer after another typed the exact characters and was refused. I sat down, typed the same string, and was let in on the first attempt.

Then smartphones arrived. Soft keyboards, touch targets and autocorrect flattened the timings the whole thing depended on, and the prototype went in a drawer for sixteen years.

What brought it back is that the attack changed. Credentials leak by the billion and attackers no longer break in — they log in, with valid details, increasingly through automation that fills a form faster and more evenly than any hand can. The useful question stopped being what secret was entered and became who or what entered it. That is a question a keyboard can still answer.

The engine

Two measurements, taken from typing you were doing anyway.

Dwell · n values

How long a key is held

From keydown to keyup on the same key. Weighted 1.0.

Flight · n−1 values

The gap to the next key

Release of key k to press of k+1. The most personal of the three, so it is weighted 1.5.

Digraph · n−1 values

Press to press

Down of k to down of k+1. Captures pairs your hands have learned as one motion. Weighted 1.0.

Globals · 7 values

The shape of the whole

Total time, and the mean and spread of each of the three. Weighted 0.5 — context, not evidence.

A credential of n keystrokes therefore produces a vector of exactly 3n + 5 numbers. Timings come from performance.now(), which browsers deliberately coarsen to roughly a tenth of a millisecond to blunt timing attacks — more than enough resolution here, and worth stating plainly rather than claiming microseconds we do not get.

On the wire

The server never learns your passphrase, and never answers with a score.

// The browser derives everything first. Argon2id at m=64 MiB, then HKDF.
POST /auth/login
x-ck-device, x-ck-nonce, x-ck-timestamp, x-ck-signature: Ed25519 over the body
{
  "username":      "engineer@domain.com",
  "authHash":      "qN7v…",   // a derived key, never the passphrase
  "featureVector": [74, 91, 62, … ]  // 3n+5 timings, scored then dropped
}

// A band and a sentence. No number, ever — see below.
200 { "band": "pass", "keyVersion": 4 }
200 { "band": "grey", "stepUp": ["backup_code"] }
401 { "band": "fail", "error": "rhythm_mismatch" }

The vector is scored, not stored

It is compared against your profile and discarded in the same request. What persists is per-feature means and standard deviations, floored at 8 ms — and a mean cannot be replayed as typing.

No score, ever

You are told a band and a sentence. A live number is the one signal an attacker can iterate against — type, read 0.31, adjust, read 0.38 — and it gives an honest user nothing they can act on.

Every request is signed

Each device holds an Ed25519 keypair and signs a nonce, a timestamp and a hash of the body. A captured request cannot be replayed from anywhere else.

Phantom Keys

A Backspace or an Escape that leaves no character behind is still a keystroke, and still part of the secret. Two people typing the same visible text can be typing different scripts.

The maths

Bounded similarity, not distance.

Enrolment takes eight samples and reduces them to a mean μ and a standard deviation σ per feature, with σ floored at 8 ms so a feature you happen to be very consistent on cannot become a hair trigger. A later attempt is scored feature by feature:

zk = |xk − μk| / σk

        Σ wk · 1 / (1 + (zk / 2)²)
score = ────────────────────────────────
                  Σ wk

The choice worth explaining is why each feature is passed through 1/(1 + (z/2)²) rather than summed as a squared distance. A Euclidean distance is unbounded, so one freak value — a sneeze, a doorbell, a key that stuck — dominates every other feature and refuses a legitimate person. This saturates: a wildly wrong feature contributes almost nothing, and the verdict rests on the pattern rather than on its worst moment.

The score falls into one of three bands, and the thresholds are yours to set. Relaxed passes at 0.55, Medium at 0.62, Strict at 0.70. Between the pass and grey thresholds you are asked to type once more; below the grey threshold the attempt is refused and the server share of your vault key is not released.

The Rhythm Trial runs this exact code in your browser → — same tokenizer, same vector, same weights, same bands, and no network requests at all.

Threat model

Where it helps, and where it does not.

Threat Password vault Hardware key CypherKey
Credential database leak Vulnerable Protected Protected — the cadence is not in the dump
Credential stuffing by a bot Vulnerable Protected Protected — synthetic timing does not match a person
Reverse-proxy phishing Vulnerable Protected Partial — the vector is bound to the device signature
Keylogger that records timing Compromised Protected Partial — a replayed cadence can pass
Someone watching you type Compromised Protected Partial — a good mimic on a new device meets a step-up
Losing the hardware n/a Locked out until replaced Nothing to lose
Enrolment friction None Register every device Eight samples, once

The two "partial" rows are the honest ones. A keylogger that captures inter-key timing as well as characters has captured the factor, and anyone claiming otherwise is selling something. What rhythm defends against is the attack that actually happens at scale: a credential leaked from someone else's breach, replayed from somewhere else by someone — or something — that has never watched you type. A new device always meets a second confirmation, which is what bounds the rest.