Skip to content

Commit a708c3a

Browse files
committed
first version with SCRAM-SHA256 client
1 parent 5b40e95 commit a708c3a

File tree

3 files changed

+143
-0
lines changed

3 files changed

+143
-0
lines changed

scram.nimble

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
version = "0.1.0"
2+
author = "Huy Doan"
3+
description = "Salted Challenge Response Authentication Mechanism (SCRAM) "
4+
license = "MIT"
5+
6+
requires "nim >= 0.17.0", "hmac >= 0.1.2"

scram/client.nim

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import base64, pegs, random, strutils, hmac, nimSHA2, private/utils
2+
3+
type
4+
ScramError* = object of SystemError
5+
6+
ScramState = enum
7+
INITIAL
8+
FIRST_PREPARED
9+
FINAL_PREPARED
10+
ENDED
11+
12+
DigestType* = enum
13+
SHA1
14+
SHA256
15+
SHA512
16+
17+
ScramClient = ref object of RootObj
18+
clientNonce: string
19+
clientFirstBareMessage: string
20+
digestType: DigestType
21+
state: ScramState
22+
isSuccessful: bool
23+
serverSignature: string
24+
25+
const
26+
GS2_HEADER = "n,,"
27+
INT_1 = "\x0\x0\x0\x1"
28+
CLIENT_KEY = "Client Key"
29+
SERVER_KEY = "Server Key"
30+
31+
let
32+
SERVER_FIRST_MESSAGE = peg"'r='{[^,]*}',s='{[^,]*}',i='{\d+}$"
33+
SERVER_FINAL_MESSAGE = peg"'v='{[^,]*}$"
34+
35+
proc hi(s: ScramClient, password, salt: string, iterations: int): string =
36+
var previous: string
37+
result = $hmac_sha256(password, salt & INT_1)
38+
previous = result
39+
for _ in 1..<iterations:
40+
previous = $hmac_sha256(password, previous)
41+
result ^= previous
42+
43+
proc newScramClient*(digestType: DigestType): ScramClient =
44+
result = new(ScramClient)
45+
result.state = INITIAL
46+
result.digestType = digestType
47+
result.clientNonce = makeNonce()
48+
result.isSuccessful = false
49+
50+
proc prepareFirstMessage*(s: ScramClient, username: string): string {.raises: [ScramError]} =
51+
if username.isNilOrEmpty:
52+
raise newException(ScramError, "username cannot be nil or empty")
53+
54+
var username = username.replace("=", "=3D").replace(",", "=2C")
55+
56+
57+
s.clientFirstBareMessage = "n="
58+
s.clientFirstBareMessage.add(username)
59+
s.clientFirstBareMessage.add(",r=")
60+
s.clientFirstBareMessage.add(s.clientNonce)
61+
62+
result = GS2_HEADER & s.clientFirstBareMessage
63+
s.state = FIRST_PREPARED
64+
65+
proc prepareFinalMessage*(s: ScramClient, password, serverFirstMessage: string): string {.raises: [ScramError, OverflowError, ValueError].} =
66+
var
67+
nonce, salt: string
68+
iterations: int
69+
70+
if s.state != FIRST_PREPARED:
71+
raise newException(ScramError, "First message have not been prepared, call prepareFirstMessage() first")
72+
73+
if serverFirstMessage =~ SERVER_FIRST_MESSAGE:
74+
nonce = matches[0]
75+
salt = decode(matches[1])
76+
iterations = parseInt(matches[2])
77+
else:
78+
s.state = ENDED
79+
return nil
80+
81+
if not nonce.startsWith(s.clientNonce) or iterations < 0:
82+
s.state = ENDED
83+
return nil
84+
85+
let
86+
saltedPassword = s.hi(password, salt, iterations)
87+
clientKey = $hmac_sha256(saltedPassword, CLIENT_KEY)
88+
storedKey = $computeSHA256(clientKey)
89+
serverKey = $hmac_sha256(saltedPassword, SERVER_KEY)
90+
clientFinalMessageWithoutProof = "c=" & encode(GS2_HEADER) & ",r=" & nonce
91+
authMessage = s.clientFirstBareMessage & "," & serverFirstMessage & "," & clientFinalMessageWithoutProof
92+
clientSignature = $hmac_sha256(storedKey, authMessage)
93+
94+
s.serverSignature = $hmac_sha256(serverKey, authMessage)
95+
96+
var clientProof = clientKey
97+
clientProof ^= clientSignature
98+
s.state = FINAL_PREPARED
99+
result = clientFinalMessageWithoutProof & ",p=" & encode(clientProof, newLine="")
100+
101+
proc verifyServerFinalMessage*(s: ScramClient, serverFinalMessage: string): bool =
102+
if s.state != FINAL_PREPARED:
103+
raise newException(ScramError, "You can call this method only once after calling prepareFinalMessage()")
104+
s.state = ENDED
105+
if serverFinalMessage =~ SERVER_FINAL_MESSAGE:
106+
let proposedServerSignature = decode(matches[0])
107+
s.isSuccessful = proposedServerSignature == s.serverSignature
108+
109+
result = s.isSuccessful
110+
111+
proc isSuccessful*(s: ScramClient): bool =
112+
if s.state != ENDED:
113+
raise newException(ScramError, "You cannot call this method before authentication is ended")
114+
return s.isSuccessful
115+
116+
proc isEnded*(s: ScramClient): bool =
117+
result = s.state == ENDED
118+
119+
proc getState*(s: ScramClient): ScramState =
120+
result = s.state
121+
122+
when isMainModule:
123+
var s = newScramClient(SHA256)
124+
s.clientNonce = "VeAOLsQ22fn/tjalHQIz7cQT"
125+
126+
echo s.prepareFirstMessage("bob")
127+
let finalMessage = s.prepareFinalMessage("secret", "r=VeAOLsQ22fn/tjalHQIz7cQTmeE5qJh8qKEe8wALMut1,s=ldZSefTzKxPNJhP73AmW/A==,i=4096")
128+
echo finalMessage
129+
assert(finalMessage == "c=biws,r=VeAOLsQ22fn/tjalHQIz7cQTmeE5qJh8qKEe8wALMut1,p=AtNtxGzsMA8evcWBM0MXFjxN8OcG1KRkLkFyoHlupOU=")

scram/private/utils.nim

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import random, base64
2+
randomize()
3+
4+
proc makeNonce*(): string {.inline.} = result = encode($random(1.0))[0..^3]
5+
6+
template `^=`*[T](a, b: T) =
7+
for x in 0..<a.len:
8+
a[x] = (a[x].int32 xor b[x].int32).char

0 commit comments

Comments
 (0)