Files
Baradb/src/barabadb/protocol/auth.nim
T
dimgigov 967c0855a5 Bug fixes: composite PK, nl_to_sql sandbox, FK check, SQL injection, storage correctness
Critical fixes:
- Composite PK: execInsert + validateConstraints use all PK columns
- nl_to_sql: non-SELECT SQL no longer executed directly during validation
- FK check: removed O(N) scanMemTable fallback, uses db.get() with SSTables
- exprToSql: nkIdent wrapped in quotes to prevent SQL injection
- restoreSchema: try/except around tokenize/parse for crash resilience
- recovery.nim: lastTxnId tracking + putUnsafe/deleteUnsafe without WAL
- SCRAM: verifyClientProof length check + DefaultIterationCount restored

Storage fixes:
- lsm.nim: SSTable sort order fixed (ascending), close() flushes all memtables
- compaction.nim: tombstones preserved during compaction
- wal.nim: header written for empty existing files, readEntries checks magic
- btree.nim: B+ tree leaf split keeps boundary key
- bloom.nim: deserialize raises on short data
- mmap.nim: bounds checks for adviseWillNeed/DontNeed + posix.close()

Protocol fixes:
- zerocopy.nim: readString bounds check
- wire.nim: deserializeValue 32-bit underflow check
- auth.nim: JWT JSON escaping for claims
- server.nim: readUint32BE bounds check + specific exception handling
- raft.nim: readData checks + specific exception handling

Tests:
- Added Composite Primary Key test suite (4 tests)

Build: 0 warnings, 0 errors
2026-05-18 11:33:11 +03:00

301 lines
10 KiB
Nim

## 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
amNone
amSCRAMSHA256
amJWT
amToken
AuthCredentials* = object
authMethod*: AuthMethod
username*: string
payload*: string
JWTClaims* = object
sub*: string
iss*: string
aud*: string
exp*: int64
iat*: int64
nbf*: int64
jti*: string
role*: string
database*: string
AuthResult* = object
authenticated*: bool
username*: string
role*: string
database*: string
error*: string
AuthManager* = ref object
secretKey*: string
tokens*: seq[string]
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](),
scramUsers: initTable[string, ScramCredential](),
scramSessions: initTable[string, ScramServerState](),
)
# ---------------------------------------------------------------------------
# Base64 URL-safe helpers
# ---------------------------------------------------------------------------
proc base64UrlEncode(data: string): string =
result = encode(data)
result = result.replace("+", "-").replace("/", "_").replace("=", "")
proc base64UrlDecode(data: string): string =
var s = data.replace("-", "+").replace("_", "/")
while s.len mod 4 != 0:
s &= "="
return decode(s)
# ---------------------------------------------------------------------------
# Legacy HMAC-SHA-256 (for JWT signing)
# ---------------------------------------------------------------------------
proc hmacSha256(key, message: string): string =
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))
let outerHash = outerCtx.digest()
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 jsonEscape(s: string): string =
result = s.replace("\\", "\\\\").replace("\"", "\\\"")
proc createToken*(am: AuthManager, claims: JWTClaims): string =
let header = base64UrlEncode("{\"alg\":\"HS256\",\"typ\":\"JWT\"}")
var payloadJson = "{\"sub\":\"" & jsonEscape(claims.sub) & "\",\"role\":\"" & jsonEscape(claims.role) &
"\",\"database\":\"" & jsonEscape(claims.database) & "\""
if claims.exp > 0:
payloadJson &= ",\"exp\":" & $claims.exp
if claims.iat > 0:
payloadJson &= ",\"iat\":" & $claims.iat
if claims.nbf > 0:
payloadJson &= ",\"nbf\":" & $claims.nbf
payloadJson &= "}"
let payload = base64UrlEncode(payloadJson)
let data = header & "." & payload
let signature = hmacSha256(am.secretKey, data)
am.tokens.add(data & "." & base64UrlEncode(signature))
return am.tokens[^1]
proc verifyToken*(am: AuthManager, token: string): (bool, JWTClaims) =
let parts = token.split(".")
if parts.len != 3:
return (false, JWTClaims())
let data = parts[0] & "." & parts[1]
let sig = hmacSha256(am.secretKey, data)
if not constantTimeCompare(base64UrlEncode(sig), parts[2]):
return (false, JWTClaims())
# Parse payload
let payload = base64UrlDecode(parts[1])
var claims = JWTClaims()
# Simple JSON parse: {"key":"val","key2":"val2"}
var i = 1 # skip {
while i < payload.len:
if payload[i] == '}':
break
if payload[i] == '"':
var key = ""
inc i
while i < payload.len and payload[i] != '"':
key &= payload[i]
inc i
inc i # skip closing quote
inc i # skip :
var val = ""
if i < payload.len and payload[i] == '"':
inc i
while i < payload.len and payload[i] != '"':
val &= payload[i]
inc i
inc i
elif i < payload.len and payload[i] in {'0'..'9', '-'}:
while i < payload.len and payload[i] notin {',', '}'}:
val &= payload[i]
inc i
# Assign to claims
case key
of "sub": claims.sub = val
of "role": claims.role = val
of "database": claims.database = val
of "iss": claims.iss = val
of "aud": claims.aud = val
of "exp": claims.exp = parseInt(val)
of "iat": claims.iat = parseInt(val)
of "nbf": claims.nbf = parseInt(val)
of "jti": claims.jti = val
else: discard
if i < payload.len and payload[i] == ',':
inc i
inc i
# Validate expiration
let now = int64(getTime().toUnix())
if claims.exp > 0 and now > claims.exp:
return (false, JWTClaims())
if claims.nbf > 0 and now < claims.nbf:
return (false, JWTClaims())
if claims.iat > 0 and now < claims.iat - 60:
# Allow 60 seconds clock skew
return (false, JWTClaims())
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 =
case creds.authMethod
of amNone:
return AuthResult(authenticated: true, username: "anonymous", role: "default",
database: "default")
of amToken, amJWT:
if creds.payload in am.tokens:
let (valid, claims) = am.verifyToken(creds.payload)
if valid:
return AuthResult(authenticated: true, username: claims.sub,
role: claims.role, database: claims.database)
return AuthResult(authenticated: false, error: "Invalid token")
of amSCRAMSHA256:
## Legacy fallback: simple hash comparison for backward compatibility.
## Real SCRAM should use startScram() / finishScram().
if creds.username in am.users:
let stored = am.users[creds.username]
let clientHash = if creds.payload.len > 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)
proc revokeToken*(am: var AuthManager, token: string) =
var idx = am.tokens.find(token)
if idx >= 0:
am.tokens.del(idx)
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