Merge pull request #204 from LukasKorba/157_KeyStoring_protocol
Implementation of KeyStoring protocol
This commit is contained in:
commit
7bcb9c5596
|
@ -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 */,
|
||||
);
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue