From 4e7e8ef599268d06a0a0fe03dd6b959628e57bd4 Mon Sep 17 00:00:00 2001 From: Calle Erlandsson Date: Sun, 18 Nov 2018 20:57:07 +0100 Subject: [PATCH] Re-indent with 4 spaces --- Tofu/Account.swift | 96 ++-- Tofu/AccountCell.swift | 220 ++++---- Tofu/AccountCreationDelegate.swift | 2 +- Tofu/AccountCreationViewController.swift | 180 +++---- Tofu/AccountSearchResultsViewController.swift | 126 ++--- Tofu/AccountUpdateDelegate.swift | 2 +- Tofu/AccountUpdateViewController.swift | 38 +- Tofu/AccountsTableViewUpdater.swift | 36 +- Tofu/AccountsViewController.swift | 486 +++++++++--------- Tofu/Algorithm.swift | 48 +- Tofu/AlgorithmSelectionDelegate.swift | 2 +- Tofu/AlgorithmsViewController.swift | 72 +-- Tofu/AppDelegate.swift | 12 +- Tofu/CircularProgressView.swift | 72 +-- Tofu/Data.swift | 148 +++--- Tofu/Keychain.swift | 196 +++---- Tofu/Password.swift | 52 +- Tofu/ScanningViewController.swift | 140 ++--- TofuTests/AccountTests.swift | 226 ++++---- TofuTests/DataTests.swift | 60 +-- TofuTests/PasswordTests.swift | 156 +++--- TofuUITests/TofuUITests.swift | 4 +- 22 files changed, 1187 insertions(+), 1187 deletions(-) diff --git a/Tofu/Account.swift b/Tofu/Account.swift index 8a53696..220adcc 100644 --- a/Tofu/Account.swift +++ b/Tofu/Account.swift @@ -1,58 +1,58 @@ import Foundation final class Account { - var persistentRef: Data? - var name: String? - var issuer: String? - var password = Password() + var persistentRef: Data? + var name: String? + var issuer: String? + var password = Password() - convenience init?(url: URL) { - let label = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) - guard let host = url.host, host == "hotp" || host == "totp" else { return nil } - let labelComponents = label.components(separatedBy: ":") - guard labelComponents.count > 0, - let components = URLComponents(url: url, resolvingAgainstBaseURL: false), - let queryItems = components.queryItems, - queryItems.count > 0 - else { return nil } + convenience init?(url: URL) { + let label = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + guard let host = url.host, host == "hotp" || host == "totp" else { return nil } + let labelComponents = label.components(separatedBy: ":") + guard labelComponents.count > 0, + let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let queryItems = components.queryItems, + queryItems.count > 0 + else { return nil } - self.init() + self.init() - name = labelComponents.last?.trimmingCharacters(in: CharacterSet.whitespaces) - issuer = labelComponents.count > 1 ? labelComponents.first : nil - password.timeBased = host == "totp" - for queryItem in queryItems { - switch queryItem.name { - case "secret": - guard let secretString = queryItem.value, - let secret = Data(base32Encoded: secretString) - else { break } - password.secret = secret - case "algorithm": - switch queryItem.value { - case .some("SHA256"): password.algorithm = .sha256 - case .some("SHA512"): password.algorithm = .sha512 - default: break + name = labelComponents.last?.trimmingCharacters(in: CharacterSet.whitespaces) + issuer = labelComponents.count > 1 ? labelComponents.first : nil + password.timeBased = host == "totp" + for queryItem in queryItems { + switch queryItem.name { + case "secret": + guard let secretString = queryItem.value, + let secret = Data(base32Encoded: secretString) + else { break } + password.secret = secret + case "algorithm": + switch queryItem.value { + case .some("SHA256"): password.algorithm = .sha256 + case .some("SHA512"): password.algorithm = .sha512 + default: break + } + case "digits": + guard let string = queryItem.value, let digits = Int(string) else { break } + password.digits = digits + case "issuer": issuer = queryItem.value + case "counter": + guard let string = queryItem.value, let counter = Int(string) else { break } + password.counter = counter + case "period": + guard let string = queryItem.value, let period = Int(string) else { break } + password.period = period + default: break + } } - case "digits": - guard let string = queryItem.value, let digits = Int(string) else { break } - password.digits = digits - case "issuer": issuer = queryItem.value - case "counter": - guard let string = queryItem.value, let counter = Int(string) else { break } - password.counter = counter - case "period": - guard let string = queryItem.value, let period = Int(string) else { break } - password.period = period - default: break - } + if password.secret.count == 0 { return nil } } - if password.secret.count == 0 { return nil } - } - var description: String { - guard let issuer = issuer, issuer.count > 0 else { return name ?? "" } - guard let name = name, name.count > 0 else { return issuer } - return "\(issuer) (\(name))" - } + var description: String { + guard let issuer = issuer, issuer.count > 0 else { return name ?? "" } + guard let name = name, name.count > 0 else { return issuer } + return "\(issuer) (\(name))" + } } diff --git a/Tofu/AccountCell.swift b/Tofu/AccountCell.swift index 48712f3..66c1393 100644 --- a/Tofu/AccountCell.swift +++ b/Tofu/AccountCell.swift @@ -1,131 +1,131 @@ import UIKit private func placeholderImageWithText(_ text: String) -> UIImage { - let image = UIImage(named: "Placeholder")! - UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale) - image.draw(at: CGPoint.zero) - defer { UIGraphicsEndImageContext() } - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .center - let fontSize: CGFloat = 36 - let attributes: [NSAttributedStringKey: Any] = [ - .font: UIFont.systemFont(ofSize: fontSize, weight: UIFont.Weight.ultraLight), - .foregroundColor: UIColor.lightGray, - .paragraphStyle: paragraphStyle, - ] - let origin = CGPoint(x: 0, y: (image.size.height - fontSize) / 2 - 0.1 * fontSize) - text.draw(with: CGRect(origin: origin, size: image.size), options: .usesLineFragmentOrigin, - attributes: attributes, context: nil) - return UIGraphicsGetImageFromCurrentImageContext()! + let image = UIImage(named: "Placeholder")! + UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale) + image.draw(at: CGPoint.zero) + defer { UIGraphicsEndImageContext() } + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + let fontSize: CGFloat = 36 + let attributes: [NSAttributedStringKey: Any] = [ + .font: UIFont.systemFont(ofSize: fontSize, weight: UIFont.Weight.ultraLight), + .foregroundColor: UIColor.lightGray, + .paragraphStyle: paragraphStyle, + ] + let origin = CGPoint(x: 0, y: (image.size.height - fontSize) / 2 - 0.1 * fontSize) + text.draw(with: CGRect(origin: origin, size: image.size), options: .usesLineFragmentOrigin, + attributes: attributes, context: nil) + return UIGraphicsGetImageFromCurrentImageContext()! } private func imageForAccount(_ account: Account) -> UIImage { - switch account.issuer { - case .some("Adobe ID"): return UIImage(named: "Adobe")! - case .some("Amazon"): return UIImage(named: "Amazon")! - case .some("AWS"): return UIImage(named: "AWS")! - case .some("Backblaze"): return UIImage(named: "Backblaze")! - case .some("Bitbucket"): return UIImage(named: "Bitbucket")! - case .some("Coinbase"): return UIImage(named: "Coinbase")! - case .some("DigitalOcean"): return UIImage(named: "DigitalOcean")! - case .some("DNSimple"): return UIImage(named: "DNSimple")! - case .some("Dropbox"): return UIImage(named: "Dropbox")! - case .some("Electronic Arts"): return UIImage(named: "ElectronicArts")! - case .some("Evernote"): return UIImage(named: "Evernote")! - case .some("Facebook"): return UIImage(named: "Facebook")! - case .some("FastMail"): return UIImage(named: "FastMail")! - case .some("GitHub"): return UIImage(named: "GitHub")! - case .some("Google"): return UIImage(named: "Google")! - case .some("GreenAddress"): return UIImage(named: "GreenAddress")! - case .some("Heroku"): return UIImage(named: "Heroku")! - case .some("Hover"): return UIImage(named: "Hover")! - case .some("HumbleBundle"): return UIImage(named: "HumbleBundle")! - case .some("IFTTT"): return UIImage(named: "IFTTT")! - case .some("Intercom"): return UIImage(named: "Intercom")! - case .some("Kickstarter"): return UIImage(named: "Kickstarter")! - case .some("LinodeManager"): return UIImage(named: "Linode")! - case .some("LocalBitcoins"): return UIImage(named: "LocalBitcoins")! - case .some("Microsoft"): return UIImage(named: "Microsoft")! - case .some("Name.com"): return UIImage(named: "Name.com")! - case .some("ownCloud"): return UIImage(named: "ownCloud")! - case .some("Privacy.com"): return UIImage(named: "Privacy")! - case .some("Slack"): return UIImage(named: "Slack")! - case .some("Stripe"): return UIImage(named: "Stripe")! - case .some("Tumblr"): return UIImage(named: "Tumblr")! - case .some("Ubisoft"): return UIImage(named: "Ubisoft")! - case .some("WordPress"): return UIImage(named: "WordPress")! - case .some("www.fastmail.com"): return UIImage(named: "FastMail")! - default: - let text = String(account.description.first ?? "?").uppercased() - return placeholderImageWithText(text) - } + switch account.issuer { + case .some("Adobe ID"): return UIImage(named: "Adobe")! + case .some("Amazon"): return UIImage(named: "Amazon")! + case .some("AWS"): return UIImage(named: "AWS")! + case .some("Backblaze"): return UIImage(named: "Backblaze")! + case .some("Bitbucket"): return UIImage(named: "Bitbucket")! + case .some("Coinbase"): return UIImage(named: "Coinbase")! + case .some("DigitalOcean"): return UIImage(named: "DigitalOcean")! + case .some("DNSimple"): return UIImage(named: "DNSimple")! + case .some("Dropbox"): return UIImage(named: "Dropbox")! + case .some("Electronic Arts"): return UIImage(named: "ElectronicArts")! + case .some("Evernote"): return UIImage(named: "Evernote")! + case .some("Facebook"): return UIImage(named: "Facebook")! + case .some("FastMail"): return UIImage(named: "FastMail")! + case .some("GitHub"): return UIImage(named: "GitHub")! + case .some("Google"): return UIImage(named: "Google")! + case .some("GreenAddress"): return UIImage(named: "GreenAddress")! + case .some("Heroku"): return UIImage(named: "Heroku")! + case .some("Hover"): return UIImage(named: "Hover")! + case .some("HumbleBundle"): return UIImage(named: "HumbleBundle")! + case .some("IFTTT"): return UIImage(named: "IFTTT")! + case .some("Intercom"): return UIImage(named: "Intercom")! + case .some("Kickstarter"): return UIImage(named: "Kickstarter")! + case .some("LinodeManager"): return UIImage(named: "Linode")! + case .some("LocalBitcoins"): return UIImage(named: "LocalBitcoins")! + case .some("Microsoft"): return UIImage(named: "Microsoft")! + case .some("Name.com"): return UIImage(named: "Name.com")! + case .some("ownCloud"): return UIImage(named: "ownCloud")! + case .some("Privacy.com"): return UIImage(named: "Privacy")! + case .some("Slack"): return UIImage(named: "Slack")! + case .some("Stripe"): return UIImage(named: "Stripe")! + case .some("Tumblr"): return UIImage(named: "Tumblr")! + case .some("Ubisoft"): return UIImage(named: "Ubisoft")! + case .some("WordPress"): return UIImage(named: "WordPress")! + case .some("www.fastmail.com"): return UIImage(named: "FastMail")! + default: + let text = String(account.description.first ?? "?").uppercased() + return placeholderImageWithText(text) + } } private func imageWithColor(_ color: UIColor, size: CGSize) -> UIImage { - UIGraphicsBeginImageContext(size) - color.setFill() - UIRectFill(CGRect(x: 0, y: 0, width: size.width, height: size.height)) - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return image!; + UIGraphicsBeginImageContext(size) + color.setFill() + UIRectFill(CGRect(x: 0, y: 0, width: size.width, height: size.height)) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image!; } private func formattedValue(_ value: String) -> String { - let length = value.count - let prefix = String(value.prefix(length / 2)) - let suffix = String(value.suffix(length - length / 2)) - return "\(prefix) \(suffix)" + let length = value.count + let prefix = String(value.prefix(length / 2)) + let suffix = String(value.suffix(length - length / 2)) + return "\(prefix) \(suffix)" } final class AccountCell: UITableViewCell { - @IBOutlet weak var accountImageView: UIImageView! - @IBOutlet weak var valueLabel: UILabel! - @IBOutlet weak var identifierLabel: UILabel! - var delegate: AccountUpdateDelegate? - fileprivate let button = UIButton(type: .custom) - fileprivate let progressView = CircularProgressView() + @IBOutlet weak var accountImageView: UIImageView! + @IBOutlet weak var valueLabel: UILabel! + @IBOutlet weak var identifierLabel: UILabel! + var delegate: AccountUpdateDelegate? + fileprivate let button = UIButton(type: .custom) + fileprivate let progressView = CircularProgressView() - var account: Account! { - didSet { - accessoryView = account.password.timeBased ? progressView : button - let now = Date() - updateWithDate(now) + var account: Account! { + didSet { + accessoryView = account.password.timeBased ? progressView : button + let now = Date() + updateWithDate(now) + } } - } - override func awakeFromNib() { - let featureSettings: [[UIFontDescriptor.FeatureKey: Any]] = - [[.featureIdentifier: kNumberSpacingType, .typeIdentifier: kMonospacedNumbersSelector]] - let fontDescriptor = valueLabel.font.fontDescriptor.addingAttributes([.featureSettings: featureSettings]) - valueLabel.font = UIFont(descriptor: fontDescriptor, size: 0) - button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 13) - button.setTitle("NEXT", for: UIControlState()) - button.setTitleColor(tintColor, for: UIControlState()) - button.setTitleColor(UIColor.white, for: .highlighted) - button.setTitleColor(UIColor.white, for: .selected) - button.layer.borderColor = button.tintColor.cgColor - button.layer.borderWidth = 1 - button.layer.cornerRadius = 4 - button.contentEdgeInsets = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10) - button.sizeToFit() - let image = imageWithColor(tintColor, size: button.bounds.size) - button.setBackgroundImage(image, for: .highlighted) - button.setBackgroundImage(image, for: .selected) - button.clipsToBounds = true - button.addTarget(self, action: #selector(didPressButton(_:)), for: .touchUpInside) - } + override func awakeFromNib() { + let featureSettings: [[UIFontDescriptor.FeatureKey: Any]] = + [[.featureIdentifier: kNumberSpacingType, .typeIdentifier: kMonospacedNumbersSelector]] + let fontDescriptor = valueLabel.font.fontDescriptor.addingAttributes([.featureSettings: featureSettings]) + valueLabel.font = UIFont(descriptor: fontDescriptor, size: 0) + button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 13) + button.setTitle("NEXT", for: UIControlState()) + button.setTitleColor(tintColor, for: UIControlState()) + button.setTitleColor(UIColor.white, for: .highlighted) + button.setTitleColor(UIColor.white, for: .selected) + button.layer.borderColor = button.tintColor.cgColor + button.layer.borderWidth = 1 + button.layer.cornerRadius = 4 + button.contentEdgeInsets = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10) + button.sizeToFit() + let image = imageWithColor(tintColor, size: button.bounds.size) + button.setBackgroundImage(image, for: .highlighted) + button.setBackgroundImage(image, for: .selected) + button.clipsToBounds = true + button.addTarget(self, action: #selector(didPressButton(_:)), for: .touchUpInside) + } - @objc func didPressButton(_ sender: UIButton) { - account.password.counter += 1 - delegate?.updateAccount(account) - } + @objc func didPressButton(_ sender: UIButton) { + account.password.counter += 1 + delegate?.updateAccount(account) + } - func updateWithDate(_ date: Date) { - accountImageView.image = imageForAccount(account) - valueLabel.text = formattedValue(account.password.valueForDate(date)) - identifierLabel.text = account.description - progressView.progress = account.password.progressForDate(date) - progressView.tintColor = account.password.timeIntervalRemainingForDate(date) < 5 ? - .red : tintColor - } + func updateWithDate(_ date: Date) { + accountImageView.image = imageForAccount(account) + valueLabel.text = formattedValue(account.password.valueForDate(date)) + identifierLabel.text = account.description + progressView.progress = account.password.progressForDate(date) + progressView.tintColor = account.password.timeIntervalRemainingForDate(date) < 5 ? + .red : tintColor + } } diff --git a/Tofu/AccountCreationDelegate.swift b/Tofu/AccountCreationDelegate.swift index b42ffcf..9c01410 100644 --- a/Tofu/AccountCreationDelegate.swift +++ b/Tofu/AccountCreationDelegate.swift @@ -1,3 +1,3 @@ protocol AccountCreationDelegate: class { - func createAccount(_ account: Account) + func createAccount(_ account: Account) } diff --git a/Tofu/AccountCreationViewController.swift b/Tofu/AccountCreationViewController.swift index 0e7d761..82a03c0 100644 --- a/Tofu/AccountCreationViewController.swift +++ b/Tofu/AccountCreationViewController.swift @@ -1,114 +1,114 @@ import UIKit private let formatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .none - return formatter + let formatter = NumberFormatter() + formatter.numberStyle = .none + return formatter }() final class AccountCreationViewController: UITableViewController, AlgorithmSelectionDelegate { - @IBOutlet weak var doneItem: UIBarButtonItem! - @IBOutlet weak var nameField: UITextField! - @IBOutlet weak var issuerField: UITextField! - @IBOutlet weak var secretField: UITextField! - @IBOutlet weak var algorithmLabel: UILabel! - @IBOutlet weak var eightDigitsSwitch: UISwitch! - @IBOutlet weak var timeBasedSwitch: UISwitch! - @IBOutlet weak var periodCounterCell: UITableViewCell! - @IBOutlet weak var periodCounterLabel: UILabel! - @IBOutlet weak var periodCounterField: UITextField! - var delegate: AccountCreationDelegate? - fileprivate var algorithm = Algorithm.sha1 - fileprivate var periodString: String? - fileprivate var counterString: String? - fileprivate var period: Int? { - guard periodCounterField.text?.count ?? 0 > 0 else { return 30 } - return formatter.number(from: periodCounterField.text!)?.intValue - } - fileprivate var counter: Int? { - guard periodCounterField.text?.count ?? 0 > 0 else { return 0 } - return formatter.number(from: periodCounterField.text!)?.intValue - } + @IBOutlet weak var doneItem: UIBarButtonItem! + @IBOutlet weak var nameField: UITextField! + @IBOutlet weak var issuerField: UITextField! + @IBOutlet weak var secretField: UITextField! + @IBOutlet weak var algorithmLabel: UILabel! + @IBOutlet weak var eightDigitsSwitch: UISwitch! + @IBOutlet weak var timeBasedSwitch: UISwitch! + @IBOutlet weak var periodCounterCell: UITableViewCell! + @IBOutlet weak var periodCounterLabel: UILabel! + @IBOutlet weak var periodCounterField: UITextField! + var delegate: AccountCreationDelegate? + fileprivate var algorithm = Algorithm.sha1 + fileprivate var periodString: String? + fileprivate var counterString: String? + fileprivate var period: Int? { + guard periodCounterField.text?.count ?? 0 > 0 else { return 30 } + return formatter.number(from: periodCounterField.text!)?.intValue + } + fileprivate var counter: Int? { + guard periodCounterField.text?.count ?? 0 > 0 else { return 0 } + return formatter.number(from: periodCounterField.text!)?.intValue + } - @IBAction func didPressCancel(_ sender: UIBarButtonItem) { - presentingViewController?.dismiss(animated: true, completion: nil) - } + @IBAction func didPressCancel(_ sender: UIBarButtonItem) { + presentingViewController?.dismiss(animated: true, completion: nil) + } - @IBAction func didPressDone(_ sender: UIBarButtonItem) { - let password = Password() - password.timeBased = timeBasedSwitch.isOn - password.algorithm = algorithm - password.digits = eightDigitsSwitch.isOn ? 8 : 6 - password.secret = Data(base32Encoded: secretField.text!)! + @IBAction func didPressDone(_ sender: UIBarButtonItem) { + let password = Password() + password.timeBased = timeBasedSwitch.isOn + password.algorithm = algorithm + password.digits = eightDigitsSwitch.isOn ? 8 : 6 + password.secret = Data(base32Encoded: secretField.text!)! - if timeBasedSwitch.isOn { - password.period = period! - } else { - password.counter = counter! - } + if timeBasedSwitch.isOn { + password.period = period! + } else { + password.counter = counter! + } - let account = Account() - account.name = nameField.text - account.issuer = issuerField.text - account.password = password + let account = Account() + account.name = nameField.text + account.issuer = issuerField.text + account.password = password - presentingViewController?.dismiss(animated: true) { - self.delegate?.createAccount(account) + presentingViewController?.dismiss(animated: true) { + self.delegate?.createAccount(account) + } } - } - @IBAction func editingChangedForTextField(_ textField: UITextField) { - validate() - } - - @IBAction func valueChangedForTimeBasedSwitch() { - if self.timeBasedSwitch.isOn { - counterString = periodCounterField.text - } else { - periodString = periodCounterField.text + @IBAction func editingChangedForTextField(_ textField: UITextField) { + validate() } - UIView.transition(with: periodCounterCell, - duration: 0.2, - options: .transitionCrossDissolve, - animations: { + + @IBAction func valueChangedForTimeBasedSwitch() { if self.timeBasedSwitch.isOn { - self.periodCounterLabel.text = "Period" - self.periodCounterField.placeholder = String(30) - self.periodCounterField.text = self.periodString + counterString = periodCounterField.text } else { - self.periodCounterLabel.text = "Counter" - self.periodCounterField.placeholder = String(0) - self.periodCounterField.text = self.counterString + periodString = periodCounterField.text } - }, completion: { _ in - self.validate() - }) - } + UIView.transition(with: periodCounterCell, + duration: 0.2, + options: .transitionCrossDissolve, + animations: { + if self.timeBasedSwitch.isOn { + self.periodCounterLabel.text = "Period" + self.periodCounterField.placeholder = String(30) + self.periodCounterField.text = self.periodString + } else { + self.periodCounterLabel.text = "Counter" + self.periodCounterField.placeholder = String(0) + self.periodCounterField.text = self.counterString + } + }, completion: { _ in + self.validate() + }) + } - override func viewDidLoad() { - super.viewDidLoad() - nameField.becomeFirstResponder() - algorithmLabel.text = algorithm.name - } + override func viewDidLoad() { + super.viewDidLoad() + nameField.becomeFirstResponder() + algorithmLabel.text = algorithm.name + } - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if let algorithmsController = segue.destination as? AlgorithmsViewController { - algorithmsController.algorithms = [.sha1, .sha256, .sha512] - algorithmsController.selected = algorithm - algorithmsController.delegate = self + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if let algorithmsController = segue.destination as? AlgorithmsViewController { + algorithmsController.algorithms = [.sha1, .sha256, .sha512] + algorithmsController.selected = algorithm + algorithmsController.delegate = self + } } - } - fileprivate func validate() { - doneItem.isEnabled = secretField.text?.count ?? 0 > 0 && - Data(base32Encoded: secretField.text!) != nil && - (timeBasedSwitch.isOn ? period != nil : counter != nil) - } + fileprivate func validate() { + doneItem.isEnabled = secretField.text?.count ?? 0 > 0 && + Data(base32Encoded: secretField.text!) != nil && + (timeBasedSwitch.isOn ? period != nil : counter != nil) + } - // MARK: AlgorithmSelectionDelegate + // MARK: AlgorithmSelectionDelegate - func selectAlgorithm(_ algorithm: Algorithm) { - self.algorithm = algorithm - algorithmLabel.text = algorithm.name - } + func selectAlgorithm(_ algorithm: Algorithm) { + self.algorithm = algorithm + algorithmLabel.text = algorithm.name + } } diff --git a/Tofu/AccountSearchResultsViewController.swift b/Tofu/AccountSearchResultsViewController.swift index f897a9b..ee01481 100644 --- a/Tofu/AccountSearchResultsViewController.swift +++ b/Tofu/AccountSearchResultsViewController.swift @@ -3,68 +3,68 @@ import UIKit private let accountCellIdentifier = "AccountCell" final class AccountSearchResultsViewController: UITableViewController, AccountUpdateDelegate { - @IBOutlet var emptyView: UIView! - var accounts: [Account]! { - didSet { - tableView.reloadData() - tableView.backgroundView = accounts.count == 0 ? emptyView : nil - tableView.separatorStyle = accounts.count == 0 ? .none : .singleLine + @IBOutlet var emptyView: UIView! + var accounts: [Account]! { + didSet { + tableView.reloadData() + tableView.backgroundView = accounts.count == 0 ? emptyView : nil + tableView.separatorStyle = accounts.count == 0 ? .none : .singleLine + } + } + + override func viewDidLoad() { + super.viewDidLoad() + let updater = AccountsTableViewUpdater(tableView: tableView) + updater.startUpdating() + } + + // MARK: UITableViewDataSource + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return accounts.count + } + + override func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: accountCellIdentifier, + for: indexPath) as! AccountCell + cell.account = accounts[indexPath.row] + cell.delegate = self + return cell + } + + // MARK: UITableViewDelegate + + override func tableView(_ tableView: UITableView, + shouldShowMenuForRowAt indexPath: IndexPath) -> Bool { + return true + } + + override func tableView(_ tableView: UITableView, canPerformAction action: Selector, + forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool { + return action == #selector(copy(_:)) + } + + override func tableView(_ tableView: UITableView, performAction action: Selector, + forRowAt indexPath: IndexPath, withSender sender: Any?) { + if action == #selector(copy(_:)) { + let cell = tableView.cellForRow(at: indexPath) as! AccountCell + UIPasteboard.general.string = cell.valueLabel.text? + .replacingOccurrences(of: " ", with: "") + } + } + + // MARK: AccountUpdateDelegate + + func updateAccount(_ account: Account) { + (presentingViewController as! AccountUpdateDelegate).updateAccount(account) + let row = accounts.index { $0 === account }! + let indexPath = IndexPath(row: row, section: 0) + guard let cell = tableView.cellForRow(at: indexPath) as? AccountCell else { return } + cell.updateWithDate(Date()) } - } - - override func viewDidLoad() { - super.viewDidLoad() - let updater = AccountsTableViewUpdater(tableView: tableView) - updater.startUpdating() - } - - // MARK: UITableViewDataSource - - override func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return accounts.count - } - - override func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: accountCellIdentifier, - for: indexPath) as! AccountCell - cell.account = accounts[indexPath.row] - cell.delegate = self - return cell - } - - // MARK: UITableViewDelegate - - override func tableView(_ tableView: UITableView, - shouldShowMenuForRowAt indexPath: IndexPath) -> Bool { - return true - } - - override func tableView(_ tableView: UITableView, canPerformAction action: Selector, - forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool { - return action == #selector(copy(_:)) - } - - override func tableView(_ tableView: UITableView, performAction action: Selector, - forRowAt indexPath: IndexPath, withSender sender: Any?) { - if action == #selector(copy(_:)) { - let cell = tableView.cellForRow(at: indexPath) as! AccountCell - UIPasteboard.general.string = cell.valueLabel.text? - .replacingOccurrences(of: " ", with: "") - } - } - - // MARK: AccountUpdateDelegate - - func updateAccount(_ account: Account) { - (presentingViewController as! AccountUpdateDelegate).updateAccount(account) - let row = accounts.index { $0 === account }! - let indexPath = IndexPath(row: row, section: 0) - guard let cell = tableView.cellForRow(at: indexPath) as? AccountCell else { return } - cell.updateWithDate(Date()) - } } diff --git a/Tofu/AccountUpdateDelegate.swift b/Tofu/AccountUpdateDelegate.swift index ad98a3a..65db404 100644 --- a/Tofu/AccountUpdateDelegate.swift +++ b/Tofu/AccountUpdateDelegate.swift @@ -1,3 +1,3 @@ protocol AccountUpdateDelegate: class { - func updateAccount(_ account: Account) + func updateAccount(_ account: Account) } diff --git a/Tofu/AccountUpdateViewController.swift b/Tofu/AccountUpdateViewController.swift index 5ba462c..cf2c046 100644 --- a/Tofu/AccountUpdateViewController.swift +++ b/Tofu/AccountUpdateViewController.swift @@ -1,23 +1,23 @@ import UIKit final class AccountUpdateViewController: UITableViewController { - @IBOutlet weak var nameField: UITextField! - @IBOutlet weak var issuerField: UITextField! - var delegate: AccountUpdateDelegate? - var account: Account! - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - nameField.text = account.name - issuerField.text = account.issuer - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - account.name = nameField.text - account.issuer = issuerField.text - delegate?.updateAccount(account) - } + @IBOutlet weak var nameField: UITextField! + @IBOutlet weak var issuerField: UITextField! + var delegate: AccountUpdateDelegate? + var account: Account! + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + nameField.text = account.name + issuerField.text = account.issuer + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + account.name = nameField.text + account.issuer = issuerField.text + delegate?.updateAccount(account) + } } diff --git a/Tofu/AccountsTableViewUpdater.swift b/Tofu/AccountsTableViewUpdater.swift index 8f20359..3497b87 100644 --- a/Tofu/AccountsTableViewUpdater.swift +++ b/Tofu/AccountsTableViewUpdater.swift @@ -1,23 +1,23 @@ import UIKit final class AccountsTableViewUpdater: NSObject { - var tableView: UITableView - - init(tableView: UITableView) { - self.tableView = tableView - } - - func startUpdating() { - let timer = Timer(timeInterval: 1, target: self, selector: #selector(updateCells), - userInfo: nil, repeats: true) - RunLoop.main.add(timer, forMode: RunLoopMode.commonModes) - } - - @objc func updateCells() { - let now = Date() - for cell in tableView.visibleCells { - let accountCell = cell as! AccountCell - accountCell.updateWithDate(now) + var tableView: UITableView + + init(tableView: UITableView) { + self.tableView = tableView + } + + func startUpdating() { + let timer = Timer(timeInterval: 1, target: self, selector: #selector(updateCells), + userInfo: nil, repeats: true) + RunLoop.main.add(timer, forMode: RunLoopMode.commonModes) + } + + @objc func updateCells() { + let now = Date() + for cell in tableView.visibleCells { + let accountCell = cell as! AccountCell + accountCell.updateWithDate(now) + } } - } } diff --git a/Tofu/AccountsViewController.swift b/Tofu/AccountsViewController.swift index bb34dad..7bf75cf 100644 --- a/Tofu/AccountsViewController.swift +++ b/Tofu/AccountsViewController.swift @@ -9,274 +9,274 @@ private let editAccountSegueIdentifier = "EditAccountSegue" final class AccountsViewController: UITableViewController, UISearchResultsUpdating, AccountCreationDelegate, AccountUpdateDelegate { - @IBOutlet weak var emptyView: UIView! - fileprivate let keychain = Keychain() - fileprivate let userDefaults = UserDefaults.standard - fileprivate var accounts: [Account]! - fileprivate var searchController: UISearchController! - fileprivate var alertController: UIAlertController! - - @IBAction func didPressAdd(_ sender: UIBarButtonItem) { - present(alertController, animated: true, completion: nil) - } - - override func viewDidLoad() { - super.viewDidLoad() - - if #available(iOS 11.0, *) { - navigationController?.navigationBar.prefersLargeTitles = true + @IBOutlet weak var emptyView: UIView! + fileprivate let keychain = Keychain() + fileprivate let userDefaults = UserDefaults.standard + fileprivate var accounts: [Account]! + fileprivate var searchController: UISearchController! + fileprivate var alertController: UIAlertController! + + @IBAction func didPressAdd(_ sender: UIBarButtonItem) { + present(alertController, animated: true, completion: nil) } - accounts = keychain.accounts - let persistentRefs = userDefaults.array(forKey: persistentRefsKey) as? [Data] ?? [] - accounts.sort { a, b in - let aIndex = persistentRefs.index(of: a.persistentRef! as Data) ?? 0 - let bIndex = persistentRefs.index(of: b.persistentRef! as Data) ?? 0 - return aIndex < bIndex + override func viewDidLoad() { + super.viewDidLoad() + + if #available(iOS 11.0, *) { + navigationController?.navigationBar.prefersLargeTitles = true + } + + accounts = keychain.accounts + let persistentRefs = userDefaults.array(forKey: persistentRefsKey) as? [Data] ?? [] + accounts.sort { a, b in + let aIndex = persistentRefs.index(of: a.persistentRef! as Data) ?? 0 + let bIndex = persistentRefs.index(of: b.persistentRef! as Data) ?? 0 + return aIndex < bIndex + } + persistAccountOrder() + + let searchResultsController = storyboard?.instantiateViewController( + withIdentifier: accountSearchResultsViewControllerIdentifier) as! AccountSearchResultsViewController + searchController = UISearchController(searchResultsController: searchResultsController) + searchController.searchResultsUpdater = self + if #available(iOS 11.0, *) { + navigationItem.searchController = searchController + } + + alertController = UIAlertController( + title: "Add Account", + message: "Add an account by scanning a QR code or enter a secret manually.", + preferredStyle: .actionSheet) + + let scanQRCodeAction = UIAlertAction(title: "Scan QR Code", style: .default) { + [unowned self] _ in + self.performSegue(withIdentifier: scanSegueIdentifier, sender: self) + } + + let enterManuallyAction = UIAlertAction(title: "Enter Manually", style: .default) { + [unowned self] _ in + self.performSegue(withIdentifier: manualSegueIdentifier, sender: self) + } + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + + alertController.addAction(scanQRCodeAction) + alertController.addAction(enterManuallyAction) + alertController.addAction(cancelAction) + + let updater = AccountsTableViewUpdater(tableView: tableView) + updater.startUpdating() + + updateEditing() + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if let navigationController = segue.destination as? UINavigationController { + if let accountCreationViewController = navigationController.topViewController + as? AccountCreationViewController { + accountCreationViewController.delegate = self + } else { + let scanningViewController = navigationController.topViewController + as! ScanningViewController + scanningViewController.delegate = self + } + } else { + let accountUpdateViewController = segue.destination + as! AccountUpdateViewController + let cell = sender as! AccountCell + accountUpdateViewController.delegate = self + accountUpdateViewController.account = cell.account + } + } + + fileprivate func persistAccountOrder() { + let persistentRefs = accounts.map { $0.persistentRef! } + userDefaults.set(persistentRefs, forKey: persistentRefsKey) } - persistAccountOrder() - - let searchResultsController = storyboard?.instantiateViewController( - withIdentifier: accountSearchResultsViewControllerIdentifier) as! AccountSearchResultsViewController - searchController = UISearchController(searchResultsController: searchResultsController) - searchController.searchResultsUpdater = self - if #available(iOS 11.0, *) { - navigationItem.searchController = searchController + + fileprivate func updateEditing() { + if accounts.count == 0 { + if #available(iOS 11.0, *) { + // In this case the search bar is rendered in the navigation bar and there's no need to hide it when there are no accounts. + } else { + tableView.tableHeaderView = nil + } + tableView.backgroundView = emptyView + tableView.separatorStyle = .none + navigationItem.leftBarButtonItem = nil + setEditing(false, animated: true) + } else { + if #available(iOS 11.0, *) { + // Since the search bar is already rendered in the navigation bar we don't need to render it in the table header view. + } else { + tableView.tableHeaderView = searchController.searchBar + } + tableView.backgroundView = nil + tableView.separatorStyle = .singleLine + navigationItem.leftBarButtonItem = editButtonItem + } } - alertController = UIAlertController( - title: "Add Account", - message: "Add an account by scanning a QR code or enter a secret manually.", - preferredStyle: .actionSheet) + // MARK: UITableViewDataSource + + override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> + Bool { + return true + } - let scanQRCodeAction = UIAlertAction(title: "Scan QR Code", style: .default) { - [unowned self] _ in - self.performSegue(withIdentifier: scanSegueIdentifier, sender: self) + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 } - let enterManuallyAction = UIAlertAction(title: "Enter Manually", style: .default) { - [unowned self] _ in - self.performSegue(withIdentifier: manualSegueIdentifier, sender: self) + override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, + to destinationIndexPath: IndexPath) { + accounts.insert(accounts.remove(at: sourceIndexPath.row), + at: destinationIndexPath.row) + persistAccountOrder() } - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) - - alertController.addAction(scanQRCodeAction) - alertController.addAction(enterManuallyAction) - alertController.addAction(cancelAction) - - let updater = AccountsTableViewUpdater(tableView: tableView) - updater.startUpdating() - - updateEditing() - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if let navigationController = segue.destination as? UINavigationController { - if let accountCreationViewController = navigationController.topViewController - as? AccountCreationViewController { - accountCreationViewController.delegate = self - } else { - let scanningViewController = navigationController.topViewController - as! ScanningViewController - scanningViewController.delegate = self - } - } else { - let accountUpdateViewController = segue.destination - as! AccountUpdateViewController - let cell = sender as! AccountCell - accountUpdateViewController.delegate = self - accountUpdateViewController.account = cell.account + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return accounts.count } - } - - fileprivate func persistAccountOrder() { - let persistentRefs = accounts.map { $0.persistentRef! } - userDefaults.set(persistentRefs, forKey: persistentRefsKey) - } - - fileprivate func updateEditing() { - if accounts.count == 0 { - if #available(iOS 11.0, *) { - // In this case the search bar is rendered in the navigation bar and there's no need to hide it when there are no accounts. - } else { - tableView.tableHeaderView = nil - } - tableView.backgroundView = emptyView - tableView.separatorStyle = .none - navigationItem.leftBarButtonItem = nil - setEditing(false, animated: true) - } else { - if #available(iOS 11.0, *) { - // Since the search bar is already rendered in the navigation bar we don't need to render it in the table header view. - } else { - tableView.tableHeaderView = searchController.searchBar - } - tableView.backgroundView = nil - tableView.separatorStyle = .singleLine - navigationItem.leftBarButtonItem = editButtonItem + + override func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: accountCellIdentifier, + for: indexPath) as! AccountCell + cell.account = accounts[indexPath.row] + cell.delegate = self + return cell + } + + override func tableView( + _ tableView: UITableView, + commit editingStyle: UITableViewCellEditingStyle, + forRowAt indexPath: IndexPath) { + if editingStyle == .delete { + let alertController = UIAlertController( + title: "Deleting This Account Will Not Turn Off Two-Factor Authentication", + message: "Please make sure two-factor authentication is turned off in the issuer's sett" + + "ings before deleting this account to prevent being locked out.", + preferredStyle: .actionSheet) + + let deleteAccountAction = UIAlertAction(title: "Delete Account", style: .destructive) { _ in + self.deleteAccountForRowAtIndexPath(indexPath) + } + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + + alertController.addAction(deleteAccountAction) + alertController.addAction(cancelAction) + + present(alertController, animated: true, completion: nil) + } } - } - - // MARK: UITableViewDataSource - - override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> - Bool { - return true - } - - override func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, - to destinationIndexPath: IndexPath) { - accounts.insert(accounts.remove(at: sourceIndexPath.row), - at: destinationIndexPath.row) - persistAccountOrder() - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return accounts.count - } - - override func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: accountCellIdentifier, - for: indexPath) as! AccountCell - cell.account = accounts[indexPath.row] - cell.delegate = self - return cell - } - - override func tableView( - _ tableView: UITableView, - commit editingStyle: UITableViewCellEditingStyle, - forRowAt indexPath: IndexPath) { - if editingStyle == .delete { - let alertController = UIAlertController( - title: "Deleting This Account Will Not Turn Off Two-Factor Authentication", - message: "Please make sure two-factor authentication is turned off in the issuer's sett" + - "ings before deleting this account to prevent being locked out.", - preferredStyle: .actionSheet) - - let deleteAccountAction = UIAlertAction(title: "Delete Account", style: .destructive) { _ in - self.deleteAccountForRowAtIndexPath(indexPath) + + fileprivate func deleteAccountForRowAtIndexPath(_ indexPath: IndexPath) { + let account = self.accounts[indexPath.row] + guard self.keychain.deleteAccount(account) else { + presentTryAgainAlertWithTitle( + "Could Not Delete Account", + message: "An error occurred when deleting the account from the keychain.") { + self.deleteAccountForRowAtIndexPath(indexPath) + } + return } + accounts.remove(at: indexPath.row) + persistAccountOrder() + tableView.deleteRows(at: [indexPath], with: .automatic) + updateEditing() + } - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + // MARK: UITableViewDelegate - alertController.addAction(deleteAccountAction) - alertController.addAction(cancelAction) + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + if let cell = tableView.cellForRow(at: indexPath) as? AccountCell { + performSegue(withIdentifier: editAccountSegueIdentifier, sender: cell) + } + } - present(alertController, animated: true, completion: nil) - } - } - - fileprivate func deleteAccountForRowAtIndexPath(_ indexPath: IndexPath) { - let account = self.accounts[indexPath.row] - guard self.keychain.deleteAccount(account) else { - presentTryAgainAlertWithTitle( - "Could Not Delete Account", - message: "An error occurred when deleting the account from the keychain.") { - self.deleteAccountForRowAtIndexPath(indexPath) - } - return + override func tableView(_ tableView: UITableView, + shouldShowMenuForRowAt indexPath: IndexPath) -> Bool { + return true } - accounts.remove(at: indexPath.row) - persistAccountOrder() - tableView.deleteRows(at: [indexPath], with: .automatic) - updateEditing() - } - - // MARK: UITableViewDelegate - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - if let cell = tableView.cellForRow(at: indexPath) as? AccountCell { - performSegue(withIdentifier: editAccountSegueIdentifier, sender: cell) + + override func tableView(_ tableView: UITableView, canPerformAction action: Selector, + forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool { + return action == #selector(copy(_:)) } - } - - override func tableView(_ tableView: UITableView, - shouldShowMenuForRowAt indexPath: IndexPath) -> Bool { - return true - } - - override func tableView(_ tableView: UITableView, canPerformAction action: Selector, - forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool { - return action == #selector(copy(_:)) - } - - override func tableView(_ tableView: UITableView, performAction action: Selector, - forRowAt indexPath: IndexPath, withSender sender: Any?) { - if action == #selector(copy(_:)) { - let cell = tableView.cellForRow(at: indexPath) as! AccountCell - UIPasteboard.general.string = cell.valueLabel.text? - .replacingOccurrences(of: " ", with: "") - } - } - - // MARK: UISearchResultsUpdating - - func updateSearchResults(for searchController: UISearchController) { - let accountSearchResultsViewController = searchController.searchResultsController - as! AccountSearchResultsViewController - accountSearchResultsViewController.accounts = accounts.filter { - guard let string = searchController.searchBar.text else { return false } - return $0.description.range(of: string, options: .caseInsensitive, range: nil, - locale: nil) != nil + + override func tableView(_ tableView: UITableView, performAction action: Selector, + forRowAt indexPath: IndexPath, withSender sender: Any?) { + if action == #selector(copy(_:)) { + let cell = tableView.cellForRow(at: indexPath) as! AccountCell + UIPasteboard.general.string = cell.valueLabel.text? + .replacingOccurrences(of: " ", with: "") + } } - } - - // MARK: AccountCreationDelegate - - func createAccount(_ account: Account) { - guard keychain.insertAccount(account) else { - presentTryAgainAlertWithTitle( - "Could Not Create Account", - message: "An error occurred when inserting the account into the keychain.") { - self.createAccount(account) - } - return + + // MARK: UISearchResultsUpdating + + func updateSearchResults(for searchController: UISearchController) { + let accountSearchResultsViewController = searchController.searchResultsController + as! AccountSearchResultsViewController + accountSearchResultsViewController.accounts = accounts.filter { + guard let string = searchController.searchBar.text else { return false } + return $0.description.range(of: string, options: .caseInsensitive, range: nil, + locale: nil) != nil + } } - accounts.append(account) - persistAccountOrder() - let lastRow = accounts.count - 1 - let indexPaths = [IndexPath(row: lastRow, section: 0)] - tableView.insertRows(at: indexPaths, with: .automatic) - updateEditing() - } - - // MARK: AccountUpdateDelegate - - func updateAccount(_ account: Account) { - guard keychain.updateAccount(account) else { - presentTryAgainAlertWithTitle( - "Could Not Update Account", - message: "An error occurred when persisting the account updates to the keychain.") { - self.updateAccount(account) - } - return + + // MARK: AccountCreationDelegate + + func createAccount(_ account: Account) { + guard keychain.insertAccount(account) else { + presentTryAgainAlertWithTitle( + "Could Not Create Account", + message: "An error occurred when inserting the account into the keychain.") { + self.createAccount(account) + } + return + } + accounts.append(account) + persistAccountOrder() + let lastRow = accounts.count - 1 + let indexPaths = [IndexPath(row: lastRow, section: 0)] + tableView.insertRows(at: indexPaths, with: .automatic) + updateEditing() } - let row = accounts.index { $0 === account }! - let indexPath = IndexPath(row: row, section: 0) - guard let cell = tableView.cellForRow(at: indexPath) as? AccountCell else { return } - cell.updateWithDate(Date()) - } - fileprivate func presentTryAgainAlertWithTitle(_ title: String, message: String, handler: @escaping () -> Void) { - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + // MARK: AccountUpdateDelegate - let tryAgainAccountAction = UIAlertAction(title: "Try again", style: .default) { _ in - handler() + func updateAccount(_ account: Account) { + guard keychain.updateAccount(account) else { + presentTryAgainAlertWithTitle( + "Could Not Update Account", + message: "An error occurred when persisting the account updates to the keychain.") { + self.updateAccount(account) + } + return + } + let row = accounts.index { $0 === account }! + let indexPath = IndexPath(row: row, section: 0) + guard let cell = tableView.cellForRow(at: indexPath) as? AccountCell else { return } + cell.updateWithDate(Date()) } - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + fileprivate func presentTryAgainAlertWithTitle(_ title: String, message: String, handler: @escaping () -> Void) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - alertController.addAction(tryAgainAccountAction) - alertController.addAction(cancelAction) + let tryAgainAccountAction = UIAlertAction(title: "Try again", style: .default) { _ in + handler() + } + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) - present(alertController, animated: true, completion: nil) - } + alertController.addAction(tryAgainAccountAction) + alertController.addAction(cancelAction) + + present(alertController, animated: true, completion: nil) + } } diff --git a/Tofu/Algorithm.swift b/Tofu/Algorithm.swift index ffdf9eb..53997b8 100644 --- a/Tofu/Algorithm.swift +++ b/Tofu/Algorithm.swift @@ -1,31 +1,31 @@ import Foundation enum Algorithm { - case sha1 - case sha256 - case sha512 - - var name: String { - switch self { - case .sha1: return "SHA1" - case .sha256: return "SHA256" - case .sha512: return "SHA512" + case sha1 + case sha256 + case sha512 + + var name: String { + switch self { + case .sha1: return "SHA1" + case .sha256: return "SHA256" + case .sha512: return "SHA512" + } } - } - - var digestLength: Int { - switch self { - case .sha1: return Int(CC_SHA1_DIGEST_LENGTH) - case .sha256: return Int(CC_SHA256_DIGEST_LENGTH) - case .sha512: return Int(CC_SHA512_DIGEST_LENGTH) + + var digestLength: Int { + switch self { + case .sha1: return Int(CC_SHA1_DIGEST_LENGTH) + case .sha256: return Int(CC_SHA256_DIGEST_LENGTH) + case .sha512: return Int(CC_SHA512_DIGEST_LENGTH) + } } - } - - var hmacAlgorithm: CCHmacAlgorithm { - switch self { - case .sha1: return CCHmacAlgorithm(kCCHmacAlgSHA1) - case .sha256: return CCHmacAlgorithm(kCCHmacAlgSHA256) - case .sha512: return CCHmacAlgorithm(kCCHmacAlgSHA512) + + var hmacAlgorithm: CCHmacAlgorithm { + switch self { + case .sha1: return CCHmacAlgorithm(kCCHmacAlgSHA1) + case .sha256: return CCHmacAlgorithm(kCCHmacAlgSHA256) + case .sha512: return CCHmacAlgorithm(kCCHmacAlgSHA512) + } } - } } diff --git a/Tofu/AlgorithmSelectionDelegate.swift b/Tofu/AlgorithmSelectionDelegate.swift index 13054d9..12b603a 100644 --- a/Tofu/AlgorithmSelectionDelegate.swift +++ b/Tofu/AlgorithmSelectionDelegate.swift @@ -1,3 +1,3 @@ protocol AlgorithmSelectionDelegate: class { - func selectAlgorithm(_ algorithm: Algorithm) + func selectAlgorithm(_ algorithm: Algorithm) } diff --git a/Tofu/AlgorithmsViewController.swift b/Tofu/AlgorithmsViewController.swift index 3d3f688..1641057 100644 --- a/Tofu/AlgorithmsViewController.swift +++ b/Tofu/AlgorithmsViewController.swift @@ -3,40 +3,40 @@ import UIKit private let algorithmCellIdentifier = "AlgorithmCell" final class AlgorithmsViewController: UITableViewController { - var algorithms = [Algorithm]() - var selected: Algorithm! - var delegate: AlgorithmSelectionDelegate? - - // MARK: UITableViewDataSource - - override func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return algorithms.count - } - - override func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: algorithmCellIdentifier, - for: indexPath) - let algorithm = algorithms[indexPath.row] - cell.textLabel?.text = algorithm.name - cell.accessoryType = selected == algorithm ? .checkmark : .none - return cell - } - - // MARK: UITableViewDelegate - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - let previouslySelectedCell = tableView.cellForRow( - at: IndexPath(row: algorithms.index(of: selected)!, section: 0))! - previouslySelectedCell.accessoryType = .none - let selectedCell = tableView.cellForRow(at: indexPath)! - selectedCell.accessoryType = .checkmark - selected = algorithms[indexPath.row] - delegate?.selectAlgorithm(selected) - } + var algorithms = [Algorithm]() + var selected: Algorithm! + var delegate: AlgorithmSelectionDelegate? + + // MARK: UITableViewDataSource + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return algorithms.count + } + + override func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: algorithmCellIdentifier, + for: indexPath) + let algorithm = algorithms[indexPath.row] + cell.textLabel?.text = algorithm.name + cell.accessoryType = selected == algorithm ? .checkmark : .none + return cell + } + + // MARK: UITableViewDelegate + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let previouslySelectedCell = tableView.cellForRow( + at: IndexPath(row: algorithms.index(of: selected)!, section: 0))! + previouslySelectedCell.accessoryType = .none + let selectedCell = tableView.cellForRow(at: indexPath)! + selectedCell.accessoryType = .checkmark + selected = algorithms[indexPath.row] + delegate?.selectAlgorithm(selected) + } } diff --git a/Tofu/AppDelegate.swift b/Tofu/AppDelegate.swift index a023ad9..9ae9e9e 100644 --- a/Tofu/AppDelegate.swift +++ b/Tofu/AppDelegate.swift @@ -2,10 +2,10 @@ import UIKit @UIApplicationMain final class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - - func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { - return true - } + var window: UIWindow? + + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + return true + } } diff --git a/Tofu/CircularProgressView.swift b/Tofu/CircularProgressView.swift index 5414f2c..b11309f 100644 --- a/Tofu/CircularProgressView.swift +++ b/Tofu/CircularProgressView.swift @@ -1,44 +1,44 @@ import UIKit final class CircularProgressView: UIView { - var progress: Double = 0 { - didSet { - maskLayer.strokeEnd = min(max(CGFloat(progress), 0), 1) + var progress: Double = 0 { + didSet { + maskLayer.strokeEnd = min(max(CGFloat(progress), 0), 1) + } } - } - override var tintColor: UIColor! { - didSet { - imageView.tintColor = tintColor - backgroundImageView.tintColor = tintColor + override var tintColor: UIColor! { + didSet { + imageView.tintColor = tintColor + backgroundImageView.tintColor = tintColor + } } - } - fileprivate let maskLayer = CAShapeLayer() - fileprivate let imageView: UIImageView - fileprivate let backgroundImageView: UIImageView + fileprivate let maskLayer = CAShapeLayer() + fileprivate let imageView: UIImageView + fileprivate let backgroundImageView: UIImageView - init() { - let backgroundImage = UIImage(named: "CircularProgressViewBorderThin")! - backgroundImageView = UIImageView(image: backgroundImage) - let image = UIImage(named: "CircularProgressViewBorderThick")! - imageView = UIImageView(image: image) - super.init(frame: backgroundImageView.frame) - let x = frame.size.width / 2 - let y = frame.size.height / 2 - let radius = max(x, y) - let path = CGMutablePath() - path.move(to: CGPoint(x: x, y: y - radius / 2)) - path.addArc(center: CGPoint(x: x, y: y), radius: radius / 2, startAngle: -CGFloat.pi / 2, endAngle: 3 * CGFloat.pi / 2, clockwise: false) - maskLayer.fillColor = UIColor.clear.cgColor - maskLayer.strokeColor = UIColor.black.cgColor - maskLayer.lineWidth = radius - maskLayer.path = path - maskLayer.strokeEnd = CGFloat(progress) - imageView.layer.mask = maskLayer - addSubview(backgroundImageView) - addSubview(imageView) - } + init() { + let backgroundImage = UIImage(named: "CircularProgressViewBorderThin")! + backgroundImageView = UIImageView(image: backgroundImage) + let image = UIImage(named: "CircularProgressViewBorderThick")! + imageView = UIImageView(image: image) + super.init(frame: backgroundImageView.frame) + let x = frame.size.width / 2 + let y = frame.size.height / 2 + let radius = max(x, y) + let path = CGMutablePath() + path.move(to: CGPoint(x: x, y: y - radius / 2)) + path.addArc(center: CGPoint(x: x, y: y), radius: radius / 2, startAngle: -CGFloat.pi / 2, endAngle: 3 * CGFloat.pi / 2, clockwise: false) + maskLayer.fillColor = UIColor.clear.cgColor + maskLayer.strokeColor = UIColor.black.cgColor + maskLayer.lineWidth = radius + maskLayer.path = path + maskLayer.strokeEnd = CGFloat(progress) + imageView.layer.mask = maskLayer + addSubview(backgroundImageView) + addSubview(imageView) + } - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } } diff --git a/Tofu/Data.swift b/Tofu/Data.swift index c42501b..fc07b83 100644 --- a/Tofu/Data.swift +++ b/Tofu/Data.swift @@ -1,101 +1,101 @@ import Foundation private enum DecodedByte { - case valid(UInt8) - case invalid - case padding + case valid(UInt8) + case invalid + case padding } private let padding: UInt8 = 61 // = private let byteMappings: [CountableRange] = [ - 65 ..< 91, // A-Z - 50 ..< 56, // 2-7 + 65 ..< 91, // A-Z + 50 ..< 56, // 2-7 ] private func decode(byte encodedByte: UInt8) -> DecodedByte { - if encodedByte == padding { return .padding } - var decodedStart: UInt8 = 0 - for range in byteMappings { - if range.contains(encodedByte) { - let result = decodedStart + (encodedByte - range.lowerBound) - return .valid(result) + if encodedByte == padding { return .padding } + var decodedStart: UInt8 = 0 + for range in byteMappings { + if range.contains(encodedByte) { + let result = decodedStart + (encodedByte - range.lowerBound) + return .valid(result) + } + decodedStart += range.upperBound - range.lowerBound } - decodedStart += range.upperBound - range.lowerBound - } - return .invalid + return .invalid } private func decoded(bytes encodedBytes: [UInt8]) -> [UInt8]? { - var decodedBytes = [UInt8]() - decodedBytes.reserveCapacity(encodedBytes.count / 8 * 5) + var decodedBytes = [UInt8]() + decodedBytes.reserveCapacity(encodedBytes.count / 8 * 5) - var decodedByte: UInt8 = 0 - var characterCount = 0 - var paddingCount = 0 - var index = 0 + var decodedByte: UInt8 = 0 + var characterCount = 0 + var paddingCount = 0 + var index = 0 - for encodedByte in encodedBytes { - let value: UInt8 + for encodedByte in encodedBytes { + let value: UInt8 - switch decode(byte: encodedByte) { - case .valid(let v): - value = v - characterCount += 1 - case .invalid: - return nil - case .padding: - paddingCount += 1 - continue - } + switch decode(byte: encodedByte) { + case .valid(let v): + value = v + characterCount += 1 + case .invalid: + return nil + case .padding: + paddingCount += 1 + continue + } - // Only allow padding at the end of the sequence - if paddingCount > 0 { return nil } + // Only allow padding at the end of the sequence + if paddingCount > 0 { return nil } - switch index % 8 { - case 0: - decodedByte = value << 3 - case 1: - decodedByte |= value >> 2 - decodedBytes.append(decodedByte) - decodedByte = value << 6 - case 2: - decodedByte |= value << 1 - case 3: - decodedByte |= value >> 4 - decodedBytes.append(decodedByte) - decodedByte = value << 4 - case 4: - decodedByte |= value >> 1 - decodedBytes.append(decodedByte) - decodedByte = value << 7 - case 5: - decodedByte |= value << 2 - case 6: - decodedByte |= value >> 3 - decodedBytes.append(decodedByte) - decodedByte = value << 5 - case 7: - decodedByte |= value - decodedBytes.append(decodedByte) - default: - fatalError() - } + switch index % 8 { + case 0: + decodedByte = value << 3 + case 1: + decodedByte |= value >> 2 + decodedBytes.append(decodedByte) + decodedByte = value << 6 + case 2: + decodedByte |= value << 1 + case 3: + decodedByte |= value >> 4 + decodedBytes.append(decodedByte) + decodedByte = value << 4 + case 4: + decodedByte |= value >> 1 + decodedBytes.append(decodedByte) + decodedByte = value << 7 + case 5: + decodedByte |= value << 2 + case 6: + decodedByte |= value >> 3 + decodedBytes.append(decodedByte) + decodedByte = value << 5 + case 7: + decodedByte |= value + decodedBytes.append(decodedByte) + default: + fatalError() + } - index += 1 - } + index += 1 + } - let characterCountIsValid = [0, 2, 4, 5, 7].contains(characterCount % 8) - let paddingCountIsValid = paddingCount == 0 || (characterCount + paddingCount) % 8 == 0 - guard characterCountIsValid && paddingCountIsValid else { return nil } + let characterCountIsValid = [0, 2, 4, 5, 7].contains(characterCount % 8) + let paddingCountIsValid = paddingCount == 0 || (characterCount + paddingCount) % 8 == 0 + guard characterCountIsValid && paddingCountIsValid else { return nil } - return decodedBytes + return decodedBytes } extension Data { - init?(base32Encoded string: String) { - let encodedBytes = Array(string.uppercased().utf8) - guard let decodedBytes = decoded(bytes: encodedBytes) else { return nil } - self.init(bytes: decodedBytes) - } + init?(base32Encoded string: String) { + let encodedBytes = Array(string.uppercased().utf8) + guard let decodedBytes = decoded(bytes: encodedBytes) else { return nil } + self.init(bytes: decodedBytes) + } } diff --git a/Tofu/Keychain.swift b/Tofu/Keychain.swift index ead76d2..c77d655 100644 --- a/Tofu/Keychain.swift +++ b/Tofu/Keychain.swift @@ -1,123 +1,123 @@ import Foundation private func archivedDataWithAccount(_ account: Account) -> Data { - let data = NSMutableData() - let coder = NSKeyedArchiver(forWritingWith: data) + let data = NSMutableData() + let coder = NSKeyedArchiver(forWritingWith: data) - coder.encode(account.password.timeBased, forKey: "timeBased") - let algorithmIdentifier: Int32 - switch account.password.algorithm { - case .sha1: algorithmIdentifier = 0 - case .sha256: algorithmIdentifier = 1 - case .sha512: algorithmIdentifier = 2 - } - coder.encode(algorithmIdentifier, forKey: "algorithm") - coder.encode(Int32(account.password.digits), forKey: "digits") - coder.encode(account.password.secret, forKey: "secret") - coder.encode(Int32(account.password.counter), forKey: "counter") - coder.encode(Int32(account.password.period), forKey: "period") - coder.encode(account.name, forKey: "name") - coder.encode(account.issuer, forKey: "issuer") - coder.finishEncoding() + coder.encode(account.password.timeBased, forKey: "timeBased") + let algorithmIdentifier: Int32 + switch account.password.algorithm { + case .sha1: algorithmIdentifier = 0 + case .sha256: algorithmIdentifier = 1 + case .sha512: algorithmIdentifier = 2 + } + coder.encode(algorithmIdentifier, forKey: "algorithm") + coder.encode(Int32(account.password.digits), forKey: "digits") + coder.encode(account.password.secret, forKey: "secret") + coder.encode(Int32(account.password.counter), forKey: "counter") + coder.encode(Int32(account.password.period), forKey: "period") + coder.encode(account.name, forKey: "name") + coder.encode(account.issuer, forKey: "issuer") + coder.finishEncoding() - var version: UInt8 = 1 - let size = MemoryLayout.size(ofValue: version) - let versionedData = NSMutableData(bytes: &version, length: size) - versionedData.append(data as Data) + var version: UInt8 = 1 + let size = MemoryLayout.size(ofValue: version) + let versionedData = NSMutableData(bytes: &version, length: size) + versionedData.append(data as Data) - return versionedData as Data + return versionedData as Data } private func unarchiveAccountWithData(_ data: Data) -> Account? { - let version = data.first - guard version == 1 else { return nil } - let coder = NSKeyedUnarchiver(forReadingWith: data.subdata(in: 1.. Account? { - let query: [NSString: AnyObject] = [ - kSecClass: kSecClassGenericPassword, - kSecValuePersistentRef: persistentRef as AnyObject, - kSecReturnData: kCFBooleanTrue, - ] - var result: AnyObject? - let code = SecItemCopyMatching(query as CFDictionary, &result) - guard code == errSecSuccess, - let data = result as? Data, - let account = unarchiveAccountWithData(data) else { return nil } - account.persistentRef = persistentRef - return account -} - -final class Keychain { - var accounts: [Account] { let query: [NSString: AnyObject] = [ - kSecClass: kSecClassGenericPassword, - kSecReturnPersistentRef: kCFBooleanTrue, - kSecMatchLimit: kSecMatchLimitAll, + kSecClass: kSecClassGenericPassword, + kSecValuePersistentRef: persistentRef as AnyObject, + kSecReturnData: kCFBooleanTrue, ] var result: AnyObject? let code = SecItemCopyMatching(query as CFDictionary, &result) - guard code == errSecSuccess, let persistentRefs = result as? [Data] else { return [] } - return persistentRefs.compactMap { accountWithPersistentRef($0) } - } + guard code == errSecSuccess, + let data = result as? Data, + let account = unarchiveAccountWithData(data) else { return nil } + account.persistentRef = persistentRef + return account +} - func insertAccount(_ account: Account) -> Bool { - let query: [NSString: AnyObject] = [ - kSecClass: kSecClassGenericPassword, - kSecAttrAccount: ProcessInfo().globallyUniqueString as AnyObject, - kSecAttrDescription: account.description as AnyObject, - kSecValueData: archivedDataWithAccount(account) as AnyObject, - kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked, - kSecReturnPersistentRef: true as AnyObject, - ] - var result: AnyObject? - guard SecItemAdd(query as CFDictionary, &result) == errSecSuccess else { return false } - account.persistentRef = (result as! Data) - return true - } +final class Keychain { + var accounts: [Account] { + let query: [NSString: AnyObject] = [ + kSecClass: kSecClassGenericPassword, + kSecReturnPersistentRef: kCFBooleanTrue, + kSecMatchLimit: kSecMatchLimitAll, + ] + var result: AnyObject? + let code = SecItemCopyMatching(query as CFDictionary, &result) + guard code == errSecSuccess, let persistentRefs = result as? [Data] else { return [] } + return persistentRefs.compactMap { accountWithPersistentRef($0) } + } - func updateAccount(_ account: Account) -> Bool { - let query: [NSString: Any] = [ - kSecClass: kSecClassGenericPassword, - kSecValuePersistentRef: account.persistentRef! - ] - let attributes: [NSString: AnyObject] = [ - kSecAttrDescription: account.description as AnyObject, - kSecValueData: archivedDataWithAccount(account) as AnyObject, - kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked, - ] - return SecItemUpdate(query as CFDictionary, attributes as CFDictionary) == errSecSuccess - } + func insertAccount(_ account: Account) -> Bool { + let query: [NSString: AnyObject] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: ProcessInfo().globallyUniqueString as AnyObject, + kSecAttrDescription: account.description as AnyObject, + kSecValueData: archivedDataWithAccount(account) as AnyObject, + kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked, + kSecReturnPersistentRef: true as AnyObject, + ] + var result: AnyObject? + guard SecItemAdd(query as CFDictionary, &result) == errSecSuccess else { return false } + account.persistentRef = (result as! Data) + return true + } - func deleteAccount(_ account: Account) -> Bool { - let query: [NSString: Any] = [ - kSecClass: kSecClassGenericPassword, - kSecValuePersistentRef: account.persistentRef! - ] - return SecItemDelete(query as CFDictionary) == errSecSuccess - } + func updateAccount(_ account: Account) -> Bool { + let query: [NSString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecValuePersistentRef: account.persistentRef! + ] + let attributes: [NSString: AnyObject] = [ + kSecAttrDescription: account.description as AnyObject, + kSecValueData: archivedDataWithAccount(account) as AnyObject, + kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked, + ] + return SecItemUpdate(query as CFDictionary, attributes as CFDictionary) == errSecSuccess + } + + func deleteAccount(_ account: Account) -> Bool { + let query: [NSString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecValuePersistentRef: account.persistentRef! + ] + return SecItemDelete(query as CFDictionary) == errSecSuccess + } } diff --git a/Tofu/Password.swift b/Tofu/Password.swift index 28c1ab8..a230b01 100644 --- a/Tofu/Password.swift +++ b/Tofu/Password.swift @@ -1,30 +1,30 @@ import Foundation final class Password { - var algorithm: Algorithm = .sha1 - var counter = 0 - var digits = 6 - var period = 30 - var secret = Data() - var timeBased = false - - func valueForDate(_ date: Date) -> String { - let counter = timeBased ? UInt64(date.timeIntervalSince1970) / UInt64(period) : UInt64(self.counter) - var input = counter.bigEndian - let digest = UnsafeMutablePointer.allocate(capacity: algorithm.digestLength) - defer { digest.deallocate() } - secret.withUnsafeBytes { secretBytes in CCHmac(algorithm.hmacAlgorithm, secretBytes, secret.count, &input, MemoryLayout.size(ofValue: input), digest) } - let offset = digest[algorithm.digestLength - 1] & 0x0f - let number = (digest + Int(offset)).withMemoryRebound(to: UInt32.self, capacity: 1) { UInt32(bigEndian: $0.pointee) } & 0x7fffffff - return String(format: "%0\(digits)d", number % UInt32(pow(10, Float(digits)))) - } - - func progressForDate(_ date: Date) -> Double { - return timeIntervalRemainingForDate(date) / Double(period) - } - - func timeIntervalRemainingForDate(_ date: Date) -> Double { - let period = Double(self.period) - return period - (date.timeIntervalSince1970.truncatingRemainder(dividingBy: period)) - } + var algorithm: Algorithm = .sha1 + var counter = 0 + var digits = 6 + var period = 30 + var secret = Data() + var timeBased = false + + func valueForDate(_ date: Date) -> String { + let counter = timeBased ? UInt64(date.timeIntervalSince1970) / UInt64(period) : UInt64(self.counter) + var input = counter.bigEndian + let digest = UnsafeMutablePointer.allocate(capacity: algorithm.digestLength) + defer { digest.deallocate() } + secret.withUnsafeBytes { secretBytes in CCHmac(algorithm.hmacAlgorithm, secretBytes, secret.count, &input, MemoryLayout.size(ofValue: input), digest) } + let offset = digest[algorithm.digestLength - 1] & 0x0f + let number = (digest + Int(offset)).withMemoryRebound(to: UInt32.self, capacity: 1) { UInt32(bigEndian: $0.pointee) } & 0x7fffffff + return String(format: "%0\(digits)d", number % UInt32(pow(10, Float(digits)))) + } + + func progressForDate(_ date: Date) -> Double { + return timeIntervalRemainingForDate(date) / Double(period) + } + + func timeIntervalRemainingForDate(_ date: Date) -> Double { + let period = Double(self.period) + return period - (date.timeIntervalSince1970.truncatingRemainder(dividingBy: period)) + } } diff --git a/Tofu/ScanningViewController.swift b/Tofu/ScanningViewController.swift index 3d4ed5d..718697a 100644 --- a/Tofu/ScanningViewController.swift +++ b/Tofu/ScanningViewController.swift @@ -3,78 +3,78 @@ import AVFoundation final class ScanningViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate { - @IBOutlet weak var allowCameraAccessView: UIView! - var delegate: AccountCreationDelegate? - fileprivate var session = AVCaptureSession() - fileprivate let output = AVCaptureMetadataOutput() - fileprivate var layer: AVCaptureVideoPreviewLayer? - - @IBAction func didPressCancel(_ sender: UIBarButtonItem) { - output.setMetadataObjectsDelegate(nil, queue: nil) - presentingViewController?.dismiss(animated: true, completion: nil) - } - - override func viewDidLoad() { - super.viewDidLoad() - - if AVCaptureDevice.authorizationStatus(for: .video) == .authorized { - startScanning() - } else { - AVCaptureDevice.requestAccess(for: .video) { granted in - guard granted else { return } - DispatchQueue.main.async { - self.startScanning() + @IBOutlet weak var allowCameraAccessView: UIView! + var delegate: AccountCreationDelegate? + fileprivate var session = AVCaptureSession() + fileprivate let output = AVCaptureMetadataOutput() + fileprivate var layer: AVCaptureVideoPreviewLayer? + + @IBAction func didPressCancel(_ sender: UIBarButtonItem) { + output.setMetadataObjectsDelegate(nil, queue: nil) + presentingViewController?.dismiss(animated: true, completion: nil) + } + + override func viewDidLoad() { + super.viewDidLoad() + + if AVCaptureDevice.authorizationStatus(for: .video) == .authorized { + startScanning() + } else { + AVCaptureDevice.requestAccess(for: .video) { granted in + guard granted else { return } + DispatchQueue.main.async { + self.startScanning() + } + } } - } } - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - updateLayerFrameAndOrientation() - } - - fileprivate func startScanning() { - let device = AVCaptureDevice.default(for: .video) - if let input = try? AVCaptureDeviceInput(device: device!) { - allowCameraAccessView.isHidden = true - navigationItem.prompt = "Point your camera at a QR code to scan it." - session.addInput(input) - session.addOutput(output) - output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) - output.metadataObjectTypes = [.qr] - layer = AVCaptureVideoPreviewLayer(session: session) - layer!.videoGravity = .resizeAspectFill - view.layer.addSublayer(layer!) - updateLayerFrameAndOrientation() - session.startRunning() + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + updateLayerFrameAndOrientation() } - } - - fileprivate func updateLayerFrameAndOrientation() { - layer?.frame = view.layer.bounds - switch UIDevice.current.orientation { - case .landscapeLeft: - layer?.connection?.videoOrientation = .landscapeRight - case .landscapeRight: - layer?.connection?.videoOrientation = .landscapeLeft - default: - layer?.connection?.videoOrientation = .portrait + + fileprivate func startScanning() { + let device = AVCaptureDevice.default(for: .video) + if let input = try? AVCaptureDeviceInput(device: device!) { + allowCameraAccessView.isHidden = true + navigationItem.prompt = "Point your camera at a QR code to scan it." + session.addInput(input) + session.addOutput(output) + output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) + output.metadataObjectTypes = [.qr] + layer = AVCaptureVideoPreviewLayer(session: session) + layer!.videoGravity = .resizeAspectFill + view.layer.addSublayer(layer!) + updateLayerFrameAndOrientation() + session.startRunning() + } + } + + fileprivate func updateLayerFrameAndOrientation() { + layer?.frame = view.layer.bounds + switch UIDevice.current.orientation { + case .landscapeLeft: + layer?.connection?.videoOrientation = .landscapeRight + case .landscapeRight: + layer?.connection?.videoOrientation = .landscapeLeft + default: + layer?.connection?.videoOrientation = .portrait + } + } + + // MARK: AVCaptureMetadataOutputObjectsDelegate + + func metadataOutput( + _ output: AVCaptureMetadataOutput, + didOutput metadataObjects: [AVMetadataObject], + from connection: AVCaptureConnection) { + guard metadataObjects.count > 0, + let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, metadataObject.type == AVMetadataObject.ObjectType.qr, + let url = URL(string: metadataObject.stringValue!), + let account = Account(url: url) else { return } + output.setMetadataObjectsDelegate(nil, queue: nil) + delegate?.createAccount(account) + presentingViewController?.dismiss(animated: true, completion: nil) } - } - - // MARK: AVCaptureMetadataOutputObjectsDelegate - - func metadataOutput( - _ output: AVCaptureMetadataOutput, - didOutput metadataObjects: [AVMetadataObject], - from connection: AVCaptureConnection) { - guard metadataObjects.count > 0, - let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, metadataObject.type == AVMetadataObject.ObjectType.qr, - let url = URL(string: metadataObject.stringValue!), - let account = Account(url: url) else { return } - output.setMetadataObjectsDelegate(nil, queue: nil) - delegate?.createAccount(account) - presentingViewController?.dismiss(animated: true, completion: nil) - } } diff --git a/TofuTests/AccountTests.swift b/TofuTests/AccountTests.swift index 05b208d..6b34a10 100644 --- a/TofuTests/AccountTests.swift +++ b/TofuTests/AccountTests.swift @@ -2,117 +2,117 @@ import XCTest @testable import Tofu class AccountTests: XCTestCase { - func testInitWithURL() { - var account = Account(url: URL( - string: "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example")!) - XCTAssertEqual(account?.name, "alice@example.com") - XCTAssertEqual(account?.issuer, "Example") - XCTAssertEqual(account?.password.timeBased, true) - XCTAssertEqual(account?.password.secret, Data(base32Encoded: "JBSWY3DPEHPK3PXP")!) - XCTAssertEqual(account?.password.algorithm, .sha1) - XCTAssertEqual(account?.password.digits, 6) - XCTAssertEqual(account?.password.period, 30) - - account = Account(url: URL( - string: "otpauth://hotp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&counter=1")!) - XCTAssertEqual(account?.name, "alice@example.com") - XCTAssertEqual(account?.issuer, "Example") - XCTAssertEqual(account?.password.timeBased, false) - XCTAssertEqual(account?.password.secret, Data(base32Encoded: "JBSWY3DPEHPK3PXP")!) - XCTAssertEqual(account?.password.algorithm, .sha1) - XCTAssertEqual(account?.password.digits, 6) - XCTAssertEqual(account?.password.counter, 1) - - account = Account(url: URL( - string: "otpauth://totp/alice@example.com?secret=JBSWY3DPEHPK3PXP")!) - XCTAssertEqual(account?.name, "alice@example.com") - - account = Account(url: URL( - string: "otpauth://totp/Example%3Aalice@example.com?secret=JBSWY3DPEHPK3PXP")!) - XCTAssertEqual(account?.name, "alice@example.com") - - account = Account(url: URL( - string: "otpauth://totp/Example:%20%20alice@example.com?secret=JBSWY3DPEHPK3PXP")!) - XCTAssertEqual(account?.name, "alice@example.com") - - account = Account(url: URL( - string: "otpauth://totp/Example%3A%20%20alice@example.com?secret=JBSWY3DPEHPK3PXP")!) - XCTAssertEqual(account?.name, "alice@example.com") - - account = Account(url: URL( - string: "otpauth://totp/example.com/alice?secret=JBSWY3DPEHPK3PXP")!) - XCTAssertEqual(account?.name, "example.com/alice") - - account = Account(url: URL( - string: "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP")!) - XCTAssertEqual(account?.issuer, "Example") - - account = Account(url: URL(string: "otpauth://totp/alice@example.com")!) - XCTAssertNil(account) - - account = Account(url: URL(string: "otpauth://totp/alice@example.com?secret=AAA")!) - XCTAssertNil(account) - - account = Account(url: URL( - string: "otpauth://totp/alice@example.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1")!) - XCTAssertEqual(account?.password.algorithm, .sha1) - - account = Account(url: URL( - string: "otpauth://totp/alice@example.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256")!) - XCTAssertEqual(account?.password.algorithm, .sha256) - - account = Account(url: URL( - string: "otpauth://totp/alice@example.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA512")!) - XCTAssertEqual(account?.password.algorithm, .sha512) - - account = Account(url: URL( - string: "otpauth://totp/alice@example.com?secret=JBSWY3DPEHPK3PXP&digits=6")!) - XCTAssertEqual(account?.password.digits, 6) - - account = Account(url: URL( - string: "otpauth://totp/alice@example.com?secret=JBSWY3DPEHPK3PXP&digits=8")!) - XCTAssertEqual(account?.password.digits, 8) - - account = Account(url: URL( - string: "otpauth://hotp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP")!) - XCTAssertEqual(account?.password.timeBased, false) - XCTAssertEqual(account?.password.digits, 6) - XCTAssertEqual(account?.password.counter, 0) - - account = Account(url: URL( - string: "otpauth://totp/alice@example.com?secret=JBSWY3DPEHPK3PXP&period=60")!) - XCTAssertEqual(account?.password.period, 60) - } - - func testDescription() { - let account = Account() - - account.name = "test@example.com" - account.issuer = "Example" - XCTAssertEqual(account.description, "Example (test@example.com)") - - account.name = "test@example.com" - account.issuer = nil - XCTAssertEqual(account.description, "test@example.com") - - account.name = "test@example.com" - account.issuer = "" - XCTAssertEqual(account.description, "test@example.com") - - account.name = nil - account.issuer = "Example" - XCTAssertEqual(account.description, "Example") - - account.name = "" - account.issuer = "Example" - XCTAssertEqual(account.description, "Example") - - account.name = nil - account.issuer = nil - XCTAssertEqual(account.description, "") - - account.name = "" - account.issuer = "" - XCTAssertEqual(account.description, "") - } + func testInitWithURL() { + var account = Account(url: URL( + string: "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example")!) + XCTAssertEqual(account?.name, "alice@example.com") + XCTAssertEqual(account?.issuer, "Example") + XCTAssertEqual(account?.password.timeBased, true) + XCTAssertEqual(account?.password.secret, Data(base32Encoded: "JBSWY3DPEHPK3PXP")!) + XCTAssertEqual(account?.password.algorithm, .sha1) + XCTAssertEqual(account?.password.digits, 6) + XCTAssertEqual(account?.password.period, 30) + + account = Account(url: URL( + string: "otpauth://hotp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&counter=1")!) + XCTAssertEqual(account?.name, "alice@example.com") + XCTAssertEqual(account?.issuer, "Example") + XCTAssertEqual(account?.password.timeBased, false) + XCTAssertEqual(account?.password.secret, Data(base32Encoded: "JBSWY3DPEHPK3PXP")!) + XCTAssertEqual(account?.password.algorithm, .sha1) + XCTAssertEqual(account?.password.digits, 6) + XCTAssertEqual(account?.password.counter, 1) + + account = Account(url: URL( + string: "otpauth://totp/alice@example.com?secret=JBSWY3DPEHPK3PXP")!) + XCTAssertEqual(account?.name, "alice@example.com") + + account = Account(url: URL( + string: "otpauth://totp/Example%3Aalice@example.com?secret=JBSWY3DPEHPK3PXP")!) + XCTAssertEqual(account?.name, "alice@example.com") + + account = Account(url: URL( + string: "otpauth://totp/Example:%20%20alice@example.com?secret=JBSWY3DPEHPK3PXP")!) + XCTAssertEqual(account?.name, "alice@example.com") + + account = Account(url: URL( + string: "otpauth://totp/Example%3A%20%20alice@example.com?secret=JBSWY3DPEHPK3PXP")!) + XCTAssertEqual(account?.name, "alice@example.com") + + account = Account(url: URL( + string: "otpauth://totp/example.com/alice?secret=JBSWY3DPEHPK3PXP")!) + XCTAssertEqual(account?.name, "example.com/alice") + + account = Account(url: URL( + string: "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP")!) + XCTAssertEqual(account?.issuer, "Example") + + account = Account(url: URL(string: "otpauth://totp/alice@example.com")!) + XCTAssertNil(account) + + account = Account(url: URL(string: "otpauth://totp/alice@example.com?secret=AAA")!) + XCTAssertNil(account) + + account = Account(url: URL( + string: "otpauth://totp/alice@example.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1")!) + XCTAssertEqual(account?.password.algorithm, .sha1) + + account = Account(url: URL( + string: "otpauth://totp/alice@example.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256")!) + XCTAssertEqual(account?.password.algorithm, .sha256) + + account = Account(url: URL( + string: "otpauth://totp/alice@example.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA512")!) + XCTAssertEqual(account?.password.algorithm, .sha512) + + account = Account(url: URL( + string: "otpauth://totp/alice@example.com?secret=JBSWY3DPEHPK3PXP&digits=6")!) + XCTAssertEqual(account?.password.digits, 6) + + account = Account(url: URL( + string: "otpauth://totp/alice@example.com?secret=JBSWY3DPEHPK3PXP&digits=8")!) + XCTAssertEqual(account?.password.digits, 8) + + account = Account(url: URL( + string: "otpauth://hotp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP")!) + XCTAssertEqual(account?.password.timeBased, false) + XCTAssertEqual(account?.password.digits, 6) + XCTAssertEqual(account?.password.counter, 0) + + account = Account(url: URL( + string: "otpauth://totp/alice@example.com?secret=JBSWY3DPEHPK3PXP&period=60")!) + XCTAssertEqual(account?.password.period, 60) + } + + func testDescription() { + let account = Account() + + account.name = "test@example.com" + account.issuer = "Example" + XCTAssertEqual(account.description, "Example (test@example.com)") + + account.name = "test@example.com" + account.issuer = nil + XCTAssertEqual(account.description, "test@example.com") + + account.name = "test@example.com" + account.issuer = "" + XCTAssertEqual(account.description, "test@example.com") + + account.name = nil + account.issuer = "Example" + XCTAssertEqual(account.description, "Example") + + account.name = "" + account.issuer = "Example" + XCTAssertEqual(account.description, "Example") + + account.name = nil + account.issuer = nil + XCTAssertEqual(account.description, "") + + account.name = "" + account.issuer = "" + XCTAssertEqual(account.description, "") + } } diff --git a/TofuTests/DataTests.swift b/TofuTests/DataTests.swift index 0646217..b8381b1 100644 --- a/TofuTests/DataTests.swift +++ b/TofuTests/DataTests.swift @@ -2,35 +2,35 @@ import XCTest @testable import Tofu class DataTests: XCTestCase { - func testInitBase32Encoded() { - let tests = [ - ("", ""), - ("MY======", "f"), - ("MZXQ====", "fo"), - ("MZXW6===", "foo"), - ("MZXW6YQ=", "foob"), - ("MZXW6YTB", "fooba"), - ("MZXW6YTBOI======", "foobar"), - ("MY", "f"), - ("MZXQ", "fo"), - ("MZXW6", "foo"), - ("MZXW6YQ", "foob"), - ("MZXW6YTB", "fooba"), - ("MZXW6YTBOI", "foobar"), - ("mzxw6ytboi", "foobar"), - ] - - for (actual, expected) in tests { - XCTAssertEqual(Data(base32Encoded: actual), - expected.data(using: String.Encoding.ascii)) + func testInitBase32Encoded() { + let tests = [ + ("", ""), + ("MY======", "f"), + ("MZXQ====", "fo"), + ("MZXW6===", "foo"), + ("MZXW6YQ=", "foob"), + ("MZXW6YTB", "fooba"), + ("MZXW6YTBOI======", "foobar"), + ("MY", "f"), + ("MZXQ", "fo"), + ("MZXW6", "foo"), + ("MZXW6YQ", "foob"), + ("MZXW6YTB", "fooba"), + ("MZXW6YTBOI", "foobar"), + ("mzxw6ytboi", "foobar"), + ] + + for (actual, expected) in tests { + XCTAssertEqual(Data(base32Encoded: actual), + expected.data(using: String.Encoding.ascii)) + } + + XCTAssertNil(Data(base32Encoded: "1")) // Invalid character + XCTAssertNil(Data(base32Encoded: "A")) // Invalid length + XCTAssertNil(Data(base32Encoded: "AAA")) + XCTAssertNil(Data(base32Encoded: "AAAAAA")) + XCTAssertNil(Data(base32Encoded: "MY==")) // Invalid padding + XCTAssertNil(Data(base32Encoded: "MY=====")) + XCTAssertNil(Data(base32Encoded: "MZXW6Y===")) } - - XCTAssertNil(Data(base32Encoded: "1")) // Invalid character - XCTAssertNil(Data(base32Encoded: "A")) // Invalid length - XCTAssertNil(Data(base32Encoded: "AAA")) - XCTAssertNil(Data(base32Encoded: "AAAAAA")) - XCTAssertNil(Data(base32Encoded: "MY==")) // Invalid padding - XCTAssertNil(Data(base32Encoded: "MY=====")) - XCTAssertNil(Data(base32Encoded: "MZXW6Y===")) - } } diff --git a/TofuTests/PasswordTests.swift b/TofuTests/PasswordTests.swift index 293e2b7..4149e4b 100644 --- a/TofuTests/PasswordTests.swift +++ b/TofuTests/PasswordTests.swift @@ -2,84 +2,84 @@ import XCTest @testable import Tofu class PasswordTests: XCTestCase { - func testValueForDate() { - let secret = "12345678901234567890".data(using: String.Encoding.ascii)! - let counterBasedTests: [(Int, String, String, String)] = [ - (0, "755224", "875740", "125165"), - (1, "287082", "247374", "342147"), - (2, "359152", "254785", "730102"), - (3, "969429", "496144", "778726"), - (4, "338314", "480556", "937510"), - (5, "254676", "697997", "848329"), - (6, "287922", "191609", "266680"), - (7, "162583", "579288", "588359"), - (8, "399871", "895912", "039399"), - (9, "520489", "184989", "643409"), - ] - let timeBasedTests = [ - (Date(timeIntervalSince1970: 59), "94287082", "32247374", "69342147"), - (Date(timeIntervalSince1970: 1111111109), "07081804", "34756375", "63049338"), - (Date(timeIntervalSince1970: 1111111111), "14050471", "74584430", "54380122"), - (Date(timeIntervalSince1970: 1234567890), "89005924", "42829826", "76671578"), - (Date(timeIntervalSince1970: 2000000000), "69279037", "78428693", "56464532"), - (Date(timeIntervalSince1970: 20000000000), "65353130", "24142410", "69481994"), - ] - let counterBasedSHA1Password = passwordWithSecret(secret, algorithm: .sha1, digits: 6, - timeBased: false) - let counterBasedSHA256Password = passwordWithSecret(secret, algorithm: .sha256, digits: 6, - timeBased: false) - let counterBasedSHA512Password = passwordWithSecret(secret, algorithm: .sha512, digits: 6, - timeBased: false) - let timeBasedSHA1Password = passwordWithSecret(secret, algorithm: .sha1, digits: 8, - timeBased: true) - let timeBasedSHA256Password = passwordWithSecret(secret, algorithm: .sha256, digits: 8, - timeBased: true) - let timeBasedSHA512Password = passwordWithSecret(secret, algorithm: .sha512, digits: 8, - timeBased: true) - - for (counter, expSHA1, expSHA256, expSHA512) in counterBasedTests { - counterBasedSHA1Password.counter = counter - XCTAssertEqual(counterBasedSHA1Password.valueForDate(Date()), expSHA1) - counterBasedSHA256Password.counter = counter - XCTAssertEqual(counterBasedSHA256Password.valueForDate(Date()), expSHA256) - counterBasedSHA512Password.counter = counter - XCTAssertEqual(counterBasedSHA512Password.valueForDate(Date()), expSHA512) + func testValueForDate() { + let secret = "12345678901234567890".data(using: String.Encoding.ascii)! + let counterBasedTests: [(Int, String, String, String)] = [ + (0, "755224", "875740", "125165"), + (1, "287082", "247374", "342147"), + (2, "359152", "254785", "730102"), + (3, "969429", "496144", "778726"), + (4, "338314", "480556", "937510"), + (5, "254676", "697997", "848329"), + (6, "287922", "191609", "266680"), + (7, "162583", "579288", "588359"), + (8, "399871", "895912", "039399"), + (9, "520489", "184989", "643409"), + ] + let timeBasedTests = [ + (Date(timeIntervalSince1970: 59), "94287082", "32247374", "69342147"), + (Date(timeIntervalSince1970: 1111111109), "07081804", "34756375", "63049338"), + (Date(timeIntervalSince1970: 1111111111), "14050471", "74584430", "54380122"), + (Date(timeIntervalSince1970: 1234567890), "89005924", "42829826", "76671578"), + (Date(timeIntervalSince1970: 2000000000), "69279037", "78428693", "56464532"), + (Date(timeIntervalSince1970: 20000000000), "65353130", "24142410", "69481994"), + ] + let counterBasedSHA1Password = passwordWithSecret(secret, algorithm: .sha1, digits: 6, + timeBased: false) + let counterBasedSHA256Password = passwordWithSecret(secret, algorithm: .sha256, digits: 6, + timeBased: false) + let counterBasedSHA512Password = passwordWithSecret(secret, algorithm: .sha512, digits: 6, + timeBased: false) + let timeBasedSHA1Password = passwordWithSecret(secret, algorithm: .sha1, digits: 8, + timeBased: true) + let timeBasedSHA256Password = passwordWithSecret(secret, algorithm: .sha256, digits: 8, + timeBased: true) + let timeBasedSHA512Password = passwordWithSecret(secret, algorithm: .sha512, digits: 8, + timeBased: true) + + for (counter, expSHA1, expSHA256, expSHA512) in counterBasedTests { + counterBasedSHA1Password.counter = counter + XCTAssertEqual(counterBasedSHA1Password.valueForDate(Date()), expSHA1) + counterBasedSHA256Password.counter = counter + XCTAssertEqual(counterBasedSHA256Password.valueForDate(Date()), expSHA256) + counterBasedSHA512Password.counter = counter + XCTAssertEqual(counterBasedSHA512Password.valueForDate(Date()), expSHA512) + } + + for (date, expSHA1, expSHA256, expSHA512) in timeBasedTests { + XCTAssertEqual(timeBasedSHA1Password.valueForDate(date), expSHA1) + XCTAssertEqual(timeBasedSHA256Password.valueForDate(date), expSHA256) + XCTAssertEqual(timeBasedSHA512Password.valueForDate(date), expSHA512) + } } - - for (date, expSHA1, expSHA256, expSHA512) in timeBasedTests { - XCTAssertEqual(timeBasedSHA1Password.valueForDate(date), expSHA1) - XCTAssertEqual(timeBasedSHA256Password.valueForDate(date), expSHA256) - XCTAssertEqual(timeBasedSHA512Password.valueForDate(date), expSHA512) + + func testProgressForDate() { + let password = Password() + password.period = 30 + + XCTAssertEqual(password.progressForDate(Date(timeIntervalSince1970: 0)), 1) + XCTAssertEqual(password.progressForDate(Date(timeIntervalSince1970: 15)), 0.5) + XCTAssertEqual(password.progressForDate(Date(timeIntervalSince1970: 22.5)), 0.25) + XCTAssertEqual(password.progressForDate(Date(timeIntervalSince1970: 30)), 1) + } + + func timeIntervalRemainingForDate() { + let password = Password() + password.period = 30 + + XCTAssertEqual(password.timeIntervalRemainingForDate(Date(timeIntervalSince1970: 0)), 30) + XCTAssertEqual(password.timeIntervalRemainingForDate(Date(timeIntervalSince1970: 15)), 15) + XCTAssertEqual(password.timeIntervalRemainingForDate(Date(timeIntervalSince1970: 22.5)), 7.5) + XCTAssertEqual(password.timeIntervalRemainingForDate(Date(timeIntervalSince1970: 30)), 0) + } + + fileprivate func passwordWithSecret(_ secret: Data, algorithm: Algorithm, digits: Int, + timeBased: Bool) -> Password { + let password = Password() + password.secret = secret + password.algorithm = algorithm + password.digits = digits + password.timeBased = timeBased + return password } - } - - func testProgressForDate() { - let password = Password() - password.period = 30 - - XCTAssertEqual(password.progressForDate(Date(timeIntervalSince1970: 0)), 1) - XCTAssertEqual(password.progressForDate(Date(timeIntervalSince1970: 15)), 0.5) - XCTAssertEqual(password.progressForDate(Date(timeIntervalSince1970: 22.5)), 0.25) - XCTAssertEqual(password.progressForDate(Date(timeIntervalSince1970: 30)), 1) - } - - func timeIntervalRemainingForDate() { - let password = Password() - password.period = 30 - - XCTAssertEqual(password.timeIntervalRemainingForDate(Date(timeIntervalSince1970: 0)), 30) - XCTAssertEqual(password.timeIntervalRemainingForDate(Date(timeIntervalSince1970: 15)), 15) - XCTAssertEqual(password.timeIntervalRemainingForDate(Date(timeIntervalSince1970: 22.5)), 7.5) - XCTAssertEqual(password.timeIntervalRemainingForDate(Date(timeIntervalSince1970: 30)), 0) - } - - fileprivate func passwordWithSecret(_ secret: Data, algorithm: Algorithm, digits: Int, - timeBased: Bool) -> Password { - let password = Password() - password.secret = secret - password.algorithm = algorithm - password.digits = digits - password.timeBased = timeBased - return password - } } diff --git a/TofuUITests/TofuUITests.swift b/TofuUITests/TofuUITests.swift index 8793d45..2c9e8c8 100644 --- a/TofuUITests/TofuUITests.swift +++ b/TofuUITests/TofuUITests.swift @@ -9,7 +9,7 @@ import XCTest class TofuUITests: XCTestCase { - + override func setUp() { super.setUp() @@ -19,7 +19,7 @@ class TofuUITests: XCTestCase { continueAfterFailure = false // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. XCUIApplication().launch() - + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. }