Skip to content

Commit

Permalink
Merge pull request #2 from trifork/tkc/keychain-errors
Browse files Browse the repository at this point in the history
Replaced keychain errors in TIMEncryptedStorageError
  • Loading branch information
Thomas Kalhøj Clemensen authored Feb 25, 2021
2 parents 56fd133 + 8293e71 commit 5197b5a
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 98 deletions.
2 changes: 1 addition & 1 deletion .azure-pipelines/azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
ios14:
IMAGE_NAME: 'macos-10.15'
XCODE_DEVELOPER_PATH: /Applications/Xcode_12.4.app
IOS_SIMULATORS: 'iPhone 12 mini,OS=14.3'
IOS_SIMULATORS: 'iPhone 12 mini,OS=14.4'

pool:
vmImage: $(IMAGE_NAME)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,22 @@ public final class TIMKeychain {
/// - Parameters:
/// - data: Data to save.
/// - item: The item to identify the data.
/// - Returns: `true` if it was a success, otherwise `false`
public static func store(data: Data, item: TIMKeychainStoreItem) -> Bool {
/// - Returns: Result indicating whether it was a success or not.
public static func store(data: Data, item: TIMKeychainStoreItem) -> Result<Void, TIMKeychainError> {
remove(item: item) //Remove before adding to avoid override errors
var mutableParameters = item.parameters
mutableParameters.updateValue(data, forKey: kSecValueData as String)
let status = SecItemAdd(mutableParameters as CFDictionary, nil)
return status == noErr
return mapStoreStatusToResult(status)
}


/// Saves data in the keychain with biometric protection (meaning that only TouchID or FaceID can unlock the access to the data)
/// - Parameters:
/// - data: Data to save.
/// - item: The item to identify the data.
/// - Returns: `true` if it was a success, otherwise `false`
public static func storeBiometricProtected(data: Data, item: TIMKeychainStoreItem) -> Bool {
/// - Returns: Result indicating whether it was a success or not.
public static func storeBiometricProtected(data: Data, item: TIMKeychainStoreItem) -> Result<Void, TIMKeychainError> {
let biometricFlag: SecAccessControlCreateFlags
if #available(iOS 11.3, *) {
biometricFlag = .biometryAny
Expand All @@ -77,44 +77,41 @@ public final class TIMKeychain {
nil
)

let result: Bool
let result: Result<Void, TIMKeychainError>
if let safeAccessControl = sacObject {
var mutableItem = item
mutableItem.enableUseAuthenticationUI(kSecUseAuthenticationUIAllow)
mutableItem.enableSafeAccessControl(safeAccessControl)
result = store(data: data, item: mutableItem)
} else {
result = false
result = .failure(.failedToStoreData)
}
return result
}


/// Gets data from the keychain.
/// - Parameter item: The item that identifies the data (and which is was saved with)
/// - Returns: If there is data for the specified item, it will return the Data object, otherwise `nil`
public static func get(item: TIMKeychainStoreItem) -> Data? {
var result: AnyObject?
/// - Returns: If there is data for the specified item, it will return the Data object, otherwise a failing result with a matching error
public static func get(item: TIMKeychainStoreItem) -> Result<Data, TIMKeychainError> {
var dataResult: AnyObject?
var mutableParameters = item.parameters
mutableParameters.updateValue(kSecMatchLimitOne, forKey: kSecMatchLimit as String)
mutableParameters.updateValue(kCFBooleanTrue as Any, forKey: kSecReturnData as String)

// Search
let status = withUnsafeMutablePointer(to: &result) {
let status = withUnsafeMutablePointer(to: &dataResult) {
SecItemCopyMatching(mutableParameters as CFDictionary, UnsafeMutablePointer($0))
}
if status == noErr, let optData = (result as? Data) {
return optData
} else {
return nil
}

return mapLoadStatusToResult(status, data: dataResult)
}


/// Gets biometric protected data from the keychain - this will prompt the user for biometric verification.
/// - Parameter item: The item that identifies the data (and which is was saved with)
/// - Returns: If there is data for the specified item, it will return the Data object, otherwise `nil`
public static func getBiometricProtected(item: TIMKeychainStoreItem) -> Data? {
public static func getBiometricProtected(item: TIMKeychainStoreItem) -> Result<Data, TIMKeychainError> {
var mutableItem = item
mutableItem.enableUseAuthenticationUI(kSecUseAuthenticationUIAllow)
return get(item: mutableItem)
Expand Down Expand Up @@ -142,4 +139,34 @@ public final class TIMKeychain {
mutableItem.enableUseAuthenticationUI(kSecUseAuthenticationUIFail)
return hasValue(item: mutableItem)
}

static func mapStoreStatusToResult(_ status: OSStatus) -> Result<Void, TIMKeychainError> {
let result: Result<Void, TIMKeychainError>
switch status {
case errSecAuthFailed:
result = .failure(.authenticationFailedForData)
case noErr:
result = .success(Void())
default:
result = .failure(.failedToStoreData)
}
return result
}

static func mapLoadStatusToResult(_ status: OSStatus, data: AnyObject?) -> Result<Data, TIMKeychainError> {
let result: Result<Data, TIMKeychainError>
switch status {
case errSecAuthFailed, errSecUserCanceled:
result = Result.failure(.authenticationFailedForData)
case noErr:
if let optData = (data as? Data) {
result = .success(optData)
} else {
result = .failure(.failedToLoadData)
}
default:
result = .failure(.failedToLoadData)
}
return result
}
}
52 changes: 34 additions & 18 deletions Sources/TIMEncryptedStorage/Models/ErrorModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,32 @@ public enum TIMEncryptedStorageError: Error, LocalizedError {
case invalidEncryptionMethod
case invalidEncryptionKey

// MARK: - Default store / get
case failedToStoreInKeychain
case failedToLoadDataInKeychain

// MARK: - Biometric longSecret store / get
case failedToStoreLongSecretViaBiometric
case failedToLoadLongSecretViaBiometric

// MARK: - KeySever errors
case keyServiceFailed(TIMKeyServiceError)

// MARK: - Keychain errors
case keychainFailed(TIMKeychainError)

// MARK: - Unexpected data from Keychain
case unexpectedData


public var errorDescription: String? {
switch self {
case .failedToEncryptData:
return "Failed to encrypt data with specified key."
case .failedToDecryptData:
return "Failed to decrypt data with specified key."
case .failedToStoreInKeychain:
return "Something went wrong, while saving data to keychain."
case .failedToLoadDataInKeychain:
return "Something went wrong, while loading data from keychain."
case .failedToStoreLongSecretViaBiometric:
return "Something went wrong, while saving the longSecret to keychain (with biometric protection)"
case .failedToLoadLongSecretViaBiometric:
return "Something went wrong, while loading the longSecret from keychain (with biometric protection)"
case .keyServiceFailed(let error):
return "The KeyService failed with error: \(error)"
case .invalidEncryptionMethod:
return "The encryption method is invalid. Did you remember to call the configure method?"
case .invalidEncryptionKey:
return "The encryption key is invalid."
case .keyServiceFailed(let error):
return "The KeyService failed with error: \(error)"
case .keychainFailed(let error):
return "The Keychain failed with error: \(error)"
case .unexpectedData:
return "The Keychain loaded unexpected data. Failed to use the data."
}
}
}
Expand Down Expand Up @@ -83,6 +77,28 @@ public enum TIMKeyServiceError: Error, Equatable, LocalizedError {
}
}

public enum TIMKeychainError : Error, LocalizedError {
/// Failed to store data
case failedToStoreData

/// Failed to load data
case failedToLoadData

/// Authentication failed for data retrieve (e.g. TouchID/FaceID)
case authenticationFailedForData

public var errorDescription: String? {
switch self {
case .authenticationFailedForData:
return "The authentication failed for data, e.g. the user failed to unlock or cancelled the biometric ID prompt."
case .failedToLoadData:
return "Failed to load data from keychain."
case .failedToStoreData:
return "Failed to store data in keychain."
}
}
}

func mapKeyServerError(_ error: Error?) -> TIMKeyServiceError {
guard let err = error else {
return .unknown(nil, nil)
Expand Down
62 changes: 35 additions & 27 deletions Sources/TIMEncryptedStorage/TIM/TIMEncryptedStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,8 @@ public final class TIMEncryptedStorage {

private static func storeLongSecret(keyId: String, longSecret: String) -> Result<Void, TIMEncryptedStorageError> {
let keychainKey = longSecretKeychainId(keyId: keyId)
let success = TIMKeychain.storeBiometricProtected(data: Data(longSecret.utf8), item: TIMKeychainStoreItem(id: keychainKey))
if success {
return .success(())
} else {
return .failure(.failedToStoreLongSecretViaBiometric)
}
let result = TIMKeychain.storeBiometricProtected(data: Data(longSecret.utf8), item: TIMKeychainStoreItem(id: keychainKey))
return result.mapError({ TIMEncryptedStorageError.keychainFailed($0) })
}

private static func handleKeyServerResultAndEncryptData(keyServerResult: Result<TIMKeyModel, TIMKeyServiceError>, id: StoreID, data: Data) -> Result<Void, TIMEncryptedStorageError> {
Expand All @@ -102,12 +98,8 @@ public final class TIMEncryptedStorage {
let result: Result<Void, TIMEncryptedStorageError>
do {
let encryptedData = try keyModel.encrypt(data: data)
let success = TIMKeychain.store(data: encryptedData, item: TIMKeychainStoreItem(id: id))
if success {
result = .success(())
} else {
result = .failure(.failedToStoreInKeychain)
}
let storeResult = TIMKeychain.store(data: encryptedData, item: TIMKeychainStoreItem(id: id))
result = storeResult.mapError({ TIMEncryptedStorageError.keychainFailed($0) })
}
catch let error as TIMEncryptedStorageError {
result = .failure(error)
Expand All @@ -120,7 +112,10 @@ public final class TIMEncryptedStorage {

private static func loadFromKeychainAndDecrypt(id: StoreID, keyModel: TIMKeyModel) -> Result<Data, TIMEncryptedStorageError> {
let result: Result<Data, TIMEncryptedStorageError>
if let encryptedData = TIMKeychain.get(item: TIMKeychainStoreItem(id: id)) {
let loadResult: Result<Data, TIMKeychainError> = TIMKeychain.get(item: TIMKeychainStoreItem(id: id))

switch loadResult {
case .success(let encryptedData):
do {
let decryptedData = try keyModel.decrypt(data: encryptedData)
result = .success(decryptedData)
Expand All @@ -131,9 +126,10 @@ public final class TIMEncryptedStorage {
catch {
result = .failure(.failedToDecryptData)
}
} else {
result = .failure(.failedToLoadDataInKeychain)
case .failure(let keychainError):
result = .failure(.keychainFailed(keychainError))
}

return result
}
}
Expand Down Expand Up @@ -293,11 +289,17 @@ public extension TIMEncryptedStorage {
// 3. Return result of store function

let keychainKey = longSecretKeychainId(keyId: keyId)
if let longSecretData = TIMKeychain.getBiometricProtected(item: TIMKeychainStoreItem(id: keychainKey)),
let longSecret = String(data: longSecretData, encoding: .utf8) {
store(id: id, data: data, keyId: keyId, longSecret: longSecret, completion: completion)
} else {
completion(.failure(.failedToLoadLongSecretViaBiometric))
let loadResult = TIMKeychain.getBiometricProtected(item: TIMKeychainStoreItem(id: keychainKey))

switch loadResult {
case .failure(let keychainError):
completion(.failure(.keychainFailed(keychainError)))
case .success(let longSecretData):
if let longSecret = String(data: longSecretData, encoding: .utf8) {
store(id: id, data: data, keyId: keyId, longSecret: longSecret, completion: completion)
} else {
completion(.failure(.unexpectedData))
}
}
}

Expand Down Expand Up @@ -365,14 +367,20 @@ public extension TIMEncryptedStorage {
// 4. Decrypt data with encryption key
// 5. Return decrypted data + longSecret
let keychainKey = longSecretKeychainId(keyId: keyId)
if let longSecretData = TIMKeychain.getBiometricProtected(item: TIMKeychainStoreItem(id: keychainKey)),
let longSecret = String(data: longSecretData, encoding: .utf8) {
TIMKeyService.getKeyViaLongSecret(longSecret: longSecret, keyId: keyId) { (keyServerResult) in
let result = handleKeyServerResultAndDecryptData(keyServerResult: keyServerResult, id: id)
completion(result.map({ TIMESBiometricLoadResult(data: $0, longSecret: longSecret) }))
let longSecretResult = TIMKeychain.getBiometricProtected(item: TIMKeychainStoreItem(id: keychainKey))

switch longSecretResult {
case .success(let longSecretData):
if let longSecret = String(data: longSecretData, encoding: .utf8) {
TIMKeyService.getKeyViaLongSecret(longSecret: longSecret, keyId: keyId) { (keyServerResult) in
let result = handleKeyServerResultAndDecryptData(keyServerResult: keyServerResult, id: id)
completion(result.map({ TIMESBiometricLoadResult(data: $0, longSecret: longSecret) }))
}
} else {
completion(.failure(.unexpectedData))
}
} else {
completion(.failure(.failedToLoadLongSecretViaBiometric))
case .failure(let keychainError):
completion(.failure(.keychainFailed(keychainError)))
}
}

Expand Down
35 changes: 0 additions & 35 deletions Tests/TIMEncryptedStorageTests/KeychainStoreItemTests.swift

This file was deleted.

Loading

0 comments on commit 5197b5a

Please sign in to comment.