Skip to content

Commit 815ad27

Browse files
authored
chore: relax hmac access to not require faceid (#9)
* chore: relax hmac access to not require faceid * chore: bump version to `2.4.0`
1 parent 5566f69 commit 815ad27

File tree

4 files changed

+88
-72
lines changed

4 files changed

+88
-72
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tauri-plugin-keystore"
3-
version = "2.3.1"
3+
version = "2.4.0"
44
authors = ["0x330a"]
55
description = "Interact with the device-native key storage (Android Keystore, iOS Keychain) & perform ecdh operations for generating symmetric keys"
66
edition = "2021"

ios/Sources/KeystorePlugin/KeystoreCore.swift

Lines changed: 84 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

ios/Sources/KeystorePlugin/PluginShim.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,4 @@ class KeystorePlugin: Plugin {
117117

118118
@_cdecl("init_plugin_keystore") func initPluginKeystore() -> Plugin {
119119
return KeystorePlugin()
120-
}
120+
}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@metasig/tauri-plugin-keystore-api",
3-
"version": "2.3.1",
3+
"version": "2.4.0",
44
"author": "0x330a",
55
"description": "Interact with the device-native key storage (Android Keystore, iOS Keychain) & perform ecdh operations for generating symmetric keys",
66
"type": "module",
@@ -36,4 +36,4 @@
3636
"type": "git",
3737
"url": "git+https://github.com/Metasig/tauri-plugin-keystore.git"
3838
}
39-
}
39+
}

0 commit comments

Comments
 (0)