From 0.8.0 (beta; internal/breach, internal/admin/breaches.go,
internal/httpapi/breach.go). The threat it answers: a site where a person
of the household has an account is breached; the address and the password
leak, and they are tried on every other site (credential stuffing) or used
for a convincing phishing. A DNS filter cannot see a breach at a third
party: Warda tells the household that an address of theirs is in a known
breach, and whether a password is among the leaked ones, without sending
either.
Protection → Data breaches (#/breaches), in two parts:
- Addresses watched (subscription,
breaches; open to everyone during the beta). The accounts that change the household (administrators, rolesfullandeveryday) add up to 20 e-mail addresses, each optionally linked to a person of the household, remove them (Stop watching, confirmed) and ask a check (Check now). The rolereadonlysees the list without changing it. A member (an account without the household area) sees on the same page only the addresses linked to its own person, read-only ("An administrator of Warda adds the addresses to watch and links them to you"). Each address shows the address as typed, its person, the last check, "No known breach" or the breaches found (the name of the site and the date Warda found it), and what to do: change the password of these sites and of every site where the same one was used, turn on two-step verification, beware of the phishing that uses the leaked data. The next regular check is shown. - Test a password (free,
breaches.password): every signed-in account types a password and presses Test: "This password was seen N times in leaks: stop using it…" or "This password is not in the known leaks. That alone does not make it strong…". The field is cleared after each test.
The page credits the data: "Known breaches and leaked passwords: Have I Been Pwned (CC BY 4.0)", with links to the site and to the licence.
How it works (k-anonymity). A fingerprint (SHA-1) has 40 hexadecimal characters; Warda sends only its beginning, 6 characters for an address, 5 for a password. Across the billions of addresses known to the provider, hundreds share any 6 first characters (hundreds of leaked passwords share 5): the service answers with all of them, and Warda looks for its own at home. The service learns that someone asked about one of these hundreds, never which one.
- Addresses: the box hashes the normalised address (trimmed, lower
case, as Have I Been Pwned hashes them) and sends
{"prefix": "<6 upper hex>"}to the online service (POST /v1/breaches/range, signed with the key of the box,tls/cloud.key), which asks Have I Been Pwned with its own API key and returns the suffixes with their breaches; the box keeps the breaches of its addresses. Addresses whose hashes share a prefix are asked in one request. An address with non-ASCII letters may be lower-cased otherwise by the provider, and a domain written in Unicode and the same in punycode give two hashes (Warda does not convert one into the other): type the address as it is used to sign in to the sites. - Password: the browser computes the SHA-1 of the password
(WebCrypto) and sends only its 5 first characters to the box (
GET /api/v1/pwned-range?prefix=ABCDE, HTTPS, signed-in session); the box asks Pwned Passwords itself (https://api.pwnedpasswords.com/range/<5 hex>,Add-Padding: true,User-Agent: warda/<version>; free, no key, the online service of Warda is not asked) and returns the suffixes with their counts (the padding lines, count 0, dropped); the browser compares. The password never reaches the box; nothing is logged, not even the prefix; the answer isCache-Control: no-store; the field is not in a form, has no name and takesautocomplete="new-password"with the opt-outs of 1Password, LastPass, Bitwarden and Dashlane, so that the browser neither saves it nor fills in a password it knows (a browser may offer a "strong password" there: harmless).
Alerts. Each (address, breach) newly found is one alert in
Alerts (kind breach, group Security; "{address} is in a data
breach: {name}", the short advice and See the data breaches; SIEM
severity 6, detail address|breach name). It is written in the same
transaction as the breach found, so a breach is never kept without being
told nor told twice, also after a restore (the breaches are in the
backup). Removing an address, or the person it is linked to, forgets its
breaches and removes its alerts; added again, its breaches are told
again (a new watch). The address is whole in the SIEM export on purpose
(the security team needs it to reset that account's passwords; the export
already carries personal identifiers of that kind); the administration log
masks it.
Rhythm. The regular check runs once a week, at a minute of the week
of its own per box (a hash of the identifier of the installation, never
sent), task Known data breaches (breaches); new addresses are checked
a minute after the first one is added (several added together in one run).
Check now: once a day per address (the next time is shown and the
button is off until then), 30 seconds apart for any two addresses, one
check at a time. In a pair only the primary checks;
the secondary receives the addresses and the breaches with the
configuration, so a breach is told once (the time of a check is not a
change of the configuration; a breach found is). Nothing is checked, weekly
or on demand, before an administrator accepted the terms of use in force
(those of 2026-10-12 named this check; from 0.8.2, those of
2026-10-13; see Terms of use and anonymous
statistics): the page says so,
Check now is off and answers 409 meanwhile; accepting the terms starts
the checks at once. The password test does not wait.
The state until the service opens (expected now). The online service
needs an API key of Have I Been Pwned whose plan includes the k-anonymity
search of addresses; until the owner of Warda sets it, the service answers
503 {"error": "not configured"} and the page says: "The breach service of
Warda is not open yet: your addresses are kept and will be checked as soon
as it opens. The password test below works already." The box tries again
once a day meanwhile, so the watch starts within a day of the key being
set, and a Check now answered so does not use up the daily check of
the address. The other states, each retried by itself: limit (the
service allows 20 prefixes a day per box and 100 per address: "Too many
checks today", retried after the delay it asks, 10 minutes to 24 hours),
busy (the provider busy: the delay asked, else an hour), unreachable
and error (an hour, then 2, 4… up to 24 hours).
Limits. 20 addresses; an address of 254 characters at most, one @, a
local part of 64 at most, a domain with a dot, no space nor "(),:;<>[\],
no | in the domain; a duplicate is refused whatever its case (409); 200
breaches at most kept per address, names of 128 printable characters. The
password test: 20 tests per 10 minutes per session (429 beyond), 10 s to
answer, 1 MiB or 20,000 lines at most, no redirection. Only the breaches
known to Have I Been Pwned are told, and only for the addresses typed.
Stored. In the database (encrypted, as everything): breach_addresses
(the address as typed, its normalised form, the person, created, last
check, last check by hand) and breach_found (address, breach, date
found); both are in the backups. The state of the checks is in meta
(breaches, not in the backups). The administration log has
breach.address.add, breach.address.remove and breach.check, the
address masked (j…@example.com) with the id of the person; the log of
the service never holds an address, a prefix nor a breach name (only "new
known breaches found for the watched addresses count=N").
What leaves the box, and to whom.
| Part | What leaves | To whom | Never |
|---|---|---|---|
| Addresses watched | 6 hexadecimal characters of the SHA-1 of each normalised address, signed with the key of the box | warda-cloud, which passes the prefix to Have I Been Pwned (and keeps only counts by day) |
the address, the rest of its hash, the person, how many addresses |
| Password test | 5 hexadecimal characters of the SHA-1 of the password, computed in the browser | Pwned Passwords (Have I Been Pwned), directly from the box | the password, its full hash; warda-cloud |
API (503 without the database, like the others):
| Route | Permission | Body and answer |
|---|---|---|
GET /api/v1/breaches |
household read |
{"addresses": [{id, address, person_id, person_name, created, checked, manual, next_manual, breaches: [{name, found}]}], "max": 20, "state": {status, at, last_ok, last_full, retry_at, failures}, "next", "allowed"} (allowed: the terms of use in force are accepted) |
POST /api/v1/breaches |
household written |
{"address": "…", "person_id": 3} → the view; 400 invalid, 409 already there or 20 reached, 402 without the plan |
DELETE /api/v1/breaches/{id} |
household written |
always allowed, even without the plan |
POST /api/v1/breaches/{id}/check |
household written |
the view with state.status; 429 {"error", "next"} and Retry-After when asked too soon; 409 while a check runs or before the terms of use in force are accepted |
GET /api/v1/me/breaches |
own account | the addresses of the person of the account ("mine": true) |
GET /api/v1/pwned-range?prefix=ABCDE |
own account | {"suffixes": [{"suffix": "<35 hex>", "count": N}]}; 400 bad prefix, 429 per session, 502 provider |
Have I Been Pwned (Troy Hunt) publishes its breach data and Pwned Passwords under Creative Commons Attribution 4.0.