Wrapped Security and Keychain handling

This commit is contained in:
Lukas Korba 2022-04-12 14:32:58 +02:00
parent 6ed490b232
commit c22e18b07e
6 changed files with 223 additions and 11 deletions

View File

@ -100,6 +100,8 @@
9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */; }; 9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */; };
9E69A24D27FB002800A55317 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* Welcome.swift */; }; 9E69A24D27FB002800A55317 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* Welcome.swift */; };
9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */; }; 9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */; };
9EAFEB84280597B700199FC9 /* WrappedSecItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB83280597B700199FC9 /* WrappedSecItem.swift */; };
9EAFEB862805A23100199FC9 /* WrappedSecItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB852805A23100199FC9 /* WrappedSecItemTests.swift */; };
9EBEF87A27CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */; }; 9EBEF87A27CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */; };
9ECAE56827FC713C0089A0EF /* DatabaseFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */; }; 9ECAE56827FC713C0089A0EF /* DatabaseFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */; };
9EF8135C27ECC25E0075AF48 /* WalletStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */; }; 9EF8135C27ECC25E0075AF48 /* WalletStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */; };
@ -252,6 +254,8 @@
9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecantButtonStyles.swift; sourceTree = "<group>"; }; 9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecantButtonStyles.swift; sourceTree = "<group>"; };
9E69A24C27FB002800A55317 /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = "<group>"; }; 9E69A24C27FB002800A55317 /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = "<group>"; };
9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorage.swift; sourceTree = "<group>"; }; 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorage.swift; sourceTree = "<group>"; };
9EAFEB83280597B700199FC9 /* WrappedSecItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedSecItem.swift; sourceTree = "<group>"; };
9EAFEB852805A23100199FC9 /* WrappedSecItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedSecItemTests.swift; sourceTree = "<group>"; };
9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseTestPreambleView.swift; sourceTree = "<group>"; }; 9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseTestPreambleView.swift; sourceTree = "<group>"; };
9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFiles.swift; sourceTree = "<group>"; }; 9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFiles.swift; sourceTree = "<group>"; };
9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletStorageTests.swift; sourceTree = "<group>"; }; 9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletStorageTests.swift; sourceTree = "<group>"; };
@ -727,6 +731,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
9E02B56927FED43E005B809B /* WrappedFileManager.swift */, 9E02B56927FED43E005B809B /* WrappedFileManager.swift */,
9EAFEB83280597B700199FC9 /* WrappedSecItem.swift */,
); );
path = Wrappers; path = Wrappers;
sourceTree = "<group>"; sourceTree = "<group>";
@ -761,6 +766,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */, 9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */,
9EAFEB852805A23100199FC9 /* WrappedSecItemTests.swift */,
9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */, 9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */,
9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */, 9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */,
); );
@ -1193,6 +1199,7 @@
663FAB9C271D874D00E495F8 /* ActiveButton.swift in Sources */, 663FAB9C271D874D00E495F8 /* ActiveButton.swift in Sources */,
F9C165C02740403600592F76 /* ApproveView.swift in Sources */, F9C165C02740403600592F76 /* ApproveView.swift in Sources */,
0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */, 0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */,
9EAFEB84280597B700199FC9 /* WrappedSecItem.swift in Sources */,
9E2AC10327DA28200042AA47 /* WalletStorage.swift in Sources */, 9E2AC10327DA28200042AA47 /* WalletStorage.swift in Sources */,
9ECAE56827FC713C0089A0EF /* DatabaseFiles.swift in Sources */, 9ECAE56827FC713C0089A0EF /* DatabaseFiles.swift in Sources */,
F9971A6B27680E1000A2DB75 /* WalletInfo.swift in Sources */, F9971A6B27680E1000A2DB75 /* WalletInfo.swift in Sources */,
@ -1233,6 +1240,7 @@
files = ( files = (
0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */, 0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */,
6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */, 6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */,
9EAFEB862805A23100199FC9 /* WrappedSecItemTests.swift in Sources */,
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */, 0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */,
0D4E7A1B26B364180058B01E /* secantTests.swift in Sources */, 0D4E7A1B26B364180058B01E /* secantTests.swift in Sources */,
0DFE93E6272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift in Sources */, 0DFE93E6272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift in Sources */,

View File

@ -7,7 +7,6 @@
import Foundation import Foundation
import MnemonicSwift import MnemonicSwift
import Security
/// Zcash implementation of the keychain that is not universal but designed to deliver functionality needed by the wallet itself. /// Zcash implementation of the keychain that is not universal but designed to deliver functionality needed by the wallet itself.
/// All the APIs should be thread safe according to official doc: /// All the APIs should be thread safe according to official doc:
@ -35,6 +34,12 @@ struct WalletStorage {
case unsupportedLanguage(MnemonicLanguageType) case unsupportedLanguage(MnemonicLanguageType)
} }
private let secItem: WrappedSecItem
init(secItem: WrappedSecItem) {
self.secItem = secItem
}
func importWallet( func importWallet(
bip39 phrase: String, bip39 phrase: String,
birthday: BlockHeight?, birthday: BlockHeight?,
@ -176,11 +181,7 @@ struct WalletStorage {
return query return query
} }
}
// MARK: - Wallet Storage Helper Functions
private extension WalletStorage {
/// Restore data for key /// Restore data for key
func data( func data(
forKey: String, forKey: String,
@ -189,7 +190,7 @@ private extension WalletStorage {
let query = restoreQuery(forAccount: account, andKey: forKey) let query = restoreQuery(forAccount: account, andKey: forKey)
var result: AnyObject? var result: AnyObject?
_ = SecItemCopyMatching(query as CFDictionary, &result) _ = secItem.copyMatching(query as CFDictionary, &result)
return result as? Data return result as? Data
} }
@ -202,7 +203,7 @@ private extension WalletStorage {
) -> Bool { ) -> Bool {
let query = baseQuery(forAccount: account, andKey: forKey) let query = baseQuery(forAccount: account, andKey: forKey)
let status = SecItemDelete(query as CFDictionary) let status = secItem.delete(query as CFDictionary)
return status == noErr return status == noErr
} }
@ -216,7 +217,8 @@ private extension WalletStorage {
var query = baseQuery(forAccount: account, andKey: forKey) var query = baseQuery(forAccount: account, andKey: forKey)
query[kSecValueData as String] = data as AnyObject query[kSecValueData as String] = data as AnyObject
let status = SecItemAdd(query as CFDictionary, nil) var result: AnyObject?
let status = secItem.add(query as CFDictionary, &result)
guard status != errSecDuplicateItem else { guard status != errSecDuplicateItem else {
throw KeychainError.duplicate throw KeychainError.duplicate
@ -239,7 +241,7 @@ private extension WalletStorage {
kSecValueData as String: data as AnyObject kSecValueData as String: data as AnyObject
] ]
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) let status = secItem.update(query as CFDictionary, attributes as CFDictionary)
guard status != errSecItemNotFound else { guard status != errSecItemNotFound else {
throw KeychainError.noDataFound throw KeychainError.noDataFound

View File

@ -76,7 +76,7 @@ struct WalletStorageInteractor {
} }
extension WalletStorageInteractor { extension WalletStorageInteractor {
public static func live(walletStorage: WalletStorage = WalletStorage()) -> Self { public static func live(walletStorage: WalletStorage = WalletStorage(secItem: .live)) -> Self {
Self( Self(
importWallet: { bip39, birthday, language, hasUserPassedPhraseBackupTest in importWallet: { bip39, birthday, language, hasUserPassedPhraseBackupTest in
try walletStorage.importWallet( try walletStorage.importWallet(

View File

@ -0,0 +1,33 @@
//
// WrappedSecItem.swift
// secant-testnet
//
// Created by Lukáš Korba on 12.04.2022.
//
import Foundation
import Security
struct WrappedSecItem {
let copyMatching: (CFDictionary, inout CFTypeRef?) -> OSStatus
let add: (CFDictionary, inout CFTypeRef?) -> OSStatus
let update: (CFDictionary, CFDictionary) -> OSStatus
let delete: (CFDictionary) -> OSStatus
}
extension WrappedSecItem {
static let live = WrappedSecItem(
copyMatching: { query, result in
SecItemCopyMatching(query, &result)
},
add: { attributes, result in
SecItemAdd(attributes, &result)
},
update: { query, attributesToUpdate in
SecItemUpdate(query, attributesToUpdate)
},
delete: { query in
SecItemDelete(query)
}
)
}

View File

@ -25,7 +25,7 @@ class WalletStorageTests: XCTestCase {
let birthday = BlockHeight(12345678) let birthday = BlockHeight(12345678)
let seedPhrase = "one two three" let seedPhrase = "one two three"
let language = MnemonicLanguageType.english let language = MnemonicLanguageType.english
var storage = WalletStorage() var storage = WalletStorage(secItem: .live)
override func setUp() { override func setUp() {
super.setUp() super.setUp()

View File

@ -0,0 +1,169 @@
//
// WrappedSecItemTests.swift
// secantTests
//
// Created by Lukáš Korba on 12.04.2022.
//
import XCTest
@testable import secant_testnet
extension WalletStorage.KeychainError {
var debugValue: String {
switch self {
case .decoding: return "decoding"
case .duplicate: return "duplicate"
case .encoding: return "encoding"
case .noDataFound: return "noDataFound"
case .unknown: return "unknown"
}
}
}
class WrappedSecItemTests: XCTestCase {
func test_secItemAdd_KeychainErrorDuplicate() throws {
let secItemDuplicate = WrappedSecItem(
copyMatching: { _, _ in errSecSuccess },
add: { _, _ in errSecDuplicateItem },
update: { _, _ in errSecSuccess },
delete: { _ in errSecSuccess }
)
let walletStorage = WalletStorage(secItem: secItemDuplicate)
do {
try walletStorage.setData(Data(), forKey: "")
XCTFail("WrappedSecItem: test_secItemAdd_KeychainErrorDuplicate expected to fail but passed.")
} catch {
guard let error = error as? WalletStorage.KeychainError else {
XCTFail("WrappedSecItem: the error is expected to be WalletStorage.KeychainError but it's \(error).")
return
}
XCTAssertEqual(
error.debugValue,
WalletStorage.KeychainError.duplicate.debugValue,
"WrappedSecItem: error must be .duplicate but it's \(error)."
)
}
}
func test_secItemAdd_KeychainErrorUnknown() throws {
let secItemDuplicate = WrappedSecItem(
copyMatching: { _, _ in errSecSuccess },
add: { _, _ in errSecCoreFoundationUnknown },
update: { _, _ in errSecSuccess },
delete: { _ in errSecSuccess }
)
let walletStorage = WalletStorage(secItem: secItemDuplicate)
do {
try walletStorage.setData(Data(), forKey: "")
XCTFail("WrappedSecItem: test_secItemAdd_KeychainErrorUnknown expected to fail but passed.")
} catch {
guard let error = error as? WalletStorage.KeychainError else {
XCTFail("WrappedSecItem: the error is expected to be WalletStorage.KeychainError but it's \(error).")
return
}
XCTAssertEqual(
error.debugValue,
WalletStorage.KeychainError.unknown(0).debugValue,
"WrappedSecItem: error must be .unknown but it's \(error)."
)
}
}
func test_secItemUpdate_KeychainErrorNoDataFound() throws {
let secItemDuplicate = WrappedSecItem(
copyMatching: { _, _ in errSecSuccess },
add: { _, _ in errSecSuccess },
update: { _, _ in errSecItemNotFound },
delete: { _ in errSecSuccess }
)
let walletStorage = WalletStorage(secItem: secItemDuplicate)
do {
try walletStorage.updateData(Data(), forKey: "")
XCTFail("WrappedSecItem: test_secItemUpdate_KeychainErrorNoDataFound expected to fail but passed.")
} catch {
guard let error = error as? WalletStorage.KeychainError else {
XCTFail("WrappedSecItem: the error is expected to be WalletStorage.KeychainError but it's \(error).")
return
}
XCTAssertEqual(
error.debugValue,
WalletStorage.KeychainError.noDataFound.debugValue,
"WrappedSecItem: error must be .noDataFound but it's \(error)."
)
}
}
func test_secItemUpdate_KeychainErrorUnknown() throws {
let secItemDuplicate = WrappedSecItem(
copyMatching: { _, _ in errSecSuccess },
add: { _, _ in errSecSuccess },
update: { _, _ in errSecCoreFoundationUnknown },
delete: { _ in errSecSuccess }
)
let walletStorage = WalletStorage(secItem: secItemDuplicate)
do {
try walletStorage.updateData(Data(), forKey: "")
XCTFail("WrappedSecItem: test_secItemUpdate_KeychainErrorUnknown expected to fail but passed.")
} catch {
guard let error = error as? WalletStorage.KeychainError else {
XCTFail("WrappedSecItem: the error is expected to be WalletStorage.KeychainError but it's \(error).")
return
}
XCTAssertEqual(
error.debugValue,
WalletStorage.KeychainError.unknown(0).debugValue,
"WrappedSecItem: error must be .unknown but it's \(error)."
)
}
}
func test_secItemDelete_Succeeded() throws {
let secItemDuplicate = WrappedSecItem(
copyMatching: { _, _ in errSecSuccess },
add: { _, _ in errSecSuccess },
update: { _, _ in errSecSuccess },
delete: { _ in noErr }
)
let walletStorage = WalletStorage(secItem: secItemDuplicate)
let result = walletStorage.deleteData(forKey: "")
XCTAssertTrue(result)
}
func test_secItemDelete_Failed() throws {
let secItemDuplicate = WrappedSecItem(
copyMatching: { _, _ in errSecSuccess },
add: { _, _ in errSecSuccess },
update: { _, _ in errSecSuccess },
delete: { _ in errSecCoreFoundationUnknown }
)
let walletStorage = WalletStorage(secItem: secItemDuplicate)
let result = walletStorage.deleteData(forKey: "")
XCTAssertFalse(result)
}
}