Skip to content

Commit 3d47d8e

Browse files
committed
add server implementation
1 parent 0201fb1 commit 3d47d8e

File tree

5 files changed

+200
-57
lines changed

5 files changed

+200
-57
lines changed

scram.nimble

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version = "0.1.1"
1+
version = "0.1.2"
22
author = "Huy Doan"
33
description = "Salted Challenge Response Authentication Mechanism (SCRAM) "
44
license = "MIT"

scram/client.nim

Lines changed: 13 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,20 @@
11
import base64, pegs, random, strutils, hmac, nimSHA2, securehash, md5, private/[utils,types]
22

3-
export MD5Digest, Sha256Digest, Sha512Digest
3+
export MD5Digest, SHA1Digest, SHA256Digest, SHA512Digest
44

55
type
66
ScramClient[T] = ref object of RootObj
77
clientNonce: string
8-
clientFirstBareMessage: string
8+
clientFirstMessageBare: string
99
state: ScramState
1010
isSuccessful: bool
1111
serverSignature: T
1212

13-
const
14-
GS2_HEADER = "n,,"
15-
INT_1 = "\x0\x0\x0\x1"
16-
CLIENT_KEY = "Client Key"
17-
SERVER_KEY = "Server Key"
18-
1913
let
2014
SERVER_FIRST_MESSAGE = peg"'r='{[^,]*}',s='{[^,]*}',i='{\d+}$"
2115
SERVER_FINAL_MESSAGE = peg"'v='{[^,]*}$"
2216

2317

24-
proc HMAC[T](password, salt: string): T =
25-
when T is MD5Digest:
26-
result = hmac_md5(password, salt)
27-
elif T is Sha1Digest:
28-
result = Sha1Digest(hmac_sha1(password, salt))
29-
elif T is Sha256Digest:
30-
result = hmac_sha256(password, salt)
31-
elif T is Sha512Digest:
32-
result = hmac_sha512(password, salt)
33-
34-
proc HASH[T](s: string): T =
35-
when T is MD5Digest:
36-
result = hash_md5(s)
37-
elif T is Sha1Digest:
38-
result = Sha1Digest(hash_sha1(s))
39-
elif T is Sha256Digest:
40-
result = hash_sha256(s)
41-
elif T is Sha512Digest:
42-
result = hash_sha512(s)
43-
44-
45-
proc hi[T](s: ScramClient[T], password, salt: string, iterations: int): T =
46-
var previous: T
47-
result = HMAC[T](password, salt & INT_1)
48-
previous = result
49-
for _ in 1..<iterations:
50-
previous = HMAC[T](password, $previous)
51-
result ^= previous
52-
5318
proc newScramClient*[T](): ScramClient[T] =
5419
result = new(ScramClient[T])
5520
result.state = INITIAL
@@ -60,27 +25,24 @@ proc prepareFirstMessage*(s: ScramClient, username: string): string {.raises: [S
6025
if username.isNilOrEmpty:
6126
raise newException(ScramError, "username cannot be nil or empty")
6227
var username = username.replace("=", "=3D").replace(",", "=2C")
63-
s.clientFirstBareMessage = "n="
64-
s.clientFirstBareMessage.add(username)
65-
s.clientFirstBareMessage.add(",r=")
66-
s.clientFirstBareMessage.add(s.clientNonce)
28+
s.clientFirstMessageBare = "n="
29+
s.clientFirstMessageBare.add(username)
30+
s.clientFirstMessageBare.add(",r=")
31+
s.clientFirstMessageBare.add(s.clientNonce)
6732

68-
result = GS2_HEADER & s.clientFirstBareMessage
33+
result = GS2_HEADER & s.clientFirstMessageBare
6934
s.state = FIRST_PREPARED
7035

7136
proc prepareFinalMessage*[T](s: ScramClient[T], password, serverFirstMessage: string): string {.raises: [ScramError, OverflowError, ValueError].} =
7237
if s.state != FIRST_PREPARED:
7338
raise newException(ScramError, "First message have not been prepared, call prepareFirstMessage() first")
74-
7539
var
7640
nonce, salt: string
7741
iterations: int
78-
7942
var matches: array[3, string]
8043
if match(serverFirstMessage, SERVER_FIRST_MESSAGE, matches):
81-
# if serverFirstMessage =~ SERVER_FIRST_MESSAGE:
8244
nonce = matches[0]
83-
salt = decode(matches[1])
45+
salt = base64.decode(matches[1])
8446
iterations = parseInt(matches[2])
8547
else:
8648
s.state = ENDED
@@ -91,28 +53,26 @@ proc prepareFinalMessage*[T](s: ScramClient[T], password, serverFirstMessage: st
9153
return nil
9254

9355
let
94-
saltedPassword = s.hi(password, salt, iterations)
56+
saltedPassword = hi[T](password, salt, iterations)
9557
clientKey = HMAC[T]($saltedPassword, CLIENT_KEY)
9658
storedKey = HASH[T]($clientKey)
9759
serverKey = HMAC[T]($saltedPassword, SERVER_KEY)
98-
clientFinalMessageWithoutProof = "c=" & encode(GS2_HEADER) & ",r=" & nonce
99-
authMessage = s.clientFirstBareMessage & "," & serverFirstMessage & "," & clientFinalMessageWithoutProof
60+
clientFinalMessageWithoutProof = "c=" & base64.encode(GS2_HEADER) & ",r=" & nonce
61+
authMessage = s.clientFirstMessageBare & "," & serverFirstMessage & "," & clientFinalMessageWithoutProof
10062
clientSignature = HMAC[T]($storedKey, authMessage)
101-
10263
s.serverSignature = HMAC[T]($serverKey, authMessage)
103-
10464
var clientProof = clientKey
10565
clientProof ^= clientSignature
10666
s.state = FINAL_PREPARED
107-
result = clientFinalMessageWithoutProof & ",p=" & encode(clientProof, newLine="")
67+
result = clientFinalMessageWithoutProof & ",p=" & base64.encode(clientProof, newLine="")
10868

10969
proc verifyServerFinalMessage*(s: ScramClient, serverFinalMessage: string): bool =
11070
if s.state != FINAL_PREPARED:
11171
raise newException(ScramError, "You can call this method only once after calling prepareFinalMessage()")
11272
s.state = ENDED
11373
var matches: array[1, string]
11474
if match(serverFinalMessage, SERVER_FINAL_MESSAGE, matches):
115-
let proposedServerSignature = decode(matches[0])
75+
let proposedServerSignature = base64.decode(matches[0])
11676
s.isSuccessful = proposedServerSignature == s.serverSignature
11777
result = s.isSuccessful
11878

scram/private/types.nim

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,11 @@ type
1313
INITIAL
1414
FIRST_PREPARED
1515
FINAL_PREPARED
16+
FIRST_CLIENT_MESSAGE_HANDLED
1617
ENDED
18+
19+
const
20+
GS2_HEADER* = "n,,"
21+
INT_1* = "\x0\x0\x0\x1"
22+
CLIENT_KEY* = "Client Key"
23+
SERVER_KEY* = "Server Key"

scram/private/utils.nim

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,49 @@
1-
import random, base64, strutils, types
1+
import random, base64, strutils, types, hmac
22
randomize()
33

44
proc `$`*(sha: Sha1Digest): string =
55
result = ""
66
for v in sha:
77
result.add(toHex(int(v), 2))
88

9-
proc makeNonce*(): string {.inline.} = result = encode($random(1.0))[0..^3]
9+
proc makeNonce*(): string {.inline.} = result = base64.encode($random(1.0))[0..^3]
1010

1111
template `^=`*[T](a, b: T) =
12-
for x in 0..<sizeof(a):
12+
for x in 0..<a.len:
1313
when T is Sha1Digest:
1414
a[x] = (a[x].int32 xor b[x].int32).uint8
1515
else:
1616
a[x] = (a[x].int32 xor b[x].int32).char
17+
18+
proc HMAC*[T](password, salt: string): T =
19+
when T is MD5Digest:
20+
result = hmac_md5(password, salt)
21+
elif T is Sha1Digest:
22+
result = Sha1Digest(hmac_sha1(password, salt))
23+
elif T is Sha256Digest:
24+
result = hmac_sha256(password, salt)
25+
elif T is Sha512Digest:
26+
result = hmac_sha512(password, salt)
27+
28+
proc HASH*[T](s: string): T =
29+
when T is MD5Digest:
30+
result = hash_md5(s)
31+
elif T is Sha1Digest:
32+
result = Sha1Digest(hash_sha1(s))
33+
elif T is Sha256Digest:
34+
result = hash_sha256(s)
35+
elif T is Sha512Digest:
36+
result = hash_sha512(s)
37+
38+
proc hi*[T](password, salt: string, iterations: int): T =
39+
var previous: T
40+
result = HMAC[T](password, salt & INT_1)
41+
previous = result
42+
for _ in 1..<iterations:
43+
previous = HMAC[T](password, $previous)
44+
result ^= previous
45+
46+
proc debug*[T](s: T): string =
47+
result = ""
48+
for x in s:
49+
result.add x.uint8.toHex & " "

scram/server.nim

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import base64, pegs, random, strutils, hmac, nimSHA2, securehash, md5, private/[utils,types]
2+
3+
type
4+
ScramServer*[T] = ref object of RootObj
5+
serverNonce: string
6+
clientFirstMessageBare: string
7+
serverFirstMessage: string
8+
state: ScramState
9+
isSuccessful: bool
10+
userData: UserData
11+
12+
UserData* = object
13+
salt: string
14+
iterations: int
15+
serverKey: string
16+
storedKey: string
17+
18+
let
19+
CLIENT_FIRST_MESSAGE = peg"^([pny]'='?([^,]*)','([^,]*)','){('m='([^,]*)',')?'n='{[^,]*}',r='{[^,]*}(','(.*))*}$"
20+
CLIENT_FINAL_MESSAGE = peg"{'c='{[^,]*}',r='{[^,]*}}',p='{.*}$"
21+
22+
proc initUserData*(password: string, iterations = 4096): UserData =
23+
var iterations = iterations
24+
var password = password
25+
if password.isNilOrEmpty:
26+
password =""
27+
iterations = 1
28+
29+
let
30+
salt = makeNonce()[0..24]
31+
saltedPassword = hi[SHA256Digest](password, salt, iterations)
32+
clientKey = HMAC[SHA256Digest]($saltedPassword, CLIENT_KEY)
33+
storedKey = HASH[SHA256Digest]($clientKey)
34+
serverKey = HMAC[SHA256Digest]($saltedPassword, SERVER_KEY)
35+
36+
result.salt = base64.encode(salt)
37+
result.iterations = iterations
38+
result.storedKey = base64.encode($storedKey)
39+
result.serverKey = base64.encode($serverKey)
40+
41+
proc initUserData*(salt: string, iterations: int, serverKey, storedKey: string): UserData =
42+
result.salt = salt
43+
result.iterations = iterations
44+
result.serverKey = serverKey
45+
result.storedKey = storedKey
46+
47+
proc newScramServer*[T](salt: string = nil, iterations = 4096): ScramServer[T] =
48+
result = new(ScramServer[T])
49+
result.state = INITIAL
50+
result.isSuccessful = false
51+
52+
proc handleClientFirstMessage*[T](s: ScramServer[T],clientFirstMessage: string): string =
53+
var matches: array[3, string]
54+
if not match(clientFirstMessage, CLIENT_FIRST_MESSAGE, matches):
55+
s.state = ENDED
56+
return nil
57+
s.clientFirstMessageBare = matches[0]
58+
s.serverNonce = matches[2] & makeNonce()
59+
s.state = FIRST_CLIENT_MESSAGE_HANDLED
60+
result = matches[1] # username
61+
62+
proc prepareFirstMessage*(s: ScramServer, userData: UserData): string =
63+
s.state = FIRST_PREPARED
64+
s.userData = userData
65+
s.serverFirstMessage = "r=$#,s=$#,i=$#" % [s.serverNonce, userData.salt, $userData.iterations]
66+
result = s.serverFirstMessage
67+
68+
proc prepareFinalMessage*[T](s: ScramServer[T], clientFinalMessage: string): string =
69+
var matches: array[4, string]
70+
if not match(clientFinalMessage, CLIENT_FINAL_MESSAGE, matches):
71+
s.state = ENDED
72+
return nil
73+
let
74+
clientFinalMessageWithoutProof = matches[0]
75+
nonce = matches[2]
76+
proof = matches[3]
77+
78+
if nonce != s.serverNonce:
79+
s.state = ENDED
80+
return nil
81+
82+
let
83+
authMessage = join([s.clientFirstMessageBare, s.serverFirstMessage, clientFinalMessageWithoutProof], ",")
84+
storedKey = base64.decode(s.userData.storedKey)
85+
clientSignature = HMAC[T](storedKey, authMessage)
86+
serverSignature = HMAC[T](decode(s.userData.serverKey), authMessage)
87+
decodedProof = base64.decode(proof)
88+
var clientKey = $clientSignature
89+
clientKey ^= decodedProof
90+
91+
let resultKey = $HASH[T](clientKey)
92+
echo "Result Key: ", base64.encode(resultKey)
93+
if resultKey != storedKey:
94+
return nil
95+
96+
s.isSuccessful = true
97+
s.state = ENDED
98+
result = "v=" & base64.encode(serverSignature, newLine="")
99+
100+
101+
proc isSuccessful*(s: ScramServer): bool =
102+
if s.state != ENDED:
103+
raise newException(ScramError, "You cannot call this method before authentication is ended")
104+
return s.isSuccessful
105+
106+
proc isEnded*(s: ScramServer): bool =
107+
result = s.state == ENDED
108+
109+
proc getState*(s: ScramServer): ScramState =
110+
result = s.state
111+
112+
when isMainModule:
113+
import client as c
114+
var
115+
username = "bob"
116+
password = "secret"
117+
userdata = initUserData(password)
118+
119+
server = newScramServer[SHA256Digest]()
120+
client = newScramClient[SHA256Digest]()
121+
122+
let clientFirstMessage = client.prepareFirstMessage(username)
123+
echo "Client first message: ", clientFirstMessage
124+
125+
let username1 = server.handleClientFirstMessage(clientFirstMessage)
126+
assert(username1 == username)
127+
128+
let serverFirstMessage = server.prepareFirstMessage(userdata)
129+
echo "Server first message: ", serverFirstMessage
130+
131+
let clientFinalMessage = client.prepareFinalMessage(password, serverFirstMessage)
132+
echo "Client final mesage: ", clientFinalMessage
133+
134+
let serverFinalMessage = server.prepareFinalMessage(clientFinalMessage)
135+
echo "Server final mesage: ", serverFinalMessage
136+
137+
assert client.verifyServerFinalMessage(serverFinalMessage) == true
138+
assert client.isSuccessful() == true
139+
140+
141+
142+
143+

0 commit comments

Comments
 (0)