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:
2026-05-13 09:50:07 +03:00
parent 422df08ab9
commit ac55237b72
7 changed files with 488 additions and 14 deletions
+83
View File
@@ -7,6 +7,7 @@ import std/asyncdispatch
import std/times
import std/random
import std/monotimes
import std/base64
import barabadb/core/types
import barabadb/core/mvcc
@@ -50,6 +51,7 @@ import barabadb/fts/engine as fts
import barabadb/protocol/wire
import barabadb/protocol/pool
import barabadb/protocol/auth
import barabadb/protocol/scram
import barabadb/protocol/ratelimit
import barabadb/schema/schema as schema
@@ -707,6 +709,87 @@ suite "Authentication":
authMethod: amToken, payload: "invalid_token"))
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":
test "Scalar quantization 8-bit":
var sq = newScalarQuantizer(4, bits = 8)