Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for issue #711: The specified item already exists in the keychain, iOS Code -25299, -25300 #751

Merged
merged 9 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
A Flutter plugin to store data in secure storage:

- [Keychain](https://developer.apple.com/library/content/documentation/Security/Conceptual/keychainServConcepts/01introduction/introduction.html#//apple_ref/doc/uid/TP30000897-CH203-TP1) is used for iOS
- AES encryption is used for Android. AES secret key is encrypted with RSA and RSA key is stored in [KeyStore](https://developer.android.com/training/articles/keystore.html).
By default following algorithms are used for AES and secret key encryption: AES/CBC/PKCS7Padding and RSA/ECB/PKCS1Padding
From Android 6 you can use newer, recommended algoritms:
AES/GCM/NoPadding and RSA/ECB/OAEPWithSHA-256AndMGF1Padding
- AES encryption is used for Android. AES secret key is encrypted with RSA and RSA key is stored in [KeyStore](https://developer.android.com/training/articles/keystore.html).
By default following algorithms are used for AES and secret key encryption: AES/CBC/PKCS7Padding and RSA/ECB/PKCS1Padding
From Android 6 you can use newer, recommended algoritms:
AES/GCM/NoPadding and RSA/ECB/OAEPWithSHA-256AndMGF1Padding
You can set them in Android options like so:
```dart
AndroidOptions _getAndroidOptions() => const AndroidOptions(
Expand Down Expand Up @@ -56,7 +56,7 @@ If not present already, please call WidgetsFlutterBinding.ensureInitialized() in
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

// Create storage
final storage = new FlutterSecureStorage();
final storage = FlutterSecureStorage();

// Read value
String value = await storage.read(key: key);
Expand Down
5 changes: 4 additions & 1 deletion flutter_secure_storage/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 9.2.3
* [iOS] Fix for issue #711: The specified item already exists in the keychain.

## 9.2.2
[iOS, macOS] Fixed an issue which caused the readAll and deleteAll to not work properly.

Expand All @@ -13,7 +16,7 @@ New Features:
Bugs Fixed:
* [iOS] Return nil on iOS read if key is not found
* [macOS] Also set kSecUseDataProtectionKeychain on read for macos.

## 9.1.1
Reverts new feature because of breaking changes.
* [iOS, macOS] Added isProtectedDataAvailable, A boolean value that indicates whether content protection is active.
Expand Down
6 changes: 3 additions & 3 deletions flutter_secure_storage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Note: usage of encryptedSharedPreference
When using the `encryptedSharedPreferences` parameter on Android, make sure to pass the option to the
constructor instead of the function like so:
constructor instead of the function like so:
```dart
AndroidOptions _getAndroidOptions() => const AndroidOptions(
encryptedSharedPreferences: true,
Expand All @@ -22,7 +22,7 @@ A Flutter plugin to store data in secure storage:
AndroidOptions _getAndroidOptions() => const AndroidOptions(
encryptedSharedPreferences: true,
);
```
```
For more information see the example app.
- [`libsecret`](https://wiki.gnome.org/Projects/Libsecret) is used for Linux.

Expand All @@ -34,7 +34,7 @@ _Note_ KeyStore was introduced in Android 4.3 (API level 18). The plugin wouldn'
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

// Create storage
final storage = new FlutterSecureStorage();
final storage = FlutterSecureStorage();

// Read value
String value = await storage.read(key: key);
Expand Down
2 changes: 1 addition & 1 deletion flutter_secure_storage/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ buildscript {
}

dependencies {
classpath 'com.android.tools.build:gradle:8.3.2'
classpath 'com.android.tools.build:gradle:8.5.1'
}
}

Expand Down
182 changes: 105 additions & 77 deletions flutter_secure_storage/ios/Classes/FlutterSecureStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

import Foundation

class FlutterSecureStorage{
class FlutterSecureStorage {
private func parseAccessibleAttr(accessibility: String?) -> CFString {
guard let accessibility = accessibility else {
return kSecAttrAccessibleWhenUnlocked
}

switch accessibility {
case "passcode":
return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
Expand All @@ -31,156 +31,184 @@ class FlutterSecureStorage{

private func baseQuery(key: String?, groupId: String?, accountName: String?, synchronizable: Bool?, accessibility: String?, returnData: Bool?) -> Dictionary<CFString, Any> {
var keychainQuery: [CFString: Any] = [
kSecClass : kSecClassGenericPassword,
kSecAttrAccessible : parseAccessibleAttr(accessibility: accessibility),
kSecClass : kSecClassGenericPassword
]


if (accessibility != nil) {
keychainQuery[kSecAttrAccessible] = parseAccessibleAttr(accessibility: accessibility)
}

if (key != nil) {
keychainQuery[kSecAttrAccount] = key
}

if (groupId != nil) {
keychainQuery[kSecAttrAccessGroup] = groupId
}

if (accountName != nil) {
keychainQuery[kSecAttrService] = accountName
}

if (synchronizable != nil) {
keychainQuery[kSecAttrSynchronizable] = synchronizable
}

if (returnData != nil) {
keychainQuery[kSecReturnData] = returnData
}
return keychainQuery
}

internal func containsKey(key: String, groupId: String?, accountName: String?, synchronizable: Bool?, accessibility: String?) -> Result<Bool, OSSecError> {
let keychainQuery = baseQuery(key: key, groupId: groupId, accountName: accountName, synchronizable: synchronizable, accessibility: accessibility, returnData: false)

let status = SecItemCopyMatching(keychainQuery as CFDictionary, nil)
switch status {
case errSecSuccess:
return .success(true)
case errSecItemNotFound:
return .success(false)
default:
return .failure(OSSecError(status: status))
}

internal func containsKey(key: String, groupId: String?, accountName: String?) -> Result<Bool, OSSecError> {
// The accessibility parameter has no influence on uniqueness.
func queryKeychain(synchronizable: Bool) -> OSStatus {
let keychainQuery = baseQuery(key: key, groupId: groupId, accountName: accountName, synchronizable: synchronizable, accessibility: nil, returnData: false)
return SecItemCopyMatching(keychainQuery as CFDictionary, nil)
}

let statusSynchronizable = queryKeychain(synchronizable: true)
if statusSynchronizable == errSecSuccess {
return .success(true)
} else if statusSynchronizable != errSecItemNotFound {
return .failure(OSSecError(status: statusSynchronizable))
}

let statusNonSynchronizable = queryKeychain(synchronizable: false)
switch statusNonSynchronizable {
case errSecSuccess:
return .success(true)
case errSecItemNotFound:
return .success(false)
default:
return .failure(OSSecError(status: statusNonSynchronizable))
}
}

internal func readAll(groupId: String?, accountName: String?, synchronizable: Bool?, accessibility: String?) -> FlutterSecureStorageResponse {
var keychainQuery = baseQuery(key: nil, groupId: groupId, accountName: accountName, synchronizable: synchronizable, accessibility: accessibility, returnData: true)

keychainQuery[kSecMatchLimit] = kSecMatchLimitAll
keychainQuery[kSecReturnAttributes] = true

var ref: AnyObject?
let status = SecItemCopyMatching(
keychainQuery as CFDictionary,
&ref
)

if (status == errSecItemNotFound) {
// readAll() returns all elements, so return nil if the items does not exist
return FlutterSecureStorageResponse(status: errSecSuccess, value: nil)
}

var results: [String: String] = [:]

if (status == noErr) {
(ref as! NSArray).forEach { item in
let key: String = (item as! NSDictionary)[kSecAttrAccount] as! String
let value: String = String(data: (item as! NSDictionary)[kSecValueData] as! Data, encoding: .utf8) ?? ""
results[key] = value
}
}

return FlutterSecureStorageResponse(status: status, value: results)
}

internal func read(key: String, groupId: String?, accountName: String?, synchronizable: Bool?, accessibility: String?) -> FlutterSecureStorageResponse {
let keychainQuery = baseQuery(key: key, groupId: groupId, accountName: accountName, synchronizable: synchronizable, accessibility: accessibility, returnData: true)

var ref: AnyObject?
let status = SecItemCopyMatching(
keychainQuery as CFDictionary,
&ref
)

// Return nil if the key is not found
if (status == errSecItemNotFound) {
return FlutterSecureStorageResponse(status: errSecSuccess, value: nil)
}

var value: String? = nil

if (status == noErr) {
value = String(data: ref as! Data, encoding: .utf8)
internal func read(key: String, groupId: String?, accountName: String?) -> FlutterSecureStorageResponse {
// Function to retrieve a value considering the synchronizable parameter.
func readValue(synchronizable: Bool?) -> FlutterSecureStorageResponse {
let keychainQuery = baseQuery(key: key, groupId: groupId, accountName: accountName, synchronizable: synchronizable, accessibility: nil, returnData: true)

var ref: AnyObject?
let status = SecItemCopyMatching(
keychainQuery as CFDictionary,
&ref
)

// Return nil if the key is not found.
if status == errSecItemNotFound {
return FlutterSecureStorageResponse(status: errSecSuccess, value: nil)
}

var value: String? = nil

if status == noErr, let data = ref as? Data {
value = String(data: data, encoding: .utf8)
}

return FlutterSecureStorageResponse(status: status, value: value)
}

return FlutterSecureStorageResponse(status: status, value: value)
// First, query without synchronizable, then with synchronizable if no value is found.
let responseWithoutSynchronizable = readValue(synchronizable: nil)
return responseWithoutSynchronizable.value != nil ? responseWithoutSynchronizable : readValue(synchronizable: true)
}
internal func deleteAll(groupId: String?, accountName: String?, synchronizable: Bool?, accessibility: String?) -> FlutterSecureStorageResponse {
let keychainQuery = baseQuery(key: nil, groupId: groupId, accountName: accountName, synchronizable: synchronizable, accessibility: accessibility, returnData: nil)

internal func deleteAll(groupId: String?, accountName: String?) -> FlutterSecureStorageResponse {
let keychainQuery = baseQuery(key: nil, groupId: groupId, accountName: accountName, synchronizable: nil, accessibility: nil, returnData: nil)
let status = SecItemDelete(keychainQuery as CFDictionary)

if (status == errSecItemNotFound) {
// deleteAll() deletes all items, so return nil if the items does not exist
return FlutterSecureStorageResponse(status: errSecSuccess, value: nil)
}

return FlutterSecureStorageResponse(status: status, value: nil)
}
internal func delete(key: String, groupId: String?, accountName: String?, synchronizable: Bool?, accessibility: String?) -> FlutterSecureStorageResponse {
let keychainQuery = baseQuery(key: key, groupId: groupId, accountName: accountName, synchronizable: synchronizable, accessibility: accessibility, returnData: true)

internal func delete(key: String, groupId: String?, accountName: String?) -> FlutterSecureStorageResponse {
let keychainQuery = baseQuery(key: key, groupId: groupId, accountName: accountName, synchronizable: nil, accessibility: nil, returnData: true)
let status = SecItemDelete(keychainQuery as CFDictionary)

// Return nil if the key is not found
if (status == errSecItemNotFound) {
return FlutterSecureStorageResponse(status: errSecSuccess, value: nil)
}

return FlutterSecureStorageResponse(status: status, value: nil)
}
internal func write(key: String, value: String, groupId: String?, accountName: String?, synchronizable: Bool?, accessibility: String?) -> FlutterSecureStorageResponse {

internal func write(key: String, value: String, groupId: String?, accountName: String?, synchronizable: Bool?, accessibility: String?) -> FlutterSecureStorageResponse {
var keyExists: Bool = false

switch containsKey(key: key, groupId: groupId, accountName: accountName, synchronizable: synchronizable, accessibility: accessibility) {
case .success(let exists):
keyExists = exists
break;
case .failure(let err):
return FlutterSecureStorageResponse(status: err.status, value: nil)
// Check if the key exists but without accessibility.
// This parameter has no effect on the uniqueness of the key.
switch containsKey(key: key, groupId: groupId, accountName: accountName) {
case .success(let exists):
keyExists = exists
break;
case .failure(let err):
return FlutterSecureStorageResponse(status: err.status, value: nil)
}

var keychainQuery = baseQuery(key: key, groupId: groupId, accountName: accountName, synchronizable: synchronizable, accessibility: accessibility, returnData: nil)

if (keyExists) {
let attrAccessible = parseAccessibleAttr(accessibility: accessibility)

// Entry exists, try to update it. Change of kSecAttrAccessible not possible via update.
let update: [CFString: Any?] = [
kSecValueData: value.data(using: String.Encoding.utf8),
kSecAttrAccessible: attrAccessible,
kSecAttrSynchronizable: synchronizable
]

let status = SecItemUpdate(keychainQuery as CFDictionary, update as CFDictionary)
return FlutterSecureStorageResponse(status: status, value: nil)
} else {
keychainQuery[kSecValueData] = value.data(using: String.Encoding.utf8)
let status = SecItemAdd(keychainQuery as CFDictionary, nil)

return FlutterSecureStorageResponse(status: status, value: nil)

if status == errSecSuccess {
return FlutterSecureStorageResponse(status: status, value: nil)
}

// Update failed, possibly due to different kSecAttrAccessible.
// Delete the entry and create a new one in the next step.
delete(key: key, groupId: groupId, accountName: accountName)
}
}

// Entry does not exist or was deleted, create a new entry.
keychainQuery[kSecValueData] = value.data(using: String.Encoding.utf8)

let status = SecItemAdd(keychainQuery as CFDictionary, nil)

return FlutterSecureStorageResponse(status: status, value: nil)
}
}

struct FlutterSecureStorageResponse {
Expand Down
Loading