minato_saslprep (minato v0.18.6)
View SourceSASLprep (RFC 4013) preparation of a password, as far as OTP can carry it.
SCRAM hashes Normalize(password) rather than the password, so a client that
normalises differently from the server derives a different key and fails against
a verifier the server itself computed. This module exists to agree with
PostgreSQL, not to be a general stringprep library.
What is handled
RFC 4013 is a profile of stringprep (RFC 3454) and runs in four steps. Three of them are here:
Mapping. The "commonly mapped to nothing" characters of table B.1 are removed, and the non-ASCII space characters of table C.1.2 become SPACE (U+0020). B.1 is applied first, which settles U+200B ZERO WIDTH SPACE: it is listed in both tables, and removal is what the wider profile family does with it.
Normalisation. Unicode NFKC, through
unicode:characters_to_nfkc_list/1.Prohibited output. Every character of tables C.1.2, C.2.1, C.2.2, C.3, C.4, C.5, C.6, C.7, C.8 and C.9 is rejected. The check runs after normalisation, because NFKC can produce characters the input did not hold. Those tables overlap each other, and a character in more than one of them is reported under the first that claims it, so the name in a
reason/0says which rule caught it and not that it was the only applicable rule.Running the check on the output rather than the input is what RFC 3454 section 7 orders, and it has a consequence worth knowing: a prohibited character that normalisation replaces is never seen by this step. U+0340 COMBINING GRAVE TONE MARK is prohibited by table C.8 and decomposed by NFKC to U+0300, which is not, so a password holding one is accepted with the substitution made. That is the specified behaviour and not a hole: the character has gone.
Printable ASCII, which is what almost every password is, takes none of those branches: nothing in it is mapped, NFKC is the identity on it, and nothing in it is prohibited, so the bytes come back exactly as they went in. Control characters are the one ASCII case not passed through, and table C.2.1 prohibits them rather than this module ignoring them.
What is not handled
- The bidirectional check of RFC 3454 section 6. It needs the bidirectional category of every code point, which is tables D.1 and D.2 of RFC 3454, and OTP publishes neither. The rule it enforces is that a string mixing right-to-left and left-to-right characters must not display ambiguously, which is a presentation property of an identifier. A password is never displayed.
- The unassigned code point check of RFC 3454 table A.1. It is defined against Unicode 3.2 specifically, while OTP normalises against whatever version it was built with, so there is no table to check against and no way to obtain one that would still give the 3.2 answer.
Neither omission can make this module accept a password and derive a different
key from a server that ran the whole algorithm. Both are input rejection rules,
so the only strings they cover are ones that would otherwise have been refused,
and prepare/1 already falls back to the raw bytes for those.
Falling back to the raw password
PostgreSQL documents that it prepares a password with SASLprep as though it were UTF-8 whatever encoding it is really in, and that "if it is not a legal UTF-8 byte sequence, or it contains UTF-8 byte sequences that are prohibited by the SASLprep algorithm, the raw password will be used without SASLprep processing, instead of throwing an error".
That decision is taken on the server when the verifier is stored, so a client
that rejected those passwords instead could not authenticate against a verifier
the server was perfectly happy to write. prepare/1 therefore returns the
untouched input whenever preparation cannot be completed, which reaches the same
bytes the server hashed. normalise/1 reports the reason instead, for callers
and tests that want to see the decision rather than its result.
Summary
Types
Why a password could not be prepared.
An RFC 3454 table of prohibited output, under a name for what it holds.
Functions
Prepare a password, reporting why when it cannot be done.
Prepare a password for SCRAM, falling back to the raw bytes.
Types
Why a password could not be prepared.
not_utf8 is a byte sequence that is not valid UTF-8. {prohibited, Table, Codepoint} is a character RFC 4013 forbids in output, named for the RFC 3454
table that forbids it.
-type table() ::
ascii_control | non_ascii_control | non_ascii_space | private_use | non_character |
surrogate | inappropriate_for_plain_text | inappropriate_for_canonical_representation |
change_display_properties | tagging.
An RFC 3454 table of prohibited output, under a name for what it holds.
Functions
Prepare a password, reporting why when it cannot be done.
1> minato_saslprep:normalise(~"pencil").
{ok,<<"pencil">>}
2> minato_saslprep:normalise(<<"pen", 7, "cil">>).
{error,{prohibited,ascii_control,7}}
3> minato_saslprep:normalise(<<16#FF>>).
{error,not_utf8}
Prepare a password for SCRAM, falling back to the raw bytes.
1> minato_saslprep:prepare(~"pencil").
<<"pencil">>
2> minato_saslprep:prepare(<<"a", 16#C2, 16#A0, "b">>).
<<"a b">>Never raises. A password this module cannot prepare comes back exactly as it arrived, because that is the password PostgreSQL hashed when it stored the verifier. See the module documentation.