Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions async_postgres/pg_auth.nim
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,27 @@ 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]
let n = randomBytes(nonceBuf)
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*(
user: string, nonce: string, state: var ScramState
): 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*(
Expand Down
24 changes: 24 additions & 0 deletions tests/test_auth.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
Loading