Evidence

Claim ledger

Every factual claim we make in public, with the command that reproduces it — and, in the same table, the 9 things we do not have.

How can I verify what Keepsake claims?

Every public claim is listed at securekeepsake.com/claims with the command that reproduces it, and in machine-readable form at securekeepsake.com/claims.json. 35 claims are backed by a named automated check or a published artefact; 9 are things Keepsake does not have, listed in the same table — including the absence of an independent security audit, a native iOS app, email ingestion and hardware-key support.

The same ledger as JSON →

Why this exists

A security claim you cannot check is a sentence, and this whole product is a security claim. So the numbers on this website are generated rather than typed: claims.json is built from the artefacts the claims came from — the benchmark report, the feature audit, the plan catalogue — and the build fails if any of them stops agreeing. It also renders the pages each claim is published on and fails if the claim is no longer there.

That gate exists because of a real mistake. The OCR figure on /purpleocr stayed at a superseded number for months, inside FAQ markup where nobody reading the page could see it — and precisely the form an answer engine lifts and repeats as ours.

Generated 2026-09-28 · 44 claims: 35 with evidence, 9 absent.

Cryptography

ClaimHow to check itWhere we say it
Documents are encrypted on your device with AES-256-GCM under a key derived by Argon2id, before anything is written to disk or sent to any cloud. dotnet test Keepsake/KeepsakeVault.Tests/KeepsakeVault.Tests.csproj
The cryptographic and licence suite, which prints its own total when you run it — a number written down here instead would be one nothing fails on when it changes. With it, the byte-level format spec at WebHome/spec/VAULT_FORMAT_V2.md, which is enough to decrypt a vault without any of our code.
/security · /features/vault
A stored document that has been altered by even one bit is refused, not returned altered. Every blob is AES-256-GCM, so the authentication tag covers all of it, and the read fails in Keepsake's own published wording rather than handing back bytes that are no longer the ones that were filed. This is the property disk- and container-encryption gives up: XTS-AES, which VeraCrypt, BitLocker and LUKS use, is length-preserving by design and therefore has nowhere to put a tag, so the same altered bit comes back as a changed document with no error at all. That is a trade rather than a defect, and the claim here is about detection and nothing else. pwsh tools/compare/tamper.ps1 -SelfTest
tools/compare/TamperDemo/Program.cs stores the same specimen document both ways, flips the same bit in each and asserts nine things about the two outcomes; the dated transcript it prints is published on /security. The same refusal is pinned independently in the vault suite (A_Tampered_File_Is_Refused_In_Keepsakes_Own_Words), in the desktop smoke suite against a real vault on disk, and on Android and the web, so the demonstration is not the only thing holding it up.
/security · /compare/veracrypt
Keepsake never presents your documents as a drive. There is no filesystem-mount code in it on any platform - no Dokan, no WinFsp, no FUSE, no DefineDosDevice - so there is no drive letter for a thumbnailer to preview, for a second cloud client to pick files up from, or for any other process running as you to read. That last one is measured, not assumed: a script with no passphrase read every document out of a mounted Cryptomator vault. That is the property a mounted container or vault gives up in exchange for every program being able to open your files. The claim is narrow on purpose: it is about a mount, not about plaintext never reaching a disk. Opening a document in another application writes a temporary file, because handing a PDF to a PDF reader means handing it a path, and it is deleted when the viewer window closes. The measured difference is scope and duration - one document while you look at it, against every document in the vault for as long as it is unlocked. pwsh tools/compare/mount-exposure.ps1 -SelfTest
The script reads every shipped source file on all three platforms and fails if any mounting API appears in one, so the absence is a property of the code rather than a setting. It also inventories Keepsake's own plaintext-writing paths - four staging directories that overwrite before deleting, plus the external-open temp file - and reports what is sitting in them on the machine it runs on; that second half is what corrected this page's earlier wording, which had said plaintext reached memory and nowhere else. The competitor half has now been run too, on 2026-09-14, and the blocker that had held it up was half imaginary: Cryptomator ships a GPG-signed portable CLI whose WebDAV mounter uses the Windows redirector already in the OS, so no installation and no VM were needed. A script holding no passphrase walked a mounted Cryptomator vault and read all 7 documents, 99,816 bytes, out of it. The search-indexer half of the measurement came back against us and is published that way: a document planted in the WebDAV-mounted vault contributed neither its text nor its file name to the Windows Search index, while an identical control document in an ordinary folder was fully indexed, so this page no longer says a mounted vault gets indexed. Not measured, and stated nowhere: Cryptomator's default WinFsp mounter, which presents a local volume, and VeraCrypt, which needs a kernel-mode driver and therefore a throwaway machine - that arm reports NOT RUN rather than a zero. Transcripts are in tools/compare/reports/.
/compare/veracrypt · /compare/cryptomator
Keepsake operates no server that receives your documents. There is no key escrow, no server-side decryption and no recovery path that runs through us — which also means we cannot recover a vault for you. bash tools/verify-portal.sh https://securekeepsake.com
The portal has no document endpoint to probe. Independently checkable by running any Keepsake app behind a proxy: the only traffic is to the cloud provider you chose.
/security

Sync

ClaimHow to check itWhere we say it
Editing one document uploads that document and nothing else, and the figure does not grow with the size of the vault. Measured on a 64 MB vault of 65 documents: a 200 KB document edited once costs 200.2 KB to send - the re-encrypted document, one small op-log file, and 207 bytes of overhead on the document itself. The comparison this is drawn against is a container, which is one file: edit anything inside it and its timestamp changes, so a whole-file sync client uploads the container again. How much that costs depends on the provider rather than on the encryption, which is why the pages state the two brackets and refuse to state a single number for somebody else's cloud account. dotnet run --project tools/compare/SyncCost -- --selftest
The benchmark builds a real vault with the shipped KVF2 keyring, syncs it with the shipped SyncEngine and LocalFolderProvider, edits one document, syncs again, and diffs the bytes in the cloud folder - so the number is a measurement of the code that ships rather than a model of it. Two of its checks are the claim itself: that an edit uploads one document, and that the figure is unchanged by the size of the vault. The XTS propagation figure beside it is labelled MODELLED and uses the same code the tamper demonstration uses; the Cryptomator figure used to be labelled ARITHMETIC and derived from that product's published format. Since 2026-09-23 it is measured, off a copy of a real vault mounted by Cryptomator's own GPG-verified command-line tool, and measuring it showed the arithmetic had been 12% too high in our favour: it rounded the document's last content chunk up to a full 32 KiB when a part-used chunk costs only what it holds. The corrected figure is 200.3 KB against Keepsake's 200.2 KB, which is no meaningful difference, and the page says so. Still unmeasured: the conflict behaviour of both competitors, which needs two machines and a real cloud provider. Transcripts in tools/compare/reports/, measurement in tools/compare/cryptomator-edit-cost.json.
/compare/veracrypt · /compare/cryptomator
When two devices edit the same document while both are offline, both edits survive and no conflict copy is left behind. The unit of conflict is a field, not a file: one device setting an expiry date and the other setting a title at the same minute merge deterministically, both devices reach the same state, and fields neither touched are unchanged. A vault whose unit is a file cannot do this - the provider keeps both versions and names the second after itself, which in an encrypted vault means a file the vault will not show you. dotnet run --project tools/compare/SyncCost -- --selftest
Measured by replaying two offline op-logs through the shipped OpLogState on both roots and comparing the results: three checks assert that the two devices converge, that both edits survive, and that the cloud folder holds zero conflict copies. What the other products do in the same situation is NOT measured and the report says so - a container cannot safely be open on two machines at once, and what a cloud client does with two versions of one Cryptomator file is the client's behaviour rather than the vault's. Both need the products installed on a scratch machine, so the pages describe them as expectations and publish no number for either.
/compare/cryptomator

Openness

ClaimHow to check itWhere we say it
The vault format is documented byte for byte, so an independent tool can decrypt your vault with your password and nothing else. Read WebHome/spec/VAULT_FORMAT_V2.md and implement it
WebHome/spec/VAULT_FORMAT_V2.md, WebHome/spec/SECURITY_MODEL.md, WebHome/spec/SECURE_SEND.md
/security

Licensing

ClaimHow to check itWhere we say it
Licences are Ed25519-signed and verified entirely offline. There is no activation server, so there is nothing that can be switched off, and no check-in that reveals you are using the app. dotnet test Keepsake/KeepsakeVault.Tests/KeepsakeVault.Tests.csproj
Licence public key 42c93824c77f65dbf057f2cf26148f20488853e90d9deb0bc9ed87894b037881, compiled into every client and printed on /security.
/security
A vault stays fully readable and exportable with no licence at all. Nothing in Keepsake holds your documents hostage to a payment. dotnet run --project Keepsake/Keepsake.SmokeTest
158 end-to-end checks against a real vault, including the expired-licence path. tools/test-licence-cron.php additionally fails the build if any renewal copy implies documents are at risk.
/pricing
Renewal reminders arrive once at 30 days, once at 7 days, once after a licence lapses, and then never again. php tools/test-licence-cron.php
96 checks. The thresholds exist in four implementations (WebHome/lib/licences.php, LicenseNotice.cs, LicenseNotice.kt, licenseNotice.ts) and all four are pinned.
not published as a headline claim

Supply chain

ClaimHow to check itWhere we say it
Keepsake's builds are reproducible. The same commit, compiled with the SDK global.json pins, produces byte-identical binaries on any machine and in any directory, and one command checks it: tools/reproduce.ps1 builds the commit twice in two differently named directories and compares SHA-256 over every file the build produced. Two consequences are visible in the shipped files themselves - no path from a build machine is recorded inside them, and a release can publish the hash manifest that produced them. It applies from 7 September 2026 onward; binaries published before that date were built without it. pwsh tools/reproduce.ps1
Deterministic, PathMap and portable symbols are set in Directory.Build.props, with the NuGet package cache mapped in Directory.Build.targets because a few packages compile source into the assembly from under the building user's home directory. Measured on 2026-09-07: without those properties two builds of the same commit from two different directories produced 8 differing assemblies; with them all 252 files match byte for byte. The four-minute command is not a unit test, so Keepsake/KeepsakeVault.Tests/ReproducibleBuildTests.cs guards the settings it depends on by reading the assembly it is running inside - the recorded PDB path and every source path in the symbols must be remapped, and a drive letter in any of them fails the suite on the next build.
/security
Every release ships a manifest of SHA-256 checksums signed with an Ed25519 release key that never touches the web server, and the auto-updater verifies it before applying anything. Keepsake.exe --verify <file>, or the /verify page
Release public key 3b0e3f9d86279b9aac72bbb497996f5b053f95c62422ab75f00c72598320af76.
/security

OCR accuracy

ClaimHow to check itWhere we say it
Overall character error rate of 0.6% against raw Tesseract 5's 19.3%, on a synthetic ground-truth set across six degradations. dotnet run -c Release --project PurpleOCR/PurpleOCR.Benchmark -- accuracy
PurpleOCR/PurpleOCR.Benchmark/reports/accuracy-baseline.md
/features/ocr · /purpleocr
Field accuracy — the share of document numbers, dates and amounts readable in the output — of 97.7% against raw Tesseract 5's 77.4%. dotnet run -c Release --project PurpleOCR/PurpleOCR.Benchmark -- accuracy
PurpleOCR/PurpleOCR.Benchmark/reports/accuracy-baseline.md
/features/ocr · /purpleocr
On the noise degradation the two engines are level at 0.6% character error rate. Preprocessing does not help every page, and this row is published beside the ones we win. dotnet run -c Release --project PurpleOCR/PurpleOCR.Benchmark -- accuracy
The 'noise' row of PurpleOCR/PurpleOCR.Benchmark/reports/accuracy-baseline.md.
not published as a headline claim
OCR runs entirely on your device. No page image, and no text extracted from one, is uploaded for recognition. dotnet test PurpleOCR/PurpleOCR.Tests/PurpleOCR.Tests.vbproj
40 OCR tests; and there is no recognition endpoint on the portal to send anything to.
/features/ocr

Verification

ClaimHow to check itWhere we say it
Of 170 feature-and-platform pairs, 158 are GREEN — a named automated check ran and passed — 12 are AMBER because they need a device, a human, a credential or a browser, and none are RED. pwsh tools/audit/audit.ps1
docs/FEATURE_AUDIT.md, regenerated by the audit itself.
not published as a headline claim
Every structured-data claim on this site corresponds to text a reader can see on the page. Markup without matching content fails the build. php tools/test-seo.php
The suite renders every live route and cross-checks its JSON-LD against the rendered HTML.
not published as a headline claim

Privacy

ClaimHow to check itWhere we say it
This website sets no cookie on public pages, loads no third-party script and stores no visitor address. Unique visitors are a HyperLogLog sketch salted per calendar month, so a month's daily registers merge and two months' cannot be linked. php tools/test-stats.php
79 checks, which fail if a tracker, a cookie or a full referring URL is ever added.
/security · /legal/privacy

Pricing

ClaimHow to check itWhere we say it
Premium is £19 a year, Family £29 and Teams £79, and the free tier is not a trial. php tools/test-invoice.php
shared/plans.json is the catalogue; 200 checks fail when either copy of it drifts.
/pricing

Organisation

ClaimHow to check itWhere we say it
The Family File checklist lists the documents a household should hold — why each matters, what it unlocks, what happens if it is lost, and who issues a replacement in five countries. It is free, it quotes no fee, and every link goes to the issuing authority. php tools/test-seo.php
One pack, shared/family-file.json, generates the public page and is scored inside all three apps; roughly 250 checks assert that the page and the apps cannot disagree, that no row quotes a fee, and that no issuer link is an affiliate link.
/compare/how-to-choose
Keepsake files documents by rule: ordered rules that match what arrived - the file name, the recognised text, the extension, the size, the sender, the subject, the category, the tags, the issuer, the number - and then set a category, add a tag, rename it from a template, append a note, set the issuer or set how many days before expiry that document should remind you. Rules run in order, the first confident one can stop the rest, and running them over documents already filed is previewed first and shows the count before anything is written. No rule can delete, move, share, export or overwrite a document, because no such action exists in the engine. Every application is written to the document's history, and a rule set exports as a JSON file that describes rules and carries no document, path or account. dotnet test Keepsake/KeepsakeVault.Tests/KeepsakeVault.Tests.csproj && dotnet run --project Keepsake/Keepsake.SmokeTest
The policy is one file - shared/workflows.json - holding the triggers, the fields, the operators, the seven actions, the ceilings and the wording of all 25 refusals, read by Keepsake/KeepsakeVault/Workflows.cs with WorkflowTests asserting every decision against no database at all. That suite pins the safety claim in both directions: it fails if any forbidden word (delete, remove, move, share, export, send, upload, run, exec) appears in the pack's action list, and it fails if the set of action ids is anything other than the exact seven. The smoke suite then drives Keepsake/Keepsake/Classes/WorkflowManager.vb over a real vault and asserts that a preview writes nothing before asserting that a run writes the category, the tag, the templated title, the reminder lead and the history entry - the preview being the same code path with persistence off, not a second implementation describing the first.
/guides/workflows

Legacy

ClaimHow to check itWhere we say it
The check-in schedule is a date and an address on our server and nothing else. Miss a check-in and we email you, email you again a week later, email the person you named three weeks after that, and then stop. We cannot open your vault for them, and we cannot tell them what is in it. php tools/test-legacy-cron.php
70 checks over WebHome/lib/legacy.php and the shared pack shared/legacy-release.json, which is byte-copied into all three apps and drift-tested. They assert the three rungs, that there is never a fourth message, that a paused or checked-in row is never written to, and that no message in the rail says or implies the documents are at risk.
/legacy
The Legacy Access Kit can be rehearsed. Two shares are tested against the live vault on the owner's own machine and the answer is a verdict — these shares open this vault, or they do not, and why. The reconstructed key is never returned to the caller, and it is wiped before the check returns. dotnet test Keepsake/KeepsakeVault.Tests/KeepsakeVault.Tests.csproj
LegacyRelease.Rehearse takes a predicate rather than handing back a key, in three implementations — Keepsake/KeepsakeVault/LegacyRelease.cs, KeepsakeAndroid/app/src/main/java/com/alimuhammad/keepsake/vault/LegacyRelease.kt and pwa/src/vault/legacyRelease.ts — each with its own suite, and each comparing the candidate in constant time.
not published as a headline claim

Getting documents in

ClaimHow to check itWhere we say it
Save an email as a file and drop it in, and Keepsake files the attachments rather than the message. Only the attachments are imported: the body is not read, not stored and not indexed. What may be imported is an allow-list of document and image types, so an executable, an archive or a macro-enabled spreadsheet is refused by name, on screen, with the reason beside it. The parsing happens on your machine; no mail passes through us. dotnet test Keepsake/KeepsakeVault.Tests/KeepsakeVault.Tests.csproj && cd KeepsakeAndroid && ./gradlew :app:testDebugUnitTest && cd pwa && npm test
The policy is one file - shared/mail-ingest.json - read by three separate parsers, Keepsake/KeepsakeVault/MailIngest.cs, KeepsakeAndroid/app/src/main/java/com/alimuhammad/keepsake/vault/MailIngest.kt and pwa/src/vault/mailIngest.ts, each with its own suite asserting the same refusals. The desktop watcher picks up .eml and .msg through the existing drop folder.
/features/capture · /compare/how-to-choose
Point Keepsake at a folder and it imports what is in it: a Trustworthy or Everplans download, a paperless-ngx export, a folder with a sidecar list, or an ordinary folder of scans. The source is worked out from the folder's own contents, titles and categories and dates come across where the source carries them, and every file it will not take is named on screen with the reason. A date it cannot read unambiguously is left empty rather than guessed at, and a file the vault already holds is skipped and said to be skipped, so the same import can be run again safely. dotnet test Keepsake/KeepsakeVault.Tests/KeepsakeVault.Tests.csproj && cd KeepsakeAndroid && ./gradlew :app:testDebugUnitTest && cd pwa && npm test
The policy is one file - shared/import-sources.json - read by three separate parsers, Keepsake/KeepsakeVault/ImportSources.cs, KeepsakeAndroid/app/src/main/java/com/alimuhammad/keepsake/vault/ImportSources.kt and pwa/src/vault/importSources.ts, each with its own suite asserting the same decisions. The sidecar format is documented at /guides/import-folder.
/features/capture · /guides/import-folder
No import signs in to anybody. There is no Google Drive, OneDrive, Dropbox or mailbox connector, no OAuth prompt and no stored access token: every importer reads a folder that is already on your device. The cost is that you sync or download the folder first; the benefit is an import path that works offline and cannot stop working because another company changed its terms. There is no OAuth, Drive, OneDrive or Dropbox API client code in the repository.
shared/import-sources.json defines every source as a shape of folder, not as a service. The cloud-storage importer is the fallback precisely because it requires nothing to be true about where the files came from.
/guides/import-from-cloud-storage
Keepsake can check a mailbox you own for documents, and the connection is made by the app on your own device straight to your own IMAP server: no address of ours to forward to, no server of ours in the path, and no account of yours linked to anything of ours. It signs in over TLS on port 993 and refuses any other port before a socket is opened; it will contact only the host you configured; it files the attachments of new messages and never reads the body into any field; and the only change it makes to your mailbox is marking a message read once its attachments are filed. Nothing is deleted or moved, and every message it leaves behind is named with the reason. dotnet test Keepsake/KeepsakeVault.Tests/KeepsakeVault.Tests.csproj && dotnet run --project Keepsake/Keepsake.SmokeTest && cd KeepsakeAndroid && ./gradlew :app:testDebugUnitTest
The policy is one file - shared/mailbox-poll.json - read by Keepsake/KeepsakeVault/MailboxPoll.cs and KeepsakeAndroid/app/src/main/java/com/alimuhammad/keepsake/vault/MailboxPoll.kt, each with a suite asserting the same decisions. The clients, ImapClient.cs and ImapClient.kt, contain no DELETE and no EXPUNGE at all, which is how 'nothing is deleted' stays true. The smoke suite drives a scripted IMAP server through the real desktop vault and asserts that no call leaves the machine except to the configured host, that a planted string in a message body reaches no field of any document, and that no plaintext message is left staged afterwards.
/features/capture · /guides/mailbox-check

Getting your documents in

ClaimHow to check itWhere we say it
Keepsake reads a Cryptomator vault directly on Windows - format 8, both SIV-GCM and SIV-CTRMAC. You give it the vault directory and the vault password; it unwraps the masterkey with scrypt, walks the encrypted directory tree, decrypts the filenames and imports the documents, which are then typed, read and dated like anything else. It is read-only: nothing is written to the source vault, an unrecognised format version stops the import rather than being guessed at, and every skipped file is reported by name. dotnet test Keepsake/KeepsakeVault.Tests
Asserted against four committed fixture vaults under docs/interop-fixtures/, two per cipher combo. Two are synthesised from the published specification by a Python writer sharing no code with the reader; the other two were written by Cryptomator's own cryptofs 2.8.0, lifted from a GPG-verified cryptomator-cli release archive, so a reader that quietly depended on our writer fails on the second pair. That pair earned itself: real Cryptomator uses a different scrypt cost than our writer chose, and it drops a vault.cryptomator.*.bkup sidecar our writer never made - both now pinned. Every fixture covers nested directories, a shortened .c9s name and a name at the path limit, and the two rules that matter more than the decryption: the source vault is byte-identical after an import, and no file is dropped silently. What is still not claimed is that a person drove the Cryptomator desktop GUI - the app writes with this same library, but that is a different sentence, and docs/interop-fixtures/cryptomator/PROVENANCE.md says which one we are making. The audit row is 'Cryptomator vault import (format 8): read-only, never writes to the source'.
/compare/cryptomator

Getting answers out

ClaimHow to check itWhere we say it
An assistant on your own machine - Claude, or anything else that speaks MCP - can answer questions from your real documents without any of them being uploaded. The server runs on your PC, opens no network port, and forwards each request over a Windows named pipe to the copy of Keepsake in front of you, which is the only process holding your key. It answers nothing until a person unlocks the vault and grants a scope in the app, and that scope is a fixed set of documents computed at that moment: a document outside it is absent from search, from what expires soon, from a lookup by id and from Ask alike. It is read-only by construction - there is no tool that changes, moves, shares or deletes anything - it never returns the file itself, and the grant, every read and every refusal are written into the vault's hash-chained audit log. dotnet test Keepsake/KeepsakeVault.Tests/KeepsakeVault.Tests.csproj && dotnet run --project Keepsake/Keepsake.SmokeTest
The policy is one file - shared/mcp-server.json - read by Keepsake/KeepsakeVault/Mcp.cs, with McpTests asserting every decision against no database at all. The smoke suite then drives the real JSON-RPC entry point against a real unlocked vault: it grants one document, reads it back, and proves a second document of the same user is unreachable through all four tools; it checks the grant, the reads and a refusal are in the audit chain, that nothing an assistant did broke a link in it, and that the document count and every title are unchanged afterwards. The launcher, Keepsake.Mcp, contains no socket type at all - the transport is stdio out and a named pipe in - which is how 'no port is opened' stays true rather than being a setting.
/features/ask · /guides/mcp-server
Asking your vault a question in your own words puts the right document first 22 of 22 times on a fixed set of questions, against 17 of 22 for the substring search it replaced. Words are folded and stemmed, 27 synonym groups let you call a bill an invoice, a word common to every document you own counts for less than a rare one, and a title OCR misread by one letter still matches. None of that loosens what may be said: ranking decides only which documents are READ, and the answer is still a field Keepsake extracted or a sentence copied out of the document, or nothing at all. dotnet test Keepsake/KeepsakeVault.Tests/KeepsakeVault.Tests.csproj, then cd pwa && npm test, then cd KeepsakeAndroid && ./gradlew :app:testDebugUnitTest
shared/retrieval.json holds the whole policy and the 22-query, 12-document evaluation set the numbers are measured on, including the 17 the old ranker scored. All three suites reimplement that old ranker to recompute the baseline rather than trusting the written number, assert the new one beats it, and then assert the guarantee that matters more: every sentence any answer quotes is checked to appear verbatim in the document it is attributed to.
/features/ask

Accessibility

ClaimHow to check itWhere we say it
The web app meets WCAG 2.2 Level AA on every screen, checked by an automated axe-core pass that boots the real application, walks it from vault creation through the dashboard, upload, a document and settings, and repeats a screen in right-to-left Urdu. Colour contrast is computed arithmetically from the stylesheet's own palette rather than skipped, because a headless browser reports it as incomplete and an incomplete result quietly counts as a pass. cd pwa && npx vitest run a11y
pwa/tests/a11y.test.ts runs axe-core 4.13 at wcag2a/wcag2aa/wcag21a/wcag21aa/wcag22aa over seven booted screens plus a right-to-left repeat, asserts a visible :focus-visible ring exists in the stylesheet, and measures every text, border and button colour pair against the ratio the standard requires. It was written before the fixes and failed on five real defects, all listed on /legal/accessibility.
/legal/accessibility
The interface is available in English, Urdu, Arabic and Hindi, with Urdu and Arabic laying the whole interface out right-to-left. All four are the same 196 strings from one catalogue, so a language cannot ship half-translated. dotnet test Keepsake/KeepsakeVault.Tests/KeepsakeVault.Tests.csproj, then cd pwa && npm test, then cd KeepsakeAndroid && ./gradlew :app:testDebugUnitTest
shared/i18n/{en,ur,ar,hi}.json, copied into the vault and Android resources with drift tests on both copies. Each platform's suite fails if a language is missing a key, defines one English does not, drops a placeholder, names itself in English, or leaves any string equal to its English source.
/legal/accessibility

Getting into your vault

ClaimHow to check itWhere we say it
A passkey or a FIDO2 security key can open a Keepsake vault in the web app, and what the key opens is the encryption, not a login. WebAuthn's PRF extension makes the authenticator produce 32 bytes that exist only while the prompt is being answered; those bytes, run through HKDF with the vault's own id, unwrap the master key from a keyring slot. No server is asked whether you may proceed, because no server is involved. The rule that goes with it is enforced before any prompt appears: a key a device holds is never the only way into a vault, so enrolling is refused until a password or a recovery phrase exists, and removing the last way in is refused outright. cd pwa && npm test && dotnet test Keepsake/KeepsakeVault.Tests/KeepsakeVault.Tests.csproj && cd KeepsakeAndroid && ./gradlew :app:testDebugUnitTest
The policy is one file - shared/device-unlock.json - copied into both platform resource trees and drift-tested. pwa/src/vault/passkey.ts calls WebAuthn through an interface a scripted authenticator can drive, so the suite exercises a key that refuses PRF, a key belonging to another vault, and a cancelled prompt. All three platforms derive the wrapping key from the same frozen vector (a PRF output of 32 bytes of 0x42 and a fixed vault id give 496a2c73440dfe0f05a976366d94c45428ae28b23b4903c7a75ec660574e72df), which is what makes 'the same key, the same secret' a fact rather than an intention.
/security · /guides/passkey-unlock
A FIDO2 security key opens a Keepsake vault in the Windows app as well as in the web app, and what it opens is the encryption rather than a login. The desktop asks the authenticator for its CTAP2 hmac-secret output directly - the same function a browser reaches through WebAuthn's PRF extension - and HKDF over those 32 bytes, with the vault's own id in the info string, unwraps the master key from a keyring slot. One physical key therefore opens the same vault on both surfaces, because both derive the wrapping key the same way. The rule that goes with it is the browser's rule, enforced before any prompt appears: a key you carry is never the only way into a vault, so enrolling is refused until a password or a recovery phrase exists, and removing the last way in is refused outright. dotnet test Keepsake/KeepsakeVault.Tests/KeepsakeVault.Tests.csproj
SecurityKeyTests drives the whole enrol, unlock, refuse and remove path against a scripted CTAP2 authenticator - including a key that cannot produce a secret, a key enrolled on another vault, a dismissed prompt, the ceiling on how many keys a vault holds, and the refusal to remove the last way in. One test pins the exact salt bytes the desktop sends, because a browser hashes the PRF salt before it reaches the authenticator and a desktop that skipped that step would derive different bytes and look like broken hardware. What is NOT covered by an automated test is the Windows transport itself: asserting Keepsake/KeepsakeVault/WindowsSecurityKey.cs needs a physical key in a USB port, which no test run has, so that file is compiled and reviewed and nothing more.
/security · /guides/passkey-unlock

What we do not have

Keepsake's weaknesses and limitations, in the words we would use ourselves. A ledger that lists only wins is marketing with a table around it, so these 9 are in the same file, generated by the same script, and scored against us on the criteria page.

If you are trying to decide whether this product is wrong for you, start here rather than with the features. The disadvantages worth knowing before you buy are all in the table below, and none of them is phrased to be reassuring.

What is missingHow you can confirm it is missingWhere we admit it
There has been no independent third-party security audit of Keepsake's cryptography. The evidence above is evidence you can check, not evidence somebody else has checked. There is nothing to run. No audit report exists.
Named as gap G1 in docs/BEST_IN_CLASS_PLAN.md and as an owner decision in OWNER_TASKS.md.
/compare/how-to-choose · /legal/disclosure · /security · /compare/veracrypt · /compare/cryptomator
There is no native iOS app. iPhone and iPad are covered by the installable web app, which works offline, and that is not the same thing. There is no iOS target in the repository.
Blocked on an Apple developer account (OWNER_TASKS.md).
/compare/how-to-choose
There is no native macOS build and no Linux package, and neither is in progress. Both platforms run the installable web app, which is a full vault peer rather than a viewer - same format, same encryption, same sync, offline, in its own window. Four things are Windows-only and stay that way in a browser: the print-to-Keepsake driver, the mailbox check, watched folders and bulk import, and the assistant bridge. Three of those are operating-system components a web page cannot be; the mailbox check is additionally a decision, because a browser cannot open a socket to a mail server without a server of ours in the middle. There is nothing to run. No macOS or Linux build exists to test.
The web platform is audited as its own surface in docs/FEATURE_AUDIT.md, so what it does and does not cover is a table rather than a promise. The row-by-row comparison is published at /docs/mac-and-linux.
/compare/how-to-choose · /compare/veracrypt · /compare/cryptomator
Keepsake has no hidden vault, no decoy password and no plausible deniability. A vault announces what it is - the manifest names the format and its parameters - and one password opens what is there. This is a design decision rather than a gap: a hidden volume must be a fixed-size container that does not grow as documents are added and is not synced casually, which is the opposite of a vault built to be reachable from a phone at a hospital desk. If being compelled to reveal a password is your threat model, VeraCrypt's hidden volumes are the tool built for it and Keepsake is not a substitute. There is nothing to run. The feature does not exist and is not planned.
Recorded as a decision, with the reasoning and the recommendation to use VeraCrypt instead, at /docs/coercion-and-hidden-volumes. Travel Mode is the nearest thing Keepsake has and that page states its limit in the same paragraph: it reduces what an unlock exposes, it does not deny that there is more.
/compare/how-to-choose · /compare/veracrypt
There has been no third-party accessibility audit, no VPAT and no testing with screen-reader users. What exists is an automated WCAG 2.2 AA pass that runs on every change, which is a floor we cannot fall through rather than a ceiling we have reached - automated tools can decide whether a control has a name, not whether the name is a good one. cd pwa && npx vitest run a11y - and note what it does not cover: reading order, focus order and the wording of error messages are not machine-verified.
/legal/accessibility states the limit in the same words, next to the pass it qualifies. Commissioning an audit is owner work (OWNER_TASKS.md), like the security one.
/legal/accessibility
There is no address you can forward documents to, and no inbox of ours that files itself. Either would mean a server of ours receiving and holding mail we promise we cannot read. The mailbox check does the same job from the other end - your device signs in to your mailbox - and the web app does not offer it at all, because a browser cannot open that connection without a server of ours in the middle. There is no inbound mail handler, mail-provider OAuth client or server-side IMAP code in the repository; grep the portal for a mailbox that receives customer documents and there is none.
shared/mailbox-poll.json places every connection on the user's own device (limitsWeState), and tools/audit/features.json records the web app as deliberately absent from the mailbox-poll row with the reason.
/features/capture · /guides/mailbox-check
A portable security key does not open the Android app. The phone unlocks with the hardware it already has - the Android Keystore, gated by fingerprint or face - and that does not travel to another device. There is no CTAP2 code in the Android app, so a security key does nothing on the phone today. The Windows app and the web app both take one. Grep the Android tree for WebAuthn or CTAP2 and there is none; the two callers in the repository are pwa/src/vault/passkey.ts and Keepsake/KeepsakeVault/WindowsSecurityKey.cs.
shared/device-unlock.json declares each method's portability - android-keystore is portable:false, webauthn-prf is portable:true - and the suites on all three platforms assert those flags, so 'enrol once, works everywhere' cannot be implied by accident.
/compare/how-to-choose
Windows installers are not Authenticode-signed, so Windows shows an unrecognised-app warning on first run. The signed release manifest is what protects you in the meantime, and it protects the update channel — which a certificate would not. Inspect any published .exe: no Authenticode signature is present.
pack-desktop.ps1 signs when a thumbprint is configured and refuses to silently ship unsigned once it is; no certificate has been bought (OWNER_TASKS.md).
/security
Keepsake is not open source, and this row stays here after the crypto core becomes readable, because the two are different sentences. The application is closed: the desktop, Android and web apps, OCR, licensing and the portal. The cryptography is being made readable: the KDF, the envelope keys and slots, the KVF2 vault manifest, the KPS2 blob format, the recovery words, the Shamir split and the notary hash chain, with the vector tests that pin them, filtered into a subtree that builds and passes those tests on its own, under a licence that permits reading, compiling, testing, publishing findings and writing your own implementation of the format - and not redistribution or reuse in another product. That subtree exists and is checked on every change. Pushing it to a public repository has not happened yet, so today a reader still cannot read it, and this row will say so until they can. A source-available licence is not an OSI-approved one and we are not going to let it be described as one. pwsh tools/publish-core.ps1 -Check
public/keepsake-core is a filtered subtree generated by tools/publish-core.ps1 from a declared file list - no glob, so publishing a file is a decision - and it builds and runs its vector suite with no reference back to the closed tree. CoreDriftTests in KeepsakeVault.Tests fails when a published file changes without the subtree being regenerated, and also fails if the subtree gains a project reference back into the monorepo or any network type, because the README says there is no network code in it. What has NOT happened yet is the publication: pushing the subtree to a public repository, and having the licence read by somebody qualified, are owner tasks and are in OWNER_TASKS.md. Until that is done the tree is readable only by people who already have this repository, which is nobody this row is addressed to.
/compare/docspell · /compare/veracrypt · /compare/cryptomator

The OCR benchmark, as a dataset

Character error rate, word error rate, field accuracy and latency for the PurpleOCR v3 pipeline against raw Tesseract 5 LSTM, over 96 synthetic documents per engine (4 templates x 4 fonts) under six degradations with exact ground truth.

Both the report and the raw per-case results are in the repository, and the run is one command:

dotnet run -c Release --project PurpleOCR/PurpleOCR.Benchmark -- accuracy

Run it on an idle machine. Tesseract's LSTM is threaded and its output moves under CPU contention — enough that a contended run once read 0.5% overall CER where the same build alone read 0.6%, and a repeat read 3.6%. If the raw column moves between runs, the harness is the variable and the numbers cannot decide anything. The full table, including the rows we do not win, is on the OCR page.

Last verified — by php tools/gen-claims.php --check. Every claim on this site is listed, with its evidence, in the claim ledger.