Skip to content

Commit c68f97a

Browse files
feature: add access control parameter for apple
1 parent c6e147e commit c68f97a

File tree

5 files changed

+117
-28
lines changed

5 files changed

+117
-28
lines changed

flutter_secure_storage/lib/options/apple_options.dart

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,37 @@ enum KeychainAccessibility {
3030
first_unlock_this_device,
3131
}
3232

33+
/// Keychain access control flags that define security conditions for accessing items.
34+
/// These flags can be combined to create complex access control policies.
35+
enum AccessControlFlag {
36+
/// Constraint to access an item with a passcode.
37+
devicePasscode,
38+
39+
/// Constraint to access an item with biometrics (Touch ID/Face ID).
40+
biometryAny,
41+
42+
/// Constraint to access an item with the currently enrolled biometrics.
43+
biometryCurrentSet,
44+
45+
/// Constraint to access an item with either biometry or passcode.
46+
userPresence,
47+
48+
/// Constraint to access an item with a paired watch.
49+
watch,
50+
51+
/// Combine multiple constraints with an OR operation.
52+
or,
53+
54+
/// Combine multiple constraints with an AND operation.
55+
and,
56+
57+
/// Use an application-provided password for encryption.
58+
applicationPassword,
59+
60+
/// Enable private key usage for signing operations.
61+
privateKeyUsage,
62+
}
63+
3364
/// Specific options for Apple platform.
3465
abstract class AppleOptions extends Options {
3566
/// Creates an instance of `AppleOptions` with configurable parameters
@@ -49,7 +80,7 @@ abstract class AppleOptions extends Options {
4980
this.resultLimit,
5081
this.shouldReturnPersistentReference,
5182
this.authenticationUIBehavior,
52-
this.accessControlSettings,
83+
this.accessControlFlags = const [],
5384
});
5485

5586
/// The default account name associated with the keychain items.
@@ -130,11 +161,27 @@ abstract class AppleOptions extends Options {
130161
/// Determines whether authentication prompts are displayed to the user.
131162
final String? authenticationUIBehavior;
132163

133-
/// `kSecAttrAccessControl`: **Shared or Unique**.
134-
/// Specifies access control settings for the item
135-
/// (e.g., biometrics, passcode).
136-
/// Shared if multiple items use the same access control.
137-
final String? accessControlSettings;
164+
/// Keychain access control flags define security conditions for accessing items.
165+
/// These flags can be combined to create custom security policies.
166+
///
167+
/// ### Using Logical Operators:
168+
/// - Use `AccessControlFlag.or` to allow access if **any** of the specified conditions are met.
169+
/// - Use `AccessControlFlag.and` to require that **all** specified conditions are met.
170+
///
171+
/// **Rules for Combining Flags:**
172+
/// - Only one logical operator (`or` or `and`) can be used per combination.
173+
/// - Logical operators should be placed after the security constraints.
174+
///
175+
/// **Supported Flags:**
176+
/// - `userPresence`: Requires user authentication via biometrics or passcode.
177+
/// - `biometryAny`: Allows access with any enrolled biometrics.
178+
/// - `biometryCurrentSet`: Requires currently enrolled biometrics.
179+
/// - `devicePasscode`: Requires device passcode authentication.
180+
/// - `watch`: Allows access with a paired Apple Watch.
181+
/// - `privateKeyUsage`: Enables use of a private key for signing operations.
182+
/// - `applicationPassword`: Uses an app-defined password for encryption.
183+
///
184+
final List<AccessControlFlag> accessControlFlags;
138185

139186
@override
140187
Map<String, String> toMap() => <String, String>{
@@ -156,7 +203,8 @@ abstract class AppleOptions extends Options {
156203
'shouldReturnPersistentReference': '$shouldReturnPersistentReference',
157204
if (authenticationUIBehavior != null)
158205
'authenticationUIBehavior': authenticationUIBehavior!,
159-
if (accessControlSettings != null)
160-
'accessControlSettings': accessControlSettings!,
206+
if (accessControlFlags.isNotEmpty)
207+
'accessControlFlags':
208+
accessControlFlags.map((e) => e.name).toList().toString(),
161209
};
162210
}

flutter_secure_storage/lib/options/ios_options.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class IOSOptions extends AppleOptions {
2121
super.resultLimit,
2222
super.shouldReturnPersistentReference,
2323
super.authenticationUIBehavior,
24-
super.accessControlSettings,
24+
super.accessControlFlags,
2525
});
2626

2727
/// A predefined `IosOptions` instance with default settings.

flutter_secure_storage/lib/options/macos_options.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class MacOsOptions extends AppleOptions {
2020
super.resultLimit,
2121
super.shouldReturnPersistentReference,
2222
super.authenticationUIBehavior,
23-
super.accessControlSettings,
23+
super.accessControlFlags,
2424
this.usesDataProtectionKeychain = true,
2525
});
2626

flutter_secure_storage_darwin/darwin/flutter_secure_storage_darwin/Sources/flutter_secure_storage_darwin/FlutterSecureStorage.swift

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ struct KeychainQueryParameters {
6060
/// `kSecUseAuthenticationUI` (iOS/macOS): Controls how authentication UI is presented during secure operations.
6161
var authenticationUIBehavior: String?
6262

63-
/// `kSecAttrAccessControl` (iOS/macOS): Specifies access control settings (e.g., biometrics, passcode).
64-
var accessControlSettings: SecAccessControl?
63+
/// `accessControlFlags` (iOS/macOS): Specifies access control settings (e.g., biometrics, passcode).
64+
var accessControlFlags: String?
6565
}
6666

6767
/// Represents the response from a keychain operation.
@@ -88,6 +88,52 @@ class FlutterSecureStorage {
8888
default: return kSecAttrAccessibleWhenUnlocked
8989
}
9090
}
91+
92+
/// Parses a string of comma-separated access control flags into SecAccessControlCreateFlags.
93+
private func parseAccessControlFlags(_ flagString: String?) -> SecAccessControlCreateFlags {
94+
guard let flagString = flagString else { return [] }
95+
var flags: SecAccessControlCreateFlags = []
96+
let flagList = flagString.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
97+
for dirtyFlag in flagList {
98+
let flag = dirtyFlag.trimmingCharacters(in: CharacterSet(charactersIn: "[]"))
99+
100+
switch flag {
101+
case "userPresence":
102+
flags.insert(.userPresence)
103+
case "biometryAny":
104+
flags.insert(.biometryAny)
105+
case "biometryCurrentSet":
106+
flags.insert(.biometryCurrentSet)
107+
case "devicePasscode":
108+
flags.insert(.devicePasscode)
109+
case "or":
110+
flags.insert(.or)
111+
case "and":
112+
flags.insert(.and)
113+
case "privateKeyUsage":
114+
flags.insert(.privateKeyUsage)
115+
case "applicationPassword":
116+
flags.insert(.applicationPassword)
117+
default:
118+
continue
119+
}
120+
}
121+
return flags
122+
}
123+
124+
/// Creates an access control object based on the provided parameters.
125+
private func createAccessControl(params: KeychainQueryParameters) -> SecAccessControl? {
126+
guard let accessibilityLevel = params.accessibilityLevel else { return nil }
127+
let protection = parseAccessibleAttr(accessibilityLevel)
128+
let flags = parseAccessControlFlags(params.accessControlFlags)
129+
var error: Unmanaged<CFError>?
130+
let accessControl = SecAccessControlCreateWithFlags(nil, protection, flags, &error)
131+
if let error = error?.takeRetainedValue() {
132+
print("Error creating access control: \(error.localizedDescription)")
133+
return nil
134+
}
135+
return accessControl
136+
}
91137

92138
/// Constructs a keychain query dictionary from the given parameters.
93139
private func baseQuery(from params: KeychainQueryParameters) -> [CFString: Any] {
@@ -108,14 +154,6 @@ class FlutterSecureStorage {
108154
query[kSecAttrService] = service
109155
}
110156

111-
if let isSynchronizable = params.isSynchronizable {
112-
query[kSecAttrSynchronizable] = isSynchronizable
113-
}
114-
115-
if let accessibilityLevel = params.accessibilityLevel {
116-
query[kSecAttrAccessible] = parseAccessibleAttr(accessibilityLevel)
117-
}
118-
119157
if let shouldReturnData = params.shouldReturnData {
120158
query[kSecReturnData] = shouldReturnData
121159
}
@@ -152,8 +190,15 @@ class FlutterSecureStorage {
152190
query[kSecUseAuthenticationUI] = authenticationUIBehavior
153191
}
154192

155-
if let accessControlSettings = params.accessControlSettings {
156-
query[kSecAttrAccessControl] = accessControlSettings
193+
if let accessControl = createAccessControl(params: params) {
194+
query[kSecAttrAccessControl] = accessControl
195+
} else {
196+
if let accessibilityLevel = params.accessibilityLevel {
197+
query[kSecAttrAccessible] = parseAccessibleAttr(accessibilityLevel)
198+
}
199+
if let isSynchronizable = params.isSynchronizable {
200+
query[kSecAttrSynchronizable] = isSynchronizable
201+
}
157202
}
158203

159204
#if os(macOS)
@@ -172,11 +217,6 @@ class FlutterSecureStorage {
172217
}
173218

174219
private func validateQueryParameters(params: KeychainQueryParameters) throws {
175-
// Accessibility and access control
176-
if params.accessibilityLevel != nil, params.accessControlSettings != nil {
177-
throw OSSecError(status: errSecParam, message: "Cannot use kSecAttrAccessible and kSecAttrAccessControl together.")
178-
}
179-
180220
// Match limit
181221
if params.resultLimit == 1, params.shouldReturnData == true {
182222
throw OSSecError(status: errSecParam, message: "Cannot use kSecMatchLimitAll when expecting a single result with kSecReturnData.")

flutter_secure_storage_darwin/darwin/flutter_secure_storage_darwin/Sources/flutter_secure_storage_darwin/FlutterSecureStorageDarwinPlugin.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ public class FlutterSecureStorageDarwinPlugin: NSObject, FlutterPlugin, FlutterS
164164
isHidden: (options["isHidden"] as? String).flatMap { Bool($0) },
165165
isPlaceholder: (options["isPlaceholder"] as? String).flatMap { Bool($0) },
166166
shouldReturnPersistentReference: (options["persistentReference"] as? String).flatMap { Bool($0) },
167-
authenticationUIBehavior: options["authenticationUIBehavior"] as? String
167+
authenticationUIBehavior: options["authenticationUIBehavior"] as? String,
168+
accessControlFlags: options["accessControlFlags"] as? String
168169
)
169170

170171
return (parameters, value)

0 commit comments

Comments
 (0)