Security
Security Architecture
This page documents how Inkrypt encrypts notes, written against the code that actually ships rather than a general description of good practice. It names every parameter, states exactly what our database holds, and describes the weaknesses of this design as directly as its strengths.
Design goal
Inkrypt is built around a single constraint: our servers must never be able to read a note's contents, even under compulsion, even if fully breached, even if we wanted to. Everything else in the design follows from that, including the features we cannot offer.
The way to achieve that is not policy but arithmetic. If the decryption key never exists on our infrastructure, then reading your note is not a matter of access control — it is a matter of breaking AES-256, which nobody can do.
Cryptographic parameters
These are the values currently in use. They are recorded here so the documentation can be checked against behaviour, and so any future change is visible on the changelog.
| Cipher | AES-256-GCM |
|---|---|
| Key length | 256 bits |
| Authentication tag | 128 bits |
| IV length | 96 bits (12 bytes) |
| IV generation | Cryptographically random, fresh per operation |
| Key derivation function | PBKDF2-HMAC-SHA256 |
| KDF iterations | 310,000 |
| Salt length | 128 bits (16 bytes) |
| Salt generation | Cryptographically random, fresh per operation |
| Key extractable | false (non-extractable CryptoKey) |
| Implementation | Web Crypto API — crypto.subtle |
| Execution location | The user's browser |
Why these values
Key derivation
The password you type is never used directly as a key. It is stretched through PBKDF2-HMAC-SHA256 with a fresh 16-byte random salt and 310,000 iterations, producing a 256-bit AES-GCM key.
The derived key is created as a non-extractable CryptoKey. The browser's crypto layer will use it to encrypt and decrypt, but will not export the raw bytes back into JavaScript — including to our own code. That closes an entire category of accidental key leakage through logging or error reporting.
The iteration count is stored alongside each note as a label such as pbkdf2-310000, and decryption uses the parameters the note was created under. This means the count can be raised for new notes without stranding older ones — the mechanism for a future migration is already in place.
Encryption flow
Generate randomness
A 16-byte salt and a 12-byte IV are generated from the browser's cryptographically secure random number generator. Both are fresh for every single encryption, including every re-save of an existing note.Derive the key
PBKDF2-HMAC-SHA256, 310,000 iterations, using the password and the new salt. Produces a 256-bit non-extractable AES-GCM key.Encrypt
AES-256-GCM encrypts the UTF-8 encoded note using the derived key and the new IV, producing ciphertext and a 128-bit authentication tag.Encode and transmit
Ciphertext, salt and IV are base64-encoded and sent to the server over TLS, together with the KDF parameter label. The password and the plaintext are not part of this request.Store
The server writes those four values. It performs no cryptographic operation on the note and holds nothing that could decrypt it.
What the server stores
Per note, exactly four values relating to content. None of them, alone or together, permits decryption.
| Field | Contents | Secret? |
|---|---|---|
| ciphertext | Base64 AES-256-GCM output including auth tag | Unreadable without the key |
| salt | Base64, 16 random bytes | No — not secret by design |
| iv | Base64, 12 random bytes | No — not secret by design |
| kdf | Parameter label, e.g. pbkdf2-310000 | No |
Salts and IVs are not secret in any correctly designed system — they exist to guarantee uniqueness, not confidentiality, and both must be available to reproduce decryption. Their security value comes entirely from being fresh and random, which they are.
Alongside these, the server holds operational metadata: the note's slug (which is its URL and therefore never private), creation time, expiry configuration, and a view counter where limits are set. What the server does not hold is any password, any password hash, any derived key, or any plaintext.
The slug is not encrypted
Transport and application security
- TLS everywhere. All traffic is HTTPS. HTTP and non-canonical hostnames are 301-redirected to the canonical origin, and HSTS is set with a one-year max-age and
includeSubDomains. - Content Security Policy,
X-Frame-Options: DENYandframe-ancestors 'none'to prevent the interface being embedded and clickjacked. - `X-Content-Type-Options: nosniff` to prevent MIME confusion.
- `Referrer-Policy: strict-origin-when-cross-origin`, so full note URLs are not leaked to third parties in referrer headers.
- Permissions-Policy disabling camera, microphone, geolocation and interest-cohort.
- No ads and no indexing on note or share pages. Advertising scripts are loaded only on editorial content pages; the note editor and share gate carry neither ads nor an index directive.
Known limitations
A security page that lists only strengths is marketing. These are the real constraints of this design, in rough order of how much they should affect your decision.
We serve the code that encrypts
This is the fundamental limitation of all browser-delivered cryptography. Your browser fetches our JavaScript on every visit and runs it. A compromised or malicious operator could serve a modified script that captures passwords before encryption. No amount of correct cryptography inside the page defends against the page itself being replaced.
Installed applications with signed, independently verifiable builds are genuinely stronger here. If your adversary is capable of compelling or compromising a service provider at the delivery layer, no browser-based tool — including this one — is the right choice.
Password strength is the binding constraint
PBKDF2 at 310,000 iterations makes each guess expensive; it does not make a weak password strong. An attacker holding exfiltrated ciphertext can attack it offline at their own pace. A note protected by a dictionary word is protected by a dictionary word, whatever the cipher.
PBKDF2 is not memory-hard
Argon2id resists GPU and ASIC attacks better because it demands memory as well as time. We use PBKDF2 because it is the only password-based KDF exposed natively by the Web Crypto API across all browsers we support, and a native constant-time primitive is preferable to a JavaScript Argon2 for the threats most users face. This is a considered trade-off, not an oversight.
Endpoint compromise defeats everything
Malware or a keylogger on your device observes plaintext as you type, before any encryption occurs. Client-side encryption cannot defend below its own layer.
No independent audit
Inkrypt has not been through a third-party cryptographic audit. The algorithms and parameters are standard and documented here so they can be evaluated, but you should weigh the absence of external review. We would rather state this than let the omission be inferred.
Reporting a problem
If you believe you have found a vulnerability, our responsible disclosure policy explains how to report it and what to expect. We would much rather hear from you than not.
Security FAQ
Can Inkrypt decrypt my notes if legally compelled?
Why is the salt stored unencrypted?
Has Inkrypt been independently audited?
Why PBKDF2 rather than Argon2id?
What happens if you raise the iteration count?
Do you log IP addresses?
Related reading
Threat model
Which adversaries this architecture defeats, and which it explicitly does not.
Read moreZero-knowledge encryption
How to verify a zero-knowledge claim on any service, including this one.
Read moreResponsible disclosure
How to report a vulnerability and what happens next.
Read moreEncryption glossary
Every term on this page, defined in plain English.
Read moreVerify it yourself
Open your browser's developer tools, watch the Network tab, and save a note. The request body should contain ciphertext — not your text.
Open the notepad