From eba79da0e606d99be4b077a626c5cd3e5a8ea695 Mon Sep 17 00:00:00 2001 From: Bob Sun Date: Sun, 30 Apr 2017 16:16:52 -0500 Subject: [PATCH 01/16] Polish and simplify PasswordStore model class --- pass.xcodeproj/project.pbxproj | 8 + pass/AppError.swift | 35 +++ .../CommitLogsTableViewController.swift | 15 +- pass/Controllers/GitCredential.swift | 107 ++++++++ pass/Models/PasswordStore.swift | 233 +++++------------- 5 files changed, 230 insertions(+), 168 deletions(-) create mode 100644 pass/AppError.swift create mode 100644 pass/Controllers/GitCredential.swift diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 7652f77..4f384b7 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -59,6 +59,8 @@ DCC408C71E307DBB00F29B0E /* SVProgressHUD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCC408C61E307DBB00F29B0E /* SVProgressHUD.framework */; }; DCC441521E8F6C06008A90C4 /* RawPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC441511E8F6C06008A90C4 /* RawPasswordViewController.swift */; }; DCC441541E916382008A90C4 /* GitSSHKeyArmorSettingTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC441531E916382008A90C4 /* GitSSHKeyArmorSettingTableViewController.swift */; }; + DCD9AD131EB678500093499A /* GitCredential.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD9AD121EB678500093499A /* GitCredential.swift */; }; + DCD9AD151EB6829A0093499A /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD9AD141EB6829A0093499A /* AppError.swift */; }; DCDDEAB01E4639F300F68193 /* LabelTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DCDDEAAF1E4639F300F68193 /* LabelTableViewCell.xib */; }; DCDDEAB31E4896BF00F68193 /* PasswordDetailTitleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCDDEAB11E4896BF00F68193 /* PasswordDetailTitleTableViewCell.swift */; }; DCFB779A1E4F3BCF008DE471 /* TitleTextFieldTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFB77981E4F3BCF008DE471 /* TitleTextFieldTableViewCell.swift */; }; @@ -142,6 +144,8 @@ DCC408C91E30BA1300F29B0E /* pass.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = pass.xcdatamodel; sourceTree = ""; }; DCC441511E8F6C06008A90C4 /* RawPasswordViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawPasswordViewController.swift; sourceTree = ""; }; DCC441531E916382008A90C4 /* GitSSHKeyArmorSettingTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitSSHKeyArmorSettingTableViewController.swift; sourceTree = ""; }; + DCD9AD121EB678500093499A /* GitCredential.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GitCredential.swift; path = pass/Controllers/GitCredential.swift; sourceTree = SOURCE_ROOT; }; + DCD9AD141EB6829A0093499A /* AppError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; DCDDEAAF1E4639F300F68193 /* LabelTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LabelTableViewCell.xib; sourceTree = ""; }; DCDDEAB11E4896BF00F68193 /* PasswordDetailTitleTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordDetailTitleTableViewCell.swift; sourceTree = ""; }; DCFB77981E4F3BCF008DE471 /* TitleTextFieldTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitleTextFieldTableViewCell.swift; sourceTree = ""; }; @@ -238,6 +242,7 @@ DCC408A31E2FCC9E00F29B0E /* PasswordStore.swift */, DC193FFF1E49E1A60077E0A3 /* PasscodeLockConfiguration.swift */, DC193FFD1E49E0760077E0A3 /* PasscodeLockRepository.swift */, + DCD9AD121EB678500093499A /* GitCredential.swift */, ); path = Models; sourceTree = ""; @@ -302,6 +307,7 @@ children = ( DC917BE21E2E8231000FDF54 /* Info.plist */, DC917BD61E2E8231000FDF54 /* AppDelegate.swift */, + DCD9AD141EB6829A0093499A /* AppError.swift */, DC19400D1E4B3A340077E0A3 /* Models */, DC19400C1E4B39400077E0A3 /* Controllers */, DC19400F1E4B3A9E0077E0A3 /* Views */, @@ -526,6 +532,7 @@ files = ( DCC408A41E2FCC9E00F29B0E /* PasswordStore.swift in Sources */, DC037CBF1E4ED4E100609409 /* TextViewTableViewCell.swift in Sources */, + DCD9AD151EB6829A0093499A /* AppError.swift in Sources */, DCC441541E916382008A90C4 /* GitSSHKeyArmorSettingTableViewController.swift in Sources */, DC8963C01E38EEB900828B09 /* SSHKeySettingTableViewController.swift in Sources */, DC193FFA1E49B4430077E0A3 /* AdvancedSettingsTableViewController.swift in Sources */, @@ -545,6 +552,7 @@ DC4914991E434600007FF592 /* PasswordDetailTableViewController.swift in Sources */, DC962CDF1E4B62C10033B5D8 /* AboutTableViewController.swift in Sources */, DC5734AE1E439AD400D09270 /* PasswordsViewController.swift in Sources */, + DCD9AD131EB678500093499A /* GitCredential.swift in Sources */, DC3E64E61E656F11009A83DE /* CommitLogsTableViewController.swift in Sources */, DC037CAA1E4B8EAE00609409 /* SpecialThanksTableViewController.swift in Sources */, DC037CA61E4B883900609409 /* OpenSourceComponentsTableViewController.swift in Sources */, diff --git a/pass/AppError.swift b/pass/AppError.swift new file mode 100644 index 0000000..34a7886 --- /dev/null +++ b/pass/AppError.swift @@ -0,0 +1,35 @@ +// +// AppError.swift +// pass +// +// Created by Mingshen Sun on 30/4/2017. +// Copyright © 2017 Bob Sun. All rights reserved. +// + +import Foundation + +enum AppError: Error { + case RepositoryNotSetError + case RepositoryRemoteMasterNotFoundError + case KeyImportError + case PasswordDuplicatedError + case GitResetError + case UnknownError + + var localizedDescription: String { + switch self { + case .RepositoryNotSetError: + return "Git repository is not set." + case .RepositoryRemoteMasterNotFoundError: + return "Cannot find remote branch origin/master." + case .KeyImportError: + return "Cannot import the key." + case .PasswordDuplicatedError: + return "Cannot add the password: password duplicated." + case .GitResetError: + return "Cannot decide how to reset." + case .UnknownError: + return "Unknown error." + } + } +} diff --git a/pass/Controllers/CommitLogsTableViewController.swift b/pass/Controllers/CommitLogsTableViewController.swift index 00f0b1d..dbe90ce 100644 --- a/pass/Controllers/CommitLogsTableViewController.swift +++ b/pass/Controllers/CommitLogsTableViewController.swift @@ -16,7 +16,7 @@ class CommitLogsTableViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() NotificationCenter.default.addObserver(self, selector: #selector(updateCommitLogs), name: .passwordStoreUpdated, object: nil) - commits = passwordStore.getRecentCommits(count: 20) + commits = getCommitLogs() self.tableView.estimatedRowHeight = 50 self.tableView.rowHeight = UITableViewAutomaticDimension } @@ -41,8 +41,17 @@ class CommitLogsTableViewController: UITableViewController { return cell } - func updateCommitLogs () { - commits = passwordStore.getRecentCommits(count: 20) + func updateCommitLogs() { + commits = getCommitLogs() tableView.reloadData() } + + private func getCommitLogs() -> [GTCommit] { + do { + return try passwordStore.getRecentCommits(count: 20) + } catch { + Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil) + return [] + } + } } diff --git a/pass/Controllers/GitCredential.swift b/pass/Controllers/GitCredential.swift new file mode 100644 index 0000000..a114658 --- /dev/null +++ b/pass/Controllers/GitCredential.swift @@ -0,0 +1,107 @@ +// +// GitCredential.swift +// pass +// +// Created by Mingshen Sun on 30/4/2017. +// Copyright © 2017 Bob Sun. All rights reserved. +// + +import Foundation +import UIKit +import SwiftyUserDefaults +import ObjectiveGit +import SVProgressHUD + +struct GitCredential { + var credential: Credential + + enum Credential { + case http(userName: String, controller: UIViewController) + case ssh(userName: String, publicKeyFile: URL, privateKeyFile: URL, controller: UIViewController) + } + + init(credential: Credential) { + self.credential = credential + } + + func credentialProvider() throws -> GTCredentialProvider { + var attempts = 0 + var lastPassword: String? = nil + return GTCredentialProvider { (_, _, _) -> (GTCredential?) in + var credential: GTCredential? = nil + + switch self.credential { + case let .http(userName, controller): + var newPassword = Utils.getPasswordFromKeychain(name: "gitPassword") + if newPassword == nil || attempts != 0 { + if let requestedPassword = self.requestGitPassword(controller, lastPassword) { + newPassword = requestedPassword + Utils.addPasswordToKeychain(name: "gitPassword", password: newPassword) + } else { + return nil + } + } + attempts += 1 + lastPassword = newPassword + credential = try? GTCredential(userName: userName, password: newPassword!) + case let .ssh(userName, publicKeyFile, privateKeyFile, controller): + var newPassword = Utils.getPasswordFromKeychain(name: "gitSSHKeyPassphrase") + if newPassword == nil || attempts != 0 { + if let requestedPassword = self.requestGitPassword(controller, lastPassword) { + newPassword = requestedPassword + Utils.addPasswordToKeychain(name: "gitSSHKeyPassphrase", password: newPassword) + } else { + return nil + } + } + attempts += 1 + lastPassword = newPassword + credential = try? GTCredential(userName: userName, publicKeyURL: publicKeyFile, privateKeyURL: privateKeyFile, passphrase: newPassword!) + } + return credential + } + } + + func delete() { + switch credential { + case .http: + Utils.removeKeychain(name: "gitPassword") + case .ssh: + Utils.removeKeychain(name: "gitSSHKeyPassphrase") + } + } + + private func requestGitPassword(_ controller: UIViewController, _ lastPassword: String?) -> String? { + let sem = DispatchSemaphore(value: 0) + var password: String? + var message = "" + switch credential { + case .http: + message = "Please fill in the password of your Git account." + case .ssh: + message = "Please fill in the password of your SSH key." + } + + DispatchQueue.main.async { + SVProgressHUD.dismiss() + let alert = UIAlertController(title: "Password", message: message, preferredStyle: UIAlertControllerStyle.alert) + alert.addTextField(configurationHandler: {(textField: UITextField!) in + textField.text = lastPassword ?? "" + textField.isSecureTextEntry = true + }) + alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in + password = alert.textFields!.first!.text + sem.signal() + })) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in + password = nil + sem.signal() + }) + controller.present(alert, animated: true, completion: nil) + } + + let _ = sem.wait(timeout: .distantFuture) + return password + } +} + diff --git a/pass/Models/PasswordStore.swift b/pass/Models/PasswordStore.swift index a80ef27..a545963 100644 --- a/pass/Models/PasswordStore.swift +++ b/pass/Models/PasswordStore.swift @@ -13,99 +13,6 @@ import SwiftyUserDefaults import ObjectiveGit import SVProgressHUD -struct GitCredential { - var credential: Credential - - enum Credential { - case http(userName: String, controller: UIViewController) - case ssh(userName: String, publicKeyFile: URL, privateKeyFile: URL, controller: UIViewController) - } - - init(credential: Credential) { - self.credential = credential - } - - func credentialProvider() throws -> GTCredentialProvider { - var attempts = 0 - var lastPassword: String? = nil - return GTCredentialProvider { (_, _, _) -> (GTCredential?) in - var credential: GTCredential? = nil - - switch self.credential { - case let .http(userName, controller): - var newPassword = Utils.getPasswordFromKeychain(name: "gitPassword") - if newPassword == nil || attempts != 0 { - if let requestedPassword = self.requestGitPassword(controller, lastPassword) { - newPassword = requestedPassword - Utils.addPasswordToKeychain(name: "gitPassword", password: newPassword) - } else { - return nil - } - } - attempts += 1 - lastPassword = newPassword - credential = try? GTCredential(userName: userName, password: newPassword!) - case let .ssh(userName, publicKeyFile, privateKeyFile, controller): - var newPassword = Utils.getPasswordFromKeychain(name: "gitSSHKeyPassphrase") - if newPassword == nil || attempts != 0 { - if let requestedPassword = self.requestGitPassword(controller, lastPassword) { - newPassword = requestedPassword - Utils.addPasswordToKeychain(name: "gitSSHKeyPassphrase", password: newPassword) - } else { - return nil - } - } - attempts += 1 - lastPassword = newPassword - credential = try? GTCredential(userName: userName, publicKeyURL: publicKeyFile, privateKeyURL: privateKeyFile, passphrase: newPassword!) - } - return credential - } - } - - func delete() { - switch credential { - case .http: - Utils.removeKeychain(name: "gitPassword") - case .ssh: - Utils.removeKeychain(name: "gitSSHKeyPassphrase") - } - } - - private func requestGitPassword(_ controller: UIViewController, _ lastPassword: String?) -> String? { - let sem = DispatchSemaphore(value: 0) - var password: String? - var message = "" - switch credential { - case .http: - message = "Please fill in the password of your Git account." - case .ssh: - message = "Please fill in the password of your SSH key." - } - - DispatchQueue.main.async { - SVProgressHUD.dismiss() - let alert = UIAlertController(title: "Password", message: message, preferredStyle: UIAlertControllerStyle.alert) - alert.addTextField(configurationHandler: {(textField: UITextField!) in - textField.text = lastPassword ?? "" - textField.isSecureTextEntry = true - }) - alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in - password = alert.textFields!.first!.text - sem.signal() - })) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in - password = nil - sem.signal() - }) - controller.present(alert, animated: true, completion: nil) - } - - let _ = sem.wait(timeout: .distantFuture) - return password - } -} - class PasswordStore { static let shared = PasswordStore() let storeURL = URL(fileURLWithPath: "\(Globals.repositoryPath)") @@ -223,16 +130,16 @@ class PasswordStore { let keyPath = Globals.pgpPublicKeyPath self.publicKey = importKey(from: keyPath) if self.publicKey == nil { - throw NSError(domain: "me.mssun.pass.error", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot import the public PGP key."]) + throw AppError.KeyImportError } case .secret: let keyPath = Globals.pgpPrivateKeyPath self.privateKey = importKey(from: keyPath) if self.privateKey == nil { - throw NSError(domain: "me.mssun.pass.error", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot import the private PGP key."]) + throw AppError.KeyImportError } default: - throw NSError(domain: "me.mssun.pass.error", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot import key: unknown PGP key type."]) + throw AppError.UnknownError } } @@ -329,9 +236,7 @@ class PasswordStore { Utils.removeFileIfExists(at: tempStoreURL) do { let credentialProvider = try credential.credentialProvider() - let options: [String: Any] = [ - GTRepositoryCloneOptionsCredentialProvider: credentialProvider, - ] + let options = [GTRepositoryCloneOptionsCredentialProvider: credentialProvider] storeRepository = try GTRepository.clone(from: remoteRepoURL, toWorkingDirectory: tempStoreURL, options: options, transferProgressBlock:transferProgressBlock) let fm = FileManager.default if fm.fileExists(atPath: storeURL.path) { @@ -352,16 +257,14 @@ class PasswordStore { } func pullRepository(credential: GitCredential, transferProgressBlock: @escaping (UnsafePointer, UnsafeMutablePointer) -> Void) throws { - if storeRepository == nil { - throw NSError(domain: "me.mssun.pass.error", code: 1, userInfo: [NSLocalizedDescriptionKey: "Git Repository is not set."]) + guard let repository = storeRepository else { + throw AppError.RepositoryNotSetError } do { let credentialProvider = try credential.credentialProvider() - let options: [String: Any] = [ - GTRepositoryRemoteOptionsCredentialProvider: credentialProvider - ] + let options = [GTRepositoryRemoteOptionsCredentialProvider: credentialProvider] let remote = try GTRemote(name: "origin", in: storeRepository!) - try storeRepository!.pull((storeRepository?.currentBranch())!, from: remote, withOptions: options, progress: transferProgressBlock) + try repository.pull(repository.currentBranch(), from: remote, withOptions: options, progress: transferProgressBlock) } catch { credential.delete() throw(error) @@ -429,21 +332,18 @@ class PasswordStore { } } - func getRecentCommits(count: Int) -> [GTCommit] { - guard storeRepository != nil else { + func getRecentCommits(count: Int) throws -> [GTCommit] { + guard let repository = storeRepository else { return [] } var commits = [GTCommit]() - do { - let enumerator = try GTEnumerator(repository: storeRepository!) - try enumerator.pushSHA(storeRepository!.headReference().targetOID.sha!) - for _ in 0 ..< count { - let commit = try enumerator.nextObject(withSuccess: nil) - commits.append(commit) - } - } catch { - print(error) - return commits + let enumerator = try GTEnumerator(repository: repository) + if let sha = try repository.headReference().targetOID.sha { + try enumerator.pushSHA(sha) + } + for _ in 0 ..< count { + let commit = try enumerator.nextObject(withSuccess: nil) + commits.append(commit) } return commits } @@ -464,7 +364,6 @@ class PasswordStore { do { if !withDir { passwordEntityFetch.predicate = NSPredicate(format: "isDir = false") - } let fetchedPasswordEntities = try context.fetch(passwordEntityFetch) as! [PasswordEntity] return fetchedPasswordEntities.sorted { $0.name!.caseInsensitiveCompare($1.name!) == .orderedAscending } @@ -511,11 +410,14 @@ class PasswordStore { func getLatestUpdateInfo(filename: String) -> String { - guard let blameHunks = try? storeRepository?.blame(withFile: filename, options: nil).hunks, - let latestCommitTime = blameHunks?.map({ + guard let repository = storeRepository else { + return "Unknown" + } + guard let blameHunks = try? repository.blame(withFile: filename, options: nil).hunks, + let latestCommitTime = blameHunks.map({ $0.finalSignature?.time?.timeIntervalSince1970 ?? 0 }).max() else { - return "unknown" + return "Unknown" } let lastCommitDate = Date(timeIntervalSince1970: latestCommitTime) let currentDate = Date() @@ -528,7 +430,7 @@ class PasswordStore { dateComponentsFormatter.unitsStyle = .full dateComponentsFormatter.maximumUnitCount = 2 dateComponentsFormatter.includesApproximationPhrase = true - autoFormattedDifference = (dateComponentsFormatter.string(from: diffDate)?.appending(" ago"))! + autoFormattedDifference = dateComponentsFormatter.string(from: diffDate)!.appending(" ago") } return autoFormattedDifference } @@ -537,21 +439,22 @@ class PasswordStore { } private func gitAdd(path: String) throws { - if let repo = storeRepository { - try repo.index().addFile(path) - try repo.index().write() + guard let repository = storeRepository else { + throw AppError.RepositoryNotSetError } + try repository.index().addFile(path) + try repository.index().write() } private func gitRm(path: String) throws { - if let repo = storeRepository { - if FileManager.default.fileExists(atPath: storeURL.appendingPathComponent(path).path) { - try FileManager.default.removeItem(at: storeURL.appendingPathComponent(path)) - } - try repo.index().removeFile(path) - try repo.index().write() + guard let repository = storeRepository else { + throw AppError.RepositoryNotSetError } - + if FileManager.default.fileExists(atPath: storeURL.appendingPathComponent(path).path) { + try FileManager.default.removeItem(at: storeURL.appendingPathComponent(path)) + } + try repository.index().removeFile(path) + try repository.index().write() } private func deleteDirectoryTree(at url: URL) throws { @@ -586,39 +489,39 @@ class PasswordStore { } private func gitCommit(message: String) throws -> GTCommit? { - if let repo = storeRepository { - let newTree = try repo.index().writeTree() - let headReference = try repo.headReference() - let commitEnum = try GTEnumerator(repository: repo) - try commitEnum.pushSHA(headReference.targetOID.sha!) - let parent = commitEnum.nextObject() as! GTCommit - let signature = gitSignatureForNow - let commit = try repo.createCommit(with: newTree, message: message, author: signature, committer: signature, parents: [parent], updatingReferenceNamed: headReference.name) - return commit + guard let repository = storeRepository else { + throw AppError.RepositoryNotSetError } - return nil + let newTree = try repository.index().writeTree() + let headReference = try repository.headReference() + let commitEnum = try GTEnumerator(repository: repository) + try commitEnum.pushSHA(headReference.targetOID.sha!) + let parent = commitEnum.nextObject() as! GTCommit + let signature = gitSignatureForNow + let commit = try repository.createCommit(with: newTree, message: message, author: signature, committer: signature, parents: [parent], updatingReferenceNamed: headReference.name) + return commit } - private func getLocalBranch(withName branchName: String) -> GTBranch? { - do { - let reference = GTBranch.localNamePrefix().appending(branchName) - let branches = try storeRepository!.branches(withPrefix: reference) - return branches[0] - } catch { - print(error) + private func getLocalBranch(withName branchName: String) throws -> GTBranch? { + guard let repository = storeRepository else { + throw AppError.RepositoryNotSetError } - return nil + let reference = GTBranch.localNamePrefix().appending(branchName) + let branches = try repository.branches(withPrefix: reference) + return branches.first } func pushRepository(credential: GitCredential, transferProgressBlock: @escaping (UInt32, UInt32, Int, UnsafeMutablePointer) -> Void) throws { + guard let repository = storeRepository else { + throw AppError.RepositoryNotSetError + } do { let credentialProvider = try credential.credentialProvider() - let options: [String: Any] = [ - GTRepositoryRemoteOptionsCredentialProvider: credentialProvider, - ] - let masterBranch = getLocalBranch(withName: "master")! - let remote = try GTRemote(name: "origin", in: storeRepository!) - try storeRepository?.push(masterBranch, to: remote, withOptions: options, progress: transferProgressBlock) + let options = [GTRepositoryRemoteOptionsCredentialProvider: credentialProvider] + if let masterBranch = try getLocalBranch(withName: "master") { + let remote = try GTRemote(name: "origin", in: repository) + try repository.push(masterBranch, to: remote, withOptions: options, progress: transferProgressBlock) + } } catch { credential.delete() throw(error) @@ -627,7 +530,7 @@ class PasswordStore { private func addPasswordEntities(password: Password) throws -> PasswordEntity? { guard !passwordExisted(password: password) else { - throw NSError(domain: "me.mssun.pass.error", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot add password: password duplicated."]) + throw AppError.PasswordDuplicatedError } var passwordURL = password.url! @@ -815,7 +718,7 @@ class PasswordStore { guard let firstLocalCommit = localCommits.last, firstLocalCommit.parents.count == 1, let newHead = firstLocalCommit.parents.first else { - throw NSError(domain: "me.mssun.pass.error", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cannot decide how to reset."]) + throw AppError.GitResetError } try self.storeRepository?.reset(to: newHead, resetType: GTRepositoryResetType.hard) self.setAllSynced() @@ -843,14 +746,14 @@ class PasswordStore { } private func getLocalCommits() throws -> [GTCommit]? { - // get the remote origin/master branch - guard let remoteBranches = try storeRepository?.remoteBranches(), - let index = remoteBranches.index(where: { $0.shortName == "master" }) - else { - throw NSError(domain: "me.mssun.pass.error", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cannot find remote branch origin/master."]) + guard let repository = storeRepository else { + throw AppError.RepositoryNotSetError } - let remoteMasterBranch = remoteBranches[index] - //print("remoteMasterBranch \(remoteMasterBranch)") + // get the remote origin/master branch + guard let index = try repository.remoteBranches().index(where: { $0.shortName == "master" }) else { + throw AppError.RepositoryRemoteMasterNotFoundError + } + let remoteMasterBranch = try repository.remoteBranches()[index] // get a list of local commits return try storeRepository?.localCommitsRelative(toRemoteBranch: remoteMasterBranch) From 53ea744bb66605ac758c2708cb0058a65c04169b Mon Sep 17 00:00:00 2001 From: Bob Sun Date: Sun, 30 Apr 2017 18:29:47 -0500 Subject: [PATCH 02/16] Simplify PasswordStore model --- pass/AppError.swift | 3 + pass/Models/PasswordStore.swift | 98 ++++++++++++++++----------------- 2 files changed, 52 insertions(+), 49 deletions(-) diff --git a/pass/AppError.swift b/pass/AppError.swift index 34a7886..b872614 100644 --- a/pass/AppError.swift +++ b/pass/AppError.swift @@ -14,6 +14,7 @@ enum AppError: Error { case KeyImportError case PasswordDuplicatedError case GitResetError + case PGPPublicKeyNotExistError case UnknownError var localizedDescription: String { @@ -28,6 +29,8 @@ enum AppError: Error { return "Cannot add the password: password duplicated." case .GitResetError: return "Cannot decide how to reset." + case .PGPPublicKeyNotExistError: + return "PGP public key doesn't exist." case .UnknownError: return "Unknown error." } diff --git a/pass/Models/PasswordStore.swift b/pass/Models/PasswordStore.swift index a545963..1da7e0b 100644 --- a/pass/Models/PasswordStore.swift +++ b/pass/Models/PasswordStore.swift @@ -257,14 +257,14 @@ class PasswordStore { } func pullRepository(credential: GitCredential, transferProgressBlock: @escaping (UnsafePointer, UnsafeMutablePointer) -> Void) throws { - guard let repository = storeRepository else { + guard let storeRepository = storeRepository else { throw AppError.RepositoryNotSetError } do { let credentialProvider = try credential.credentialProvider() let options = [GTRepositoryRemoteOptionsCredentialProvider: credentialProvider] - let remote = try GTRemote(name: "origin", in: storeRepository!) - try repository.pull(repository.currentBranch(), from: remote, withOptions: options, progress: transferProgressBlock) + let remote = try GTRemote(name: "origin", in: storeRepository) + try storeRepository.pull(storeRepository.currentBranch(), from: remote, withOptions: options, progress: transferProgressBlock) } catch { credential.delete() throw(error) @@ -333,12 +333,12 @@ class PasswordStore { } func getRecentCommits(count: Int) throws -> [GTCommit] { - guard let repository = storeRepository else { + guard let storeRepository = storeRepository else { return [] } var commits = [GTCommit]() - let enumerator = try GTEnumerator(repository: repository) - if let sha = try repository.headReference().targetOID.sha { + let enumerator = try GTEnumerator(repository: storeRepository) + if let sha = try storeRepository.headReference().targetOID.sha { try enumerator.pushSHA(sha) } for _ in 0 ..< count { @@ -410,10 +410,10 @@ class PasswordStore { func getLatestUpdateInfo(filename: String) -> String { - guard let repository = storeRepository else { + guard let storeRepository = storeRepository else { return "Unknown" } - guard let blameHunks = try? repository.blame(withFile: filename, options: nil).hunks, + guard let blameHunks = try? storeRepository.blame(withFile: filename, options: nil).hunks, let latestCommitTime = blameHunks.map({ $0.finalSignature?.time?.timeIntervalSince1970 ?? 0 }).max() else { @@ -439,22 +439,23 @@ class PasswordStore { } private func gitAdd(path: String) throws { - guard let repository = storeRepository else { + guard let storeRepository = storeRepository else { throw AppError.RepositoryNotSetError } - try repository.index().addFile(path) - try repository.index().write() + try storeRepository.index().addFile(path) + try storeRepository.index().write() } private func gitRm(path: String) throws { - guard let repository = storeRepository else { + guard let storeRepository = storeRepository else { throw AppError.RepositoryNotSetError } - if FileManager.default.fileExists(atPath: storeURL.appendingPathComponent(path).path) { - try FileManager.default.removeItem(at: storeURL.appendingPathComponent(path)) + let url = storeURL.appendingPathComponent(path) + if FileManager.default.fileExists(atPath: url.path) { + try FileManager.default.removeItem(at: url) } - try repository.index().removeFile(path) - try repository.index().write() + try storeRepository.index().removeFile(path) + try storeRepository.index().write() } private func deleteDirectoryTree(at url: URL) throws { @@ -474,53 +475,51 @@ class PasswordStore { } private func gitMv(from: String, to: String) throws { + let fromURL = storeURL.appendingPathComponent(from) + let toURL = storeURL.appendingPathComponent(to) let fm = FileManager.default - do { - guard fm.fileExists(atPath: storeURL.appendingPathComponent(from).path) else { - print("\(from) not exist") - return - } - try fm.moveItem(at: storeURL.appendingPathComponent(from), to: storeURL.appendingPathComponent(to)) - } catch { - print(error) + guard fm.fileExists(atPath: fromURL.path) else { + print("\(from) not exist") + return } + try fm.moveItem(at: fromURL, to: toURL) try gitAdd(path: to) try gitRm(path: from) } private func gitCommit(message: String) throws -> GTCommit? { - guard let repository = storeRepository else { + guard let storeRepository = storeRepository else { throw AppError.RepositoryNotSetError } - let newTree = try repository.index().writeTree() - let headReference = try repository.headReference() - let commitEnum = try GTEnumerator(repository: repository) + let newTree = try storeRepository.index().writeTree() + let headReference = try storeRepository.headReference() + let commitEnum = try GTEnumerator(repository: storeRepository) try commitEnum.pushSHA(headReference.targetOID.sha!) let parent = commitEnum.nextObject() as! GTCommit let signature = gitSignatureForNow - let commit = try repository.createCommit(with: newTree, message: message, author: signature, committer: signature, parents: [parent], updatingReferenceNamed: headReference.name) + let commit = try storeRepository.createCommit(with: newTree, message: message, author: signature, committer: signature, parents: [parent], updatingReferenceNamed: headReference.name) return commit } private func getLocalBranch(withName branchName: String) throws -> GTBranch? { - guard let repository = storeRepository else { + guard let storeRepository = storeRepository else { throw AppError.RepositoryNotSetError } let reference = GTBranch.localNamePrefix().appending(branchName) - let branches = try repository.branches(withPrefix: reference) + let branches = try storeRepository.branches(withPrefix: reference) return branches.first } func pushRepository(credential: GitCredential, transferProgressBlock: @escaping (UInt32, UInt32, Int, UnsafeMutablePointer) -> Void) throws { - guard let repository = storeRepository else { + guard let storeRepository = storeRepository else { throw AppError.RepositoryNotSetError } do { let credentialProvider = try credential.credentialProvider() let options = [GTRepositoryRemoteOptionsCredentialProvider: credentialProvider] if let masterBranch = try getLocalBranch(withName: "master") { - let remote = try GTRemote(name: "origin", in: repository) - try repository.push(masterBranch, to: remote, withOptions: options, progress: transferProgressBlock) + let remote = try GTRemote(name: "origin", in: storeRepository) + try storeRepository.push(masterBranch, to: remote, withOptions: options, progress: transferProgressBlock) } } catch { credential.delete() @@ -629,7 +628,6 @@ class PasswordStore { private func deletePasswordEntities(passwordEntity: PasswordEntity) throws { var current: PasswordEntity? = passwordEntity - print(passwordEntity.path!) while current != nil && (current!.children!.count == 0 || !current!.isDir) { let parent = current!.parent self.context.delete(current!) @@ -664,13 +662,13 @@ class PasswordStore { } func updateImage(passwordEntity: PasswordEntity, image: Data?) { - if image == nil { + guard let image = image else { return } let privateMOC = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) privateMOC.parent = context privateMOC.perform { - passwordEntity.image = NSData(data: image!) + passwordEntity.image = NSData(data: image) do { try privateMOC.save() self.context.performAndWait { @@ -699,7 +697,6 @@ class PasswordStore { Utils.removeAllKeychain() - deleteCoreData(entityName: "PasswordEntity") Defaults.removeAll() @@ -711,6 +708,9 @@ class PasswordStore { // return the number of discarded commits func reset() throws -> Int { + guard let storeRepository = storeRepository else { + throw AppError.RepositoryNotSetError + } // get a list of local commits if let localCommits = try getLocalCommits(), localCommits.count > 0 { @@ -720,7 +720,7 @@ class PasswordStore { let newHead = firstLocalCommit.parents.first else { throw AppError.GitResetError } - try self.storeRepository?.reset(to: newHead, resetType: GTRepositoryResetType.hard) + try storeRepository.reset(to: newHead, resetType: .hard) self.setAllSynced() self.updatePasswordEntityCoreData() @@ -746,24 +746,23 @@ class PasswordStore { } private func getLocalCommits() throws -> [GTCommit]? { - guard let repository = storeRepository else { + guard let storeRepository = storeRepository else { throw AppError.RepositoryNotSetError } // get the remote origin/master branch - guard let index = try repository.remoteBranches().index(where: { $0.shortName == "master" }) else { + guard let index = try storeRepository.remoteBranches().index(where: { $0.shortName == "master" }) else { throw AppError.RepositoryRemoteMasterNotFoundError } - let remoteMasterBranch = try repository.remoteBranches()[index] + let remoteMasterBranch = try storeRepository.remoteBranches()[index] // get a list of local commits - return try storeRepository?.localCommitsRelative(toRemoteBranch: remoteMasterBranch) + return try storeRepository.localCommitsRelative(toRemoteBranch: remoteMasterBranch) } func decrypt(passwordEntity: PasswordEntity, requestPGPKeyPassphrase: () -> String) throws -> Password? { - var password: Password? - let encryptedDataPath = URL(fileURLWithPath: "\(Globals.repositoryPath)/\(passwordEntity.path!)") + let encryptedDataPath = storeURL.appendingPathComponent(passwordEntity.path!) let encryptedData = try Data(contentsOf: encryptedDataPath) var passphrase = self.pgpKeyPassphrase if passphrase == nil { @@ -772,14 +771,15 @@ class PasswordStore { let decryptedData = try PasswordStore.shared.pgp.decryptData(encryptedData, passphrase: passphrase) let plainText = String(data: decryptedData, encoding: .utf8) ?? "" let escapedPath = passwordEntity.path!.stringByAddingPercentEncodingForRFC3986() ?? "" - password = Password(name: passwordEntity.name!, url: URL(string: escapedPath), plainText: plainText) - return password + return Password(name: passwordEntity.name!, url: URL(string: escapedPath), plainText: plainText) } func encrypt(password: Password) throws -> Data { + guard let publicKey = pgp.getKeysOf(.public).first else { + throw AppError.PGPPublicKeyNotExistError + } let plainData = password.getPlainData() - let pgp = PasswordStore.shared.pgp - let encryptedData = try pgp.encryptData(plainData, usingPublicKey: pgp.getKeysOf(.public)[0], armored: Defaults[.encryptInArmored]) + let encryptedData = try pgp.encryptData(plainData, usingPublicKey: publicKey, armored: Defaults[.encryptInArmored]) return encryptedData } } From c2562d31d1b79684c3019113d86cc42b01d829a1 Mon Sep 17 00:00:00 2001 From: Bob Sun Date: Mon, 1 May 2017 23:06:39 -0400 Subject: [PATCH 03/16] Give an default authentication method to avoid crashes --- .../GitServerSettingTableViewController.swift | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/pass/Controllers/GitServerSettingTableViewController.swift b/pass/Controllers/GitServerSettingTableViewController.swift index 8843ce7..c8560e1 100644 --- a/pass/Controllers/GitServerSettingTableViewController.swift +++ b/pass/Controllers/GitServerSettingTableViewController.swift @@ -19,7 +19,7 @@ class GitServerSettingTableViewController: UITableViewController { let passwordStore = PasswordStore.shared var sshLabel: UILabel? = nil - var authenticationMethod = Defaults[.gitAuthenticationMethod] + var authenticationMethod = Defaults[.gitAuthenticationMethod] ?? "Password" private func checkAuthenticationMethod(method: String) { let passwordCheckView = authPasswordCell.viewWithTag(1001)! @@ -51,9 +51,8 @@ class GitServerSettingTableViewController: UITableViewController { gitURLTextField.text = url.absoluteString } usernameTextField.text = Defaults[.gitUsername] - authenticationMethod = Defaults[.gitAuthenticationMethod] - checkAuthenticationMethod(method: authenticationMethod!) + checkAuthenticationMethod(method: authenticationMethod) authSSHKeyCell.accessoryType = .detailButton } @@ -80,10 +79,6 @@ class GitServerSettingTableViewController: UITableViewController { Utils.alert(title: "Cannot Save", message: "Git Server is not set.", controller: self, completion: nil) return false } - guard authenticationMethod != nil else { - Utils.alert(title: "Cannot Save", message: "Authentication method is not set.", controller: self, completion: nil) - return false - } } return true } @@ -101,7 +96,7 @@ class GitServerSettingTableViewController: UITableViewController { authenticationMethod = "SSH Key" } } - checkAuthenticationMethod(method: authenticationMethod!) + checkAuthenticationMethod(method: authenticationMethod) tableView.deselectRow(at: indexPath, animated: true) } From f0ca07187c45913c43114fefe0cc83b6a68e6811 Mon Sep 17 00:00:00 2001 From: Bob Sun Date: Tue, 2 May 2017 16:57:03 -0400 Subject: [PATCH 04/16] Fix crash when open git server view controller --- .../GitServerSettingTableViewController.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pass/Controllers/GitServerSettingTableViewController.swift b/pass/Controllers/GitServerSettingTableViewController.swift index c8560e1..c557e14 100644 --- a/pass/Controllers/GitServerSettingTableViewController.swift +++ b/pass/Controllers/GitServerSettingTableViewController.swift @@ -41,9 +41,9 @@ class GitServerSettingTableViewController: UITableViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // Grey out ssh option if ssh_key and ssh_key.pub are not present - sshLabel = authSSHKeyCell.subviews[0].subviews[0] as? UILabel - sshLabel!.isEnabled = gitSSHKeyExists() - + if let sshLabel = sshLabel { + sshLabel.isEnabled = gitSSHKeyExists() + } } override func viewDidLoad() { super.viewDidLoad() @@ -51,7 +51,7 @@ class GitServerSettingTableViewController: UITableViewController { gitURLTextField.text = url.absoluteString } usernameTextField.text = Defaults[.gitUsername] - + sshLabel = authSSHKeyCell.subviews[0].subviews[0] as? UILabel checkAuthenticationMethod(method: authenticationMethod) authSSHKeyCell.accessoryType = .detailButton } @@ -158,7 +158,10 @@ class GitServerSettingTableViewController: UITableViewController { let deleteAction = UIAlertAction(title: "Remove Git SSH Keys", style: .destructive) { _ in Utils.removeGitSSHKeys() Defaults[.gitSSHKeySource] = nil - self.sshLabel!.isEnabled = false + if let sshLabel = self.sshLabel { + sshLabel.isEnabled = false + self.checkAuthenticationMethod(method: "Password") + } } optionMenu.addAction(deleteAction) } From 4ca6c391742b19884f3b9aff3942ef061a96da11 Mon Sep 17 00:00:00 2001 From: Yishi Lin Date: Thu, 4 May 2017 20:17:59 +0800 Subject: [PATCH 05/16] Polish error message --- pass/AppError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pass/AppError.swift b/pass/AppError.swift index b872614..0305c36 100644 --- a/pass/AppError.swift +++ b/pass/AppError.swift @@ -28,7 +28,7 @@ enum AppError: Error { case .PasswordDuplicatedError: return "Cannot add the password: password duplicated." case .GitResetError: - return "Cannot decide how to reset." + return "Cannot identify the latest synced commit." case .PGPPublicKeyNotExistError: return "PGP public key doesn't exist." case .UnknownError: From 885319e2e19136daedb009b973a8214eb7f0da65 Mon Sep 17 00:00:00 2001 From: Yishi Lin Date: Thu, 4 May 2017 20:47:23 +0800 Subject: [PATCH 06/16] Hide the "Decrypting" message before asking for passphrase --- pass/Controllers/PasswordsViewController.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pass/Controllers/PasswordsViewController.swift b/pass/Controllers/PasswordsViewController.swift index b0707e4..8dacbf1 100644 --- a/pass/Controllers/PasswordsViewController.swift +++ b/pass/Controllers/PasswordsViewController.swift @@ -355,9 +355,15 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV textField.text = "" textField.isSecureTextEntry = true }) + // hide it so that alert is on the top of the view + SVProgressHUD.dismiss() self.present(alert, animated: true, completion: nil) } let _ = sem.wait(timeout: DispatchTime.distantFuture) + DispatchQueue.main.async { + // bring back + SVProgressHUD.show(withStatus: "Decrypting") + } if Defaults[.isRememberPassphraseOn] { self.passwordStore.pgpKeyPassphrase = passphrase } From 22877c80120ecebea2d8b199b43c078cd443634b Mon Sep 17 00:00:00 2001 From: Yishi Lin Date: Thu, 4 May 2017 21:12:37 +0800 Subject: [PATCH 07/16] Show and copy developer email if failed to open the Mail App --- pass/Controllers/BasicStaticTableViewController.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pass/Controllers/BasicStaticTableViewController.swift b/pass/Controllers/BasicStaticTableViewController.swift index a0d7536..6e975ee 100644 --- a/pass/Controllers/BasicStaticTableViewController.swift +++ b/pass/Controllers/BasicStaticTableViewController.swift @@ -98,6 +98,12 @@ class BasicStaticTableViewController: UITableViewController, MFMailComposeViewCo let subject = urlComponents.queryItems![0].value ?? "" if MFMailComposeViewController.canSendMail() { sendEmail(toRecipients: [urlComponents.path], subject: subject) + } else { + let email = urlComponents.path + let alertTitle = "Cannot open Mail App" + let alertMessage = "Email copied: \(email)" + Utils.copyToPasteboard(textToCopy: email) + Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil) } case "http", "https": let svc = SFSafariViewController(url: URL(string: link)!, entersReaderIfAvailable: false) From a6875590a4fc8a5dd9d37ad0f1a771faf49b0f48 Mon Sep 17 00:00:00 2001 From: Yishi Lin Date: Mon, 8 May 2017 20:57:39 +0800 Subject: [PATCH 08/16] Fix a cell reuse bug in password detail VC --- pass/Views/LabelTableViewCell.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pass/Views/LabelTableViewCell.swift b/pass/Views/LabelTableViewCell.swift index 2eef4e2..a0739ab 100644 --- a/pass/Views/LabelTableViewCell.swift +++ b/pass/Views/LabelTableViewCell.swift @@ -62,9 +62,11 @@ class LabelTableViewCell: UITableViewCell { case "url": type = .URL contentLabel.text = content + contentLabel.font = UIFont.systemFont(ofSize: contentLabel.font.pointSize) default: type = .other contentLabel.text = content + contentLabel.font = UIFont.systemFont(ofSize: contentLabel.font.pointSize) } updateButtons() } @@ -79,13 +81,6 @@ class LabelTableViewCell: UITableViewCell { override func awakeFromNib() { super.awakeFromNib() } - - override func layoutSubviews() { - super.layoutSubviews() - if buttons != nil { - self.accessoryView = buttons - } - } override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) @@ -208,5 +203,6 @@ class LabelTableViewCell: UITableViewCell { passwordDisplayButton = nil buttons = nil } + self.accessoryView = buttons } } From e1c6d9c0d87c14e44295edb0c31a290f560e1616 Mon Sep 17 00:00:00 2001 From: Yishi Lin Date: Mon, 8 May 2017 20:58:53 +0800 Subject: [PATCH 09/16] Remove redundant codes --- pass/Views/LabelTableViewCell.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pass/Views/LabelTableViewCell.swift b/pass/Views/LabelTableViewCell.swift index a0739ab..3facc5a 100644 --- a/pass/Views/LabelTableViewCell.swift +++ b/pass/Views/LabelTableViewCell.swift @@ -77,14 +77,6 @@ class LabelTableViewCell: UITableViewCell { return true } } - - override func awakeFromNib() { - super.awakeFromNib() - } - - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - } override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { switch type { From 20f2e22076a4ebb8fd56be53642bcb6b809b89f7 Mon Sep 17 00:00:00 2001 From: Yishi Lin Date: Mon, 8 May 2017 21:12:48 +0800 Subject: [PATCH 10/16] Fix a bug about remembering pgp passphrase - Do not keep the wrong passphrase --- pass/Controllers/PasswordDetailTableViewController.swift | 2 ++ pass/Controllers/PasswordsViewController.swift | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pass/Controllers/PasswordDetailTableViewController.swift b/pass/Controllers/PasswordDetailTableViewController.swift index 207e6cb..79e2742 100644 --- a/pass/Controllers/PasswordDetailTableViewController.swift +++ b/pass/Controllers/PasswordDetailTableViewController.swift @@ -147,6 +147,8 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni self.password = try self.passwordStore.decrypt(passwordEntity: self.passwordEntity!, requestPGPKeyPassphrase: self.requestPGPKeyPassphrase) } catch { DispatchQueue.main.async { + // remove the wrong passphrase so that users could enter it next time + self.passwordStore.pgpKeyPassphrase = nil let alert = UIAlertController(title: "Cannot Show Password", message: error.localizedDescription, preferredStyle: UIAlertControllerStyle.alert) alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {(UIAlertAction) -> Void in self.navigationController!.popViewController(animated: true) diff --git a/pass/Controllers/PasswordsViewController.swift b/pass/Controllers/PasswordsViewController.swift index 8dacbf1..3e284fe 100644 --- a/pass/Controllers/PasswordsViewController.swift +++ b/pass/Controllers/PasswordsViewController.swift @@ -386,7 +386,9 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } catch { print(error) DispatchQueue.main.async { - Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil) + // remove the wrong passphrase so that users could enter it next time + self.passwordStore.pgpKeyPassphrase = nil + Utils.alert(title: "Cannot Copy Password", message: error.localizedDescription, controller: self, completion: nil) } } } From 89f2d6e764eb155c7d159425f63b7c80e4ad19d3 Mon Sep 17 00:00:00 2001 From: Bob Sun Date: Wed, 10 May 2017 09:29:23 -0700 Subject: [PATCH 11/16] Check oid before calling localCommitsRelative to avoid crashes --- pass/Models/PasswordStore.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pass/Models/PasswordStore.swift b/pass/Models/PasswordStore.swift index 1da7e0b..44641a4 100644 --- a/pass/Models/PasswordStore.swift +++ b/pass/Models/PasswordStore.swift @@ -755,6 +755,11 @@ class PasswordStore { } let remoteMasterBranch = try storeRepository.remoteBranches()[index] + // check oid before calling localCommitsRelative + guard remoteMasterBranch.oid != nil else { + throw AppError.RepositoryRemoteMasterNotFoundError + } + // get a list of local commits return try storeRepository.localCommitsRelative(toRemoteBranch: remoteMasterBranch) } From 275b85688307d63c2f84bd0a77295c5c2c78f5d4 Mon Sep 17 00:00:00 2001 From: Bob Sun Date: Thu, 11 May 2017 22:55:08 -0700 Subject: [PATCH 12/16] Fix a mistake in app error class --- pass/AppError.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pass/AppError.swift b/pass/AppError.swift index 0305c36..d6a8397 100644 --- a/pass/AppError.swift +++ b/pass/AppError.swift @@ -16,8 +16,10 @@ enum AppError: Error { case GitResetError case PGPPublicKeyNotExistError case UnknownError - - var localizedDescription: String { +} + +extension AppError: LocalizedError { + public var errorDescription: String? { switch self { case .RepositoryNotSetError: return "Git repository is not set." From d1393c6d368960484c7cf9feacdc7f37bf66b83a Mon Sep 17 00:00:00 2001 From: Bob Sun Date: Thu, 11 May 2017 22:55:55 -0700 Subject: [PATCH 13/16] Polish validation logic of importing pgp key via url --- .../PGPKeySettingTableViewController.swift | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pass/Controllers/PGPKeySettingTableViewController.swift b/pass/Controllers/PGPKeySettingTableViewController.swift index e99222f..c7ab7db 100644 --- a/pass/Controllers/PGPKeySettingTableViewController.swift +++ b/pass/Controllers/PGPKeySettingTableViewController.swift @@ -39,22 +39,26 @@ class PGPKeySettingTableViewController: UITableViewController { override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { if identifier == "savePGPKeySegue" { - guard let pgpPublicKeyURL = URL(string: pgpPublicKeyURLTextField.text!) else { - Utils.alert(title: "Cannot Save", message: "Please set Public Key URL first.", controller: self, completion: nil) - return false - } - guard let pgpPrivateKeyURL = URL(string: pgpPrivateKeyURLTextField.text!) else { - Utils.alert(title: "Cannot Save", message: "Please set Private Key URL first.", controller: self, completion: nil) - return false - } - guard pgpPublicKeyURL.scheme! == "https", pgpPrivateKeyURL.scheme! == "https" else { - Utils.alert(title: "Cannot Save Settings", message: "HTTP connection is not supported.", controller: self, completion: nil) + guard validatePGPKeyURL(input: pgpPublicKeyURLTextField.text) == true, + validatePGPKeyURL(input: pgpPrivateKeyURLTextField.text) == true else { return false } } return true } + private func validatePGPKeyURL(input: String?) -> Bool { + guard let path = input, let url = URL(string: path) else { + Utils.alert(title: "Cannot Save PGP Key", message: "Please set PGP Key URL first.", controller: self, completion: nil) + return false + } + guard let scheme = url.scheme, scheme == "https", scheme == "https" else { + Utils.alert(title: "Cannot Save PGP Key", message: "HTTP connection is not supported.", controller: self, completion: nil) + return false + } + return true + } + @IBAction func save(_ sender: Any) { let alert = UIAlertController(title: "Passphrase", message: "Please fill in the passphrase of your PGP secret key.", preferredStyle: UIAlertControllerStyle.alert) alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in From 750532850a4c6671a0730edf012a32b247e7e091 Mon Sep 17 00:00:00 2001 From: Yishi Lin Date: Sun, 14 May 2017 20:42:59 +0800 Subject: [PATCH 14/16] Scan QR codes to insert PGP ascii-armor keys --- pass/Base.lproj/Main.storyboard | 221 +++++++++++------- ...GPKeyArmorSettingTableViewController.swift | 134 ++++++++++- 2 files changed, 276 insertions(+), 79 deletions(-) diff --git a/pass/Base.lproj/Main.storyboard b/pass/Base.lproj/Main.storyboard index 84b3c31..e9819db 100644 --- a/pass/Base.lproj/Main.storyboard +++ b/pass/Base.lproj/Main.storyboard @@ -1,5 +1,5 @@ - + @@ -31,18 +31,18 @@ - + diff --git a/pass/Controllers/PGPKeyArmorSettingTableViewController.swift b/pass/Controllers/PGPKeyArmorSettingTableViewController.swift index 16b7fd0..28f00d8 100644 --- a/pass/Controllers/PGPKeyArmorSettingTableViewController.swift +++ b/pass/Controllers/PGPKeyArmorSettingTableViewController.swift @@ -9,19 +9,102 @@ import UIKit import SwiftyUserDefaults -class PGPKeyArmorSettingTableViewController: UITableViewController, UITextViewDelegate { +class PGPKeyArmorSettingTableViewController: UITableViewController, UITextViewDelegate, QRScannerControllerDelegate { @IBOutlet weak var armorPublicKeyTextView: UITextView! @IBOutlet weak var armorPrivateKeyTextView: UITextView! + @IBOutlet weak var scanPublicKeyCell: UITableViewCell! + @IBOutlet weak var scanPrivateKeyCell: UITableViewCell! + var pgpPassphrase: String? let passwordStore = PasswordStore.shared private var recentPastedText = "" + class ScannedPGPKey { + static let maxNumberOfGif = 100 + enum KeyType { + case publicKey, privateKey + } + var keyType = KeyType.publicKey + var numberOfSegments = 0 + var previousSegment = "" + var key = "" + var message = "" + var hasStarted = false + var isDone = false + + func reset(keytype: KeyType) { + self.keyType = keytype + numberOfSegments = 0 + previousSegment = "" + key = "" + message = "Looking for the starting frame." + hasStarted = false + isDone = false + } + + func addSegment(segment: String) { + // skip duplicated segments + guard segment != previousSegment else { + return + } + previousSegment = segment + + // check whether we have found the first block + if hasStarted == false { + let findPublic = segment.contains("-----BEGIN PGP PUBLIC KEY BLOCK-----") + let findPrivate = segment.contains("-----BEGIN PGP PRIVATE KEY BLOCK-----") + switch keyType { + case .publicKey: + if findPrivate { + message = "Please scan public key." + } + hasStarted = findPublic + case .privateKey: + if findPublic { + message = "Please scan private key." + } + hasStarted = findPrivate + } + } + guard hasStarted == true else { + return + } + + // check the number of segments + numberOfSegments = numberOfSegments + 1 + guard numberOfSegments <= ScannedPGPKey.maxNumberOfGif else { + key = "Too many QR codes" + return + } + + // update full text and check whether we are done + key.append(segment) + if key.contains("-----END PGP PUBLIC KEY BLOCK-----") || key.contains("-----END PGP PRIVATE KEY BLOCK-----") { + isDone = true + } + + // update message + message = "\(numberOfSegments) scanned QR codes." + } + } + var scanned = ScannedPGPKey() + override func viewDidLoad() { super.viewDidLoad() armorPublicKeyTextView.text = Defaults[.pgpPublicKeyArmor] armorPrivateKeyTextView.text = Defaults[.pgpPrivateKeyArmor] pgpPassphrase = passwordStore.pgpKeyPassphrase + + scanPublicKeyCell?.textLabel?.text = "Scan Public Key QR Codes" + scanPublicKeyCell?.textLabel?.textColor = Globals.blue + scanPublicKeyCell?.selectionStyle = .default + scanPublicKeyCell?.accessoryType = .disclosureIndicator + + scanPrivateKeyCell?.textLabel?.text = "Scan Private Key QR Codes" + scanPrivateKeyCell?.textLabel?.textColor = Globals.blue + scanPrivateKeyCell?.selectionStyle = .default + scanPrivateKeyCell?.accessoryType = .disclosureIndicator } private func createSavePassphraseAndSegueAlert() -> UIAlertController { @@ -79,4 +162,53 @@ class PGPKeyArmorSettingTableViewController: UITableViewController, UITextViewDe } return true } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let selectedCell = tableView.cellForRow(at: indexPath) + if selectedCell == scanPublicKeyCell { + scanned.reset(keytype: ScannedPGPKey.KeyType.publicKey) + self.performSegue(withIdentifier: "showPGPScannerSegue", sender: self) + } else if selectedCell == scanPrivateKeyCell { + scanned.reset(keytype: ScannedPGPKey.KeyType.privateKey) + self.performSegue(withIdentifier: "showPGPScannerSegue", sender: self) + } + tableView.deselectRow(at: indexPath, animated: true) + } + + // MARK: - QRScannerControllerDelegate Methods + func checkScannedOutput(line: String) -> (accept: Bool, message: String) { + scanned.addSegment(segment: line) + if scanned.isDone { + return (accept: true, message: "Done") + } else { + return (accept: false, message: scanned.message) + } + } + + // MARK: - QRScannerControllerDelegate Methods + func handleScannedOutput(line: String) { + switch scanned.keyType { + case .publicKey: + armorPublicKeyTextView.text = scanned.key + case .privateKey: + armorPrivateKeyTextView.text = scanned.key + } + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "showPGPScannerSegue" { + if let navController = segue.destination as? UINavigationController { + if let viewController = navController.topViewController as? QRScannerController { + viewController.delegate = self + } + } else if let viewController = segue.destination as? QRScannerController { + viewController.delegate = self + } + } + } + + @IBAction private func cancelPGPScanner(segue: UIStoryboardSegue) { + + } + } From 6fe3c00c9f24ffcfcf7e55699a7ac29b5c06d850 Mon Sep 17 00:00:00 2001 From: Yishi Lin Date: Sun, 14 May 2017 20:52:24 +0800 Subject: [PATCH 15/16] Polish the logic about touch ID and passcode lock --- .../SettingsTableViewController.swift | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/pass/Controllers/SettingsTableViewController.swift b/pass/Controllers/SettingsTableViewController.swift index 8c0b38d..0d1946d 100644 --- a/pass/Controllers/SettingsTableViewController.swift +++ b/pass/Controllers/SettingsTableViewController.swift @@ -156,17 +156,19 @@ class SettingsTableViewController: UITableViewController { touchIDTableViewCell.accessoryView = touchIDSwitch setPGPKeyTableViewCellDetailText() setPasswordRepositoryTableViewCellDetailText() - setTouchIDSwitch() - setPasscodeLockRepositoryTableViewCellDetailText() + setPasscodeLockTouchIDCells() } - private func setPasscodeLockRepositoryTableViewCellDetailText() { + private func setPasscodeLockTouchIDCells() { if PasscodeLockRepository().hasPasscode { self.passcodeTableViewCell.detailTextLabel?.text = "On" + Globals.passcodeConfiguration.isTouchIDAllowed = true + touchIDSwitch.isOn = Defaults[.isTouchIDOn] } else { self.passcodeTableViewCell.detailTextLabel?.text = "Off" - touchIDSwitch.isEnabled = false Globals.passcodeConfiguration.isTouchIDAllowed = false + Defaults[.isTouchIDOn] = false + touchIDSwitch.isOn = Defaults[.isTouchIDOn] } } @@ -186,19 +188,10 @@ class SettingsTableViewController: UITableViewController { } } - private func setTouchIDSwitch() { - if Defaults[.isTouchIDOn] { - touchIDSwitch.isOn = true - } else { - touchIDSwitch.isOn = false - } - } - func actOnPasswordStoreErasedNotification() { setPGPKeyTableViewCellDetailText() setPasswordRepositoryTableViewCellDetailText() - setTouchIDSwitch() - setPasscodeLockRepositoryTableViewCellDetailText() + setPasscodeLockTouchIDCells() let appDelegate = UIApplication.shared.delegate as! AppDelegate appDelegate.passcodeLockPresenter = PasscodeLockPresenter(mainWindow: appDelegate.window, configuration: Globals.passcodeConfiguration) @@ -222,12 +215,12 @@ class SettingsTableViewController: UITableViewController { } func touchIDSwitchAction(uiSwitch: UISwitch) { - if uiSwitch.isOn { - Defaults[.isTouchIDOn] = true - Globals.passcodeConfiguration.isTouchIDAllowed = true + if !Globals.passcodeConfiguration.isTouchIDAllowed { + // switch off + uiSwitch.isOn = Defaults[.isTouchIDOn] // false + Utils.alert(title: "Notice", message: "Please set the passcode lock first.", controller: self, completion: nil) } else { - Defaults[.isTouchIDOn] = false - Globals.passcodeConfiguration.isTouchIDAllowed = false + Defaults[.isTouchIDOn] = uiSwitch.isOn } let appDelegate = UIApplication.shared.delegate as! AppDelegate appDelegate.passcodeLockPresenter = PasscodeLockPresenter(mainWindow: appDelegate.window, configuration: Globals.passcodeConfiguration) @@ -335,8 +328,7 @@ class SettingsTableViewController: UITableViewController { let optionMenu = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let removePasscodeAction = UIAlertAction(title: "Remove Passcode", style: .destructive) { [weak self] _ in passcodeRemoveViewController.successCallback = { _ in - self?.passcodeTableViewCell.detailTextLabel?.text = "Off" - self?.touchIDSwitch.isEnabled = false + self?.setPasscodeLockTouchIDCells() let appDelegate = UIApplication.shared.delegate as! AppDelegate appDelegate.passcodeLockPresenter = PasscodeLockPresenter(mainWindow: appDelegate.window, configuration: Globals.passcodeConfiguration) } @@ -359,8 +351,7 @@ class SettingsTableViewController: UITableViewController { func setPasscodeLock() { let passcodeSetViewController = PasscodeLockViewController(state: .set, configuration: Globals.passcodeConfiguration) passcodeSetViewController.successCallback = { _ in - self.passcodeTableViewCell.detailTextLabel?.text = "On" - self.touchIDSwitch.isEnabled = true + self.setPasscodeLockTouchIDCells() } present(passcodeSetViewController, animated: true, completion: nil) } From 4977aecadb96f43e0c0062f58d816faadc19e0bd Mon Sep 17 00:00:00 2001 From: Bob Sun Date: Mon, 22 May 2017 23:05:11 -0700 Subject: [PATCH 16/16] Version bump --- pass/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pass/Info.plist b/pass/Info.plist index a332db4..6e969a8 100644 --- a/pass/Info.plist +++ b/pass/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.2.5 + 0.2.6 CFBundleVersion 1 ITSAppUsesNonExemptEncryption