Learn · Technologies we use
Unicode normalization: why "café" can be two passphrases
The same word as two byte strings gives two different keys. NFC makes them agree, and there's an inspector for your own text.
Type the word "café" on one keyboard and on another and you can end up with two different passphrases. They look identical on screen, and they aren't. One is the five bytes 63 61 66 c3 a9 and the other is the six bytes 63 61 66 65 cc 81. A key made from the first won't open a message sealed with the second, and nothing says why. The message just refuses to open, exactly as it would for a wrong passphrase. Here's where the difference comes from, what cipher.ink does about it, and what it can't fix.
One letter, two spellings
Unicode can write many letters more than one way. The letter "é" is a single character, U+00E9 (e with acute), and it can also be written as two: a plain "e" (U+0065) followed by U+0301, the combining acute accent, which draws itself on the letter before it. A text renderer shows them identically, and a program comparing the strings sees two different sequences. The Unicode standard calls them canonically equivalent and defines normalization forms that pick one spelling for each:
- NFC (composed) uses the single character wherever one exists, so "café" is 4 code points.
- NFD (decomposed) splits every such character into a base plus marks, so the same word is 5 code points.
- NFKC and NFKD go further and also fold compatibility variants into plain characters: a fullwidth "A" becomes "A", a no-break space becomes a space, the ligature "fi" becomes "fi".
Try it. The inspector below shows the code points and the UTF-8 bytes of what you type in each form. It opens on the decomposed "café", and the buttons load the other spelling and some harder cases.
Nothing you type here leaves your browser.
Why it matters for a key
A passphrase becomes an encryption key by being encoded as bytes (UTF-8) and run through PBKDF2. Different bytes mean a different key, with no partial credit. Here's what happens with a 16-byte salt of zeros and 600,000 rounds (the numbers come from scripts/examples/unicode-pass.mjs):
| Passphrase "café" as | UTF-8 bytes | First bytes of the 256-bit key |
|---|---|---|
| NFC (é as one character) | 63 61 66 c3 a9 | 14d7e791 1c347d7b… |
| NFD (e plus accent) | 63 61 66 65 cc 81 | 1df5abf4 568e2323… |
| NFD, normalized to NFC first | 63 61 66 c3 a9 | 14d7e791 1c347d7b…, the same as the first row |
The two spellings give keys with nothing in common. Normalize the second to NFC before deriving the key and you get the first key back, so both spellings open the same message.
Where the two spellings come from
Most keyboards produce the composed form, so plenty of people never see the problem. It shows up when text arrives some other way:
- Copy and paste from a document, a PDF, a chat app or a web page that stores text decomposed. macOS file systems have historically stored file names in a decomposed form, so a name copied from a Finder window can arrive in NFD.
- Dead keys and input methods. Most systems turn an accent key followed by a letter into one character, but some input methods hand the two characters over separately.
- Scripts that compose. A Korean Hangul syllable can be one character (한, U+D55C) or three conjoining letters (jamo). The single syllable is 3 bytes in UTF-8 and the three letters are 9. Vietnamese, with its stacked accents, and many other languages have several equivalent spellings.
- Singletons. The Angstrom sign (U+212B) is a separate character from the Swedish letter Å (U+00C5) even though they look the same, and NFC maps the first to the second.
What cipher.ink does
Before a passphrase is encoded, the page puts it in NFC with String(p).normalize('NFC'), one line in crypto.js. However the "é" arrives, the bytes that go into PBKDF2 are the composed form, so the key comes out the same on a Mac, a phone and a Windows laptop. NFC is lossless in the sense that matters: it turns a string into another spelling of the same text, never into a different text.
That's a choice, and a different one is defensible. NIST's password guidance (SP 800-63B) says a verifier should apply NFKC or NFKD normalization to memorized secrets that contain Unicode characters. RFC 8265, the IETF profile for passwords, requires NFC and also maps non-ASCII spaces to an ordinary space, without folding case or width. We use NFC alone. NFKC would make a few more alternative inputs work (the fullwidth "A" of an East Asian keyboard would match "A"), but it lets visibly different strings stand for the same passphrase and puts a lossy step in the middle of a key derivation. The known edge on the other side: a passphrase typed with a no-break space on one device and an ordinary space on another still gives two different keys.
Other things that make two passphrases differ
- Smart punctuation. Some phone keyboards turn a straight apostrophe or quote into a curly one on their own, and two hyphens into a dash. A passphrase with an apostrophe in it can be a different string on a phone than on a laptop. The passphrase box on this site turns off automatic capitalization and spell-checking, but it can't control a keyboard's own substitutions.
- Emoji. An emoji can be a single code point (a key, U+1F511, 4 bytes) or a sequence. A family emoji is three people joined by two zero-width joiners, 5 code points and 18 bytes, which a keyboard on another platform may draw differently or insert as separate emoji. A heart or other symbol may or may not carry an invisible "emoji presentation" selector (U+FE0F), depending on the keyboard. Normalization won't unify these, because they're different characters, not different spellings of one.
- Invisible characters. Zero-width joiners and spaces, and trailing spaces, become part of the passphrase when pasted.
- Case. A passphrase is case-sensitive. The 2016 version of the site lowercased passphrases before using them. Old messages still open that way, and new ones are case-sensitive.
Emoji are fine in a passphrase, but "unusual" doesn't mean "strong". Strength comes from how many choices each position had, not how exotic the result looks. One emoji picked at random from a list of 1,000 adds 10 bits, no more than a word from a list of 1,024 would (passphrase generator).
Advice
- Prefer plain ASCII words if the passphrase has to work on devices you don't control. A generated passphrase of words from a list has none of these problems.
- If you use accents or another script, check that the message opens on a second device before you rely on it.
- If you tell someone a passphrase in a chat or on paper, tell them how to type any unusual character, or use a secret link, which has no passphrase at all.
How cipher.ink uses this
Passphrase mode normalizes to NFC, as above, then derives the key with PBKDF2-HMAC-SHA256 at 600,000 rounds. The passphrase box is limited to 200 characters and has autocapitalization and spell-check switched off. Secret-link messages use 32 random bytes instead of a passphrase, so none of this applies to them. Messages sealed before 2026 are a special case, with their own passphrase handling, described in the OFB article.