Skip to content
Open
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
5 changes: 3 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.7.1
// swift-tools-version:6.0
import PackageDescription

let package = Package(
Expand All @@ -20,5 +20,6 @@ let package = Package(
dependencies: ["KeychainSwift"],
exclude: ["ClearTests.swift"]
)
]
],
swiftLanguageModes: [.v6]
)
42 changes: 42 additions & 0 deletions Sources/KeychainError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// KeychainError.swift
// KeychainSwift
//
// Created by Lennard Sprong on 20/02/2025.
//

import Foundation
import Security

public struct KeychainError : RawRepresentable, CustomStringConvertible {
public init(rawValue: OSStatus) {
self.rawValue = rawValue
}

public init(_ code: OSStatus) {
rawValue = code
}

/// The result code for the operation.
public let rawValue: OSStatus

/// Retrieve the localized description for this error. This uses ``/Security/SecCopyErrorMessageString(_:_:)`` internally.
public var localizedDescription: String {
if let message = SecCopyErrorMessageString(rawValue, nil) {
return message as String
}
return description
}

public var description: String {
"KeychainError(\(rawValue))"
}
}

extension KeychainError : CustomNSError {
public static var errorDomain: String { NSOSStatusErrorDomain }
}

extension KeychainError : LocalizedError {
public var errorDescription: String? { localizedDescription }
}
143 changes: 68 additions & 75 deletions Sources/KeychainSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@ import Foundation
A collection of helper functions for saving text and data in the keychain.

*/
open class KeychainSwift {
open class KeychainSwift: @unchecked Sendable {

var lastQueryParameters: [String: Any]? // Used by the unit tests

/// Contains result code from the last operation. Value is noErr (0) for a successful result.
open var lastResultCode: OSStatus = noErr

var keyPrefix = "" // Can be useful in test.

Expand All @@ -20,8 +17,8 @@ open class KeychainSwift {
Specify an access group that will be used to access keychain items. Access groups can be used to share keychain items between applications. When access group value is nil all application access groups are being accessed. Access group name is used by all functions: set, get, delete and clear.

*/
open var accessGroup: String?

open var accessGroup: String? { _accessGroup }
private let _accessGroup: String?

/**

Expand All @@ -31,21 +28,24 @@ open class KeychainSwift {
Does not work on macOS.

*/
open var synchronizable: Bool = false
open var synchronizable: Bool { _synchronizable }
private let _synchronizable: Bool

private let lock = NSLock()


/// Instantiate a KeychainSwift object
public init() { }

/**

Instantiate a KeychainSwift object

- parameter keyPrefix: a prefix that is added before the key in get/set methods. Note that `clear` method still clears everything from the Keychain.
- parameter accessGroup: Access groups can be used to share keychain items between applications. When access group value is nil all application access groups are being accessed. Access group name is used by all functions: set, get, delete and clear.
- parameter synchronizable: Specifies whether the items can be synchronized with other devices through iCloud. Setting this property to true will add the item to other devices with the `set` method and obtain synchronizable items with the `get` command. Deleting synchronizable items will remove them from all devices. In order for keychain synchronization to work the user must enable "Keychain" in iCloud settings. Does not work on macOS.

*/
public init(keyPrefix: String) {
public init(keyPrefix: String = "", accessGroup: String? = nil, synchronizable: Bool = false) {
self.keyPrefix = keyPrefix
_accessGroup = accessGroup
_synchronizable = synchronizable
}

/**
Expand All @@ -55,19 +55,16 @@ open class KeychainSwift {
- parameter key: Key under which the text value is stored in the keychain.
- parameter value: Text string to be written to the keychain.
- parameter withAccess: Value that indicates when your app needs access to the text in the keychain item. By default the .AccessibleWhenUnlocked option is used that permits the data to be accessed only while the device is unlocked by the user.

- returns: True if the text was successfully written to the keychain.

*/
@discardableResult
open func set(_ value: String, forKey key: String,
withAccess access: KeychainSwiftAccessOptions? = nil) -> Bool {
withAccess access: KeychainSwiftAccessOptions? = nil) throws {

if let value = value.data(using: String.Encoding.utf8) {
return set(value, forKey: key, withAccess: access)
try set(value, forKey: key, withAccess: access)
} else {
throw KeychainError(errSecInvalidEncoding)
}

return false
}

/**
Expand All @@ -78,19 +75,16 @@ open class KeychainSwift {
- parameter value: Data to be written to the keychain.
- parameter withAccess: Value that indicates when your app needs access to the text in the keychain item. By default the .AccessibleWhenUnlocked option is used that permits the data to be accessed only while the device is unlocked by the user.

- returns: True if the text was successfully written to the keychain.

*/
@discardableResult
open func set(_ value: Data, forKey key: String,
withAccess access: KeychainSwiftAccessOptions? = nil) -> Bool {
withAccess access: KeychainSwiftAccessOptions? = nil) throws {

// The lock prevents the code to be run simultaneously
// from multiple threads which may result in crashing
lock.lock()
defer { lock.unlock() }

deleteNoLock(key) // Delete any existing key before saving it
try deleteNoLock(key) // Delete any existing key before saving it

let accessible = access?.value ?? KeychainSwiftAccessOptions.defaultOption.value

Expand All @@ -103,13 +97,11 @@ open class KeychainSwift {
KeychainSwiftConstants.accessible : accessible
]

query = addAccessGroupWhenPresent(query)
query = addSynchronizableIfRequired(query, addingItems: true)
addAccessGroupWhenPresent(&query)
addSynchronizableIfRequired(&query, addingItems: true)
lastQueryParameters = query

lastResultCode = SecItemAdd(query as CFDictionary, nil)

return lastResultCode == noErr
try throwIfFailed(SecItemAdd(query as CFDictionary, nil))
}

/**
Expand All @@ -120,17 +112,14 @@ open class KeychainSwift {
- parameter value: Boolean to be written to the keychain.
- parameter withAccess: Value that indicates when your app needs access to the value in the keychain item. By default the .AccessibleWhenUnlocked option is used that permits the data to be accessed only while the device is unlocked by the user.

- returns: True if the value was successfully written to the keychain.

*/
@discardableResult
open func set(_ value: Bool, forKey key: String,
withAccess access: KeychainSwiftAccessOptions? = nil) -> Bool {
withAccess access: KeychainSwiftAccessOptions? = nil) throws {

let bytes: [UInt8] = value ? [1] : [0]
let data = Data(bytes)

return set(data, forKey: key, withAccess: access)
try set(data, forKey: key, withAccess: access)
}

/**
Expand All @@ -141,14 +130,14 @@ open class KeychainSwift {
- returns: The text value from the keychain. Returns nil if unable to read the item.

*/
open func get(_ key: String) -> String? {
if let data = getData(key) {
open func get(_ key: String) throws -> String? {
if let data = try getData(key) {

if let currentString = String(data: data, encoding: .utf8) {
return currentString
}

lastResultCode = -67853 // errSecInvalidEncoding
throw KeychainError(errSecInvalidEncoding)
}

return nil
Expand All @@ -163,7 +152,7 @@ open class KeychainSwift {
- returns: The text value from the keychain. Returns nil if unable to read the item.

*/
open func getData(_ key: String, asReference: Bool = false) -> Data? {
open func getData(_ key: String, asReference: Bool = false) throws -> Data? {
// The lock prevents the code to be run simultaneously
// from multiple threads which may result in crashing
lock.lock()
Expand All @@ -183,21 +172,21 @@ open class KeychainSwift {
query[KeychainSwiftConstants.returnData] = kCFBooleanTrue
}

query = addAccessGroupWhenPresent(query)
query = addSynchronizableIfRequired(query, addingItems: false)
addAccessGroupWhenPresent(&query)
addSynchronizableIfRequired(&query, addingItems: false)
lastQueryParameters = query

var result: AnyObject?

lastResultCode = withUnsafeMutablePointer(to: &result) {
let lastResultCode = withUnsafeMutablePointer(to: &result) {
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
}

if lastResultCode == noErr {
return result as? Data
if lastResultCode != errSecItemNotFound {
try throwIfFailed(lastResultCode)
}

return nil
return result as? Data
}

/**
Expand All @@ -208,8 +197,8 @@ open class KeychainSwift {
- returns: The boolean value from the keychain. Returns nil if unable to read the item.

*/
open func getBool(_ key: String) -> Bool? {
guard let data = getData(key) else { return nil }
open func getBool(_ key: String) throws -> Bool? {
guard let data = try getData(key) else { return nil }
guard let firstBit = data.first else { return nil }
return firstBit == 1
}
Expand All @@ -223,13 +212,13 @@ open class KeychainSwift {

*/
@discardableResult
open func delete(_ key: String) -> Bool {
open func delete(_ key: String) throws -> Bool {
// The lock prevents the code to be run simultaneously
// from multiple threads which may result in crashing
lock.lock()
defer { lock.unlock() }

return deleteNoLock(key)
return try deleteNoLock(key)
}

/**
Expand All @@ -239,6 +228,9 @@ open class KeychainSwift {

*/
public var allKeys: [String] {
lock.lock()
defer { lock.unlock() }

var query: [String: Any] = [
KeychainSwiftConstants.klass : kSecClassGenericPassword,
KeychainSwiftConstants.returnData : true,
Expand All @@ -247,8 +239,8 @@ open class KeychainSwift {
KeychainSwiftConstants.matchLimit: KeychainSwiftConstants.secMatchLimitAll
]

query = addAccessGroupWhenPresent(query)
query = addSynchronizableIfRequired(query, addingItems: false)
addAccessGroupWhenPresent(&query)
addSynchronizableIfRequired(&query, addingItems: false)

var result: AnyObject?

Expand All @@ -272,59 +264,55 @@ open class KeychainSwift {
- returns: True if the item was successfully deleted.

*/
@discardableResult
func deleteNoLock(_ key: String) -> Bool {
@discardableResult
func deleteNoLock(_ key: String) throws -> Bool {
let prefixedKey = keyWithPrefix(key)

var query: [String: Any] = [
KeychainSwiftConstants.klass : kSecClassGenericPassword,
KeychainSwiftConstants.attrAccount : prefixedKey
]

query = addAccessGroupWhenPresent(query)
query = addSynchronizableIfRequired(query, addingItems: false)
addAccessGroupWhenPresent(&query)
addSynchronizableIfRequired(&query, addingItems: false)
lastQueryParameters = query

lastResultCode = SecItemDelete(query as CFDictionary)
let lastResultCode = SecItemDelete(query as CFDictionary)

return lastResultCode == noErr
guard lastResultCode != errSecItemNotFound else { return false }

try throwIfFailed(lastResultCode)
return true
}

/**

Deletes all Keychain items used by the app. Note that this method deletes all items regardless of the prefix settings used for initializing the class.

- returns: True if the keychain items were successfully deleted.

*/
@discardableResult
open func clear() -> Bool {
open func clear() throws {
// The lock prevents the code to be run simultaneously
// from multiple threads which may result in crashing
lock.lock()
defer { lock.unlock() }

var query: [String: Any] = [ kSecClass as String : kSecClassGenericPassword ]
query = addAccessGroupWhenPresent(query)
query = addSynchronizableIfRequired(query, addingItems: false)
addAccessGroupWhenPresent(&query)
addSynchronizableIfRequired(&query, addingItems: false)
lastQueryParameters = query

lastResultCode = SecItemDelete(query as CFDictionary)

return lastResultCode == noErr
try throwIfFailed(SecItemDelete(query as CFDictionary))
}

/// Returns the key with currently set prefix.
func keyWithPrefix(_ key: String) -> String {
return "\(keyPrefix)\(key)"
}

func addAccessGroupWhenPresent(_ items: [String: Any]) -> [String: Any] {
guard let accessGroup = accessGroup else { return items }
func addAccessGroupWhenPresent(_ items: inout [String: Any]) {
guard let accessGroup = accessGroup else { return }

var result: [String: Any] = items
result[KeychainSwiftConstants.accessGroup] = accessGroup
return result
items[KeychainSwiftConstants.accessGroup] = accessGroup
}

/**
Expand All @@ -337,10 +325,15 @@ open class KeychainSwift {
- returns: the dictionary with kSecAttrSynchronizable item added if it was requested. Otherwise, it returns the original dictionary.

*/
func addSynchronizableIfRequired(_ items: [String: Any], addingItems: Bool) -> [String: Any] {
if !synchronizable { return items }
var result: [String: Any] = items
result[KeychainSwiftConstants.attrSynchronizable] = addingItems == true ? true : kSecAttrSynchronizableAny
return result
func addSynchronizableIfRequired(_ items: inout [String: Any], addingItems: Bool) {
if !synchronizable { return }
items[KeychainSwiftConstants.attrSynchronizable] = addingItems == true ? true : kSecAttrSynchronizableAny
}

@inlinable
func throwIfFailed(_ status: OSStatus) throws {
if status != noErr {
throw KeychainError(status)
}
}
}
Loading