feat: real SCRAM-SHA-256 authentication (Option C)
Security: - New scram.nim module with full SCRAM-SHA-256 per RFC 7677 * PBKDF2-HMAC-SHA-256 key derivation * HMAC-SHA-256 with multiple overloads for bytes/strings * Secure nonce/salt generation via /dev/urandom * Client/server message parsing and proof verification AuthManager updates: - registerScramUser(): stores salted credentials (salt + iterations + storedKey + serverKey) - startScram(): initiates challenge-response handshake - finishScram(): verifies client proof and returns server signature - Legacy amSCRAMSHA256 path kept for backward compatibility HTTP endpoints: - POST /auth/scram/start — accepts client-first-message, returns server-first - POST /auth/scram/finish — accepts client-final-message, returns server-final Tests: - SCRAM-SHA-256 full handshake test (register → start → compute proof → finish) - SCRAM-SHA-256 invalid proof rejection test Build: 0 warnings, all tests pass
This commit is contained in:
+1
-1
@@ -107,7 +107,7 @@
|
|||||||
|
|
||||||
1. ~~**Build warnings cleanup**~~ ✅ — implicit `cstring` conversion (wal.nim), `HoleEnumConv` (server.nim), unused `os` import (logging.nim), `ImplicitDefaultValue` (ast.nim)
|
1. ~~**Build warnings cleanup**~~ ✅ — implicit `cstring` conversion (wal.nim), `HoleEnumConv` (server.nim), unused `os` import (logging.nim), `ImplicitDefaultValue` (ast.nim)
|
||||||
2. **Threadpool deprecation** — миграция към `malebolgia`/`weave`/`taskpools`
|
2. **Threadpool deprecation** — миграция към `malebolgia`/`weave`/`taskpools`
|
||||||
3. **Auth SCRAM-SHA-256** — истински challenge-response със salt + iteration count
|
3. ~~**Auth SCRAM-SHA-256**~~ ✅ — истински challenge-response със salt + iteration count
|
||||||
4. ~~**TLA+ symmetry reduction**~~ ✅ — `SYMMETRY` добавен във всички 9 `.cfg` файла + `Permutations` дефиниции в `.tla` спековете
|
4. ~~**TLA+ symmetry reduction**~~ ✅ — `SYMMETRY` добавен във всички 9 `.cfg` файла + `Permutations` дефиниции в `.tla` спековете
|
||||||
5. ~~**`crossmodal.tla`**~~ ✅ — cross-modal consistency между document/vector/graph/FTS
|
5. ~~**`crossmodal.tla`**~~ ✅ — cross-modal consistency между document/vector/graph/FTS
|
||||||
6. **Replication data transfer** — `writeLsn` да изпраща данни към replicas
|
6. **Replication data transfer** — `writeLsn` да изпраща данни към replicas
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
- Моделира sync между document/vector/graph/FTS индекси
|
- Моделира sync между document/vector/graph/FTS индекси
|
||||||
- Резултат: 10-ти TLA+ спек, пълно покритие на core модулите
|
- Резултат: 10-ти TLA+ спек, пълно покритие на core модулите
|
||||||
|
|
||||||
### Опция C: Auth hardening + SCRAM (3-4 часа)
|
### Опция C: Auth hardening + SCRAM ✅
|
||||||
- Истински SCRAM-SHA-256 със salt, iteration count, challenge-response
|
- Истински SCRAM-SHA-256 със salt, iteration count, challenge-response
|
||||||
- Резултат: production-grade auth
|
- Резултат: production-grade auth
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import ../core/mvcc
|
|||||||
import ../protocol/wire
|
import ../protocol/wire
|
||||||
import ../core/websocket
|
import ../core/websocket
|
||||||
import jwt as jwtlib
|
import jwt as jwtlib
|
||||||
|
import ../protocol/auth
|
||||||
|
|
||||||
type
|
type
|
||||||
HttpServer* = ref object
|
HttpServer* = ref object
|
||||||
@@ -27,6 +28,7 @@ type
|
|||||||
ctx: ExecutionContext
|
ctx: ExecutionContext
|
||||||
metrics*: Metrics
|
metrics*: Metrics
|
||||||
secretKey*: string
|
secretKey*: string
|
||||||
|
authManager*: AuthManager
|
||||||
ws*: WsServer
|
ws*: WsServer
|
||||||
|
|
||||||
Metrics* = ref object
|
Metrics* = ref object
|
||||||
@@ -46,8 +48,10 @@ proc newHttpServer*(config: BaraConfig): HttpServer =
|
|||||||
ctx.onChange = proc(ev: ChangeEvent) =
|
ctx.onChange = proc(ev: ChangeEvent) =
|
||||||
let msg = $ev.kind & " " & ev.table
|
let msg = $ev.kind & " " & ev.table
|
||||||
asyncCheck ws.broadcastToTable(ev.table, msg)
|
asyncCheck ws.broadcastToTable(ev.table, msg)
|
||||||
|
let am = newAuthManager(secret)
|
||||||
HttpServer(config: config, running: false, db: db, ctx: ctx,
|
HttpServer(config: config, running: false, db: db, ctx: ctx,
|
||||||
secretKey: secret,
|
secretKey: secret,
|
||||||
|
authManager: am,
|
||||||
metrics: Metrics(), ws: ws)
|
metrics: Metrics(), ws: ws)
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
@@ -211,6 +215,46 @@ proc authHandler(server: HttpServer): RequestHandler =
|
|||||||
"role": "user"
|
"role": "user"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
proc scramStartHandler(server: HttpServer): RequestHandler =
|
||||||
|
return proc(request: Request) {.gcsafe.} =
|
||||||
|
{.cast(gcsafe).}:
|
||||||
|
let ctx = newContext(request)
|
||||||
|
let body = parseJson(request.body)
|
||||||
|
if body == nil or "username" notin body or "clientFirstMessage" notin body:
|
||||||
|
ctx.json(%*{"error": "Missing username or clientFirstMessage"}, 400)
|
||||||
|
return
|
||||||
|
let username = body["username"].getStr()
|
||||||
|
let clientFirst = body["clientFirstMessage"].getStr()
|
||||||
|
try:
|
||||||
|
let serverFirst = server.authManager.startScram(clientFirst)
|
||||||
|
ctx.json(%*{
|
||||||
|
"serverFirstMessage": serverFirst,
|
||||||
|
"status": "continue"
|
||||||
|
})
|
||||||
|
except CatchableError as e:
|
||||||
|
ctx.json(%*{"error": e.msg}, 401)
|
||||||
|
|
||||||
|
proc scramFinishHandler(server: HttpServer): RequestHandler =
|
||||||
|
return proc(request: Request) {.gcsafe.} =
|
||||||
|
{.cast(gcsafe).}:
|
||||||
|
let ctx = newContext(request)
|
||||||
|
let body = parseJson(request.body)
|
||||||
|
if body == nil or "clientFinalMessage" notin body:
|
||||||
|
ctx.json(%*{"error": "Missing clientFinalMessage"}, 400)
|
||||||
|
return
|
||||||
|
let clientFinal = body["clientFinalMessage"].getStr()
|
||||||
|
try:
|
||||||
|
let (ok, serverFinal) = server.authManager.finishScram(clientFinal)
|
||||||
|
if ok:
|
||||||
|
ctx.json(%*{
|
||||||
|
"serverFinalMessage": serverFinal,
|
||||||
|
"status": "authenticated"
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
ctx.json(%*{"error": serverFinal}, 401)
|
||||||
|
except CatchableError as e:
|
||||||
|
ctx.json(%*{"error": e.msg}, 401)
|
||||||
|
|
||||||
proc openApiHandler(): RequestHandler =
|
proc openApiHandler(): RequestHandler =
|
||||||
return proc(request: Request) {.gcsafe.} =
|
return proc(request: Request) {.gcsafe.} =
|
||||||
let ctx = newContext(request)
|
let ctx = newContext(request)
|
||||||
@@ -482,6 +526,8 @@ proc run*(server: HttpServer, port: int = 9470) =
|
|||||||
router.get("/health", healthHandler())
|
router.get("/health", healthHandler())
|
||||||
router.get("/metrics", server.metricsHandler())
|
router.get("/metrics", server.metricsHandler())
|
||||||
router.post("/auth", server.authHandler())
|
router.post("/auth", server.authHandler())
|
||||||
|
router.post("/auth/scram/start", server.scramStartHandler())
|
||||||
|
router.post("/auth/scram/finish", server.scramFinishHandler())
|
||||||
router.get("/tables", server.tablesHandler())
|
router.get("/tables", server.tablesHandler())
|
||||||
router.get("/api", openApiHandler())
|
router.get("/api", openApiHandler())
|
||||||
|
|
||||||
|
|||||||
+106
-12
@@ -1,9 +1,10 @@
|
|||||||
## Authentication — JWT-based auth with SCRAM-SHA-256
|
## Authentication — JWT-based auth with real SCRAM-SHA-256
|
||||||
import std/strutils
|
import std/strutils
|
||||||
import std/base64
|
import std/base64
|
||||||
import std/tables
|
import std/tables
|
||||||
import std/times
|
import std/times
|
||||||
import checksums/sha2
|
import checksums/sha2
|
||||||
|
import scram
|
||||||
|
|
||||||
type
|
type
|
||||||
AuthMethod* = enum
|
AuthMethod* = enum
|
||||||
@@ -38,10 +39,22 @@ type
|
|||||||
AuthManager* = ref object
|
AuthManager* = ref object
|
||||||
secretKey*: string
|
secretKey*: string
|
||||||
tokens*: seq[string]
|
tokens*: seq[string]
|
||||||
users*: Table[string, string] # username -> password hash
|
users*: Table[string, string] # username -> password hash (legacy)
|
||||||
|
scramUsers*: Table[string, ScramCredential]
|
||||||
|
scramSessions*: Table[string, ScramServerState]
|
||||||
|
|
||||||
proc newAuthManager*(secretKey: string = ""): AuthManager =
|
proc newAuthManager*(secretKey: string = ""): AuthManager =
|
||||||
AuthManager(secretKey: secretKey, tokens: @[], users: initTable[string, string]())
|
AuthManager(
|
||||||
|
secretKey: secretKey,
|
||||||
|
tokens: @[],
|
||||||
|
users: initTable[string, string](),
|
||||||
|
scramUsers: initTable[string, ScramCredential](),
|
||||||
|
scramSessions: initTable[string, ScramServerState](),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Base64 URL-safe helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
proc base64UrlEncode(data: string): string =
|
proc base64UrlEncode(data: string): string =
|
||||||
result = encode(data)
|
result = encode(data)
|
||||||
@@ -53,6 +66,10 @@ proc base64UrlDecode(data: string): string =
|
|||||||
s &= "="
|
s &= "="
|
||||||
return decode(s)
|
return decode(s)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Legacy HMAC-SHA-256 (for JWT signing)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
proc hmacSha256(key, message: string): string =
|
proc hmacSha256(key, message: string): string =
|
||||||
var k = key
|
var k = key
|
||||||
if k.len > 64:
|
if k.len > 64:
|
||||||
@@ -81,6 +98,18 @@ proc hmacSha256(key, message: string): string =
|
|||||||
|
|
||||||
return $outerHash
|
return $outerHash
|
||||||
|
|
||||||
|
proc constantTimeCompare(a, b: string): bool =
|
||||||
|
if a.len != b.len:
|
||||||
|
return false
|
||||||
|
var diff = 0
|
||||||
|
for i in 0..<a.len:
|
||||||
|
diff = diff or (ord(a[i]) xor ord(b[i]))
|
||||||
|
return diff == 0
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# JWT token helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
proc createToken*(am: AuthManager, claims: JWTClaims): string =
|
proc createToken*(am: AuthManager, claims: JWTClaims): string =
|
||||||
let header = base64UrlEncode("{\"alg\":\"HS256\",\"typ\":\"JWT\"}")
|
let header = base64UrlEncode("{\"alg\":\"HS256\",\"typ\":\"JWT\"}")
|
||||||
var payloadJson = "{\"sub\":\"" & claims.sub & "\",\"role\":\"" & claims.role &
|
var payloadJson = "{\"sub\":\"" & claims.sub & "\",\"role\":\"" & claims.role &
|
||||||
@@ -98,14 +127,6 @@ proc createToken*(am: AuthManager, claims: JWTClaims): string =
|
|||||||
am.tokens.add(data & "." & base64UrlEncode(signature))
|
am.tokens.add(data & "." & base64UrlEncode(signature))
|
||||||
return am.tokens[^1]
|
return am.tokens[^1]
|
||||||
|
|
||||||
proc constantTimeCompare(a, b: string): bool =
|
|
||||||
if a.len != b.len:
|
|
||||||
return false
|
|
||||||
var result = 0
|
|
||||||
for i in 0..<a.len:
|
|
||||||
result = result or (ord(a[i]) xor ord(b[i]))
|
|
||||||
return result == 0
|
|
||||||
|
|
||||||
proc verifyToken*(am: AuthManager, token: string): (bool, JWTClaims) =
|
proc verifyToken*(am: AuthManager, token: string): (bool, JWTClaims) =
|
||||||
let parts = token.split(".")
|
let parts = token.split(".")
|
||||||
if parts.len != 3:
|
if parts.len != 3:
|
||||||
@@ -167,6 +188,73 @@ proc verifyToken*(am: AuthManager, token: string): (bool, JWTClaims) =
|
|||||||
return (false, JWTClaims())
|
return (false, JWTClaims())
|
||||||
return (true, claims)
|
return (true, claims)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SCRAM-SHA-256 challenge-response
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc registerScramUser*(am: AuthManager, username, password: string,
|
||||||
|
iterationCount: int = DefaultIterationCount) =
|
||||||
|
## Register a user with real SCRAM-SHA-256 credentials.
|
||||||
|
let cred = createScramCredential(password, iterationCount = iterationCount)
|
||||||
|
am.scramUsers[username] = cred
|
||||||
|
|
||||||
|
proc startScram*(am: AuthManager, clientFirstMessage: string): string =
|
||||||
|
## Start SCRAM authentication. Returns server-first-message.
|
||||||
|
let (_, username, clientNonce) = parseClientFirst(clientFirstMessage)
|
||||||
|
if username notin am.scramUsers:
|
||||||
|
raise newException(ValueError, "Unknown user: " & username)
|
||||||
|
|
||||||
|
let cred = am.scramUsers[username]
|
||||||
|
let serverNonce = generateNonce()
|
||||||
|
let combinedNonce = clientNonce & serverNonce
|
||||||
|
|
||||||
|
let serverFirst = buildServerFirst(combinedNonce, cred.salt, cred.iterationCount)
|
||||||
|
|
||||||
|
# Compute authMessage for later verification
|
||||||
|
let clientFirstMessageBare = "n=" & username & ",r=" & clientNonce
|
||||||
|
let authMessage = clientFirstMessageBare & "," & serverFirst
|
||||||
|
|
||||||
|
var state = ScramServerState(
|
||||||
|
username: username,
|
||||||
|
clientFirstMessageBare: clientFirstMessageBare,
|
||||||
|
serverFirstMessage: serverFirst,
|
||||||
|
authMessage: authMessage,
|
||||||
|
clientNonce: clientNonce,
|
||||||
|
serverNonce: serverNonce,
|
||||||
|
salt: cred.salt,
|
||||||
|
iterationCount: cred.iterationCount,
|
||||||
|
storedKey: cred.storedKey,
|
||||||
|
serverKey: cred.serverKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store session keyed by combined nonce
|
||||||
|
am.scramSessions[combinedNonce] = state
|
||||||
|
return serverFirst
|
||||||
|
|
||||||
|
proc finishScram*(am: AuthManager, clientFinalMessage: string): (bool, string) =
|
||||||
|
## Finish SCRAM authentication. Returns (success, server-final-message).
|
||||||
|
let (cbind, nonce, clientProof) = parseClientFinal(clientFinalMessage)
|
||||||
|
|
||||||
|
if nonce notin am.scramSessions:
|
||||||
|
return (false, "e=invalid-nonce")
|
||||||
|
|
||||||
|
var state = am.scramSessions[nonce]
|
||||||
|
am.scramSessions.del(nonce)
|
||||||
|
|
||||||
|
# Update authMessage with client-final-message-without-proof
|
||||||
|
let clientFinalWithoutProof = "c=" & cbind & ",r=" & nonce
|
||||||
|
state.authMessage = state.authMessage & "," & clientFinalWithoutProof
|
||||||
|
|
||||||
|
if not verifyClientProof(state, clientProof):
|
||||||
|
return (false, "e=invalid-proof")
|
||||||
|
|
||||||
|
let serverSignature = computeServerSignature(state)
|
||||||
|
return (true, buildServerFinal(serverSignature))
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Credential validation dispatcher
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
proc validateCredentials*(am: AuthManager, creds: AuthCredentials): AuthResult =
|
proc validateCredentials*(am: AuthManager, creds: AuthCredentials): AuthResult =
|
||||||
case creds.authMethod
|
case creds.authMethod
|
||||||
of amNone:
|
of amNone:
|
||||||
@@ -180,15 +268,20 @@ proc validateCredentials*(am: AuthManager, creds: AuthCredentials): AuthResult =
|
|||||||
role: claims.role, database: claims.database)
|
role: claims.role, database: claims.database)
|
||||||
return AuthResult(authenticated: false, error: "Invalid token")
|
return AuthResult(authenticated: false, error: "Invalid token")
|
||||||
of amSCRAMSHA256:
|
of amSCRAMSHA256:
|
||||||
|
## Legacy fallback: simple hash comparison for backward compatibility.
|
||||||
|
## Real SCRAM should use startScram() / finishScram().
|
||||||
if creds.username in am.users:
|
if creds.username in am.users:
|
||||||
let stored = am.users[creds.username]
|
let stored = am.users[creds.username]
|
||||||
# SCRAM-SHA-256: client sends SHA-256(password) as payload
|
|
||||||
let clientHash = if creds.payload.len > 0: creds.payload else: hmacSha256(am.secretKey, "")
|
let clientHash = if creds.payload.len > 0: creds.payload else: hmacSha256(am.secretKey, "")
|
||||||
if stored == clientHash or stored == hmacSha256(am.secretKey, creds.payload):
|
if stored == clientHash or stored == hmacSha256(am.secretKey, creds.payload):
|
||||||
return AuthResult(authenticated: true, username: creds.username,
|
return AuthResult(authenticated: true, username: creds.username,
|
||||||
role: "user", database: "default")
|
role: "user", database: "default")
|
||||||
return AuthResult(authenticated: false, error: "Invalid SCRAM credentials")
|
return AuthResult(authenticated: false, error: "Invalid SCRAM credentials")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Token / user management
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
proc addToken*(am: var AuthManager, token: string) =
|
proc addToken*(am: var AuthManager, token: string) =
|
||||||
am.tokens.add(token)
|
am.tokens.add(token)
|
||||||
|
|
||||||
@@ -200,4 +293,5 @@ proc revokeToken*(am: var AuthManager, token: string) =
|
|||||||
proc isAuthenticated*(r: AuthResult): bool = r.authenticated
|
proc isAuthenticated*(r: AuthResult): bool = r.authenticated
|
||||||
|
|
||||||
proc addScramUser*(am: var AuthManager, username, passwordHash: string) =
|
proc addScramUser*(am: var AuthManager, username, passwordHash: string) =
|
||||||
|
## Legacy helper: stores raw hash. Use registerScramUser() for real SCRAM.
|
||||||
am.users[username] = passwordHash
|
am.users[username] = passwordHash
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,251 @@
|
|||||||
|
## SCRAM-SHA-256 implementation per RFC 7677
|
||||||
|
## Provides: PBKDF2, HMAC-SHA-256, nonce generation, SCRAM message parsing
|
||||||
|
|
||||||
|
import std/strutils
|
||||||
|
import std/base64
|
||||||
|
import std/endians
|
||||||
|
import checksums/sha2
|
||||||
|
|
||||||
|
const
|
||||||
|
DefaultIterationCount* = 4096
|
||||||
|
NonceBytes = 24
|
||||||
|
|
||||||
|
type
|
||||||
|
ScramCredential* = object
|
||||||
|
salt*: string
|
||||||
|
iterationCount*: int
|
||||||
|
storedKey*: array[32, byte]
|
||||||
|
serverKey*: array[32, byte]
|
||||||
|
|
||||||
|
ScramServerState* = object
|
||||||
|
username*: string
|
||||||
|
clientFirstMessageBare*: string
|
||||||
|
serverFirstMessage*: string
|
||||||
|
authMessage*: string
|
||||||
|
clientNonce*: string
|
||||||
|
serverNonce*: string
|
||||||
|
salt*: string
|
||||||
|
iterationCount*: int
|
||||||
|
storedKey*: array[32, byte]
|
||||||
|
serverKey*: array[32, byte]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cryptographic primitives
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc hmacSha256*(key, message: string): array[32, byte] =
|
||||||
|
## HMAC-SHA-256 returning raw 32-byte digest.
|
||||||
|
var k = key
|
||||||
|
if k.len > 64:
|
||||||
|
var ctx = initSha_256()
|
||||||
|
ctx.update(k.toOpenArray(0, k.len-1))
|
||||||
|
let hash = ctx.digest()
|
||||||
|
k = $hash
|
||||||
|
while k.len < 64:
|
||||||
|
k &= "\x00"
|
||||||
|
|
||||||
|
var ipad = newString(64)
|
||||||
|
var opad = newString(64)
|
||||||
|
for i in 0..<64:
|
||||||
|
ipad[i] = chr(ord(k[i]) xor 0x36)
|
||||||
|
opad[i] = chr(ord(k[i]) xor 0x5c)
|
||||||
|
|
||||||
|
var innerCtx = initSha_256()
|
||||||
|
innerCtx.update(ipad.toOpenArray(0, ipad.len-1))
|
||||||
|
innerCtx.update(message.toOpenArray(0, message.len-1))
|
||||||
|
let innerHash = innerCtx.digest()
|
||||||
|
|
||||||
|
var outerCtx = initSha_256()
|
||||||
|
outerCtx.update(opad.toOpenArray(0, opad.len-1))
|
||||||
|
outerCtx.update(innerHash.toOpenArray(0, innerHash.len-1))
|
||||||
|
return cast[array[32, byte]](outerCtx.digest())
|
||||||
|
|
||||||
|
proc hmacSha256*(key: openArray[byte], message: string): array[32, byte] =
|
||||||
|
let keyStr = newString(key.len)
|
||||||
|
if key.len > 0:
|
||||||
|
copyMem(addr keyStr[0], unsafeAddr key[0], key.len)
|
||||||
|
return hmacSha256(keyStr, message)
|
||||||
|
|
||||||
|
proc hmacSha256*(key, message: openArray[byte]): array[32, byte] =
|
||||||
|
let keyStr = newString(key.len)
|
||||||
|
if key.len > 0:
|
||||||
|
copyMem(addr keyStr[0], unsafeAddr key[0], key.len)
|
||||||
|
let msgStr = newString(message.len)
|
||||||
|
if message.len > 0:
|
||||||
|
copyMem(addr msgStr[0], unsafeAddr message[0], message.len)
|
||||||
|
return hmacSha256(keyStr, msgStr)
|
||||||
|
|
||||||
|
proc hmacSha256*(key: string, message: openArray[byte]): array[32, byte] =
|
||||||
|
let msgStr = newString(message.len)
|
||||||
|
if message.len > 0:
|
||||||
|
copyMem(addr msgStr[0], unsafeAddr message[0], message.len)
|
||||||
|
return hmacSha256(key, msgStr)
|
||||||
|
|
||||||
|
proc sha256*(data: string): array[32, byte] =
|
||||||
|
var ctx = initSha_256()
|
||||||
|
ctx.update(data.toOpenArray(0, data.len-1))
|
||||||
|
return cast[array[32, byte]](ctx.digest())
|
||||||
|
|
||||||
|
proc sha256*(data: openArray[byte]): array[32, byte] =
|
||||||
|
var s = newString(data.len)
|
||||||
|
if data.len > 0:
|
||||||
|
copyMem(addr s[0], unsafeAddr data[0], data.len)
|
||||||
|
return sha256(s)
|
||||||
|
|
||||||
|
proc xorBytes*(a, b: openArray[byte]): seq[byte] =
|
||||||
|
result = newSeq[byte](a.len)
|
||||||
|
for i in 0..<a.len:
|
||||||
|
result[i] = a[i] xor b[i]
|
||||||
|
|
||||||
|
proc pbkdf2HmacSha256*(password, salt: string, iterations: int): array[32, byte] =
|
||||||
|
## PBKDF2-HMAC-SHA-256 with 32-byte output length.
|
||||||
|
var u: array[32, byte]
|
||||||
|
var t: array[32, byte]
|
||||||
|
|
||||||
|
# U_1 = HMAC(password, salt || BE32(1))
|
||||||
|
var msg = salt
|
||||||
|
var counterVal = 1'u32
|
||||||
|
var counter: array[4, byte]
|
||||||
|
bigEndian32(addr counter, unsafeAddr counterVal)
|
||||||
|
var counterStr = newString(4)
|
||||||
|
copyMem(addr counterStr[0], addr counter[0], 4)
|
||||||
|
msg.add(counterStr)
|
||||||
|
u = hmacSha256(password, msg)
|
||||||
|
|
||||||
|
for i in 0..<32:
|
||||||
|
t[i] = u[i]
|
||||||
|
|
||||||
|
for i in 2..iterations:
|
||||||
|
u = hmacSha256(password, u)
|
||||||
|
for j in 0..<32:
|
||||||
|
t[j] = t[j] xor u[j]
|
||||||
|
|
||||||
|
return t
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Random & encoding helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc generateNonce*(): string =
|
||||||
|
## Generate a cryptographically secure random nonce (base64-encoded).
|
||||||
|
when defined(linux) or defined(macosx) or defined(bsd):
|
||||||
|
let f = open("/dev/urandom")
|
||||||
|
defer: f.close()
|
||||||
|
var bytes = newString(NonceBytes)
|
||||||
|
let readLen = f.readBuffer(addr bytes[0], NonceBytes)
|
||||||
|
if readLen < NonceBytes:
|
||||||
|
raise newException(IOError, "Failed to read enough random bytes")
|
||||||
|
result = encode(bytes)
|
||||||
|
# Strip padding for GS2 / SCRAM compatibility
|
||||||
|
while result.endsWith("="):
|
||||||
|
result.setLen(result.len - 1)
|
||||||
|
result = result.replace("+", "-").replace("/", "_")
|
||||||
|
else:
|
||||||
|
# Fallback — NOT cryptographically secure, should not be used in production
|
||||||
|
raise newException(IOError, "Secure random not available on this platform")
|
||||||
|
|
||||||
|
proc generateSalt*(): string =
|
||||||
|
## Generate a random salt (raw bytes).
|
||||||
|
when defined(linux) or defined(macosx) or defined(bsd):
|
||||||
|
let f = open("/dev/urandom")
|
||||||
|
defer: f.close()
|
||||||
|
result = newString(NonceBytes)
|
||||||
|
let readLen = f.readBuffer(addr result[0], NonceBytes)
|
||||||
|
if readLen < NonceBytes:
|
||||||
|
raise newException(IOError, "Failed to read enough random bytes")
|
||||||
|
else:
|
||||||
|
raise newException(IOError, "Secure random not available on this platform")
|
||||||
|
|
||||||
|
proc toHex*(data: openArray[byte]): string =
|
||||||
|
const hexChars = "0123456789abcdef"
|
||||||
|
result = newString(data.len * 2)
|
||||||
|
for i, b in data:
|
||||||
|
result[i * 2] = hexChars[int(b shr 4)]
|
||||||
|
result[i * 2 + 1] = hexChars[int(b and 0x0f)]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SCRAM credential generation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc createScramCredential*(password: string, salt: string = "",
|
||||||
|
iterationCount: int = DefaultIterationCount): ScramCredential =
|
||||||
|
let actualSalt = if salt.len > 0: salt else: generateSalt()
|
||||||
|
let saltedPassword = pbkdf2HmacSha256(password, actualSalt, iterationCount)
|
||||||
|
let clientKey = hmacSha256(saltedPassword, "Client Key")
|
||||||
|
let storedKey = sha256(clientKey)
|
||||||
|
let serverKey = hmacSha256(saltedPassword, "Server Key")
|
||||||
|
ScramCredential(
|
||||||
|
salt: actualSalt,
|
||||||
|
iterationCount: iterationCount,
|
||||||
|
storedKey: storedKey,
|
||||||
|
serverKey: serverKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SCRAM message parsing / building
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc parseClientFirst*(msg: string): (string, string, string) =
|
||||||
|
## Parse client-first-message: gs2-header,username,nonce
|
||||||
|
## Returns: (gs2_header, username, nonce)
|
||||||
|
var parts = msg.split(",")
|
||||||
|
if parts.len < 3:
|
||||||
|
raise newException(ValueError, "Invalid client-first-message")
|
||||||
|
var gs2 = parts[0]
|
||||||
|
var username = ""
|
||||||
|
var nonce = ""
|
||||||
|
for i in 1..<parts.len:
|
||||||
|
let p = parts[i]
|
||||||
|
if p.startsWith("n="):
|
||||||
|
username = p[2..^1]
|
||||||
|
elif p.startsWith("r="):
|
||||||
|
nonce = p[2..^1]
|
||||||
|
if username.len == 0 or nonce.len == 0:
|
||||||
|
raise newException(ValueError, "Missing username or nonce in client-first-message")
|
||||||
|
return (gs2, username, nonce)
|
||||||
|
|
||||||
|
proc parseClientFinal*(msg: string): (string, string, seq[byte]) =
|
||||||
|
## Parse client-final-message: channel-binding,nonce,proof
|
||||||
|
## Returns: (channel_binding, nonce, proof_bytes)
|
||||||
|
var cbind = ""
|
||||||
|
var nonce = ""
|
||||||
|
var proofHex = ""
|
||||||
|
for p in msg.split(","):
|
||||||
|
if p.startsWith("c="):
|
||||||
|
cbind = p[2..^1]
|
||||||
|
elif p.startsWith("r="):
|
||||||
|
nonce = p[2..^1]
|
||||||
|
elif p.startsWith("p="):
|
||||||
|
proofHex = p[2..^1]
|
||||||
|
if nonce.len == 0 or proofHex.len == 0:
|
||||||
|
raise newException(ValueError, "Missing nonce or proof in client-final-message")
|
||||||
|
# proof is base64-encoded
|
||||||
|
var proof = decode(proofHex)
|
||||||
|
var proofBytes = newSeq[byte](proof.len)
|
||||||
|
if proof.len > 0:
|
||||||
|
copyMem(addr proofBytes[0], addr proof[0], proof.len)
|
||||||
|
return (cbind, nonce, proofBytes)
|
||||||
|
|
||||||
|
proc buildServerFirst*(nonce, salt: string, iterationCount: int): string =
|
||||||
|
"r=" & nonce & ",s=" & encode(salt) & ",i=" & $iterationCount
|
||||||
|
|
||||||
|
proc buildServerFinal*(serverSignature: openArray[byte]): string =
|
||||||
|
var sigStr = newString(serverSignature.len)
|
||||||
|
if serverSignature.len > 0:
|
||||||
|
copyMem(addr sigStr[0], unsafeAddr serverSignature[0], serverSignature.len)
|
||||||
|
"v=" & encode(sigStr)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SCRAM server-side verification
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc verifyClientProof*(state: ScramServerState, clientProof: openArray[byte]): bool =
|
||||||
|
let clientKey = xorBytes(clientProof, @(hmacSha256(state.storedKey, state.authMessage)))
|
||||||
|
let computedStoredKey = sha256(clientKey)
|
||||||
|
for i in 0..<32:
|
||||||
|
if computedStoredKey[i] != state.storedKey[i]:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
proc computeServerSignature*(state: ScramServerState): array[32, byte] =
|
||||||
|
return hmacSha256(state.serverKey, state.authMessage)
|
||||||
@@ -7,6 +7,7 @@ import std/asyncdispatch
|
|||||||
import std/times
|
import std/times
|
||||||
import std/random
|
import std/random
|
||||||
import std/monotimes
|
import std/monotimes
|
||||||
|
import std/base64
|
||||||
|
|
||||||
import barabadb/core/types
|
import barabadb/core/types
|
||||||
import barabadb/core/mvcc
|
import barabadb/core/mvcc
|
||||||
@@ -50,6 +51,7 @@ import barabadb/fts/engine as fts
|
|||||||
import barabadb/protocol/wire
|
import barabadb/protocol/wire
|
||||||
import barabadb/protocol/pool
|
import barabadb/protocol/pool
|
||||||
import barabadb/protocol/auth
|
import barabadb/protocol/auth
|
||||||
|
import barabadb/protocol/scram
|
||||||
import barabadb/protocol/ratelimit
|
import barabadb/protocol/ratelimit
|
||||||
import barabadb/schema/schema as schema
|
import barabadb/schema/schema as schema
|
||||||
|
|
||||||
@@ -707,6 +709,87 @@ suite "Authentication":
|
|||||||
authMethod: amToken, payload: "invalid_token"))
|
authMethod: amToken, payload: "invalid_token"))
|
||||||
check not result.authenticated
|
check not result.authenticated
|
||||||
|
|
||||||
|
test "SCRAM-SHA-256 full handshake":
|
||||||
|
var am = newAuthManager()
|
||||||
|
am.registerScramUser("alice", "wonderland")
|
||||||
|
|
||||||
|
# Step 1: ClientFirstMessage
|
||||||
|
let clientNonce = generateNonce()
|
||||||
|
let clientFirst = "n,,n=alice,r=" & clientNonce
|
||||||
|
|
||||||
|
# Step 2: ServerFirstMessage
|
||||||
|
let serverFirst = am.startScram(clientFirst)
|
||||||
|
check serverFirst.startsWith("r=")
|
||||||
|
check serverFirst.contains("s=")
|
||||||
|
check serverFirst.contains("i=4096")
|
||||||
|
|
||||||
|
# Parse serverFirst
|
||||||
|
var combinedNonce = ""
|
||||||
|
var saltB64 = ""
|
||||||
|
var iterCount = 0
|
||||||
|
for part in serverFirst.split(","):
|
||||||
|
if part.startsWith("r="): combinedNonce = part[2..^1]
|
||||||
|
elif part.startsWith("s="): saltB64 = part[2..^1]
|
||||||
|
elif part.startsWith("i="): iterCount = parseInt(part[2..^1])
|
||||||
|
check combinedNonce.len > 0
|
||||||
|
check saltB64.len > 0
|
||||||
|
check iterCount == 4096
|
||||||
|
|
||||||
|
# Step 3: Compute client proof
|
||||||
|
let salt = decode(saltB64)
|
||||||
|
let saltedPassword = pbkdf2HmacSha256("wonderland", salt, iterCount)
|
||||||
|
let clientKey = hmacSha256(saltedPassword, "Client Key")
|
||||||
|
let storedKey = sha256(clientKey)
|
||||||
|
let serverKey = hmacSha256(saltedPassword, "Server Key")
|
||||||
|
|
||||||
|
let clientFirstBare = "n=alice,r=" & clientNonce
|
||||||
|
let clientFinalWithoutProof = "c=biws,r=" & combinedNonce
|
||||||
|
let authMessage = clientFirstBare & "," & serverFirst & "," & clientFinalWithoutProof
|
||||||
|
|
||||||
|
let clientSignature = hmacSha256(storedKey, authMessage)
|
||||||
|
var clientProof = newSeq[byte](32)
|
||||||
|
for i in 0..<32:
|
||||||
|
clientProof[i] = clientKey[i] xor clientSignature[i]
|
||||||
|
var proofStr = newString(32)
|
||||||
|
copyMem(addr proofStr[0], addr clientProof[0], 32)
|
||||||
|
let proofB64 = encode(proofStr)
|
||||||
|
# Strip padding
|
||||||
|
var proofB64Clean = proofB64
|
||||||
|
while proofB64Clean.endsWith("="):
|
||||||
|
proofB64Clean.setLen(proofB64Clean.len - 1)
|
||||||
|
proofB64Clean = proofB64Clean.replace("+", "-").replace("/", "_")
|
||||||
|
|
||||||
|
let clientFinal = "c=biws,r=" & combinedNonce & ",p=" & proofB64Clean
|
||||||
|
|
||||||
|
# Step 4: Server verifies
|
||||||
|
let (ok, serverFinal) = am.finishScram(clientFinal)
|
||||||
|
check ok
|
||||||
|
check serverFinal.startsWith("v=")
|
||||||
|
|
||||||
|
# Verify server signature ourselves
|
||||||
|
let expectedServerSig = hmacSha256(serverKey, authMessage)
|
||||||
|
var expectedStr = newString(32)
|
||||||
|
copyMem(addr expectedStr[0], unsafeAddr expectedServerSig[0], 32)
|
||||||
|
let expectedB64 = encode(expectedStr)
|
||||||
|
check serverFinal == "v=" & expectedB64
|
||||||
|
|
||||||
|
test "SCRAM-SHA-256 invalid proof rejected":
|
||||||
|
var am = newAuthManager()
|
||||||
|
am.registerScramUser("bob", "builder")
|
||||||
|
|
||||||
|
let clientNonce = generateNonce()
|
||||||
|
let clientFirst = "n,,n=bob,r=" & clientNonce
|
||||||
|
let serverFirst = am.startScram(clientFirst)
|
||||||
|
|
||||||
|
var combinedNonce = ""
|
||||||
|
for part in serverFirst.split(","):
|
||||||
|
if part.startsWith("r="): combinedNonce = part[2..^1]
|
||||||
|
|
||||||
|
let clientFinal = "c=biws,r=" & combinedNonce & ",p=invalidproof"
|
||||||
|
let (ok, serverFinal) = am.finishScram(clientFinal)
|
||||||
|
check not ok
|
||||||
|
check serverFinal == "e=invalid-proof"
|
||||||
|
|
||||||
suite "Vector Quantization":
|
suite "Vector Quantization":
|
||||||
test "Scalar quantization 8-bit":
|
test "Scalar quantization 8-bit":
|
||||||
var sq = newScalarQuantizer(4, bits = 8)
|
var sq = newScalarQuantizer(4, bits = 8)
|
||||||
|
|||||||
Reference in New Issue
Block a user