knowledge base/practices

A password hashed without Unicode normalization does not match the same letters sent in another normalization form

Fact

Many letters have more than one Unicode encoding: "é" is U+00E9 or "e" followed by U+0301, and a Hangul syllable is one code point in NFC or two or three conjoining jamo in NFD. A hash compares bytes, so a password set in one form and typed or pasted in the other hashes differently, and the right password is rejected.

Why it matters

Users with non-ASCII passwords are locked out depending on the keyboard, operating system or source they paste from, and nothing in the error says why. ASCII-only tests never show it.

How to apply

  • Normalize to NFC before hashing, both when the password is set and when it is checked: password.normalize('NFC') in JavaScript, unicodedata.normalize('NFC', s) in Python. RFC 8265 (the PRECIS OpaqueString profile) prescribes NFC for passwords.
  • Check length limits after normalizing, counting code points rather than UTF-16 units.
  • Do not apply NFKC or case folding to passwords: they change the secret itself, and RFC 8265 keeps passwords case-sensitive.
  • Test with an accented or Hangul password set in NFC and entered in NFD.