diff --git a/async_postgres/pg_auth.nim b/async_postgres/pg_auth.nim index 0189d69..8f8cb70 100644 --- a/async_postgres/pg_auth.nim +++ b/async_postgres/pg_auth.nim @@ -29,6 +29,11 @@ proc md5AuthHash*(user, password: string, salt: array[4, byte]): string = saltedInput.add(char(b)) result = "md5" & getMD5(saltedInput) +proc scramEscapeUsername*(user: string): string = + ## Escape username for SCRAM per RFC 5802 Section 5.1. + ## '=' is encoded as '=3D' and ',' is encoded as '=2C'. + result = user.replace("=", "=3D").replace(",", "=2C") + proc scramClientFirstMessage*(user: string, state: var ScramState): seq[byte] = ## Generate the SCRAM-SHA-256 client-first message with a random nonce. var nonceBuf: array[24, byte] @@ -36,7 +41,7 @@ proc scramClientFirstMessage*(user: string, state: var ScramState): seq[byte] = if n != 24: raise newException(CatchableError, "SCRAM: failed to generate random nonce") state.clientNonce = base64.encode(nonceBuf) - state.clientFirstBare = "n=" & user & ",r=" & state.clientNonce + state.clientFirstBare = "n=" & scramEscapeUsername(user) & ",r=" & state.clientNonce result = toBytes("n,," & state.clientFirstBare) proc scramClientFirstMessage*( @@ -44,7 +49,7 @@ proc scramClientFirstMessage*( ): seq[byte] = ## Overload with explicit nonce for testing. state.clientNonce = nonce - state.clientFirstBare = "n=" & user & ",r=" & nonce + state.clientFirstBare = "n=" & scramEscapeUsername(user) & ",r=" & nonce result = toBytes("n,," & state.clientFirstBare) proc scramClientFinalMessage*( diff --git a/tests/test_auth.nim b/tests/test_auth.nim index 68750e9..6554f94 100644 --- a/tests/test_auth.nim +++ b/tests/test_auth.nim @@ -35,6 +35,24 @@ suite "MD5 authentication": let h2 = md5AuthHash("user", "pass", [0x05'u8, 0x06, 0x07, 0x08]) check h1 != h2 +suite "SCRAM username escaping": + test "scramEscapeUsername with no special chars": + check scramEscapeUsername("user") == "user" + + test "scramEscapeUsername escapes '='": + check scramEscapeUsername("user=1") == "user=3D1" + + test "scramEscapeUsername escapes ','": + check scramEscapeUsername("user,name") == "user=2Cname" + + test "scramEscapeUsername escapes both '=' and ','": + check scramEscapeUsername("a=b,c") == "a=3Db=2Cc" + + test "scramEscapeUsername escapes '=' before ','": + # '=' must be escaped first so that '=2C' introduced by comma escaping + # is not double-escaped. + check scramEscapeUsername("=,") == "=3D=2C" + suite "SCRAM-SHA-256": test "clientFirstMessage with fixed nonce": var state: ScramState @@ -43,6 +61,12 @@ suite "SCRAM-SHA-256": check state.clientNonce == "rOprNGfwEbeRWgbNEkqO" check state.clientFirstBare == "n=user,r=rOprNGfwEbeRWgbNEkqO" + test "clientFirstMessage escapes username": + var state: ScramState + let msg = scramClientFirstMessage("u=ser,1", "testNonce", state) + check toString(msg) == "n,,n=u=3Dser=2C1,r=testNonce" + check state.clientFirstBare == "n=u=3Dser=2C1,r=testNonce" + test "clientFirstMessage with random nonce": var state: ScramState let msg = scramClientFirstMessage("user", state)