@@ -20,56 +20,56 @@ public final class KeystoreCore {
2020 private let accessQueue : DispatchQueue = DispatchQueue ( label: " app.metasig.keystore.access " , attributes: . concurrent)
2121 private let plainPrefs = UserDefaults ( suiteName: " unencrypted_store " ) !
2222 private let keychainServiceGroupName = " app.metasig.keystore.encrypted "
23- let hmacKeyAlias = " app.metasig.hmac.key "
23+ let hmacKeyAlias = " app.metasig.hmac.key.v2 "
2424
2525 private init ( ) { }
26-
26+
2727 /**
2828 *
2929 */
3030 public func contains_unencrypted_key( _ key: String ) -> KeystoreResult < Bool > {
3131 let exists = plainPrefs. object ( forKey: key) != nil
3232 return KeystoreResult ( ok: true , data: exists)
3333 }
34-
34+
3535 /**
3636 *
3737 */
3838 public func store_unencrypted( _ key: String , value: String ) -> KeystoreResult < Bool > {
3939 plainPrefs. setValue ( value, forKey: key)
4040 return KeystoreResult ( ok: true , data: true )
4141 }
42-
42+
4343 /**
4444 *
4545 */
4646 public func retrieve_unencrypted( _ key: String ) -> KeystoreResult < String ? > {
4747 let v = plainPrefs. string ( forKey: key)
4848 return KeystoreResult ( ok: true , data: v)
4949 }
50-
50+
5151 /**
5252 *
5353 */
5454 public func contains_key( _ key: String ) -> KeystoreResult < Bool > {
5555 return accessQueue. sync {
5656 NSLog ( " 🔍 DEBUG: Checking Keychain for key: \( key) " )
57-
57+
5858 let hasKey = keychainExists ( forKey: key)
59-
59+
6060 NSLog ( " 🔒 Key ' \( key) ' check: \( hasKey) " )
61-
61+
6262 return KeystoreResult ( ok: true , data: hasKey)
6363 }
6464 }
65-
65+
6666 public func store( _ key: String , plaintext: String ) -> KeystoreResult < Bool > {
6767 return accessQueue. sync ( flags: . barrier) {
6868 NSLog ( " 🔍 Key ' \( key) ' store begin " )
6969 do {
7070 NSLog ( " 🔍 DEBUG: Key ' \( key) ' store saveToKeychain " )
7171 try saveToKeychain ( value: plaintext, forKey: key)
72-
72+
7373 return KeystoreResult ( ok: true , data: true )
7474 } catch {
7575 NSLog ( " ❌ ERROR: Key ' \( key) ' store with error \( String ( describing: error) ) " )
@@ -103,22 +103,22 @@ public final class KeystoreCore {
103103 do {
104104 // Ensure HMAC key exists
105105 try ensureHmacKey ( )
106-
106+
107107 // Retrieve the key (this will trigger biometric authentication)
108108 guard let keyBase64 = try retrieveFromKeychain ( forKey: hmacKeyAlias) ,
109109 let keyData = Data ( base64Encoded: keyBase64) else {
110110 throw NSError ( domain: " KeystoreCore " , code: - 1 , userInfo: [ NSLocalizedDescriptionKey: " Failed to retrieve HMAC key " ] )
111111 }
112-
112+
113113 let key = SymmetricKey ( data: keyData)
114-
114+
115115 // Compute the HMAC
116116 let messageData = Data ( message. utf8)
117117 let tag = HMAC< SHA256> . authenticationCode( for: messageData, using: key)
118-
118+
119119 // Convert to hexadecimal string
120120 let hexString = tag. map { String ( format: " %02x " , $0) } . joined ( )
121-
121+
122122 return KeystoreResult ( ok: true , data: hexString)
123123 } catch {
124124 NSLog ( " ❌ ERROR: HMAC computation failed: \( error) " )
@@ -134,29 +134,29 @@ public final class KeystoreCore {
134134 public func shared_secret( _ pubKeys: [ String ] ) -> KeystoreResult < [ String ] > {
135135 return KeystoreResult ( ok: false , data: nil , error: " Not implement " )
136136 }
137-
137+
138138 // MARK: - Keychain Helper Methods
139-
139+
140140 private func ensureHmacKey( ) throws {
141-
141+
142142 // Check if the key already exists
143143 if let _ = try ? retrieveFromKeychain ( forKey: hmacKeyAlias) {
144144 // Key already exists, nothing to do
145145 return
146146 }
147-
147+
148148 // Create a new key if it doesn't exist
149149 let newKey = SymmetricKey ( size: . bits256)
150150 let keyData = newKey. withUnsafeBytes { Data ( $0) }
151151 let keyBase64 = keyData. base64EncodedString ( )
152-
152+
153153 // Store the key in the keychain with biometric protection
154154 // Your existing saveToKeychain method already handles the biometric requirement
155155 try saveToKeychain ( value: keyBase64, forKey: hmacKeyAlias)
156-
156+
157157 NSLog ( " ✅ Created new HMAC key " )
158158 }
159-
159+
160160 /**
161161 *
162162 */
@@ -172,7 +172,7 @@ public final class KeystoreCore {
172172 let status = SecItemCopyMatching ( query as CFDictionary , nil )
173173 return status == errSecSuccess
174174 }
175-
175+
176176 /**
177177 *
178178 */
@@ -181,11 +181,13 @@ public final class KeystoreCore {
181181 NSLog ( " 💥 Failed to encode string " )
182182 throw NSError ( domain: " KeystoreCore " , code: - 1 , userInfo: [ NSLocalizedDescriptionKey: " Failed to encode string " ] )
183183 }
184-
184+
185185 NSLog ( " 🔒 Key ' \( key) ' store: value: [REDACTED] " )
186-
187- let access = try makeAccessControl ( requirePrivateKeyUsage: false )
188-
186+
187+ // Use relaxed access control for HMAC key, strict for others
188+ let isHmacKey = ( key == hmacKeyAlias)
189+ let access = try makeAccessControl ( requirePrivateKeyUsage: false , relaxedForHmac: isHmacKey)
190+
189191 let query : [ String : Any ] = [
190192 kSecClass as String : kSecClassGenericPassword,
191193 kSecAttrService as String : keychainServiceGroupName,
@@ -194,7 +196,7 @@ public final class KeystoreCore {
194196 kSecAttrSynchronizable as String : kCFBooleanFalse as Any ,
195197 kSecValueData as String : data
196198 ]
197-
199+
198200 // Delete existing item if present
199201 SecItemDelete ( query as CFDictionary )
200202
@@ -211,52 +213,57 @@ public final class KeystoreCore {
211213 }
212214
213215 private func retrieveFromKeychain( forKey key: String ) throws -> String ? {
214- let context = LAContext ( )
215- context. localizedReason = " Access your passkey "
216-
217- // You can explicitly enable passcode fallback
218- context. localizedFallbackTitle = " Use Passcode " // Custom text for passcode button
219-
220- let query : [ String : Any ] = [
216+ // For HMAC key, don't require authentication context (allows access when device is unlocked)
217+ // For other keys, require explicit biometric/passcode authentication
218+ let isHmacKey = ( key == hmacKeyAlias)
219+
220+ var query : [ String : Any ] = [
221221 kSecClass as String : kSecClassGenericPassword,
222222 kSecAttrService as String : keychainServiceGroupName,
223223 kSecAttrAccount as String : key,
224224 kSecReturnData as String : true ,
225- kSecMatchLimit as String : kSecMatchLimitOne,
226- kSecUseAuthenticationContext as String : context
225+ kSecMatchLimit as String : kSecMatchLimitOne
227226 ]
228227
228+ // Only add authentication context for non-HMAC keys
229+ if !isHmacKey {
230+ let context = LAContext ( )
231+ context. localizedReason = " Access your passkey "
232+ context. localizedFallbackTitle = " Use Passcode "
233+ query [ kSecUseAuthenticationContext as String ] = context
234+ }
235+
229236 var result : AnyObject ?
230237 let status = SecItemCopyMatching ( query as CFDictionary , & result)
231238
232239 if status == errSecItemNotFound {
233240 return nil
234241 }
235-
242+
236243 guard status == errSecSuccess else {
237244 NSLog ( " ❌ ERROR: Failed to retrieve key ' \( key) ' with status: \( status) " )
238245 if let error = SecCopyErrorMessageString ( status, nil ) as String ? {
239246 NSLog ( " ❌ Error message: \( error) " )
240247 }
241248 throw NSError ( domain: " KeystoreCore " , code: Int ( status) , userInfo: [ NSLocalizedDescriptionKey: " Failed to retrieve from Keychain. Status: \( status) " ] )
242249 }
243-
250+
244251 guard let data = result as? Data else {
245252 NSLog ( " ❌ ERROR: Retrieved item for key ' \( key) ' but couldn't cast to Data " )
246253 throw NSError ( domain: " KeystoreCore " , code: - 2 , userInfo: [ NSLocalizedDescriptionKey: " Failed to cast result to Data " ] )
247254 }
248-
255+
249256 guard let string = String ( data: data, encoding: . utf8) else {
250257 NSLog ( " ❌ ERROR: Retrieved Data for key ' \( key) ' but couldn't decode as UTF-8 string " )
251258 NSLog ( " ❌ DEBUG: Data length: \( data. count) bytes, first few bytes: \( data. prefix ( min ( 10 , data. count) ) . map { String ( format: " %02x " , $0) } . joined ( ) ) " )
252259 throw NSError ( domain: " KeystoreCore " , code: - 3 , userInfo: [ NSLocalizedDescriptionKey: " Failed to decode data as UTF-8 string " ] )
253260 }
254-
261+
255262 NSLog ( " ✅ SUCCESS: Retrieved and decoded item for key ' \( key) ' " )
256-
263+
257264 return string
258265 }
259-
266+
260267 /**
261268 *
262269 */
@@ -270,31 +277,40 @@ public final class KeystoreCore {
270277 SecItemDelete ( query as CFDictionary )
271278 }
272279
273- /**
274- * Policy for storing value: must require biometric or device passcode or private key if true
275- */
276- private func makeAccessControl( requirePrivateKeyUsage: Bool ) throws -> SecAccessControl {
277- // Use OR to allow multiple authentication methods
278- var flags : SecAccessControlCreateFlags = [ . or]
279-
280- // Add biometrics if available
281- flags. insert ( . biometryAny)
282-
283- // Also allow device passcode as a fallback
284- flags. insert ( . devicePasscode)
285-
286- // Add private key usage if needed
287- if requirePrivateKeyUsage {
288- flags. insert ( . privateKeyUsage)
289- }
290-
291- var error : Unmanaged < CFError > ?
292- guard
293- let ac = SecAccessControlCreateWithFlags (
294- nil , kSecAttrAccessibleWhenUnlockedThisDeviceOnly, flags, & error)
295- else {
296- throw error!. takeRetainedValue ( ) as Error
280+
281+ private func makeAccessControl( requirePrivateKeyUsage: Bool , relaxedForHmac: Bool = false ) throws -> SecAccessControl {
282+ if relaxedForHmac {
283+ // For HMAC key: only require device to be unlocked, no biometric/passcode prompt
284+ var error : Unmanaged < CFError > ?
285+ guard let ac = SecAccessControlCreateWithFlags (
286+ nil ,
287+ kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
288+ [ ] , // No additional flags - just "when unlocked"
289+ & error
290+ ) else {
291+ throw error!. takeRetainedValue ( ) as Error
292+ }
293+ return ac
294+ } else {
295+ // For other keys: strict authentication required
296+ var flags : SecAccessControlCreateFlags = [ . or]
297+ flags. insert ( . biometryAny)
298+ flags. insert ( . devicePasscode)
299+
300+ if requirePrivateKeyUsage {
301+ flags. insert ( . privateKeyUsage)
302+ }
303+
304+ var error : Unmanaged < CFError > ?
305+ guard let ac = SecAccessControlCreateWithFlags (
306+ nil ,
307+ kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
308+ flags,
309+ & error
310+ ) else {
311+ throw error!. takeRetainedValue ( ) as Error
312+ }
313+ return ac
297314 }
298- return ac
299315 }
300316}
0 commit comments