Complete keychain migration

This commit is contained in:
Ivan Grachyov 2021-08-01 19:42:27 +03:00
parent 61ac099c9f
commit 2af469ad35
9 changed files with 255 additions and 176 deletions

View File

@ -11,7 +11,6 @@ class Agent: NSObject {
private override init() { super.init() }
private var statusBarItem: NSStatusItem!
private let accountsService = AccountsService.shared
private var hasPassword = Keychain.shared.password != nil
private var didEnterPasswordOnStart = false
@ -72,9 +71,9 @@ class Agent: NSObject {
}
let windowController = Window.showNew()
let completion = onSelectedAccount(session: session)
let completion = onSelectedWallet(session: session)
let accountsList = instantiate(AccountsListViewController.self)
accountsList.onSelectedAccount = completion
accountsList.onSelectedWallet = completion
windowController.contentViewController = accountsList
}
@ -120,9 +119,9 @@ class Agent: NSObject {
showInitialScreen(wcSession: session)
}
func getAccountSelectionCompletionIfShouldSelect() -> ((LegacyAccountWithKey) -> Void)? {
func getWalletSelectionCompletionIfShouldSelect() -> ((InkWallet) -> Void)? {
let session = getSessionFromPasteboard()
return onSelectedAccount(session: session)
return onSelectedWallet(session: session)
}
lazy private var statusBarMenu: NSMenu = {
@ -223,10 +222,10 @@ class Agent: NSObject {
}
}
private func onSelectedAccount(session: WCSession?) -> ((LegacyAccountWithKey) -> Void)? {
private func onSelectedWallet(session: WCSession?) -> ((InkWallet) -> Void)? {
guard let session = session else { return nil }
return { [weak self] account in
self?.connectWallet(session: session, account: account)
return { [weak self] wallet in
self?.connectWallet(session: session, wallet: wallet)
}
}
@ -285,12 +284,12 @@ class Agent: NSObject {
}
}
private func connectWallet(session: WCSession, account: LegacyAccountWithKey) {
private func connectWallet(session: WCSession, wallet: InkWallet) {
let windowController = Window.showNew()
let window = windowController.window
windowController.contentViewController = WaitingViewController.withReason("Connecting")
WalletConnect.shared.connect(session: session, address: account.address) { [weak window] _ in
WalletConnect.shared.connect(session: session, walletId: wallet.id) { [weak window] _ in
if window?.isVisible == true {
Window.closeAllAndActivateBrowser()
}

View File

@ -6,9 +6,10 @@ import CryptoSwift
struct Ethereum {
enum Errors: Error {
enum Error: Swift.Error {
case invalidInputData
case failedToSendTransaction
case keyNotFound
}
private let queue = DispatchQueue(label: "Ethereum", qos: .default)
@ -21,8 +22,9 @@ struct Ethereum {
apiKey: Secrets.alchemy
)
func sign(message: String, account: LegacyAccountWithKey) throws -> String {
let ethPrivateKey = EthPrivateKey(hex: account.privateKey)
func sign(message: String, wallet: InkWallet) throws -> String {
guard let privateKeyString = wallet.ethereumPrivateKey else { throw Error.keyNotFound }
let ethPrivateKey = EthPrivateKey(hex: privateKeyString)
let signature = SECP256k1Signature(
privateKey: ethPrivateKey,
@ -39,32 +41,35 @@ struct Ethereum {
return data.toPrefixedHexString()
}
func signPersonal(message: String, account: LegacyAccountWithKey) throws -> String {
let ethPrivateKey = EthPrivateKey(hex: account.privateKey)
func signPersonal(message: String, wallet: InkWallet) throws -> String {
guard let privateKeyString = wallet.ethereumPrivateKey else { throw Error.keyNotFound }
let ethPrivateKey = EthPrivateKey(hex: privateKeyString)
let signed = SignedPersonalMessageBytes(message: message, signerKey: ethPrivateKey)
let data = try signed.value().toPrefixedHexString()
return data
}
func sign(typedData: String, account: LegacyAccountWithKey) throws -> String {
func sign(typedData: String, wallet: InkWallet) throws -> String {
guard let privateKeyString = wallet.ethereumPrivateKey else { throw Error.keyNotFound }
let data = try EIP712TypedData(jsonString: typedData)
let hash = EIP712Hash(domain: data.domain, typedData: data)
let privateKey = EthPrivateKey(hex: account.privateKey)
let privateKey = EthPrivateKey(hex: privateKeyString)
let signer = EIP712Signer(privateKey: privateKey)
return try signer.signatureData(hash: hash).toPrefixedHexString()
}
func send(transaction: Transaction, account: LegacyAccountWithKey) throws -> String {
let bytes = signedTransactionBytes(transaction: transaction, account: account)
func send(transaction: Transaction, wallet: InkWallet) throws -> String {
let bytes = try signedTransactionBytes(transaction: transaction, wallet: wallet)
let response = try SendRawTransactionProcedure(network: network, transactionBytes: bytes).call()
guard let hash = response["result"].string else {
throw Errors.failedToSendTransaction
throw Error.failedToSendTransaction
}
return hash
}
private func signedTransactionBytes(transaction: Transaction, account: LegacyAccountWithKey) -> EthContractCallBytes {
let senderKey = EthPrivateKey(hex: account.privateKey)
private func signedTransactionBytes(transaction: Transaction, wallet: InkWallet) throws -> EthContractCallBytes {
guard let privateKeyString = wallet.ethereumPrivateKey else { throw Error.keyNotFound }
let senderKey = EthPrivateKey(hex: privateKeyString)
let contractAddress = EthAddress(hex: transaction.to)
let functionCall = BytesFromHexString(hex: transaction.data)
let bytes: EthContractCallBytes

View File

@ -5,14 +5,13 @@ import Cocoa
class AccountsListViewController: NSViewController {
private let agent = Agent.shared
private let accountsService = AccountsService.shared
private var accounts = [LegacyAccountWithKey]()
private let walletsManager = WalletsManager.shared
private var cellModels = [CellModel]()
var onSelectedAccount: ((LegacyAccountWithKey) -> Void)?
var onSelectedWallet: ((InkWallet) -> Void)?
enum CellModel {
case account(LegacyAccountWithKey)
case wallet(InkWallet)
case addAccountOption(AddAccountOption)
}
@ -45,11 +44,14 @@ class AccountsListViewController: NSViewController {
}
}
private var wallets: [InkWallet] {
return walletsManager.wallets
}
override func viewDidLoad() {
super.viewDidLoad()
setupAccountsMenu()
reloadAccounts()
reloadTitle()
updateCellModels()
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: NSApplication.didBecomeActiveNotification, object: nil)
@ -61,30 +63,26 @@ class AccountsListViewController: NSViewController {
menu.addItem(NSMenuItem(title: "Copy address", action: #selector(didClickCopyAddress(_:)), keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "View on Zerion", action: #selector(didClickViewOnZerion(_:)), keyEquivalent: ""))
menu.addItem(.separator())
menu.addItem(NSMenuItem(title: "Show private key", action: #selector(didClickExportAccount(_:)), keyEquivalent: "")) // TODO: show different texts for secret words export
menu.addItem(NSMenuItem(title: "Show account key", action: #selector(didClickExportAccount(_:)), keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "Remove account", action: #selector(didClickRemoveAccount(_:)), keyEquivalent: ""))
menu.addItem(.separator())
menu.addItem(NSMenuItem(title: "How to WalletConnect?", action: #selector(showInstructionsAlert), keyEquivalent: ""))
tableView.menu = menu
}
private func reloadAccounts() {
accounts = accountsService.getAccounts()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
private func reloadTitle() {
titleLabel.stringValue = onSelectedAccount != nil && !accounts.isEmpty ? "Select\nAccount" : "Accounts"
addButton.isHidden = accounts.isEmpty
titleLabel.stringValue = onSelectedWallet != nil && !wallets.isEmpty ? "Select\nAccount" : "Accounts"
addButton.isHidden = wallets.isEmpty
}
@objc private func didBecomeActive() {
guard view.window?.isVisible == true else { return }
if let completion = agent.getAccountSelectionCompletionIfShouldSelect() {
onSelectedAccount = completion
if let completion = agent.getWalletSelectionCompletionIfShouldSelect() {
onSelectedWallet = completion
}
reloadTitle()
}
@ -107,8 +105,7 @@ class AccountsListViewController: NSViewController {
}
@objc private func didClickCreateAccount() {
accountsService.createAccount()
reloadAccounts()
_ = try? walletsManager.createWallet()
reloadTitle()
updateCellModels()
tableView.reloadData()
@ -117,14 +114,13 @@ class AccountsListViewController: NSViewController {
@objc private func didClickImportAccount() {
let importViewController = instantiate(ImportViewController.self)
importViewController.onSelectedAccount = onSelectedAccount
importViewController.onSelectedWallet = onSelectedWallet
view.window?.contentViewController = importViewController
}
@objc private func didClickViewOnZerion(_ sender: AnyObject) {
let row = tableView.deselectedRow
guard row >= 0 else { return }
let address = accounts[row].address
guard row >= 0, let address = wallets[row].ethereumAddress else { return }
if let url = URL(string: "https://app.zerion.io/\(address)/overview") {
NSWorkspace.shared.open(url)
}
@ -132,8 +128,8 @@ class AccountsListViewController: NSViewController {
@objc private func didClickCopyAddress(_ sender: AnyObject) {
let row = tableView.deselectedRow
guard row >= 0 else { return }
NSPasteboard.general.clearAndSetString(accounts[row].address)
guard row >= 0, let address = wallets[row].ethereumAddress else { return }
NSPasteboard.general.clearAndSetString(address)
}
@objc private func didClickRemoveAccount(_ sender: AnyObject) {
@ -155,34 +151,46 @@ class AccountsListViewController: NSViewController {
}
@objc private func didClickExportAccount(_ sender: AnyObject) {
// TODO: show different texts for secret words export
let row = tableView.deselectedRow
guard row >= 0 else { return }
let isMnemonic = wallets[row].isMnemonic
let alert = Alert()
alert.messageText = "Private key gives full access to your funds."
alert.messageText = "\(isMnemonic ? "Secret words give" : "Private key gives") full access to your funds."
alert.alertStyle = .critical
alert.addButton(withTitle: "I understand the risks")
alert.addButton(withTitle: "Cancel")
if alert.runModal() == .alertFirstButtonReturn {
agent.askAuthentication(on: view.window, getBackTo: self, onStart: false, reason: "Show private key") { [weak self] allowed in
let reason = "Show \(isMnemonic ? "secret words" : "private key")"
agent.askAuthentication(on: view.window, getBackTo: self, onStart: false, reason: reason) { [weak self] allowed in
Window.activateWindow(self?.view.window)
if allowed {
self?.showPrivateKey(index: row)
self?.showKey(index: row, mnemonic: isMnemonic)
}
}
}
}
private func showPrivateKey(index: Int) {
let privateKey = accounts[index].privateKey
private func showKey(index: Int, mnemonic: Bool) {
let wallet = wallets[index]
let secret: String
if mnemonic, let mnemonicString = try? walletsManager.exportMnemonic(wallet: wallet) {
secret = mnemonicString
} else if let data = try? walletsManager.exportPrivateKey(wallet: wallet) {
secret = data.hexString
} else {
return
}
let alert = Alert()
alert.messageText = "Private key"
alert.informativeText = privateKey
alert.messageText = mnemonic ? "Secret words" : "Private key"
alert.informativeText = secret
alert.alertStyle = .informational
alert.addButton(withTitle: "OK")
alert.addButton(withTitle: "Copy")
if alert.runModal() != .alertFirstButtonReturn {
NSPasteboard.general.clearAndSetString(privateKey)
NSPasteboard.general.clearAndSetString(secret)
}
}
@ -191,19 +199,19 @@ class AccountsListViewController: NSViewController {
}
private func removeAccountAtIndex(_ index: Int) {
accountsService.removeAccount(accounts[index])
accounts.remove(at: index)
let wallet = wallets[index]
try? walletsManager.delete(wallet: wallet)
reloadTitle()
updateCellModels()
tableView.reloadData()
}
private func updateCellModels() {
if accounts.isEmpty {
if wallets.isEmpty {
cellModels = [.addAccountOption(.createNew), .addAccountOption(.importExisting)]
tableView.shouldShowRightClickMenu = false
} else {
cellModels = accounts.map { .account($0) }
cellModels = wallets.map { .wallet($0) }
tableView.shouldShowRightClickMenu = true
}
}
@ -217,9 +225,9 @@ extension AccountsListViewController: NSTableViewDelegate {
let model = cellModels[row]
switch model {
case let .account(account):
if let onSelectedAccount = onSelectedAccount {
onSelectedAccount(account)
case let .wallet(wallet):
if let onSelectedWallet = onSelectedWallet {
onSelectedWallet(wallet)
} else {
Timer.scheduledTimer(withTimeInterval: 0.01, repeats: false) { [weak self] _ in
var point = NSEvent.mouseLocation
@ -246,9 +254,9 @@ extension AccountsListViewController: NSTableViewDataSource {
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
let model = cellModels[row]
switch model {
case let .account(account):
case let .wallet(wallet):
let rowView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("AccountCellView"), owner: self) as? AccountCellView
rowView?.setup(address: account.address)
rowView?.setup(address: wallet.ethereumAddress ?? "")
return rowView
case let .addAccountOption(addAccountOption):
let rowView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("AddAccountOptionCellView"), owner: self) as? AddAccountOptionCellView
@ -258,7 +266,7 @@ extension AccountsListViewController: NSTableViewDataSource {
}
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
if case .account = cellModels[row] {
if case .wallet = cellModels[row] {
return 50
} else {
return 44

View File

@ -4,9 +4,9 @@ import Cocoa
class ImportViewController: NSViewController {
private let accountsService = AccountsService.shared
var onSelectedAccount: ((LegacyAccountWithKey) -> Void)?
private var inputValidationResult = AccountsService.InputValidationResult.invalid
private let walletsManager = WalletsManager.shared
var onSelectedWallet: ((InkWallet) -> Void)?
private var inputValidationResult = WalletsManager.InputValidationResult.invalid
@IBOutlet weak var textField: NSTextField! {
didSet {
@ -51,16 +51,17 @@ class ImportViewController: NSViewController {
}
private func importWith(input: String, password: String?) {
if accountsService.addAccount(input: input, password: password) != nil {
do {
_ = try walletsManager.addWallet(input: input, inputPassword: password)
showAccountsList()
} else {
} catch {
Alert.showWithMessage("Failed to import account", style: .critical)
}
}
private func showAccountsList() {
let accountsListViewController = instantiate(AccountsListViewController.self)
accountsListViewController.onSelectedAccount = onSelectedAccount
accountsListViewController.onSelectedWallet = onSelectedWallet
view.window?.contentViewController = accountsListViewController
}
@ -73,7 +74,7 @@ class ImportViewController: NSViewController {
extension ImportViewController: NSTextFieldDelegate {
func controlTextDidChange(_ obj: Notification) {
inputValidationResult = accountsService.validateAccountInput(textField.stringValue)
inputValidationResult = walletsManager.validateWalletInput(textField.stringValue)
okButton.isEnabled = inputValidationResult != .invalid
}

View File

@ -7,7 +7,7 @@ class SessionStorage {
struct Item: Codable {
let session: WCSession
let address: String
let walletId: String
let clientId: String
let sessionDetails: WCSessionRequestParam
}
@ -51,8 +51,8 @@ class SessionStorage {
}
}
func add(interactor: WCInteractor, address: String, sessionDetails: WCSessionRequestParam) {
let item = Item(session: interactor.session, address: address, clientId: interactor.clientId, sessionDetails: sessionDetails)
func add(interactor: WCInteractor, walletId: String, sessionDetails: WCSessionRequestParam) {
let item = Item(session: interactor.session, walletId: walletId, clientId: interactor.clientId, sessionDetails: sessionDetails)
WCSessionStore.store(interactor.session, peerId: sessionDetails.peerId, peerMeta: sessionDetails.peerMeta)
Defaults.storedSessions[interactor.clientId] = item
didInteractWith(clientId: interactor.clientId)

View File

@ -15,7 +15,7 @@ class AccountCellView: NSTableRowView {
@IBOutlet weak var addressTextField: NSTextField!
func setup(address: String) {
addressImageView.image = Blockies(seed: address).createImage()
addressImageView.image = Blockies(seed: address.lowercased()).createImage()
let without0x = address.dropFirst(2)
addressTextField.stringValue = without0x.prefix(4) + "..." + without0x.suffix(4)
}

View File

@ -8,7 +8,7 @@ class WalletConnect {
private let sessionStorage = SessionStorage.shared
private let networkMonitor = NetworkMonitor.shared
private let ethereum = Ethereum.shared
private let accountsService = AccountsService.shared
private let walletsManager = WalletsManager.shared
static let shared = WalletConnect()
@ -28,10 +28,10 @@ class WalletConnect {
return WCSession.from(string: link)
}
func connect(session: WCSession, address: String, uuid: UUID = UUID(), completion: @escaping ((Bool) -> Void)) {
func connect(session: WCSession, walletId: String, uuid: UUID = UUID(), completion: @escaping ((Bool) -> Void)) {
let clientMeta = WCPeerMeta(name: "Encrypted Ink", url: "https://encrypted.ink", description: "Ethereum agent for macOS", icons: ["https://encrypted.ink/icon.png"])
let interactor = WCInteractor(session: session, meta: clientMeta, uuid: uuid)
configure(interactor: interactor, address: address)
configure(interactor: interactor, walletId: walletId)
interactor.connect().done { connected in
completion(connected)
@ -49,7 +49,7 @@ class WalletConnect {
for item in items {
guard let uuid = UUID(uuidString: item.clientId) else { continue }
connect(session: item.session, address: item.address, uuid: uuid) { _ in }
connect(session: item.session, walletId: item.walletId, uuid: uuid) { _ in }
peers[item.clientId] = item.sessionDetails.peerMeta
}
}
@ -87,7 +87,8 @@ class WalletConnect {
return peers[id]
}
private func configure(interactor: WCInteractor, address: String) {
private func configure(interactor: WCInteractor, walletId: String) {
guard let address = walletsManager.getWallet(id: walletId)?.ethereumAddress else { return }
let accounts = [address]
let chainId = 1
@ -96,7 +97,7 @@ class WalletConnect {
interactor.onSessionRequest = { [weak self, weak interactor] (id, peerParam) in
guard let interactor = interactor else { return }
self?.peers[interactor.clientId] = peerParam.peerMeta
self?.sessionStorage.add(interactor: interactor, address: address, sessionDetails: peerParam)
self?.sessionStorage.add(interactor: interactor, walletId: walletId, sessionDetails: peerParam)
interactor.approveSession(accounts: accounts, chainId: chainId).cauterize()
}
@ -107,12 +108,12 @@ class WalletConnect {
}
interactor.eth.onSign = { [weak self, weak interactor] (id, payload) in
self?.approveSign(id: id, payload: payload, address: address, interactor: interactor)
self?.approveSign(id: id, payload: payload, walletId: walletId, interactor: interactor)
self?.sessionStorage.didInteractWith(clientId: interactor?.clientId)
}
interactor.eth.onTransaction = { [weak self, weak interactor] (id, event, transaction) in
self?.approveTransaction(id: id, wct: transaction, address: address, interactor: interactor)
self?.approveTransaction(id: id, wct: transaction, walletId: walletId, interactor: interactor)
self?.sessionStorage.didInteractWith(clientId: interactor?.clientId)
}
}
@ -128,7 +129,7 @@ class WalletConnect {
}
}
private func approveTransaction(id: Int64, wct: WCEthereumTransaction, address: String, interactor: WCInteractor?) {
private func approveTransaction(id: Int64, wct: WCEthereumTransaction, walletId: String, interactor: WCInteractor?) {
guard let to = wct.to else {
rejectRequest(id: id, interactor: interactor, message: "Something went wrong.")
return
@ -138,14 +139,14 @@ class WalletConnect {
let transaction = Transaction(from: wct.from, to: to, nonce: wct.nonce, gasPrice: wct.gasPrice, gas: wct.gas, value: wct.value, data: wct.data)
Agent.shared.showApprove(transaction: transaction, peerMeta: peer) { [weak self, weak interactor] transaction in
if let transaction = transaction {
self?.sendTransaction(transaction, address: address, requestId: id, interactor: interactor)
self?.sendTransaction(transaction, walletId: walletId, requestId: id, interactor: interactor)
} else {
self?.rejectRequest(id: id, interactor: interactor, message: "Cancelled")
}
}
}
private func approveSign(id: Int64, payload: WCEthereumSignPayload, address: String, interactor: WCInteractor?) {
private func approveSign(id: Int64, payload: WCEthereumSignPayload, walletId: String, interactor: WCInteractor?) {
var message: String?
let title: String
switch payload {
@ -165,7 +166,7 @@ class WalletConnect {
let peer = getPeerOfInteractor(interactor)
Agent.shared.showApprove(title: title, meta: message ?? "", peerMeta: peer) { [weak self, weak interactor] approved in
if approved {
self?.sign(id: id, message: message, payload: payload, address: address, interactor: interactor)
self?.sign(id: id, message: message, payload: payload, walletId: walletId, interactor: interactor)
} else {
self?.rejectRequest(id: id, interactor: interactor, message: "Cancelled")
}
@ -176,31 +177,31 @@ class WalletConnect {
interactor?.rejectRequest(id: id, message: message).cauterize()
}
private func sendTransaction(_ transaction: Transaction, address: String, requestId: Int64, interactor: WCInteractor?) {
guard let account = accountsService.getAccountForAddress(address) else {
private func sendTransaction(_ transaction: Transaction, walletId: String, requestId: Int64, interactor: WCInteractor?) {
guard let wallet = walletsManager.getWallet(id: walletId) else {
rejectRequest(id: requestId, interactor: interactor, message: "Something went wrong.")
return
}
guard let hash = try? ethereum.send(transaction: transaction, account: account) else {
guard let hash = try? ethereum.send(transaction: transaction, wallet: wallet) else {
rejectRequest(id: requestId, interactor: interactor, message: "Failed to send")
return
}
interactor?.approveRequest(id: requestId, result: hash).cauterize()
}
private func sign(id: Int64, message: String?, payload: WCEthereumSignPayload, address: String, interactor: WCInteractor?) {
guard let message = message, let account = accountsService.getAccountForAddress(address) else {
private func sign(id: Int64, message: String?, payload: WCEthereumSignPayload, walletId: String, interactor: WCInteractor?) {
guard let message = message, let wallet = walletsManager.getWallet(id: walletId) else {
rejectRequest(id: id, interactor: interactor, message: "Something went wrong.")
return
}
var signed: String?
switch payload {
case .personalSign:
signed = try? ethereum.signPersonal(message: message, account: account)
signed = try? ethereum.signPersonal(message: message, wallet: wallet)
case .signTypeData:
signed = try? ethereum.sign(typedData: message, account: account)
signed = try? ethereum.sign(typedData: message, wallet: wallet)
case .sign:
signed = try? ethereum.sign(message: message, account: account)
signed = try? ethereum.sign(message: message, wallet: wallet)
}
guard let result = signed else {
rejectRequest(id: id, interactor: interactor, message: "Something went wrong.")

View File

@ -43,3 +43,21 @@ final class InkWallet: Hashable, Equatable {
}
}
extension InkWallet {
var ethereumAddress: String? {
return accounts.first(where: { $0.coin == .ethereum })?.address
}
var ethereumPrivateKey: String? {
guard let password = Keychain.shared.password,
let privateKey = try? privateKey(password: password, coin: .ethereum).data else { return nil }
return privateKey.hexString
}
var isMnemonic: Bool {
return key.isMnemonic
}
}

View File

@ -6,6 +6,15 @@ import WalletCore
final class WalletsManager {
enum Error: Swift.Error {
case keychainAccessFailure
case invalidInput
}
enum InputValidationResult {
case valid, invalid, requiresPassword
}
static let shared = WalletsManager()
private let keychain = Keychain.shared
private(set) var wallets = [InkWallet]()
@ -17,6 +26,127 @@ final class WalletsManager {
try? migrateFromLegacyIfNeeded()
}
func validateWalletInput(_ input: String) -> InputValidationResult {
if Mnemonic.isValid(mnemonic: input) {
return .valid
} else if let data = Data(hexString: input) {
return PrivateKey.isValid(data: data, curve: CoinType.ethereum.curve) ? .valid : .invalid
} else {
return input.maybeJSON ? .requiresPassword : .invalid
}
}
func createWallet() throws -> InkWallet {
guard let password = keychain.password else { throw Error.keychainAccessFailure }
let name = "Wallet \(wallets.count + 1)" // TODO: finalize naming convention
return try createWallet(name: name, password: password, coin: .ethereum)
}
func getWallet(id: String) -> InkWallet? {
return wallets.first(where: { $0.id == id })
}
func addWallet(input: String, inputPassword: String?) throws -> InkWallet {
guard let password = keychain.password else { throw Error.keychainAccessFailure }
let name = "Wallet \(wallets.count + 1)" // TODO: finalize naming convention
let coin = CoinType.ethereum
if Mnemonic.isValid(mnemonic: input) {
return try importMnemonic(input, name: name, encryptPassword: password, coin: coin)
} else if let data = Data(hexString: input), let privateKey = PrivateKey(data: data) {
return try importPrivateKey(privateKey, name: name, password: password, coin: coin)
} else if input.maybeJSON, let inputPassword = inputPassword, let json = input.data(using: .utf8) {
return try importJSON(json, name: name, password: inputPassword, newPassword: password, coin: coin)
} else {
throw Error.invalidInput
}
}
private func createWallet(name: String, password: String, coin: CoinType) throws -> InkWallet {
let key = StoredKey(name: name, password: Data(password.utf8))
let id = makeNewWalletId()
let wallet = InkWallet(id: id, key: key)
_ = try wallet.getAccount(password: password, coin: coin)
wallets.append(wallet)
try save(wallet: wallet)
return wallet
}
private func importJSON(_ json: Data, name: String, password: String, newPassword: String, coin: CoinType) throws -> InkWallet {
guard let key = StoredKey.importJSON(json: json) else { throw KeyStore.Error.invalidKey }
guard let data = key.decryptPrivateKey(password: Data(password.utf8)) else { throw KeyStore.Error.invalidPassword }
if let mnemonic = checkMnemonic(data) { return try self.importMnemonic(mnemonic, name: name, encryptPassword: newPassword, coin: coin) }
guard let privateKey = PrivateKey(data: data) else { throw KeyStore.Error.invalidKey }
return try self.importPrivateKey(privateKey, name: name, password: newPassword, coin: coin)
}
private func checkMnemonic(_ data: Data) -> String? {
guard let mnemonic = String(data: data, encoding: .ascii), Mnemonic.isValid(mnemonic: mnemonic) else { return nil }
return mnemonic
}
private func importPrivateKey(_ privateKey: PrivateKey, name: String, password: String, coin: CoinType) throws -> InkWallet {
guard let newKey = StoredKey.importPrivateKey(privateKey: privateKey.data, name: name, password: Data(password.utf8), coin: coin) else { throw KeyStore.Error.invalidKey }
let id = makeNewWalletId()
let wallet = InkWallet(id: id, key: newKey)
_ = try wallet.getAccount(password: password, coin: coin)
wallets.append(wallet)
try save(wallet: wallet)
return wallet
}
private func importMnemonic(_ mnemonic: String, name: String, encryptPassword: String, coin: CoinType) throws -> InkWallet {
guard let key = StoredKey.importHDWallet(mnemonic: mnemonic, name: name, password: Data(encryptPassword.utf8), coin: coin) else { throw KeyStore.Error.invalidMnemonic }
let id = makeNewWalletId()
let wallet = InkWallet(id: id, key: key)
_ = try wallet.getAccount(password: encryptPassword, coin: coin)
wallets.append(wallet)
try save(wallet: wallet)
return wallet
}
func exportPrivateKey(wallet: InkWallet) throws -> Data {
guard let password = keychain.password else { throw Error.keychainAccessFailure }
guard let key = wallet.key.decryptPrivateKey(password: Data(password.utf8)) else { throw KeyStore.Error.invalidPassword }
return key
}
func exportMnemonic(wallet: InkWallet) throws -> String {
guard let password = keychain.password else { throw Error.keychainAccessFailure }
guard let mnemonic = wallet.key.decryptMnemonic(password: Data(password.utf8)) else { throw KeyStore.Error.invalidPassword }
return mnemonic
}
func update(wallet: InkWallet, password: String, newPassword: String) throws {
try update(wallet: wallet, password: password, newPassword: newPassword, newName: wallet.key.name)
}
func update(wallet: InkWallet, password: String, newName: String) throws {
try update(wallet: wallet, password: password, newPassword: password, newName: newName)
}
func delete(wallet: InkWallet) throws {
guard let password = keychain.password else { throw Error.keychainAccessFailure }
guard let index = wallets.firstIndex(of: wallet) else { throw KeyStore.Error.accountNotFound }
guard var privateKey = wallet.key.decryptPrivateKey(password: Data(password.utf8)) else { throw KeyStore.Error.invalidKey }
defer { privateKey.resetBytes(in: 0..<privateKey.count) }
wallets.remove(at: index)
try keychain.removeWallet(id: wallet.id)
}
func destroy() throws {
wallets.removeAll(keepingCapacity: false)
try keychain.removeAllWallets()
}
private func load() throws {
let ids = keychain.getAllWalletsIds()
for id in ids {
guard let data = keychain.getWalletData(id: id), let key = StoredKey.importJSON(json: data) else { continue }
let wallet = InkWallet(id: id, key: key)
wallets.append(wallet)
}
}
private func migrateFromLegacyIfNeeded() throws {
let legacyAccountsWithKeys = try keychain.getLegacyAccounts()
guard !legacyAccountsWithKeys.isEmpty, let password = keychain.password else { return }
@ -28,76 +158,6 @@ final class WalletsManager {
try keychain.removeLegacyAccounts()
}
private func load() throws {
let ids = keychain.getAllWalletsIds()
for id in ids {
guard let data = keychain.getWalletData(id: id), let key = StoredKey.importJSON(json: data) else { continue }
let wallet = InkWallet(id: id, key: key)
wallets.append(wallet)
}
}
func createWallet(name: String, password: String, coin: CoinType) throws -> InkWallet {
let key = StoredKey(name: name, password: Data(password.utf8))
let id = makeNewWalletId()
let wallet = InkWallet(id: id, key: key)
_ = try wallet.getAccount(password: password, coin: coin)
wallets.append(wallet)
try save(wallet: wallet)
return wallet
}
func importJSON(_ json: Data, name: String, password: String, newPassword: String, coin: CoinType) throws -> InkWallet {
guard let key = StoredKey.importJSON(json: json) else { throw KeyStore.Error.invalidKey }
guard let data = key.decryptPrivateKey(password: Data(password.utf8)) else { throw KeyStore.Error.invalidPassword }
if let mnemonic = checkMnemonic(data) { return try self.importMnemonic(mnemonic, name: name, encryptPassword: newPassword, coin: coin) }
guard let privateKey = PrivateKey(data: data) else { throw KeyStore.Error.invalidKey }
return try self.importPrivateKey(privateKey, name: name, password: newPassword, coin: coin)
}
func checkMnemonic(_ data: Data) -> String? {
guard let mnemonic = String(data: data, encoding: .ascii), Mnemonic.isValid(mnemonic: mnemonic) else { return nil }
return mnemonic
}
func importPrivateKey(_ privateKey: PrivateKey, name: String, password: String, coin: CoinType) throws -> InkWallet {
guard let newKey = StoredKey.importPrivateKey(privateKey: privateKey.data, name: name, password: Data(password.utf8), coin: coin) else { throw KeyStore.Error.invalidKey }
let id = makeNewWalletId()
let wallet = InkWallet(id: id, key: newKey)
_ = try wallet.getAccount(password: password, coin: coin)
wallets.append(wallet)
try save(wallet: wallet)
return wallet
}
func importMnemonic(_ mnemonic: String, name: String, encryptPassword: String, coin: CoinType) throws -> InkWallet {
guard let key = StoredKey.importHDWallet(mnemonic: mnemonic, name: name, password: Data(encryptPassword.utf8), coin: coin) else { throw KeyStore.Error.invalidMnemonic }
let id = makeNewWalletId()
let wallet = InkWallet(id: id, key: key)
_ = try wallet.getAccount(password: encryptPassword, coin: coin)
wallets.append(wallet)
try save(wallet: wallet)
return wallet
}
func exportPrivateKey(wallet: InkWallet, password: String) throws -> Data {
guard let key = wallet.key.decryptPrivateKey(password: Data(password.utf8)) else { throw KeyStore.Error.invalidPassword }
return key
}
func exportMnemonic(wallet: InkWallet, password: String) throws -> String {
guard let mnemonic = wallet.key.decryptMnemonic(password: Data(password.utf8)) else { throw KeyStore.Error.invalidPassword }
return mnemonic
}
func update(wallet: InkWallet, password: String, newPassword: String) throws {
try update(wallet: wallet, password: password, newPassword: newPassword, newName: wallet.key.name)
}
func update(wallet: InkWallet, password: String, newName: String) throws {
try update(wallet: wallet, password: password, newPassword: password, newName: newName)
}
private func update(wallet: InkWallet, password: String, newPassword: String, newName: String) throws {
guard let index = wallets.firstIndex(of: wallet) else { throw KeyStore.Error.accountNotFound }
guard var privateKeyData = wallet.key.decryptPrivateKey(password: Data(password.utf8)) else { throw KeyStore.Error.invalidPassword }
@ -119,19 +179,6 @@ final class WalletsManager {
try save(wallet: wallets[index])
}
func delete(wallet: InkWallet, password: String) throws {
guard let index = wallets.firstIndex(of: wallet) else { throw KeyStore.Error.accountNotFound }
guard var privateKey = wallet.key.decryptPrivateKey(password: Data(password.utf8)) else { throw KeyStore.Error.invalidKey }
defer { privateKey.resetBytes(in: 0..<privateKey.count) }
wallets.remove(at: index)
try keychain.removeWallet(id: wallet.id)
}
func destroy() throws {
wallets.removeAll(keepingCapacity: false)
try keychain.removeAllWallets()
}
private func save(wallet: InkWallet) throws {
guard let data = wallet.key.exportJSON() else { throw KeyStore.Error.invalidPassword }
try keychain.saveWallet(id: wallet.id, data: data)