From ac55237b72896437902f6aac2760c261b967fc5b Mon Sep 17 00:00:00 2001 From: dimgigov Date: Wed, 13 May 2026 09:50:07 +0300 Subject: [PATCH] feat: real SCRAM-SHA-256 authentication (Option C) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- BUG_AUDIT.md | 2 +- PLAN.md | 2 +- src/barabadb/core/httpserver.nim | 46 ++++++ src/barabadb/protocol/auth.nim | 118 +++++++++++++-- src/barabadb/protocol/scram | Bin 0 -> 94104 bytes src/barabadb/protocol/scram.nim | 251 +++++++++++++++++++++++++++++++ tests/test_all.nim | 83 ++++++++++ 7 files changed, 488 insertions(+), 14 deletions(-) create mode 100755 src/barabadb/protocol/scram create mode 100644 src/barabadb/protocol/scram.nim diff --git a/BUG_AUDIT.md b/BUG_AUDIT.md index 9b1285e..c72d4c5 100644 --- a/BUG_AUDIT.md +++ b/BUG_AUDIT.md @@ -107,7 +107,7 @@ 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` -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` спековете 5. ~~**`crossmodal.tla`**~~ ✅ — cross-modal consistency между document/vector/graph/FTS 6. **Replication data transfer** — `writeLsn` да изпраща данни към replicas diff --git a/PLAN.md b/PLAN.md index 2b69a56..0ebdea6 100644 --- a/PLAN.md +++ b/PLAN.md @@ -104,7 +104,7 @@ - Моделира sync между document/vector/graph/FTS индекси - Резултат: 10-ти TLA+ спек, пълно покритие на core модулите -### Опция C: Auth hardening + SCRAM (3-4 часа) +### Опция C: Auth hardening + SCRAM ✅ - Истински SCRAM-SHA-256 със salt, iteration count, challenge-response - Резултат: production-grade auth diff --git a/src/barabadb/core/httpserver.nim b/src/barabadb/core/httpserver.nim index fcba950..f6e8711 100644 --- a/src/barabadb/core/httpserver.nim +++ b/src/barabadb/core/httpserver.nim @@ -18,6 +18,7 @@ import ../core/mvcc import ../protocol/wire import ../core/websocket import jwt as jwtlib +import ../protocol/auth type HttpServer* = ref object @@ -27,6 +28,7 @@ type ctx: ExecutionContext metrics*: Metrics secretKey*: string + authManager*: AuthManager ws*: WsServer Metrics* = ref object @@ -46,8 +48,10 @@ proc newHttpServer*(config: BaraConfig): HttpServer = ctx.onChange = proc(ev: ChangeEvent) = let msg = $ev.kind & " " & ev.table asyncCheck ws.broadcastToTable(ev.table, msg) + let am = newAuthManager(secret) HttpServer(config: config, running: false, db: db, ctx: ctx, secretKey: secret, + authManager: am, metrics: Metrics(), ws: ws) # ---------------------------------------------------------------------- @@ -211,6 +215,46 @@ proc authHandler(server: HttpServer): RequestHandler = "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 = return proc(request: Request) {.gcsafe.} = let ctx = newContext(request) @@ -482,6 +526,8 @@ proc run*(server: HttpServer, port: int = 9470) = router.get("/health", healthHandler()) router.get("/metrics", server.metricsHandler()) 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("/api", openApiHandler()) diff --git a/src/barabadb/protocol/auth.nim b/src/barabadb/protocol/auth.nim index ad8c20c..b73f545 100644 --- a/src/barabadb/protocol/auth.nim +++ b/src/barabadb/protocol/auth.nim @@ -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/base64 import std/tables import std/times import checksums/sha2 +import scram type AuthMethod* = enum @@ -38,10 +39,22 @@ type AuthManager* = ref object secretKey*: 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 = - 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 = result = encode(data) @@ -53,6 +66,10 @@ proc base64UrlDecode(data: string): string = s &= "=" return decode(s) +# --------------------------------------------------------------------------- +# Legacy HMAC-SHA-256 (for JWT signing) +# --------------------------------------------------------------------------- + proc hmacSha256(key, message: string): string = var k = key if k.len > 64: @@ -81,6 +98,18 @@ proc hmacSha256(key, message: string): string = return $outerHash +proc constantTimeCompare(a, b: string): bool = + if a.len != b.len: + return false + var diff = 0 + for i in 0.. 0: creds.payload else: hmacSha256(am.secretKey, "") if stored == clientHash or stored == hmacSha256(am.secretKey, creds.payload): return AuthResult(authenticated: true, username: creds.username, role: "user", database: "default") return AuthResult(authenticated: false, error: "Invalid SCRAM credentials") +# --------------------------------------------------------------------------- +# Token / user management +# --------------------------------------------------------------------------- + proc addToken*(am: var AuthManager, token: string) = am.tokens.add(token) @@ -200,4 +293,5 @@ proc revokeToken*(am: var AuthManager, token: string) = proc isAuthenticated*(r: AuthResult): bool = r.authenticated proc addScramUser*(am: var AuthManager, username, passwordHash: string) = + ## Legacy helper: stores raw hash. Use registerScramUser() for real SCRAM. am.users[username] = passwordHash diff --git a/src/barabadb/protocol/scram b/src/barabadb/protocol/scram new file mode 100755 index 0000000000000000000000000000000000000000..cec9518155c449ab7d77a03c4aec4630836c99d6 GIT binary patch literal 94104 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 V9?bsS#Y&T}k!y9CUh4rj{|$^#R)+up literal 0 HcmV?d00001 diff --git a/src/barabadb/protocol/scram.nim b/src/barabadb/protocol/scram.nim new file mode 100644 index 0000000..d69c7e6 --- /dev/null +++ b/src/barabadb/protocol/scram.nim @@ -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.. 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.. 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) diff --git a/tests/test_all.nim b/tests/test_all.nim index d1aef2f..2942370 100644 --- a/tests/test_all.nim +++ b/tests/test_all.nim @@ -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)