Skip to content

Commit 499f680

Browse files
authored
Merge pull request #20 from mildred/mildred-fix-scram
Fix for Nim 1.6.6
2 parents ae3006a + 561b873 commit 499f680

File tree

7 files changed

+147
-28
lines changed

7 files changed

+147
-28
lines changed

scram/client.nim

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import strformat
12
import base64, pegs, strutils, hmac, sha1, nimSHA2, md5, private/[utils,types]
23

34
export MD5Digest, SHA1Digest, SHA224Digest, SHA256Digest, SHA384Digest, SHA512Digest, Keccak512Digest
@@ -53,9 +54,13 @@ proc prepareFinalMessage*[T](s: ScramClient[T], password, serverFirstMessage: st
5354
iterations: int
5455
var matches: array[3, string]
5556
if match(serverFirstMessage, SERVER_FIRST_MESSAGE, matches):
56-
nonce = matches[0]
57-
salt = base64.decode(matches[1])
58-
iterations = parseInt(matches[2])
57+
for kv in serverFirstMessage.split(','):
58+
if kv[0..1] == "i=":
59+
iterations = parseInt(kv[2..^1])
60+
elif kv[0..1] == "r=":
61+
nonce = kv[2..^1]
62+
elif kv[0..1] == "s=":
63+
salt = base64.decode(kv[2..^1])
5964
else:
6065
s.state = ENDED
6166
return ""
@@ -89,7 +94,10 @@ proc verifyServerFinalMessage*(s: ScramClient, serverFinalMessage: string): bool
8994
s.state = ENDED
9095
var matches: array[1, string]
9196
if match(serverFinalMessage, SERVER_FINAL_MESSAGE, matches):
92-
let proposedServerSignature = base64.decode(matches[0])
97+
var proposedServerSignature: string
98+
for kv in serverFinalMessage.split(','):
99+
if kv[0..1] == "v=":
100+
proposedServerSignature = base64.decode(kv[2..^1])
93101
s.isSuccessful = proposedServerSignature == $%s.serverSignature
94102
s.isSuccessful
95103

scram/private/types.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
type
2-
ScramError* = object of Exception
2+
ScramError* = object of CatchableError
33

44
DigestType* = enum
55
MD5

scram/private/utils.nim

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import random, base64, strutils, types, hmac
1+
import random, base64, strutils, types, hmac, bitops
22
from md5 import MD5Digest
33
from sha1 import Sha1Digest
44
from nimSHA2 import Sha224Digest, Sha256Digest, Sha384Digest, Sha512Digest
@@ -20,6 +20,21 @@ template `^=`*[T](a, b: T) =
2020
else:
2121
a[x] = (a[x].int32 xor b[x].int32).char
2222

23+
proc custom_xor*[T](bytes: T, str: string): string =
24+
if bytes.len != str.len:
25+
raise newException(RangeDefect, "xor must have both arguments of the same size")
26+
result = str
27+
for x in 0..<bytes.len:
28+
result[x] = (bytes[x].uint8 xor str[x].uint8).char
29+
30+
proc constantTimeEqual*(a, b: string): bool =
31+
if a.len != b.len:
32+
raise newException(RangeDefect, "must have both arguments of the same size")
33+
var res: uint8 = 0
34+
for x in 0..<a.len:
35+
res = bitor(res, bitxor(a[x].uint8, b[x].uint8))
36+
result = (res == 0)
37+
2338
proc HMAC*[T](password, salt: string): T =
2439
when T is MD5Digest:
2540
result = hmac_md5(password, salt)
@@ -36,6 +51,12 @@ proc HMAC*[T](password, salt: string): T =
3651
elif T is Keccak512Digest:
3752
result = hmac_keccak512(password, salt)
3853

54+
proc raw_str*[T](digest: T): string =
55+
when T is Sha1Digest:
56+
for c in digest: result.add(char(c))
57+
else:
58+
result = $digest
59+
3960
proc HASH*[T](s: string): T =
4061
when T is MD5Digest:
4162
result = hash_md5(s)
@@ -57,7 +78,6 @@ proc debug[T](s: T): string =
5778
for x in s:
5879
result.add strutils.toHex(x.uint8).toLowerAscii
5980

60-
6181
proc hi*[T](password, salt: string, iterations: int): T =
6282
var previous = HMAC[T](password, salt & INT_1)
6383
result = previous

scram/server.nim

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import strformat, strutils
12
import base64, pegs, strutils, hmac, nimSHA2, private/[utils,types]
23

34
type
45
ScramServer*[T] = ref object of RootObj
5-
serverNonce: string
6+
serverNonce*: string
67
clientFirstMessageBare: string
78
serverFirstMessage: string
8-
state: ScramState
9+
state*: ScramState
910
isSuccessful: bool
1011
userData: UserData
1112

@@ -19,22 +20,25 @@ let
1920
CLIENT_FIRST_MESSAGE = peg"^([pny]'='?([^,]*)','([^,]*)','){('m='([^,]*)',')?'n='{[^,]*}',r='{[^,]*}(','(.*))*}$"
2021
CLIENT_FINAL_MESSAGE = peg"{'c='{[^,]*}',r='{[^,]*}}',p='{.*}$"
2122

22-
proc initUserData*(password: string, iterations = 4096): UserData =
23+
proc initUserData*[T](typ: typedesc[T], password: string, iterations = 4096): UserData =
2324
var iterations = iterations
2425
if password.len == 0:
2526
iterations = 1
2627
let
2728
salt = makeNonce()[0..24]
28-
saltedPassword = hi[SHA256Digest](password, salt, iterations)
29-
clientKey = HMAC[SHA256Digest]($%saltedPassword, CLIENT_KEY)
30-
storedKey = HASH[SHA256Digest]($%clientKey)
31-
serverKey = HMAC[SHA256Digest]($%saltedPassword, SERVER_KEY)
29+
saltedPassword = hi[T](password, salt, iterations)
30+
clientKey = HMAC[T]($%saltedPassword, CLIENT_KEY)
31+
storedKey = HASH[T]($%clientKey)
32+
serverKey = HMAC[T]($%saltedPassword, SERVER_KEY)
3233

3334
result.salt = base64.encode(salt)
3435
result.iterations = iterations
3536
result.storedKey = base64.encode($%storedKey)
3637
result.serverKey = base64.encode($%serverKey)
3738

39+
proc initUserData*(password: string, iterations = 4096): UserData =
40+
initUserData(Sha256Digest, password, iterations)
41+
3842
proc initUserData*(salt: string, iterations: int, serverKey, storedKey: string): UserData =
3943
result.salt = salt
4044
result.iterations = iterations
@@ -45,14 +49,19 @@ proc newScramServer*[T](): ScramServer[T] {.deprecated: "use `new ScramServer[T]
4549
new ScramServer[T]
4650

4751
proc handleClientFirstMessage*[T](s: ScramServer[T],clientFirstMessage: string): string =
52+
let parts = clientFirstMessage.split(',', 2)
4853
var matches: array[3, string]
49-
if not match(clientFirstMessage, CLIENT_FIRST_MESSAGE, matches):
54+
if not match(clientFirstMessage, CLIENT_FIRST_MESSAGE, matches) or not parts.len == 3:
5055
s.state = ENDED
5156
return
52-
s.clientFirstMessageBare = matches[0]
53-
s.serverNonce = matches[2] & makeNonce()
57+
s.clientFirstMessageBare = parts[2]
58+
5459
s.state = FIRST_CLIENT_MESSAGE_HANDLED
55-
matches[1] # username
60+
for kv in s.clientFirstMessageBare.split(','):
61+
if kv[0..1] == "n=":
62+
result = kv[2..^1]
63+
elif kv[0..1] == "r=":
64+
s.serverNonce = kv[2..^1] & makeNonce()
5665

5766
proc prepareFirstMessage*(s: ScramServer, userData: UserData): string =
5867
s.state = FIRST_PREPARED
@@ -65,10 +74,16 @@ proc prepareFinalMessage*[T](s: ScramServer[T], clientFinalMessage: string): str
6574
if not match(clientFinalMessage, CLIENT_FINAL_MESSAGE, matches):
6675
s.state = ENDED
6776
return
68-
let
69-
clientFinalMessageWithoutProof = matches[0]
70-
nonce = matches[2]
71-
proof = matches[3]
77+
var clientFinalMessageWithoutProof, nonce, proof: string
78+
for kv in clientFinalMessage.split(','):
79+
if kv[0..1] == "p=":
80+
proof = kv[2..^1]
81+
else:
82+
if clientFinalMessageWithoutProof.len > 0:
83+
clientFinalMessageWithoutProof.add(',')
84+
clientFinalMessageWithoutProof.add(kv)
85+
if kv[0..1] == "r=":
86+
nonce = kv[2..^1]
7287

7388
if nonce != s.serverNonce:
7489
s.state = ENDED
@@ -80,19 +95,21 @@ proc prepareFinalMessage*[T](s: ScramServer[T], clientFinalMessage: string): str
8095
clientSignature = HMAC[T](storedKey, authMessage)
8196
serverSignature = HMAC[T](decode(s.userData.serverKey), authMessage)
8297
decodedProof = base64.decode(proof)
83-
var clientKey = $clientSignature
84-
clientKey ^= decodedProof
98+
clientKey = custom_xor(clientSignature, decodedProof)
99+
let resultKey = HASH[T](clientKey).raw_str
85100

86-
let resultKey = $HASH[T](clientKey)
87-
if resultKey != storedKey:
101+
# SECURITY: constant time HMAC check
102+
if not constantTimeEqual(resultKey, storedKey):
103+
let k1 = base64.encode(resultKey)
104+
let k2 = base64.encode(storedKey)
88105
return
89106

90107
s.isSuccessful = true
91108
s.state = ENDED
92109
when NimMajor >= 1 and (NimMinor >= 1 or NimPatch >= 2):
93-
"v=" & base64.encode(serverSignature)
110+
result = "v=" & base64.encode(serverSignature)
94111
else:
95-
"v=" & base64.encode(serverSignature, newLine="")
112+
result = "v=" & base64.encode(serverSignature, newLine="")
96113

97114

98115
proc isSuccessful*(s: ScramServer): bool =

tests/test_both.nim

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import unittest, scram/server, scram/client, sha1, nimSHA2, base64, scram/private/[utils,types]
2+
3+
4+
proc test[T](user, password: string) =
5+
var client = newScramClient[T]()
6+
var server = new ScramServer[T]
7+
let cfirst = client.prepareFirstMessage(user)
8+
assert server.handleClientFirstMessage(cfirst) == user, "incorrect detected username"
9+
assert server.state == FIRST_CLIENT_MESSAGE_HANDLED, "incorrect state"
10+
let sfirst = server.prepareFirstMessage(initUserData(T, password))
11+
let cfinal = client.prepareFinalMessage(password, sfirst)
12+
let sfinal = server.prepareFinalMessage(cfinal)
13+
assert client.verifyServerFinalMessage(sfinal), "incorrect server final message"
14+
15+
suite "Scram Client-Server tests":
16+
test "SCRAM-SHA1":
17+
test[Sha1Digest](
18+
"user",
19+
"pencil"
20+
)
21+
22+
test "SCRAM-SHA256":
23+
test[Sha256Digest](
24+
"bob",
25+
"secret"
26+
)
File renamed without changes.

tests/test_server.nim

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import unittest, scram/server, sha1, nimSHA2, base64, scram/private/[utils,types]
2+
3+
4+
proc test[T](user, password, nonce, salt, cfirst, sfirst, cfinal, sfinal: string) =
5+
var server = new ScramServer[T]
6+
assert server.handleClientFirstMessage(cfirst) == user, "incorrect detected username"
7+
assert server.state == FIRST_CLIENT_MESSAGE_HANDLED, "incorrect state"
8+
server.serverNonce = nonce
9+
let
10+
iterations = 4096
11+
decodedSalt = base64.decode(salt)
12+
saltedPassword = hi[T](password, decodedSalt, iterations)
13+
clientKey = HMAC[T]($%saltedPassword, CLIENT_KEY)
14+
storedKey = HASH[T]($%clientKey)
15+
serverKey = HMAC[T]($%saltedPassword, SERVER_KEY)
16+
ud = UserData(
17+
salt: base64.encode(decodedSalt),
18+
iterations: iterations,
19+
storedKey: base64.encode($%storedKey),
20+
serverKey: base64.encode($%serverKey))
21+
assert ud.salt == salt, "Incorrect salt initialization"
22+
assert server.prepareFirstMessage(ud) == sfirst, "incorrect first message"
23+
assert server.prepareFinalMessage(cfinal) == sfinal, "incorrect last message"
24+
25+
suite "Scram Server tests":
26+
test "SCRAM-SHA1":
27+
test[Sha1Digest](
28+
"user",
29+
"pencil",
30+
"fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j",
31+
"QSXCR+Q6sek8bf92",
32+
"n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL",
33+
"r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096",
34+
"c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=",
35+
"v=rmF9pqV8S7suAoZWja4dJRkFsKQ="
36+
)
37+
38+
test "SCRAM-SHA256":
39+
test[Sha256Digest](
40+
"bob",
41+
"secret",
42+
"VeAOLsQ22fn/tjalHQIz7cQTmeE5qJh8qKEe8wALMut1",
43+
"ldZSefTzKxPNJhP73AmW/A==",
44+
"n,,n=bob,r=VeAOLsQ22fn/tjalHQIz7cQT",
45+
"r=VeAOLsQ22fn/tjalHQIz7cQTmeE5qJh8qKEe8wALMut1,s=ldZSefTzKxPNJhP73AmW/A==,i=4096",
46+
"c=biws,r=VeAOLsQ22fn/tjalHQIz7cQTmeE5qJh8qKEe8wALMut1,p=AtNtxGzsMA8evcWBM0MXFjxN8OcG1KRkLkFyoHlupOU=",
47+
"v=jeEn7M7PgnBZ7GRd+f3Ikaj40dw4EGKZ0x8FcQztLLs="
48+
)

0 commit comments

Comments
 (0)