1212import Foundation
1313import Crypto
1414
15+ // MARK: - SaltedPassword cache
16+ //
17+ // PostgreSQL stores a fixed salt per user in pg_authid (it only changes when the user
18+ // resets their password). Every connection by the same user sees the same server-first
19+ // salt string. Caching SaltedPassword = Hi(password, salt, iterations) eliminates
20+ // 4096 HMAC-SHA256 rounds on every cold connect after the first one.
21+ //
22+ // Cache key: "<iterations>:<saltB64>:<password>" → [UInt8] SaltedPassword
23+ // Invalidation: automatic — password change produces a new salt from the server.
24+
25+ private let _scramCacheLock = NSLock ( )
26+ private nonisolated ( unsafe) var _scramCache: [ String : [ UInt8 ] ] = [ : ]
27+
1528// MARK: - SCRAM-SHA-256 authenticator
1629
1730struct SCRAMSHA256 {
@@ -54,12 +67,18 @@ struct SCRAMSHA256 {
5467 }
5568
5669 // SaltedPassword = Hi(Normalize(password), salt, i)
57- let saltedPassword = try hi ( password: password,
58- salt: Array ( saltBytes) ,
59- iterations: iterations)
70+ // Cached: same password+salt+iterations always produces the same result.
71+ let saltedPassword = try cachedHi ( password: password,
72+ saltB64: saltB64,
73+ salt: Array ( saltBytes) ,
74+ iterations: iterations)
75+
76+ // Reuse a single SymmetricKey for all derivations from SaltedPassword
77+ let spKey = SymmetricKey ( data: saltedPassword)
6078
6179 // ClientKey = HMAC(SaltedPassword, "Client Key")
62- let clientKey = hmacSHA256 ( key: saltedPassword, data: [ UInt8] ( " Client Key " . utf8) )
80+ let clientKey = Array ( HMAC< SHA256> . authenticationCode(
81+ for: [ UInt8] ( " Client Key " . utf8) , using: spKey) )
6382
6483 // StoredKey = H(ClientKey)
6584 let storedKey = Array ( SHA256 . hash ( data: clientKey) )
@@ -68,18 +87,23 @@ struct SCRAMSHA256 {
6887 let channelBinding = " c= " + Data( " n,, " . utf8) . base64EncodedString ( )
6988 let clientFinalWithoutProof = " \( channelBinding) ,r= \( combinedNonce) "
7089 let authMessage = " \( clientFirstMessageBare) , \( serverFirstMessage) , \( clientFinalWithoutProof) "
90+ let authBytes = [ UInt8] ( authMessage. utf8)
7191
7292 // ClientSignature = HMAC(StoredKey, AuthMessage)
73- let clientSignature = hmacSHA256 ( key: storedKey, data: [ UInt8] ( authMessage. utf8) )
93+ let storedKey_ = SymmetricKey ( data: storedKey)
94+ let clientSignature = Array ( HMAC< SHA256> . authenticationCode( for: authBytes, using: storedKey_) )
7495
75- // ClientProof = ClientKey XOR ClientSignature
76- let clientProof = zip ( clientKey, clientSignature) . map { $0 ^ $1 }
96+ // ClientProof = ClientKey XOR ClientSignature (in-place)
97+ var clientProof = clientKey
98+ for i in 0 ..< clientProof. count { clientProof [ i] ^= clientSignature [ i] }
7799
78100 // ServerKey = HMAC(SaltedPassword, "Server Key")
79- let serverKey = hmacSHA256 ( key: saltedPassword, data: [ UInt8] ( " Server Key " . utf8) )
101+ let serverKey = Array ( HMAC< SHA256> . authenticationCode(
102+ for: [ UInt8] ( " Server Key " . utf8) , using: spKey) )
80103
81104 // ServerSignature = HMAC(ServerKey, AuthMessage)
82- let serverSignature = hmacSHA256 ( key: serverKey, data: [ UInt8] ( authMessage. utf8) )
105+ let serverKey_ = SymmetricKey ( data: serverKey)
106+ let serverSignature = Array ( HMAC< SHA256> . authenticationCode( for: authBytes, using: serverKey_) )
83107
84108 let clientFinal = " \( clientFinalWithoutProof) ,p= \( Data ( clientProof) . base64EncodedString ( ) ) "
85109 return ( clientFinal, serverSignature)
@@ -103,24 +127,45 @@ struct SCRAMSHA256 {
103127
104128 // MARK: - Crypto helpers
105129
106- // PBKDF2-HMAC-SHA256: Hi(str, salt, i)
107- private static func hi( password: String , salt: [ UInt8 ] , iterations: Int ) throws -> [ UInt8 ] {
108- let passwordBytes = [ UInt8] ( password. utf8)
130+ // PBKDF2-HMAC-SHA256: Hi(str, salt, i) with SaltedPassword caching.
131+ // Cache hit: O(1) dictionary lookup — skips 4096 HMAC-SHA256 rounds.
132+ private static func cachedHi( password: String , saltB64: String ,
133+ salt: [ UInt8 ] , iterations: Int ) throws -> [ UInt8 ] {
134+ let cacheKey = " \( iterations) : \( saltB64) : \( password) "
135+ _scramCacheLock. lock ( )
136+ if let cached = _scramCache [ cacheKey] {
137+ _scramCacheLock. unlock ( )
138+ return cached
139+ }
140+ _scramCacheLock. unlock ( )
141+
142+ let result = hi ( password: password, salt: salt, iterations: iterations)
143+
144+ _scramCacheLock. lock ( )
145+ _scramCache [ cacheKey] = result
146+ _scramCacheLock. unlock ( )
147+ return result
148+ }
149+
150+ // Pure PBKDF2-HMAC-SHA256 (no cache). Creates SymmetricKey once; XORs in-place.
151+ private static func hi( password: String , salt: [ UInt8 ] , iterations: Int ) -> [ UInt8 ] {
152+ let passwordKey = SymmetricKey ( data: Data ( password. utf8) ) // created once
109153
110154 // U1 = HMAC(password, salt + INT(1))
111155 var saltPlusOne = salt
112156 saltPlusOne. append ( contentsOf: [ 0 , 0 , 0 , 1 ] )
113- var u = hmacSHA256 ( key : passwordBytes , data : saltPlusOne )
157+ var u = Array ( HMAC < SHA256 > . authenticationCode ( for : saltPlusOne , using : passwordKey ) )
114158 var result = u
115159
116160 for _ in 1 ..< iterations {
117- u = hmacSHA256 ( key: passwordBytes, data: u)
118- result = zip ( result, u) . map { $0 ^ $1 }
161+ u = Array ( HMAC< SHA256> . authenticationCode( for: u, using: passwordKey) )
162+ // In-place XOR — avoids allocating a new [UInt8] per iteration
163+ for i in 0 ..< result. count { result [ i] ^= u [ i] }
119164 }
120165 return result
121166 }
122167
123- // HMAC-SHA-256
168+ // HMAC-SHA-256 (used externally)
124169 private static func hmacSHA256( key: [ UInt8 ] , data: [ UInt8 ] ) -> [ UInt8 ] {
125170 let symmetricKey = SymmetricKey ( data: key)
126171 let mac = HMAC< SHA256> . authenticationCode( for: data, using: symmetricKey)
0 commit comments