Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
22 changed files
with
1,187 additions
and
1,187 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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))" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
protocol AccountCreationDelegate: class { | ||
func createAccount(_ account: Account) | ||
func createAccount(_ account: Account) | ||
} |
Oops, something went wrong.