Merge pull request #204 from LukasKorba/157_KeyStoring_protocol

Implementation of KeyStoring protocol
This commit is contained in:
Francisco Gindre 2022-03-23 15:28:10 -03:00 committed by GitHub
commit 7bcb9c5596
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 596 additions and 19 deletions

View File

@ -12,7 +12,6 @@
0D185819272723FF0046B928 /* ColoredChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D185818272723FF0046B928 /* ColoredChip.swift */; };
0D18581B272728D60046B928 /* PhraseChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D18581A272728D60046B928 /* PhraseChip.swift */; };
0D1922F226BDE29300052649 /* ZcashSDKStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */; };
0D1922F826BDEB3500052649 /* MockServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922F726BDEB3500052649 /* MockServices.swift */; };
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1C1AA227611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift */; };
0D2ACE8026C2C67100D62E3C /* Zboto.otf in Resources */ = {isa = PBXBuildFile; fileRef = 0D2ACE7F26C2C67100D62E3C /* Zboto.otf */; };
0D354A0926D5A9D000315F45 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D354A0626D5A9D000315F45 /* Services.swift */; };
@ -82,6 +81,8 @@
66DC733F271D88CC0053CBB6 /* StandardButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66DC733E271D88CC0053CBB6 /* StandardButtonStyle.swift */; };
9E2AC0FF27D8EC120042AA47 /* MnemonicSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9E2AC0FE27D8EC120042AA47 /* MnemonicSwift */; };
9E2AC10127D8EF0B0042AA47 /* MnemonicSeedPhraseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2AC10027D8EF0B0042AA47 /* MnemonicSeedPhraseProvider.swift */; };
9E2AC10327DA28200042AA47 /* RecoveryPhraseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2AC10227DA28200042AA47 /* RecoveryPhraseStorage.swift */; };
9E2AC10627DA34610042AA47 /* RecoveryPhraseStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2AC10527DA34610042AA47 /* RecoveryPhraseStorageTests.swift */; };
9E2DF99C27CF704D00649636 /* ImportWalletStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99827CF704D00649636 /* ImportWalletStore.swift */; };
9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99A27CF704D00649636 /* ImportSeedEditor.swift */; };
9E2DF99E27CF704D00649636 /* ImportWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99B27CF704D00649636 /* ImportWalletView.swift */; };
@ -141,7 +142,6 @@
0D185818272723FF0046B928 /* ColoredChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColoredChip.swift; sourceTree = "<group>"; };
0D18581A272728D60046B928 /* PhraseChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhraseChip.swift; sourceTree = "<group>"; };
0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZcashSDKStubs.swift; sourceTree = "<group>"; };
0D1922F726BDEB3500052649 /* MockServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServices.swift; sourceTree = "<group>"; };
0D1C1AA227611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseDisplayReducerTests.swift; sourceTree = "<group>"; };
0D2ACE7F26C2C67100D62E3C /* Zboto.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Zboto.otf; sourceTree = "<group>"; };
0D354A0626D5A9D000315F45 /* Services.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = "<group>"; };
@ -215,6 +215,8 @@
66D50667271D9B6100E51F0D /* NavigationButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationButtonStyle.swift; sourceTree = "<group>"; };
66DC733E271D88CC0053CBB6 /* StandardButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardButtonStyle.swift; sourceTree = "<group>"; };
9E2AC10027D8EF0B0042AA47 /* MnemonicSeedPhraseProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicSeedPhraseProvider.swift; sourceTree = "<group>"; };
9E2AC10227DA28200042AA47 /* RecoveryPhraseStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseStorage.swift; sourceTree = "<group>"; };
9E2AC10527DA34610042AA47 /* RecoveryPhraseStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseStorageTests.swift; sourceTree = "<group>"; };
9E2DF99827CF704D00649636 /* ImportWalletStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletStore.swift; sourceTree = "<group>"; };
9E2DF99A27CF704D00649636 /* ImportSeedEditor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportSeedEditor.swift; sourceTree = "<group>"; };
9E2DF99B27CF704D00649636 /* ImportWalletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletView.swift; sourceTree = "<group>"; };
@ -315,7 +317,6 @@
isa = PBXGroup;
children = (
0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */,
0D1922F726BDEB3500052649 /* MockServices.swift */,
);
path = Stubs;
sourceTree = "<group>";
@ -410,6 +411,7 @@
0D4E7A1926B364180058B01E /* secantTests */ = {
isa = PBXGroup;
children = (
9E2AC10427DA34450042AA47 /* Util */,
0DFE93E4272CB6D0000FCCA5 /* RecoveryPhraseValidationTests */,
0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */,
6654C7422715A48E00901167 /* OnboardingTests */,
@ -501,6 +503,7 @@
F93673D52742CB840099C6AF /* Previews.swift */,
0D35CC45277A36E00074316A /* ScrollableWhenScaled.swift */,
9E2AC10027D8EF0B0042AA47 /* MnemonicSeedPhraseProvider.swift */,
9E2AC10227DA28200042AA47 /* RecoveryPhraseStorage.swift */,
);
path = Util;
sourceTree = "<group>";
@ -654,6 +657,14 @@
path = CircularFrame;
sourceTree = "<group>";
};
9E2AC10427DA34450042AA47 /* Util */ = {
isa = PBXGroup;
children = (
9E2AC10527DA34610042AA47 /* RecoveryPhraseStorageTests.swift */,
);
path = Util;
sourceTree = "<group>";
};
9E2DF99727CF704D00649636 /* ImportWallet */ = {
isa = PBXGroup;
children = (
@ -1079,7 +1090,6 @@
0DA13CA526C1963000E3B610 /* Balance.swift in Sources */,
2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */,
66D50668271D9B6100E51F0D /* NavigationButtonStyle.swift in Sources */,
0D1922F826BDEB3500052649 /* MockServices.swift in Sources */,
0D3D040A2728B3A10032ABC1 /* RecoveryPhraseDisplayStore.swift in Sources */,
9E2AC10127D8EF0B0042AA47 /* MnemonicSeedPhraseProvider.swift in Sources */,
0D4E7A0B26B364170058B01E /* ContentView.swift in Sources */,
@ -1096,6 +1106,7 @@
663FAB9C271D874D00E495F8 /* ActiveButton.swift in Sources */,
F9C165C02740403600592F76 /* ApproveView.swift in Sources */,
0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */,
9E2AC10327DA28200042AA47 /* RecoveryPhraseStorage.swift in Sources */,
F9971A6B27680E1000A2DB75 /* WalletInfo.swift in Sources */,
0D185819272723FF0046B928 /* ColoredChip.swift in Sources */,
2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */,
@ -1129,6 +1140,7 @@
0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */,
6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */,
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */,
9E2AC10627DA34610042AA47 /* RecoveryPhraseStorageTests.swift in Sources */,
0D4E7A1B26B364180058B01E /* secantTests.swift in Sources */,
0DFE93E6272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift in Sources */,
);

View File

@ -15,8 +15,8 @@
"repositoryURL": "https://github.com/zcash-hackworks/MnemonicSwift",
"state": {
"branch": null,
"revision": "27711179a75a1172d6f04ceb5d86419cf0cba401",
"version": "2.1.0"
"revision": "b10b0b8ee1f297e33ea5b1bc041ced49943b6582",
"version": "2.2.3"
}
},
{

View File

@ -6,33 +6,49 @@
//
import Foundation
import MnemonicSwift
/// Representation of the wallet stored in the persistent storage (typically keychain, handled by `RecoveryPhraseStorage`).
struct StoredWallet: Codable, Equatable {
var birthday: BlockHeight?
let language: MnemonicLanguageType
let seedPhrase: String
let version: Int
}
protocol KeyStoring {
func importBirthday(_ height: BlockHeight) throws
func exportBirthday() throws -> BlockHeight
func importPhrase(bip39 phrase: String) throws
func exportPhrase() throws -> String
/**
Store recovery phrase and optionally even birthday to the secured and persistent storage.
This function creates an instance of `StoredWallet` and automatically handles versioning of the stored data.
*/
func importRecoveryPhrase(bip39 phrase: String, birthday: BlockHeight?, language: MnemonicLanguageType) throws
/**
Load the representation of the wallet from the persistent and secured storage.
*/
func exportWallet() throws -> StoredWallet
/**
Check if the wallet representation `StoredWallet` is present in the persistent storage.
*/
func areKeysPresent() throws -> Bool
/**
Use carefully: Deletes the seed phrase from the keychain
*/
func nukePhrase()
/**
Use carefully: deletes the wallet birthday from the keychain
Update the birthday in the securely stored wallet.
*/
func nukeBirthday()
func updateBirthday(_ height: BlockHeight) throws
/**
There's no fate but what we make for ourselves - Sarah Connor
Use carefully: deletes the stored wallet.
There's no fate but what we make for ourselves - Sarah Connor.
*/
func nukeWallet()
var keysPresent: Bool { get }
}
enum KeyStoringError: Error {
case alreadyImported
case uninitializedWallet
case storageError(Error)
case unsupportedVersion(Int)
case unsupportedLanguage(MnemonicLanguageType)
}

View File

@ -0,0 +1,254 @@
//
// RecoveryPhraseStorage.swift
// secant-testnet
//
// Created by Lukáš Korba on 03/10/2022.
//
import Foundation
import MnemonicSwift
/// 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:
/// https://developer.apple.com/documentation/security/certificate_key_and_trust_services/working_with_concurrency?language=objc
// swiftlint:disable convenience_type
final class RecoveryPhraseStorage {
enum Constants {
static let zcashStoredWallet = "zcashStoredWallet"
/// Versioning of the stored data
static let zcashKeychainVersion = 1
}
enum KeychainError: Error, Equatable {
case decoding
case duplicate
case encoding
case noDataFound
case unknown(OSStatus)
case unsupportedVersion(Int)
case unsupportedLanguage(MnemonicLanguageType)
}
// MARK: - Codable helpers
static func decode<T: Decodable>(json: Data, as clazz: T.Type) throws -> T? {
do {
let decoder = JSONDecoder()
let data = try decoder.decode(T.self, from: json)
return data
} catch {
throw KeychainError.decoding
}
}
static func encode<T: Codable>(object: T) throws -> Data? {
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
return try encoder.encode(object)
} catch {
throw KeychainError.encoding
}
}
// MARK: - Query Helpers
static func baseQuery(forAccount account: String = "", andKey forKey: String) -> [String: Any] {
let query:[ String: AnyObject ] = [
/// Uniquely identify this keychain accessor
kSecAttrService as String: forKey as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword,
/// The data in the keychain item can be accessed only while the device is unlocked by the user.
/// This is recommended for items that need to be accessible only while the application is in the foreground.
/// Items with this attribute do not migrate to a new device.
/// Thus, after restoring from a backup of a different device, these items will not be present.
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
return query
}
static func restoreQuery(forAccount account: String = "", andKey forKey: String) -> [String: Any] {
var query = baseQuery(forAccount: account, andKey: forKey)
query[kSecMatchLimit as String] = kSecMatchLimitOne
query[kSecReturnData as String] = kCFBooleanTrue
query[kSecReturnRef as String] = kCFBooleanFalse
query[kSecReturnPersistentRef as String] = kCFBooleanFalse
query[kSecReturnAttributes as String] = kCFBooleanFalse
return query
}
}
// MARK: - Recovery Phrase Helper Functions
private extension RecoveryPhraseStorage {
func setWallet(_ wallet: StoredWallet) throws {
guard let data = try RecoveryPhraseStorage.encode(object: wallet) else {
throw KeychainError.encoding
}
try setData(data, forKey: Constants.zcashStoredWallet)
}
func wallet() throws -> StoredWallet {
guard let data = data(forKey: Constants.zcashStoredWallet) else {
throw KeychainError.noDataFound
}
guard let wallet = try RecoveryPhraseStorage.decode(json: data, as: StoredWallet.self) else {
throw KeychainError.decoding
}
guard wallet.version == Constants.zcashKeychainVersion else {
throw KeychainError.unsupportedVersion(wallet.version)
}
return wallet
}
/// Use carefully: Deletes seed phrase from the keychain!!!
@discardableResult
func deleteWallet() -> Bool {
deleteData(forKey: Constants.zcashStoredWallet)
}
/// Restore data for key
func data(
forKey: String,
account: String = ""
) -> Data? {
let query = RecoveryPhraseStorage.restoreQuery(forAccount: account, andKey: forKey)
var result: AnyObject?
_ = SecItemCopyMatching(query as CFDictionary, &result)
return result as? Data
}
/// Use carefully: Deletes data for key
func deleteData(
forKey: String,
account: String = ""
) -> Bool {
let query = RecoveryPhraseStorage.baseQuery(forAccount: account, andKey: forKey)
let status = SecItemDelete(query as CFDictionary)
return status == noErr
}
/// Store data for key
func setData(
_ data: Data,
forKey: String,
account: String = ""
) throws {
var query = RecoveryPhraseStorage.baseQuery(forAccount: account, andKey: forKey)
query[kSecValueData as String] = data as AnyObject
let status = SecItemAdd(query as CFDictionary, nil)
guard status != errSecDuplicateItem else {
throw KeychainError.duplicate
}
guard status == errSecSuccess else {
throw KeychainError.unknown(status)
}
}
/// Use carefully: Update data for key
func updateData(
_ data: Data,
forKey: String,
account: String = ""
) throws {
let query = RecoveryPhraseStorage.baseQuery(forAccount: account, andKey: forKey)
let attributes:[ String: AnyObject ] = [
kSecValueData as String: data as AnyObject
]
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard status != errSecItemNotFound else {
throw KeychainError.noDataFound
}
guard status == errSecSuccess else {
throw KeychainError.unknown(status)
}
}
}
// MARK: - KeyStoring
extension RecoveryPhraseStorage: KeyStoring {
func importRecoveryPhrase(bip39 phrase: String, birthday: BlockHeight?, language: MnemonicLanguageType = .english) throws {
// Future-proof of the bundle to potentialy avoid migration. We enforce english mnemonic.
guard language == .english else {
throw KeyStoringError.unsupportedLanguage(language)
}
do {
let wallet = StoredWallet(
birthday: birthday,
language: language,
seedPhrase: phrase,
version: Constants.zcashKeychainVersion
)
try setWallet(wallet)
} catch KeychainError.duplicate {
throw KeyStoringError.alreadyImported
} catch {
throw KeyStoringError.storageError(error)
}
}
func exportWallet() throws -> StoredWallet {
do {
return try wallet()
} catch KeychainError.noDataFound, KeychainError.decoding {
throw KeyStoringError.uninitializedWallet
} catch KeychainError.unsupportedVersion(let version) {
throw KeyStoringError.unsupportedVersion(version)
} catch {
throw KeyStoringError.storageError(error)
}
}
func areKeysPresent() throws -> Bool {
do {
_ = try exportWallet()
} catch KeyStoringError.uninitializedWallet {
return false
} catch {
// TODO: - report & log error.localizedDescription
return false
}
return true
}
func updateBirthday(_ height: BlockHeight) throws {
do {
var wallet = try exportWallet()
wallet.birthday = height
guard let data = try RecoveryPhraseStorage.encode(object: wallet) else {
throw KeychainError.encoding
}
try updateData(data, forKey: Constants.zcashStoredWallet)
} catch {
throw error
}
}
func nukeWallet() {
deleteWallet()
}
}

View File

@ -0,0 +1,295 @@
//
// RecoveryPhraseStorageTests.swift
// secantTests
//
// Created by Lukáš Korba on 10.03.2022.
//
import XCTest
import MnemonicSwift
@testable import secant_testnet
extension KeyStoringError {
var debugValue: String {
switch self {
case .alreadyImported: return "alreadyImported"
case .uninitializedWallet: return "uninitializedWallet"
case .storageError: return "storageError"
case .unsupportedVersion: return "unsupportedVersion"
case .unsupportedLanguage: return "unsupportedLanguage"
}
}
}
class RecoveryPhraseStorageTests: XCTestCase {
let birthday = BlockHeight(12345678)
let seedPhrase = "one two three"
let language = MnemonicLanguageType.english
var storage: RecoveryPhraseStorage?
override func setUp() {
super.setUp()
storage = RecoveryPhraseStorage()
deleteData(forKey: RecoveryPhraseStorage.Constants.zcashStoredWallet)
}
override func tearDown() {
super.tearDown()
storage = nil
}
func testWalletStoredSucessfuly() throws {
do {
try storage?.importRecoveryPhrase(bip39: seedPhrase, birthday: birthday)
guard let data = data(forKey: RecoveryPhraseStorage.Constants.zcashStoredWallet) else {
return XCTFail("Keychain: no data found for key: `zcashStoredWallet`.")
}
guard let walletReceived = try RecoveryPhraseStorage.decode(json: data, as: StoredWallet.self) else {
return XCTFail("Keychain: `walletReceived` can't be decoded.")
}
XCTAssertEqual(birthday, walletReceived.birthday, "Keychain: stored birthday and retrieved one must be the same.")
XCTAssertEqual(seedPhrase, walletReceived.seedPhrase, "Keychain: stored seed phrase and retrieved one must be the same.")
} catch let err {
XCTFail("Keychain: no error is expected for `testWalletStoredSucessfuly` but received. \(err)")
}
}
func testWalletDuplicate() throws {
do {
try storage?.importRecoveryPhrase(bip39: seedPhrase, birthday: birthday)
try storage?.importRecoveryPhrase(bip39: seedPhrase, birthday: birthday)
XCTFail("Keychain: `testRecoveryPhraseDuplicate` is expected to throw a `duplicate` error but passed instead.")
} catch {
guard let error = error as? KeyStoringError else {
XCTFail("Keychain: the error is expected to be KeyStoringError but it's \(error).")
return
}
XCTAssertEqual(
error.debugValue,
KeyStoringError.alreadyImported.debugValue,
"Keychain: error must be .alreadyImported but it's \(error)."
)
}
}
func testUninitializedWallet() throws {
do {
_ = try storage?.exportWallet()
XCTFail("Keychain: `testUninitializedWallet` should fail but received some wallet.")
} catch {
guard let error = error as? KeyStoringError else {
return XCTFail("Keychain: the error is expected to be KeyStoringError but it's \(error).")
}
XCTAssertEqual(error.debugValue, KeyStoringError.uninitializedWallet.debugValue, "Keychain: error must be .uninitializedWallet")
}
}
func testDeleteWallet() throws {
do {
let wallet = StoredWallet(
birthday: birthday,
language: language,
seedPhrase: seedPhrase,
version: RecoveryPhraseStorage.Constants.zcashKeychainVersion
)
guard let walletData = try RecoveryPhraseStorage.encode(object: wallet) else {
return XCTFail("`testDeleteWallet` encoding `walletData` failed.")
}
do {
try setData(walletData, forKey: RecoveryPhraseStorage.Constants.zcashStoredWallet)
} catch {
XCTFail("`testDeleteWallet` storing `walletData` failed.")
}
storage?.nukeWallet()
let data = data(forKey: RecoveryPhraseStorage.Constants.zcashStoredWallet)
XCTAssertEqual(data, nil, "Keychain: keychain is expected to not find anything for key `zcashStoredWallet` but received some data.")
}
}
func testUpdateBirthdayOverNil() throws {
let wallet = StoredWallet(
birthday: nil,
language: language,
seedPhrase: seedPhrase,
version: RecoveryPhraseStorage.Constants.zcashKeychainVersion
)
guard let walletData = try RecoveryPhraseStorage.encode(object: wallet) else {
return XCTFail("`testUpdateBirthdayOverNil` encoding `walletData` failed.")
}
do {
try setData(walletData, forKey: RecoveryPhraseStorage.Constants.zcashStoredWallet)
} catch {
XCTFail("`testUpdateBirthdayOverNil` storing `walletData` failed.")
}
do {
try storage?.updateBirthday(birthday)
guard let data = data(forKey: RecoveryPhraseStorage.Constants.zcashStoredWallet) else {
return XCTFail("Keychain: no data found for key: `zcashStoredWallet`.")
}
guard let walletReceived = try RecoveryPhraseStorage.decode(json: data, as: StoredWallet.self) else {
return XCTFail("Keychain: `walletReceived` can't be decoded.")
}
XCTAssertEqual(birthday, walletReceived.birthday, "Keychain: stored birthday and retrieved one must be the same.")
XCTAssertEqual(seedPhrase, walletReceived.seedPhrase, "Keychain: stored seed phrase and retrieved one must be the same.")
} catch let err {
XCTFail("Keychain: no error is expected for `testUpdateBirthdayOverNil` but received. \(err)")
}
}
func testUpdateBirthdayOverSomeBirthday() throws {
let wallet = StoredWallet(
birthday: birthday,
language: language,
seedPhrase: seedPhrase,
version: RecoveryPhraseStorage.Constants.zcashKeychainVersion
)
let newBirthday = BlockHeight(87654321)
guard let walletData = try RecoveryPhraseStorage.encode(object: wallet) else {
return XCTFail("`testUpdateBirthdayOverSomeBirthday` encoding `walletData` failed.")
}
do {
try setData(walletData, forKey: RecoveryPhraseStorage.Constants.zcashStoredWallet)
} catch {
XCTFail("`testUpdateBirthdayOverSomeBirthday` storing `walletData` failed.")
}
do {
try storage?.updateBirthday(newBirthday)
guard let data = data(forKey: RecoveryPhraseStorage.Constants.zcashStoredWallet) else {
return XCTFail("Keychain: no data found for key: `zcashStoredWallet`.")
}
guard let walletReceived = try RecoveryPhraseStorage.decode(json: data, as: StoredWallet.self) else {
return XCTFail("Keychain: `walletReceived` can't be decoded.")
}
XCTAssertEqual(newBirthday, walletReceived.birthday, "Keychain: stored birthday and retrieved one must be the same.")
XCTAssertEqual(seedPhrase, walletReceived.seedPhrase, "Keychain: stored seed phrase and retrieved one must be the same.")
} catch let err {
XCTFail("Keychain: no error is expected for `testUpdateBirthdayOverNil` but received. \(err)")
}
}
func testUnsupportedVersion() throws {
let wallet = StoredWallet(
birthday: birthday,
language: language,
seedPhrase: seedPhrase,
/// older version
version: RecoveryPhraseStorage.Constants.zcashKeychainVersion - 1
)
guard let walletData = try RecoveryPhraseStorage.encode(object: wallet) else {
return XCTFail("`testUnsupportedVersion` encoding `walletData` failed.")
}
do {
try setData(walletData, forKey: RecoveryPhraseStorage.Constants.zcashStoredWallet)
} catch {
XCTFail("`testUnsupportedVersion` storing `walletData` failed.")
}
do {
_ = try storage?.exportWallet()
XCTFail("Keychain: `testUnsupportedVersion` should fail but received some wallet with correct version.")
} catch KeyStoringError.unsupportedVersion(let version) {
XCTAssertEqual(
version + 1,
RecoveryPhraseStorage.Constants.zcashKeychainVersion,
"Keychain: version should be \(RecoveryPhraseStorage.Constants.zcashKeychainVersion) but stored version is \(version)"
)
} catch {
XCTFail("Keychain: `testUnsupportedVersion` should fail with `unsupportedVersion` error but threw \(error).")
}
}
func testUnsupportedLanguage() throws {
do {
try storage?.importRecoveryPhrase(bip39: seedPhrase, birthday: birthday, language: .chinese)
XCTFail("Keychain: `testUnsupportedLanguage` should fail but imported chinese language.")
} catch KeyStoringError.unsupportedLanguage(let languageToStore) {
XCTAssertEqual(
MnemonicLanguageType.chinese,
languageToStore,
"Keychain: language should be english but received \(languageToStore)"
)
} catch {
XCTFail("Keychain: `testUnsupportedLanguage` should fail with `unsupportedLanguage` error but threw \(error).")
}
}
}
// MARK: - Misc
/// The followings methods are here purposely to not rely on `RecoveryPhraseStorage` in order to test functionality of JUST ONE method at a time
private extension RecoveryPhraseStorageTests {
private func setData(
account: String = "",
_ data: Data,
forKey: String
) throws {
var query = RecoveryPhraseStorage.baseQuery(forAccount: account, andKey: forKey)
query[kSecValueData as String] = data as AnyObject
SecItemAdd(query as CFDictionary, nil)
}
private func data(
forKey: String,
account: String = ""
) -> Data? {
let query = RecoveryPhraseStorage.restoreQuery(forAccount: account, andKey: forKey)
var result: AnyObject?
_ = SecItemCopyMatching(query as CFDictionary, &result)
return result as? Data
}
@discardableResult
private func deleteData(
forKey: String,
account: String = ""
) -> Bool {
let query = RecoveryPhraseStorage.baseQuery(forAccount: account, andKey: forKey)
let status = SecItemDelete(query as CFDictionary)
return status == noErr
}
func updateData(
_ data: Data,
forKey: String,
account: String = ""
) throws {
let query = RecoveryPhraseStorage.baseQuery(forAccount: account, andKey: forKey)
let attributes:[ String: AnyObject ] = [
kSecValueData as String: data as AnyObject
]
_ = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
}
}