// Copyright SIX DAY LLC. All rights reserved. import BigInt import Foundation import Result import KeychainSwift import CryptoSwift import TrustKeystore enum EtherKeystoreError: LocalizedError { case protectionDisabled } open class EtherKeystore: Keystore { struct Keys { static let recentlyUsedAddress: String = "recentlyUsedAddress" static let watchAddresses = "watchAddresses" } private let keychain: KeychainSwift private let datadir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] let keyStore: KeyStore private let defaultKeychainAccess: KeychainSwiftAccessOptions = .accessibleWhenUnlockedThisDeviceOnly let keystoreDirectory: URL let userDefaults: UserDefaults public init( keychain: KeychainSwift = KeychainSwift(keyPrefix: Constants.keychainKeyPrefix), keyStoreSubfolder: String = "/keystore", userDefaults: UserDefaults = UserDefaults.standard ) throws { if !UIApplication.shared.isProtectedDataAvailable { throw EtherKeystoreError.protectionDisabled } self.keystoreDirectory = URL(fileURLWithPath: datadir + keyStoreSubfolder) self.keychain = keychain self.keychain.synchronizable = false self.keyStore = try KeyStore(keydir: keystoreDirectory) self.userDefaults = userDefaults } var hasWallets: Bool { return !wallets.isEmpty } private var watchAddresses: [String] { set { let data = NSKeyedArchiver.archivedData(withRootObject: newValue) return userDefaults.set(data, forKey: Keys.watchAddresses) } get { guard let data = userDefaults.data(forKey: Keys.watchAddresses) else { return [] } return NSKeyedUnarchiver.unarchiveObject(with: data) as? [String] ?? [] } } var recentlyUsedWallet: Wallet? { set { keychain.set(newValue?.address.description ?? "", forKey: Keys.recentlyUsedAddress, withAccess: defaultKeychainAccess) } get { let address = keychain.get(Keys.recentlyUsedAddress) return wallets.filter { $0.address.description == address }.first } } static var current: Wallet? { do { return try EtherKeystore().recentlyUsedWallet } catch { return .none } } // Async @available(iOS 10.0, *) func createAccount(with password: String, completion: @escaping (Result) -> Void) { DispatchQueue.global(qos: .userInitiated).async { let account = self.createAccout(password: password) DispatchQueue.main.async { completion(.success(account)) } } } func importWallet(type: ImportType, completion: @escaping (Result) -> Void) { let newPassword = PasswordGenerator.generateRandom() switch type { case .keystore(let string, let password): importKeystore( value: string, password: password, newPassword: newPassword ) { result in switch result { case .success(let account): completion(.success(Wallet(type: .real(account)))) case .failure(let error): completion(.failure(error)) } } case .privateKey(let privateKey): keystore(for: privateKey, password: newPassword) { result in switch result { case .success(let value): self.importKeystore( value: value, password: newPassword, newPassword: newPassword ) { result in switch result { case .success(let account): completion(.success(Wallet(type: .real(account)))) case .failure(let error): completion(.failure(error)) } } case .failure(let error): completion(.failure(error)) } } case .mnemonic: let key = "" // TODO: Implement it keystore(for: key, password: newPassword) { result in switch result { case .success(let value): self.importKeystore( value: value, password: newPassword, newPassword: newPassword ) { result in switch result { case .success(let account): completion(.success(Wallet(type: .real(account)))) case .failure(let error): completion(.failure(error)) } } case .failure(let error): completion(.failure(error)) } } case .watch(let address): self.watchAddresses = [watchAddresses, [address.description]].flatMap { $0 } completion(.success(Wallet(type: .watch(address)))) } } func keystore(for privateKey: String, password: String, completion: @escaping (Result) -> Void) { DispatchQueue.global(qos: .userInitiated).async { let keystore = self.convertPrivateKeyToKeystoreFile( privateKey: privateKey, passphrase: password ) DispatchQueue.main.async { switch keystore { case .success(let result): completion(.success(result.jsonString ?? "")) case .failure(let error): completion(.failure(error)) } } } } func importKeystore(value: String, password: String, newPassword: String, completion: @escaping (Result) -> Void) { DispatchQueue.global(qos: .userInitiated).async { let result = self.importKeystore(value: value, password: password, newPassword: newPassword) DispatchQueue.main.async { switch result { case .success(let account): completion(.success(account)) case .failure(let error): completion(.failure(error)) } } } } func createAccout(password: String) -> Account { let account = try! keyStore.createAccount(password: password) let _ = setPassword(password, for: account) return account } func importKeystore(value: String, password: String, newPassword: String) -> Result { guard let data = value.data(using: .utf8) else { return (.failure(.failedToParseJSON)) } do { let account = try keyStore.import(json: data, password: password, newPassword: newPassword) let _ = setPassword(newPassword, for: account) return .success(account) } catch { if case KeyStore.Error.accountAlreadyExists = error { return .failure(.duplicateAccount) } else { return .failure(.failedToImport(error)) } } } var wallets: [Wallet] { let addresses = watchAddresses.flatMap { Address(string: $0) } return [ keyStore.accounts.map { Wallet(type: .real($0)) }, addresses.map { Wallet(type: .watch($0)) }, ].flatMap { $0 } } func export(account: Account, password: String, newPassword: String) -> Result { let result = self.exportData(account: account, password: password, newPassword: newPassword) switch result { case .success(let data): let string = String(data: data, encoding: .utf8) ?? "" return .success(string) case .failure(let error): return .failure(error) } } func export(account: Account, password: String, newPassword: String, completion: @escaping (Result) -> Void) { DispatchQueue.global(qos: .userInitiated).async { let result = self.export(account: account, password: password, newPassword: newPassword) DispatchQueue.main.async { completion(result) } } } func exportData(account: Account, password: String, newPassword: String) -> Result { guard let account = getAccount(for: account.address) else { return .failure(.accountNotFound) } do { let data = try keyStore.export(account: account, password: password, newPassword: newPassword) return (.success(data)) } catch { return (.failure(.failedToDecryptKey)) } } func delete(wallet: Wallet) -> Result { switch wallet.type { case .real(let account): guard let account = getAccount(for: account.address) else { return .failure(.accountNotFound) } guard let password = getPassword(for: account) else { return .failure(.failedToDeleteAccount) } do { try keyStore.delete(account: account, password: password) return .success(()) } catch { return .failure(.failedToDeleteAccount) } case .watch(let address): watchAddresses = watchAddresses.filter { $0 != address.description } return .success(()) } } func delete(wallet: Wallet, completion: @escaping (Result) -> Void) { DispatchQueue.global(qos: .userInitiated).async { let result = self.delete(wallet: wallet) DispatchQueue.main.async { completion(result) } } } func updateAccount(account: Account, password: String, newPassword: String) -> Result { guard let account = getAccount(for: account.address) else { return .failure(.accountNotFound) } do { try keyStore.update(account: account, password: password, newPassword: newPassword) return .success(()) } catch { return .failure(.failedToUpdatePassword) } } func signMessage(message: String, account: Account) -> Result { let formattedMessage: String = "\u{19}Ethereum Signed Message:\n" + "\(message.count)" + message guard let hash = formattedMessage.data(using: .utf8)?.sha3(.keccak256), let password = getPassword(for: account) else { return .failure(KeystoreError.failedToSignMessage) } do { var data = try keyStore.signHash(hash, account: account, password: password) // TODO: Make it configurable, instead of overriding last byte. data[64] += 27 return .success(data) } catch { return .failure(KeystoreError.failedToSignMessage) } } func signTransaction(_ transaction: SignTransaction) -> Result { guard let account = keyStore.account(for: transaction.account.address) else { return .failure(.failedToSignTransaction) } guard let password = getPassword(for: account) else { return .failure(.failedToSignTransaction) } let signer: Signer if transaction.chainID == 0 { signer = HomesteadSigner() } else { signer = EIP155Signer(chainId: BigInt(transaction.chainID)) } do { let hash = signer.hash(transaction: transaction) let signature = try keyStore.signHash(hash, account: account, password: password) let (r, s, v) = signer.values(transaction: transaction, signature: signature) let data = RLP.encode([ transaction.nonce, transaction.gasPrice, transaction.gasLimit, transaction.to?.data ?? Data(), transaction.value, transaction.data, v, r, s, ])! return .success(data) } catch { return .failure(.failedToSignTransaction) } } func getPassword(for account: Account) -> String? { return keychain.get(account.address.description.lowercased()) } @discardableResult func setPassword(_ password: String, for account: Account) -> Bool { return keychain.set(password, forKey: account.address.description.lowercased(), withAccess: defaultKeychainAccess) } func getAccount(for address: Address) -> Account? { return keyStore.account(for: address) } func convertPrivateKeyToKeystoreFile(privateKey: String, passphrase: String) -> Result<[String: Any], KeystoreError> { guard let data = Data(hexString: privateKey) else { return .failure(KeystoreError.failedToImportPrivateKey) } do { let key = try Key(password: passphrase, key: data) let data = try JSONEncoder().encode(key) let dict = try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] return .success(dict) } catch { return .failure(KeystoreError.failedToImportPrivateKey) } } }