Merge pull request #264 from LukasKorba/231_wrapped_keychain
Wrapped Security and Keychain handling
This commit is contained in:
commit
88e31f6b14
|
@ -100,6 +100,8 @@
|
|||
9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */; };
|
||||
9E69A24D27FB002800A55317 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* Welcome.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 */; };
|
||||
9ECAE56827FC713C0089A0EF /* DatabaseFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ECAE56727FC713C0089A0EF /* DatabaseFiles.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -727,6 +731,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
9E02B56927FED43E005B809B /* WrappedFileManager.swift */,
|
||||
9EAFEB83280597B700199FC9 /* WrappedSecItem.swift */,
|
||||
);
|
||||
path = Wrappers;
|
||||
sourceTree = "<group>";
|
||||
|
@ -761,6 +766,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */,
|
||||
9EAFEB852805A23100199FC9 /* WrappedSecItemTests.swift */,
|
||||
9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */,
|
||||
9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */,
|
||||
);
|
||||
|
@ -1193,6 +1199,7 @@
|
|||
663FAB9C271D874D00E495F8 /* ActiveButton.swift in Sources */,
|
||||
F9C165C02740403600592F76 /* ApproveView.swift in Sources */,
|
||||
0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */,
|
||||
9EAFEB84280597B700199FC9 /* WrappedSecItem.swift in Sources */,
|
||||
9E2AC10327DA28200042AA47 /* WalletStorage.swift in Sources */,
|
||||
9ECAE56827FC713C0089A0EF /* DatabaseFiles.swift in Sources */,
|
||||
F9971A6B27680E1000A2DB75 /* WalletInfo.swift in Sources */,
|
||||
|
@ -1233,6 +1240,7 @@
|
|||
files = (
|
||||
0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */,
|
||||
6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */,
|
||||
9EAFEB862805A23100199FC9 /* WrappedSecItemTests.swift in Sources */,
|
||||
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */,
|
||||
0D4E7A1B26B364180058B01E /* secantTests.swift in Sources */,
|
||||
0DFE93E6272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift in Sources */,
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import Foundation
|
||||
import MnemonicSwift
|
||||
import Security
|
||||
|
||||
/// 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:
|
||||
|
@ -35,6 +34,12 @@ struct WalletStorage {
|
|||
case unsupportedLanguage(MnemonicLanguageType)
|
||||
}
|
||||
|
||||
private let secItem: WrappedSecItem
|
||||
|
||||
init(secItem: WrappedSecItem) {
|
||||
self.secItem = secItem
|
||||
}
|
||||
|
||||
func importWallet(
|
||||
bip39 phrase: String,
|
||||
birthday: BlockHeight?,
|
||||
|
@ -176,11 +181,7 @@ struct WalletStorage {
|
|||
|
||||
return query
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Wallet Storage Helper Functions
|
||||
|
||||
private extension WalletStorage {
|
||||
/// Restore data for key
|
||||
func data(
|
||||
forKey: String,
|
||||
|
@ -189,7 +190,7 @@ private extension WalletStorage {
|
|||
let query = restoreQuery(forAccount: account, andKey: forKey)
|
||||
|
||||
var result: AnyObject?
|
||||
_ = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
_ = secItem.copyMatching(query as CFDictionary, &result)
|
||||
|
||||
return result as? Data
|
||||
}
|
||||
|
@ -202,7 +203,7 @@ private extension WalletStorage {
|
|||
) -> Bool {
|
||||
let query = baseQuery(forAccount: account, andKey: forKey)
|
||||
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
let status = secItem.delete(query as CFDictionary)
|
||||
|
||||
return status == noErr
|
||||
}
|
||||
|
@ -216,7 +217,8 @@ private extension WalletStorage {
|
|||
var query = baseQuery(forAccount: account, andKey: forKey)
|
||||
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 {
|
||||
throw KeychainError.duplicate
|
||||
|
@ -239,7 +241,7 @@ private extension WalletStorage {
|
|||
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 {
|
||||
throw KeychainError.noDataFound
|
||||
|
|
|
@ -76,7 +76,7 @@ struct WalletStorageInteractor {
|
|||
}
|
||||
|
||||
extension WalletStorageInteractor {
|
||||
public static func live(walletStorage: WalletStorage = WalletStorage()) -> Self {
|
||||
public static func live(walletStorage: WalletStorage = WalletStorage(secItem: .live)) -> Self {
|
||||
Self(
|
||||
importWallet: { bip39, birthday, language, hasUserPassedPhraseBackupTest in
|
||||
try walletStorage.importWallet(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
|
@ -25,7 +25,7 @@ class WalletStorageTests: XCTestCase {
|
|||
let birthday = BlockHeight(12345678)
|
||||
let seedPhrase = "one two three"
|
||||
let language = MnemonicLanguageType.english
|
||||
var storage = WalletStorage()
|
||||
var storage = WalletStorage(secItem: .live)
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue