diff --git a/README.md b/README.md index ae866e3..573f8c6 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ Pass is an iOS client compatible with [ZX2C4's Pass command line application](http://www.passwordstore.org/). It is a password manager using GPG for encryption and Git for version control. -Pass for iOS is under *TestFlight external testing*. Drop me an email for -testing. Thank you. +Pass for iOS is under *TestFlight external testing*. Drop an email to +`developer@passforios.mssun.me` for testing. Thank you. ## Features @@ -20,7 +20,7 @@ testing. Thank you. - Encrypt and decrypt password entries by PGP keys - Synchronize with your password Git repository - User-friendly interface: search, long press to copy, copy and open link, etc. -- Support one-time password (OTP) tokens +- Support one-time password (OTP) tokens (QR code and otpauth URI) - Written in Swift - No need to jailbreak your devices - Get from App Store (stay tuned) diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index e0c0996..7652f77 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 94BA784B85E071D25EE89B59 /* libPods-pass.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADCE7A5C3CCC67D7D21BB3C4 /* libPods-pass.a */; }; A217ACE21E9AB17C00A1A6CF /* OTPScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A217ACE11E9AB17C00A1A6CF /* OTPScannerController.swift */; }; + A217ACE41E9BBBBD00A1A6CF /* GitConfigSettingTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingTableViewController.swift */; }; A262A58D1E68749C006B0890 /* Base32.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A262A58C1E68749C006B0890 /* Base32.framework */; }; A27424D91E7C35960093F436 /* NotificationNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27424D81E7C35960093F436 /* NotificationNames.swift */; }; A2802BF91E70813A00879216 /* SliderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2802BF71E70813A00879216 /* SliderTableViewCell.swift */; }; @@ -84,6 +85,7 @@ /* Begin PBXFileReference section */ 274CCFCF32444A2FF46BE7F4 /* Pods-pass.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-pass.debug.xcconfig"; path = "Pods/Target Support Files/Pods-pass/Pods-pass.debug.xcconfig"; sourceTree = ""; }; A217ACE11E9AB17C00A1A6CF /* OTPScannerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTPScannerController.swift; sourceTree = ""; }; + A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = GitConfigSettingTableViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; A262A58C1E68749C006B0890 /* Base32.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Base32.framework; path = Carthage/Build/iOS/Base32.framework; sourceTree = ""; }; A27424D81E7C35960093F436 /* NotificationNames.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationNames.swift; sourceTree = ""; }; A2802BF71E70813A00879216 /* SliderTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderTableViewCell.swift; sourceTree = ""; }; @@ -107,7 +109,7 @@ DC13B14E1E8640810097803F /* passTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = passTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DC13B1501E8640810097803F /* passTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = passTests.swift; sourceTree = ""; }; DC13B1521E8640810097803F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DC193FF91E49B4430077E0A3 /* AdvancedSettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsTableViewController.swift; sourceTree = ""; }; + DC193FF91E49B4430077E0A3 /* AdvancedSettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AdvancedSettingsTableViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; DC193FFB1E49E0340077E0A3 /* PasscodeLock.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PasscodeLock.framework; path = Carthage/Build/iOS/PasscodeLock.framework; sourceTree = ""; }; DC193FFD1E49E0760077E0A3 /* PasscodeLockRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeLockRepository.swift; sourceTree = ""; }; DC193FFF1E49E1A60077E0A3 /* PasscodeLockConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeLockConfiguration.swift; sourceTree = ""; }; @@ -128,14 +130,14 @@ DC917BE21E2E8231000FDF54 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DC962CDE1E4B62C10033B5D8 /* AboutTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutTableViewController.swift; sourceTree = ""; }; DCA049951E3357E000522E8F /* SwiftyUserDefaults.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftyUserDefaults.framework; path = Carthage/Build/iOS/SwiftyUserDefaults.framework; sourceTree = ""; }; - DCA049971E33586A00522E8F /* DefaultsKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultsKeys.swift; sourceTree = ""; }; + DCA049971E33586A00522E8F /* DefaultsKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = DefaultsKeys.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; DCA049991E335CC800522E8F /* GitServerSettingTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitServerSettingTableViewController.swift; sourceTree = ""; }; DCA0499B1E3362F400522E8F /* PGPKeySettingTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PGPKeySettingTableViewController.swift; sourceTree = ""; }; DCA0499D1E33BAC100522E8F /* Globals.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Globals.swift; sourceTree = ""; }; DCA671DE1E7A73B100D3ABE1 /* OneTimePassword.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OneTimePassword.framework; path = Carthage/Build/iOS/OneTimePassword.framework; sourceTree = ""; }; DCA742D91E599ED400D54E16 /* KeychainAccess.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = KeychainAccess.framework; path = Carthage/Build/iOS/KeychainAccess.framework; sourceTree = ""; }; DCAAF7441E2FA66800AB94BC /* SettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = ""; }; - DCC408A31E2FCC9E00F29B0E /* PasswordStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordStore.swift; sourceTree = ""; }; + DCC408A31E2FCC9E00F29B0E /* PasswordStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = PasswordStore.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; DCC408C61E307DBB00F29B0E /* SVProgressHUD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SVProgressHUD.framework; path = Carthage/Build/iOS/SVProgressHUD.framework; sourceTree = ""; }; 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 = ""; }; @@ -223,6 +225,7 @@ DCC441531E916382008A90C4 /* GitSSHKeyArmorSettingTableViewController.swift */, A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */, A217ACE11E9AB17C00A1A6CF /* OTPScannerController.swift */, + A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingTableViewController.swift */, ); path = Controllers; sourceTree = ""; @@ -549,6 +552,7 @@ DCFB779E1E4F40C7008DE471 /* FillPasswordTableViewCell.swift in Sources */, A2802BF91E70813A00879216 /* SliderTableViewCell.swift in Sources */, DC037CB21E4CAB1700609409 /* AboutRepositoryTableViewController.swift in Sources */, + A217ACE41E9BBBBD00A1A6CF /* GitConfigSettingTableViewController.swift in Sources */, DC037CB01E4CA51F00609409 /* GeneralSettingsTableViewController.swift in Sources */, DC037CB81E4DD1A500609409 /* AddPasswordTableViewController.swift in Sources */, DCC441521E8F6C06008A90C4 /* RawPasswordViewController.swift in Sources */, diff --git a/pass/Base.lproj/Main.storyboard b/pass/Base.lproj/Main.storyboard index c516009..84b3c31 100644 --- a/pass/Base.lproj/Main.storyboard +++ b/pass/Base.lproj/Main.storyboard @@ -1,11 +1,11 @@ - + - + @@ -89,11 +89,11 @@ - + diff --git a/pass/Controllers/AddPasswordTableViewController.swift b/pass/Controllers/AddPasswordTableViewController.swift index dea0558..e56c1af 100644 --- a/pass/Controllers/AddPasswordTableViewController.swift +++ b/pass/Controllers/AddPasswordTableViewController.swift @@ -15,10 +15,10 @@ class AddPasswordTableViewController: PasswordEditorTableViewController { override func viewDidLoad() { tableData = [ - [[.type: PasswordEditorCellType.textFieldCell, .title: "name"]], + [[.type: PasswordEditorCellType.nameCell, .title: "name"]], [[.type: PasswordEditorCellType.fillPasswordCell, .title: "password"], [.type: PasswordEditorCellType.passwordLengthCell, .title: "passwordlength"]], - [[.type: PasswordEditorCellType.textViewCell, .title: "additions"]], + [[.type: PasswordEditorCellType.additionsCell, .title: "additions"]], [[.type: PasswordEditorCellType.scanQRCodeCell]] ] super.viewDidLoad() @@ -35,21 +35,12 @@ class AddPasswordTableViewController: PasswordEditorTableViewController { } // check name - let nameCell = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as! TextFieldTableViewCell - guard nameCell.getContent()!.isEmpty == false else { + guard nameCell?.getContent()?.isEmpty == false else { let alertTitle = "Cannot Add Password" let alertMessage = "Please fill in the name." Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil) return false } - - // check "/" - guard nameCell.getContent()!.contains("/") == false else { - let alertTitle = "Cannot Add Password" - let alertMessage = "Illegal character." - Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil) - return false - } } return true } @@ -57,23 +48,15 @@ class AddPasswordTableViewController: PasswordEditorTableViewController { override func prepare(for segue: UIStoryboardSegue, sender: Any?) { super.prepare(for: segue, sender: sender) if segue.identifier == "saveAddPasswordSegue" { - let cells = tableView.visibleCells - var cellContents = [String: String]() - for cell in cells { - if let indexPath = tableView.indexPath(for: cell), - let contentCell = cell as? ContentTableViewCell, - let cellTitle = tableData[indexPath.section][indexPath.row][.title] as? String, - let cellContent = contentCell.getContent() { - cellContents[cellTitle] = cellContent - } + var plainText = (fillPasswordCell?.getContent())! + if let additionsString = additionsCell?.getContent(), additionsString.isEmpty == false { + plainText.append("\n") + plainText.append(additionsString) } - var plainText = "" - if cellContents["additions"]! != "" { - plainText = "\(cellContents["password"]!)\n\(cellContents["additions"]!)" - } else { - plainText = "\(cellContents["password"]!)" - } - password = Password(name: cellContents["name"]!, plainText: plainText) + let encodedName = (nameCell?.getContent()?.stringByAddingPercentEncodingForRFC3986())! + let name = URL(string: encodedName)!.lastPathComponent + let url = URL(string: encodedName)!.appendingPathExtension("gpg") + password = Password(name: name, url: url, plainText: plainText) } } } diff --git a/pass/Controllers/AdvancedSettingsTableViewController.swift b/pass/Controllers/AdvancedSettingsTableViewController.swift index d8e5afd..ee2fd8a 100644 --- a/pass/Controllers/AdvancedSettingsTableViewController.swift +++ b/pass/Controllers/AdvancedSettingsTableViewController.swift @@ -13,6 +13,7 @@ import SwiftyUserDefaults class AdvancedSettingsTableViewController: UITableViewController { @IBOutlet weak var encryptInASCIIArmoredTableViewCell: UITableViewCell! + @IBOutlet weak var gitSignatureTableViewCell: UITableViewCell! @IBOutlet weak var eraseDataTableViewCell: UITableViewCell! @IBOutlet weak var discardChangesTableViewCell: UITableViewCell! let passwordStore = PasswordStore.shared @@ -30,6 +31,18 @@ class AdvancedSettingsTableViewController: UITableViewController { encryptInASCIIArmoredSwitch.isOn = Defaults[.encryptInArmored] encryptInASCIIArmoredTableViewCell.accessoryView = encryptInASCIIArmoredSwitch encryptInASCIIArmoredTableViewCell.selectionStyle = .none + setGitSignatureText() + } + + private func setGitSignatureText() { + let gitSignatureName = passwordStore.gitSignatureForNow.name! + let gitSignatureEmail = passwordStore.gitSignatureForNow.email! + self.gitSignatureTableViewCell.detailTextLabel?.font = UIFont.systemFont(ofSize: 14) + self.gitSignatureTableViewCell.detailTextLabel?.text = "\(gitSignatureName) <\(gitSignatureEmail)>" + if Defaults[.gitSignatureName] == nil && Defaults[.gitSignatureEmail] == nil { + self.gitSignatureTableViewCell.detailTextLabel?.font = UIFont.systemFont(ofSize: 17) + gitSignatureTableViewCell.detailTextLabel?.text = "Not Set" + } } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { @@ -48,27 +61,23 @@ class AdvancedSettingsTableViewController: UITableViewController { } else if tableView.cellForRow(at: indexPath) == discardChangesTableViewCell { let alert = UIAlertController(title: "Discard All Changes?", message: "Do you want to permanently discard all changes to the local copy of your password data? You cannot undo this action.", preferredStyle: UIAlertControllerStyle.alert) alert.addAction(UIAlertAction(title: "Discard All Changes", style: UIAlertActionStyle.destructive, handler: {[unowned self] (action) -> Void in - DispatchQueue.global(qos: .userInitiated).async { - SVProgressHUD.show(withStatus: "Resetting ...") - DispatchQueue.main.async { - do { - let numberDiscarded = try self.passwordStore.reset() - self.navigationController!.popViewController(animated: true) - switch numberDiscarded { - case 0: - SVProgressHUD.showSuccess(withStatus: "No local commits") - case 1: - SVProgressHUD.showSuccess(withStatus: "Discarded 1 commit") - default: - SVProgressHUD.showSuccess(withStatus: "Discarded \(numberDiscarded) commits") - } - SVProgressHUD.dismiss(withDelay: 1) - } catch { - Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil) - } + SVProgressHUD.show(withStatus: "Resetting ...") + do { + let numberDiscarded = try self.passwordStore.reset() + self.navigationController!.popViewController(animated: true) + switch numberDiscarded { + case 0: + SVProgressHUD.showSuccess(withStatus: "No local commits") + case 1: + SVProgressHUD.showSuccess(withStatus: "Discarded 1 commit") + default: + SVProgressHUD.showSuccess(withStatus: "Discarded \(numberDiscarded) commits") } + SVProgressHUD.dismiss(withDelay: 1) + } catch { + Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil) } - + })) alert.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.cancel, handler:nil)) self.present(alert, animated: true, completion: nil) @@ -78,5 +87,19 @@ class AdvancedSettingsTableViewController: UITableViewController { func encryptInASCIIArmoredAction(_ sender: Any?) { Defaults[.encryptInArmored] = encryptInASCIIArmoredSwitch.isOn } + + @IBAction func cancelGitConfigSetting(segue: UIStoryboardSegue) { + } + + @IBAction func saveGitConfigSetting(segue: UIStoryboardSegue) { + if let controller = segue.source as? GitConfigSettingTableViewController { + if let gitSignatureName = controller.nameTextField.text, + let gitSignatureEmail = controller.emailTextField.text { + Defaults[.gitSignatureName] = gitSignatureName.isEmpty ? nil : gitSignatureName + Defaults[.gitSignatureEmail] = gitSignatureEmail.isEmpty ? nil : gitSignatureEmail + } + setGitSignatureText() + } + } } diff --git a/pass/Controllers/CommitLogsTableViewController.swift b/pass/Controllers/CommitLogsTableViewController.swift index 2255c99..00f0b1d 100644 --- a/pass/Controllers/CommitLogsTableViewController.swift +++ b/pass/Controllers/CommitLogsTableViewController.swift @@ -15,6 +15,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) self.tableView.estimatedRowHeight = 50 self.tableView.rowHeight = UITableViewAutomaticDimension @@ -31,12 +32,17 @@ class CommitLogsTableViewController: UITableViewController { formatter.timeStyle = .medium let dateString = formatter.string(from: commits[indexPath.row].commitDate) - let author = cell.contentView.viewWithTag(100) as? UILabel - let dateLabel = cell.contentView.viewWithTag(101) as? UILabel - let messageLabel = cell.contentView.viewWithTag(102) as? UILabel + let author = cell.contentView.viewWithTag(200) as? UILabel + let dateLabel = cell.contentView.viewWithTag(201) as? UILabel + let messageLabel = cell.contentView.viewWithTag(202) as? UILabel author?.text = commits[indexPath.row].author?.name dateLabel?.text = dateString messageLabel?.text = commits[indexPath.row].message?.trimmingCharacters(in: .whitespacesAndNewlines) return cell } + + func updateCommitLogs () { + commits = passwordStore.getRecentCommits(count: 20) + tableView.reloadData() + } } diff --git a/pass/Controllers/EditPasswordTableViewController.swift b/pass/Controllers/EditPasswordTableViewController.swift index 0caf2f8..6e69839 100644 --- a/pass/Controllers/EditPasswordTableViewController.swift +++ b/pass/Controllers/EditPasswordTableViewController.swift @@ -11,10 +11,10 @@ import UIKit class EditPasswordTableViewController: PasswordEditorTableViewController { override func viewDidLoad() { tableData = [ - [[.type: PasswordEditorCellType.textFieldCell, .title: "name", .content: password!.name]], + [[.type: PasswordEditorCellType.nameCell, .title: "name", .content: password!.namePath]], [[.type: PasswordEditorCellType.fillPasswordCell, .title: "password", .content: password!.password], [.type: PasswordEditorCellType.passwordLengthCell, .title: "passwordlength"]], - [[.type: PasswordEditorCellType.textViewCell, .title: "additions", .content: password!.getAdditionsPlainText()]], + [[.type: PasswordEditorCellType.additionsCell, .title: "additions", .content: password!.getAdditionsPlainText()]], [[.type: PasswordEditorCellType.scanQRCodeCell], [.type: PasswordEditorCellType.deletePasswordCell]] ] @@ -23,15 +23,13 @@ class EditPasswordTableViewController: PasswordEditorTableViewController { override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { if identifier == "saveEditPasswordSegue" { - if let nameCell = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? ContentTableViewCell { - if nameCell.getContent() != password?.name { - let alertTitle = "Cannot Save Edit" - let alertMessage = "Editing name is not supported." - Utils.alert(title: alertTitle, message: alertMessage, controller: self) { - nameCell.setContent(content: self.password!.name) - } - return false - } + if let name = nameCell?.getContent(), + let path = name.stringByAddingPercentEncodingForRFC3986(), + let _ = URL(string: path) { + return true + } else { + Utils.alert(title: "Cannot Save", message: "Password name is invalid.", controller: self, completion: nil) + return false } } return true @@ -40,23 +38,17 @@ class EditPasswordTableViewController: PasswordEditorTableViewController { override func prepare(for segue: UIStoryboardSegue, sender: Any?) { super.prepare(for: segue, sender: sender) if segue.identifier == "saveEditPasswordSegue" { - let cells = tableView.visibleCells - var cellContents = [String: String]() - for cell in cells { - if let indexPath = tableView.indexPath(for: cell), - let contentCell = cell as? ContentTableViewCell, - let cellTitle = tableData[indexPath.section][indexPath.row][.title] as? String, - let cellContent = contentCell.getContent() { - cellContents[cellTitle] = cellContent - } + var plainText = (fillPasswordCell?.getContent())! + if let additionsString = additionsCell?.getContent(), additionsString.isEmpty == false { + plainText.append("\n") + plainText.append(additionsString) } - var plainText = "" - if cellContents["additions"]! != "" { - plainText = "\(cellContents["password"]!)\n\(cellContents["additions"]!)" - } else { - plainText = "\(cellContents["password"]!)" + let encodedName = (nameCell?.getContent()?.stringByAddingPercentEncodingForRFC3986())! + let name = URL(string: encodedName)!.lastPathComponent + let url = URL(string: encodedName)!.appendingPathExtension("gpg") + if password!.plainText != plainText || password!.url!.path != url.path { + password!.updatePassword(name: name, url: url, plainText: plainText) } - password!.updatePassword(name: cellContents["name"]!, plainText: plainText) } } diff --git a/pass/Controllers/GitConfigSettingTableViewController.swift b/pass/Controllers/GitConfigSettingTableViewController.swift new file mode 100644 index 0000000..cc7d23e --- /dev/null +++ b/pass/Controllers/GitConfigSettingTableViewController.swift @@ -0,0 +1,41 @@ +// +// GitConfigSettingTableViewController.swift +// pass +// +// Created by Yishi Lin on 10/4/17. +// Copyright © 2017 Yishi Lin. All rights reserved. +// + +import UIKit +import SwiftyUserDefaults + +class GitConfigSettingTableViewController: UITableViewController { + let passwordStore = PasswordStore.shared + + @IBOutlet weak var nameTextField: UITextField! + @IBOutlet weak var emailTextField: UITextField! + + override func viewDidLoad() { + super.viewDidLoad() + tableView.rowHeight = UITableViewAutomaticDimension + + let signature = passwordStore.gitSignatureForNow + nameTextField.placeholder = signature.name + emailTextField.placeholder = signature.email + nameTextField.text = Defaults[.gitSignatureName] + emailTextField.text = Defaults[.gitSignatureEmail] + } + + override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { + if identifier == "saveGitConfigSettingSegue" { + let name = nameTextField.text!.isEmpty ? Globals.gitSignatureDefaultName : nameTextField.text! + let email = emailTextField.text!.isEmpty ? Globals.gitSignatureDefaultEmail : nameTextField.text! + guard GTSignature(name: name, email: email, time: nil) != nil else { + Utils.alert(title: "Error", message: "Invalid name or email.", controller: self, completion: nil) + return false + } + } + return true + } +} + diff --git a/pass/Controllers/GitSSHKeyArmorSettingTableViewController.swift b/pass/Controllers/GitSSHKeyArmorSettingTableViewController.swift index 035f73a..b21adb7 100644 --- a/pass/Controllers/GitSSHKeyArmorSettingTableViewController.swift +++ b/pass/Controllers/GitSSHKeyArmorSettingTableViewController.swift @@ -21,28 +21,10 @@ class GitSSHKeyArmorSettingTableViewController: UITableViewController, UITextVie super.viewDidLoad() armorPublicKeyTextView.text = Defaults[.gitSSHPublicKeyArmor] armorPrivateKeyTextView.text = Defaults[.gitSSHPrivateKeyArmor] - gitSSHPrivateKeyPassphrase = passwordStore.gitSSHPrivateKeyPassphrase - armorPublicKeyTextView.delegate = self armorPrivateKeyTextView.delegate = self } - private func createSavePassphraseAlert() -> UIAlertController { - let savePassphraseAlert = UIAlertController(title: "Passphrase", message: "Do you want to save the passphrase for later sync?", preferredStyle: UIAlertControllerStyle.alert) - savePassphraseAlert.addAction(UIAlertAction(title: "No", style: UIAlertActionStyle.default) { _ in - Defaults[.isRememberPassphraseOn] = false - Defaults[.gitSSHKeySource] = "armor" - self.navigationController!.popViewController(animated: true) - }) - savePassphraseAlert.addAction(UIAlertAction(title: "Save", style: UIAlertActionStyle.destructive) {_ in - Defaults[.isRememberPassphraseOn] = true - self.passwordStore.gitSSHPrivateKeyPassphrase = self.gitSSHPrivateKeyPassphrase - Defaults[.gitSSHKeySource] = "armor" - self.navigationController!.popViewController(animated: true) - }) - return savePassphraseAlert - } - @IBAction func doneButtonTapped(_ sender: Any) { Defaults[.gitSSHPublicKeyArmor] = armorPublicKeyTextView.text Defaults[.gitSSHPrivateKeyArmor] = armorPrivateKeyTextView.text @@ -52,22 +34,13 @@ class GitSSHKeyArmorSettingTableViewController: UITableViewController, UITextVie } catch { Utils.alert(title: "Cannot Save", message: "Cannot Save SSH Key", controller: self, completion: nil) } - let alert = UIAlertController(title: "Passphrase", message: "Please fill in the passphrase of your SSH secret key.", preferredStyle: UIAlertControllerStyle.alert) - alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in - self.gitSSHPrivateKeyPassphrase = alert.textFields?.first?.text - let savePassphraseAlert = self.createSavePassphraseAlert() - self.present(savePassphraseAlert, animated: true, completion: nil) - })) - alert.addTextField(configurationHandler: {(textField: UITextField!) in - textField.text = self.gitSSHPrivateKeyPassphrase - textField.isSecureTextEntry = true - }) - self.present(alert, animated: true, completion: nil) + Defaults[.gitSSHKeySource] = "armor" + self.navigationController!.popViewController(animated: true) } func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { if text == UIPasteboard.general.string { - // user pastes somethint, get ready to clear in 10s + // user pastes something, get ready to clear in 10s recentPastedText = text DispatchQueue.global(qos: .background).asyncAfter(deadline: DispatchTime.now() + 10) { [weak weakSelf = self] in if let pasteboardString = UIPasteboard.general.string, diff --git a/pass/Controllers/GitServerSettingTableViewController.swift b/pass/Controllers/GitServerSettingTableViewController.swift index 00eb3a9..8843ce7 100644 --- a/pass/Controllers/GitServerSettingTableViewController.swift +++ b/pass/Controllers/GitServerSettingTableViewController.swift @@ -17,8 +17,8 @@ class GitServerSettingTableViewController: UITableViewController { @IBOutlet weak var authSSHKeyCell: UITableViewCell! @IBOutlet weak var authPasswordCell: UITableViewCell! let passwordStore = PasswordStore.shared - var password: String? - + var sshLabel: UILabel? = nil + var authenticationMethod = Defaults[.gitAuthenticationMethod] private func checkAuthenticationMethod(method: String) { @@ -37,24 +37,22 @@ class GitServerSettingTableViewController: UITableViewController { sshKeyCheckView.isHidden = true } } + + 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() + + } override func viewDidLoad() { super.viewDidLoad() if let url = Defaults[.gitURL] { gitURLTextField.text = url.absoluteString } usernameTextField.text = Defaults[.gitUsername] - password = passwordStore.gitPassword authenticationMethod = Defaults[.gitAuthenticationMethod] - // Grey out ssh option if ssh_key and ssh_key.pub are not present - let sshLabel = authSSHKeyCell.subviews[0].subviews[0] as! UILabel - - sshLabel.isEnabled = gitSSHKeyExists() - - if authenticationMethod == nil || !sshLabel.isEnabled { - authenticationMethod = "Password" - } - checkAuthenticationMethod(method: authenticationMethod!) authSSHKeyCell.accessoryType = .detailButton } @@ -107,24 +105,22 @@ class GitServerSettingTableViewController: UITableViewController { tableView.deselectRow(at: indexPath, animated: true) } + private func doClone() { + if self.shouldPerformSegue(withIdentifier: "saveGitServerSettingSegue", sender: self) { + self.performSegue(withIdentifier: "saveGitServerSettingSegue", sender: self) + } + } + @IBAction func save(_ sender: Any) { - if authenticationMethod == "Password" { - let alert = UIAlertController(title: "Password", message: "Please fill in the password of your Git account.", preferredStyle: UIAlertControllerStyle.alert) - alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in - self.password = alert.textFields!.first!.text - if self.shouldPerformSegue(withIdentifier: "saveGitServerSettingSegue", sender: self) { - self.performSegue(withIdentifier: "saveGitServerSettingSegue", sender: self) - } + if passwordStore.repositoryExisted() { + let alert = UIAlertController(title: "Erase Current Password Store Data?", message: "A cloned password store exists. This operation will erase all local data. Data on your remote server will not be affected.", preferredStyle: UIAlertControllerStyle.alert) + alert.addAction(UIAlertAction(title: "Erase", style: UIAlertActionStyle.destructive, handler: { _ in + self.doClone() })) - alert.addTextField(configurationHandler: {(textField: UITextField!) in - textField.text = self.password - textField.isSecureTextEntry = true - }) + alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel, handler: nil)) self.present(alert, animated: true, completion: nil) } else { - if self.shouldPerformSegue(withIdentifier: "saveGitServerSettingSegue", sender: self) { - self.performSegue(withIdentifier: "saveGitServerSettingSegue", sender: self) - } + doClone() } } @@ -158,30 +154,8 @@ class GitServerSettingTableViewController: UITableViewController { if (gitSSHKeyExists()) { let fileAction = UIAlertAction(title: fileActionTitle, style: .default) { _ in - let alert = UIAlertController( - title: "SSH Key Passphrase", - message: "Please fill in the passphrase for your Git Repository SSH key.", - preferredStyle: UIAlertControllerStyle.alert - ) - - alert.addAction( - UIAlertAction( - title: "OK", - style: UIAlertActionStyle.default, - handler: {_ in - self.passwordStore.gitSSHPrivateKeyPassphrase = alert.textFields!.first!.text! - } - ) - ) - - alert.addTextField( - configurationHandler: {(textField: UITextField!) in - textField.text = self.passwordStore.gitSSHPrivateKeyPassphrase - textField.isSecureTextEntry = true - } - ) + Defaults[.gitSSHKeySource] = "file" } - Defaults[.gitSSHKeySource] = "file" optionMenu.addAction(fileAction) } @@ -189,6 +163,7 @@ class GitServerSettingTableViewController: UITableViewController { let deleteAction = UIAlertAction(title: "Remove Git SSH Keys", style: .destructive) { _ in Utils.removeGitSSHKeys() Defaults[.gitSSHKeySource] = nil + self.sshLabel!.isEnabled = false } optionMenu.addAction(deleteAction) } diff --git a/pass/Controllers/OTPScannerController.swift b/pass/Controllers/OTPScannerController.swift index f9c71da..52bb9ec 100644 --- a/pass/Controllers/OTPScannerController.swift +++ b/pass/Controllers/OTPScannerController.swift @@ -11,7 +11,6 @@ import AVFoundation class OTPScannerController: QRScannerController { - var tempPassword: Password? var scannedOTP: String? // MARK: - AVCaptureMetadataOutputObjectsDelegate Methods @@ -27,14 +26,11 @@ class OTPScannerController: QRScannerController { // check whether it is a valid result if let scannedString = metadataObj.stringValue { if let (accept, message) = delegate?.checkScannedOutput(line: scannedString) { + scannerOutput.text = message if accept == true { captureSession?.stopRunning() scannedOTP = scannedString - tempPassword = Password(name: "empty", plainText: scannedString) - // set scannerOutput - setupOneTimePasswordMessage() - } else { - scannerOutput.text = message + presentSaveAlert() } } else { // no delegate, show the scanned result @@ -50,34 +46,28 @@ class OTPScannerController: QRScannerController { } } - private func setupOneTimePasswordMessage() { - if let password = tempPassword { - if password.otpType == .hotp { - // hotp, no need to refresh - let (title, content) = password.getOtpStrings()! - scannerOutput.text = "\(title):\(content)" - } else if password.otpType == .totp { - // totp, refresh - Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { - [weak weakSelf = self] timer in + private func presentSaveAlert() { + // initialize alert + let password = Password(name: "empty", url: nil, plainText: scannedOTP!) + let (title, content) = password.getOtpStrings()! + let alert = UIAlertController(title: "Success", message: "\(title): \(content)", preferredStyle: UIAlertControllerStyle.alert) + alert.addAction(UIAlertAction(title: "Save", style: UIAlertActionStyle.default, handler: {[unowned self] (action) -> Void in + self.delegate?.handleScannedOutput(line: self.scannedOTP!) + self.navigationController?.popViewController(animated: true) + })) + + if password.otpType == .hotp { + // hotp, no need to refresh + self.present(alert, animated: true, completion: nil) + } else if password.otpType == .totp { + // totp, refresh otp + self.present(alert, animated: true) { + let alertController = self.presentedViewController as! UIAlertController + Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {_ in let (title, content) = password.getOtpStrings()! - weakSelf?.scannerOutput.text = "\(title):\(content)" + alertController.message = "\(title): \(content)" } } } } - - override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { - if identifier == "saveAddScannedOTPSegue" { - return tempPassword != nil - } - return true - } - -// override func prepare(for segue: UIStoryboardSegue, sender: Any?) { -// super.prepare(for: segue, sender: sender) -// if segue.identifier == "saveAddScannedOTPSegue" { -// delegate?.handleScannedOutput(line: scannedOTP) -// } -// } } diff --git a/pass/Controllers/PGPKeyArmorSettingTableViewController.swift b/pass/Controllers/PGPKeyArmorSettingTableViewController.swift index 34f7649..16b7fd0 100644 --- a/pass/Controllers/PGPKeyArmorSettingTableViewController.swift +++ b/pass/Controllers/PGPKeyArmorSettingTableViewController.swift @@ -28,6 +28,7 @@ class PGPKeyArmorSettingTableViewController: UITableViewController, UITextViewDe let savePassphraseAlert = UIAlertController(title: "Passphrase", message: "Do you want to save the passphrase for later decryption?", preferredStyle: UIAlertControllerStyle.alert) savePassphraseAlert.addAction(UIAlertAction(title: "No", style: UIAlertActionStyle.default) { _ in Defaults[.isRememberPassphraseOn] = false + self.pgpPassphrase = nil self.performSegue(withIdentifier: "savePGPKeySegue", sender: self) }) savePassphraseAlert.addAction(UIAlertAction(title: "Save", style: UIAlertActionStyle.destructive) {_ in diff --git a/pass/Controllers/PasswordDetailTableViewController.swift b/pass/Controllers/PasswordDetailTableViewController.swift index 84c5773..207e6cb 100644 --- a/pass/Controllers/PasswordDetailTableViewController.swift +++ b/pass/Controllers/PasswordDetailTableViewController.swift @@ -14,7 +14,6 @@ import SVProgressHUD class PasswordDetailTableViewController: UITableViewController, UIGestureRecognizerDelegate { var passwordEntity: PasswordEntity? private var password: Password? - private var passwordCategoryText = "" private var passwordImage: UIImage? private var oneTimePasswordIndexPath : IndexPath? private var shouldPopCurrentView = false @@ -77,7 +76,6 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni tableView.register(UINib(nibName: "LabelTableViewCell", bundle: nil), forCellReuseIdentifier: "labelCell") tableView.register(UINib(nibName: "PasswordDetailTitleTableViewCell", bundle: nil), forCellReuseIdentifier: "passwordDetailTitleTableViewCell") - passwordCategoryText = passwordEntity!.getCategoryText() let tapGesture = UITapGestureRecognizer(target: self, action: #selector(PasswordDetailTableViewController.tapMenu(recognizer:))) tapGesture.cancelsTouchesInView = false tableView.addGestureRecognizer(tapGesture) @@ -96,24 +94,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni let image = UIImage(data: imageData as Data) passwordImage = image } - - var passphrase = "" - if Defaults[.isRememberPassphraseOn] && self.passwordStore.pgpKeyPassphrase != nil { - passphrase = self.passwordStore.pgpKeyPassphrase! - self.decryptThenShowPassword(passphrase: passphrase) - } else { - 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 - passphrase = alert.textFields!.first!.text! - self.decryptThenShowPassword(passphrase: passphrase) - })) - alert.addTextField(configurationHandler: {(textField: UITextField!) in - textField.text = "" - textField.isSecureTextEntry = true - }) - self.present(alert, animated: true, completion: nil) - } - + self.decryptThenShowPassword() self.setupOneTimePasswordAutoRefresh() // pop the current view because this password might be "discarded" @@ -137,14 +118,33 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni } } - private func decryptThenShowPassword(passphrase: String) { + private func requestPGPKeyPassphrase() -> String { + let sem = DispatchSemaphore(value: 0) + var passphrase = "" + DispatchQueue.main.async { + 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 + passphrase = alert.textFields!.first!.text! + sem.signal() + })) + alert.addTextField(configurationHandler: {(textField: UITextField!) in + textField.text = "" + textField.isSecureTextEntry = true + }) + self.present(alert, animated: true, completion: nil) + } + let _ = sem.wait(timeout: DispatchTime.distantFuture) if Defaults[.isRememberPassphraseOn] { self.passwordStore.pgpKeyPassphrase = passphrase } + return passphrase + } + + private func decryptThenShowPassword() { DispatchQueue.global(qos: .userInitiated).async { // decrypt password do { - self.password = try self.passwordEntity!.decrypt(passphrase: passphrase)! + self.password = try self.passwordStore.decrypt(passwordEntity: self.passwordEntity!, requestPGPKeyPassphrase: self.requestPGPKeyPassphrase) } catch { DispatchQueue.main.async { let alert = UIAlertController(title: "Cannot Show Password", message: error.localizedDescription, preferredStyle: UIAlertControllerStyle.alert) @@ -164,11 +164,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni DispatchQueue.main.async { [weak self] in self?.indicator.stopAnimating() self?.setTableData() - UIView.performWithoutAnimation { - self?.tableView.reloadData() - // add layoutIfNeeded solves the "flickering problem" during refresh - self?.tableView.layoutIfNeeded() - } + self?.tableView.reloadData() self?.editUIBarButtonItem.isEnabled = true if let urlString = self?.password?.getURLString() { if self?.passwordEntity?.image == nil { @@ -216,29 +212,26 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni } @IBAction private func saveEditPassword(segue: UIStoryboardSegue) { - if self.password!.changed { + if self.password!.changed != 0 { SVProgressHUD.show(withStatus: "Saving") - DispatchQueue.global(qos: .userInitiated).async { - self.passwordStore.update(passwordEntity: self.passwordEntity!, password: self.password!, progressBlock: { progress in - DispatchQueue.main.async { - SVProgressHUD.showProgress(progress, status: "Encrypting") - } - }) - DispatchQueue.main.async { - self.passwordEntity!.synced = false - self.passwordStore.saveUpdated(passwordEntity: self.passwordEntity!) - self.setTableData() - self.tableView.reloadData() - SVProgressHUD.showSuccess(withStatus: "Success") - SVProgressHUD.dismiss(withDelay: 1) - } + do { + self.passwordEntity = try self.passwordStore.edit(passwordEntity: self.passwordEntity!, password: self.password!) + } catch { + Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil) } + self.setTableData() + self.tableView.reloadData() + SVProgressHUD.showSuccess(withStatus: "Success") + SVProgressHUD.dismiss(withDelay: 1) } } @IBAction private func deletePassword(segue: UIStoryboardSegue) { - print("delete") - passwordStore.delete(passwordEntity: passwordEntity!) + do { + try passwordStore.delete(passwordEntity: passwordEntity!) + } catch { + Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil) + } let _ = navigationController?.popViewController(animated: true) } @@ -392,16 +385,14 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni } // commit the change of HOTP counter - if password!.changed { - DispatchQueue.global(qos: .userInitiated).async { - self.passwordStore.update(passwordEntity: self.passwordEntity!, password: self.password!, progressBlock: {_ in }) - DispatchQueue.main.async { - self.passwordEntity!.synced = false - self.passwordStore.saveUpdated(passwordEntity: self.passwordEntity!) - SVProgressHUD.showSuccess(withStatus: "Password Copied\nCounter Updated") - SVProgressHUD.dismiss(withDelay: 1) - } + if password!.changed != 0 { + do { + self.passwordEntity = try self.passwordStore.edit(passwordEntity: self.passwordEntity!, password: self.password!) + } catch { + Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil) } + SVProgressHUD.showSuccess(withStatus: "Password Copied\nCounter Updated") + SVProgressHUD.dismiss(withDelay: 1) } } @@ -432,12 +423,14 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni case .name: let cell = tableView.dequeueReusableCell(withIdentifier: "passwordDetailTitleTableViewCell", for: indexPath) as! PasswordDetailTitleTableViewCell cell.passwordImageImageView.image = passwordImage ?? #imageLiteral(resourceName: "PasswordImagePlaceHolder") - var passwordName = passwordEntity!.name! - if passwordEntity!.synced == false { - passwordName = "\(passwordName) ↻" + if let passwordName = passwordEntity!.name { + if passwordEntity!.synced == false { + cell.nameLabel.text = "\(passwordName) ↻" + } else { + cell.nameLabel.text = passwordName + } } - cell.nameLabel.text = passwordName - cell.categoryLabel.text = passwordCategoryText + cell.categoryLabel.text = passwordEntity!.getCategoryText() cell.selectionStyle = .none return cell case .main, .addition: @@ -467,7 +460,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni footerLabel.numberOfLines = 0 footerLabel.font = UIFont.preferredFont(forTextStyle: .footnote) footerLabel.textColor = UIColor.gray - let dateString = self.passwordStore.getLatestUpdateInfo(filename: (passwordEntity?.path)!) + let dateString = self.passwordStore.getLatestUpdateInfo(filename: password!.url!.path) footerLabel.text = "Last Updated: \(dateString)" view.addSubview(footerLabel) return view diff --git a/pass/Controllers/PasswordEditorTableViewController.swift b/pass/Controllers/PasswordEditorTableViewController.swift index c750f7b..3441490 100644 --- a/pass/Controllers/PasswordEditorTableViewController.swift +++ b/pass/Controllers/PasswordEditorTableViewController.swift @@ -11,14 +11,14 @@ import SwiftyUserDefaults import OneTimePassword enum PasswordEditorCellType { - case textFieldCell, textViewCell, fillPasswordCell, passwordLengthCell, deletePasswordCell, scanQRCodeCell + case nameCell, fillPasswordCell, passwordLengthCell, additionsCell, deletePasswordCell, scanQRCodeCell } enum PasswordEditorCellKey { case type, title, content, placeholders } -class PasswordEditorTableViewController: UITableViewController, FillPasswordTableViewCellDelegate, PasswordSettingSliderTableViewCellDelegate, QRScannerControllerDelegate { +class PasswordEditorTableViewController: UITableViewController, FillPasswordTableViewCellDelegate, PasswordSettingSliderTableViewCellDelegate, QRScannerControllerDelegate, UITextFieldDelegate, UITextViewDelegate { var tableData = [ [Dictionary] @@ -29,12 +29,15 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl private var sectionHeaderTitles = ["name", "password", "additions",""].map {$0.uppercased()} private var sectionFooterTitles = ["", "", "Use \"key: value\" format for additional fields.", ""] + private let nameSection = 0 private let passwordSection = 1 private let additionsSection = 2 private var hidePasswordSettings = true - private var fillPasswordCell: FillPasswordTableViewCell? + var nameCell: TextFieldTableViewCell? + var fillPasswordCell: FillPasswordTableViewCell? private var passwordLengthCell: SliderTableViewCell? + var additionsCell: TextViewTableViewCell? private var deletePasswordCell: UITableViewCell? private var scanQRCodeCell: UITableViewCell? @@ -50,6 +53,7 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl scanQRCodeCell?.textLabel?.text = "Add One-Time Password" scanQRCodeCell?.textLabel?.textColor = Globals.blue scanQRCodeCell?.selectionStyle = .default + scanQRCodeCell?.accessoryType = .disclosureIndicator // scanQRCodeCell?.imageView?.image = #imageLiteral(resourceName: "Camera").withRenderingMode(.alwaysTemplate) // scanQRCodeCell?.imageView?.tintColor = Globals.blue // scanQRCodeCell?.imageView?.contentMode = .scaleAspectFit @@ -76,10 +80,11 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl let cellData = tableData[indexPath.section][indexPath.row] switch cellData[PasswordEditorCellKey.type] as! PasswordEditorCellType { - case .textViewCell: - let cell = tableView.dequeueReusableCell(withIdentifier: "textViewCell", for: indexPath) as! ContentTableViewCell - cell.setContent(content: cellData[PasswordEditorCellKey.content] as? String) - return cell + case .nameCell: + nameCell = tableView.dequeueReusableCell(withIdentifier: "textFieldCell", for: indexPath) as? TextFieldTableViewCell + nameCell?.contentTextField.delegate = self + nameCell?.setContent(content: cellData[PasswordEditorCellKey.content] as? String) + return nameCell! case .fillPasswordCell: fillPasswordCell = tableView.dequeueReusableCell(withIdentifier: "fillPasswordCell", for: indexPath) as? FillPasswordTableViewCell fillPasswordCell?.delegate = self @@ -95,14 +100,15 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl defaultValue: lengthSetting?.def ?? 0) passwordLengthCell?.delegate = self return passwordLengthCell! + case .additionsCell: + additionsCell = tableView.dequeueReusableCell(withIdentifier: "textViewCell", for: indexPath) as?TextViewTableViewCell + additionsCell?.contentTextView.delegate = self + additionsCell?.setContent(content: cellData[PasswordEditorCellKey.content] as? String) + return additionsCell! case .deletePasswordCell: return deletePasswordCell! case .scanQRCodeCell: return scanQRCodeCell! - default: - let cell = tableView.dequeueReusableCell(withIdentifier: "textFieldCell", for: indexPath) as! ContentTableViewCell - cell.setContent(content: cellData[PasswordEditorCellKey.content] as? String) - return cell } } @@ -187,13 +193,16 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl func insertScannedOTPFields(_ otpauth: String) { // update tableData + var additionsString = "" if let additionsPlainText = (tableData[additionsSection][0][PasswordEditorCellKey.content] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines), additionsPlainText != "" { - tableData[additionsSection][0][PasswordEditorCellKey.content] = additionsPlainText + "\n" + otpauth + additionsString = additionsPlainText + "\n" + otpauth } else { - tableData[additionsSection][0][PasswordEditorCellKey.content] = otpauth + additionsString = otpauth } - // reload - tableView.reloadSections([additionsSection], with: .none) + tableData[additionsSection][0][PasswordEditorCellKey.content] = additionsString + + // reload the additions cell + additionsCell?.setContent(content: additionsString) } // MARK: - QRScannerControllerDelegate Methods @@ -226,11 +235,17 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl } - @IBAction func saveScannedOTP(segue: UIStoryboardSegue) { - if let controller = segue.source as? OTPScannerController { - if let scannedOTP = controller.scannedOTP { - insertScannedOTPFields(scannedOTP) - } + // update the data table after editing + func textFieldDidEndEditing(_ textField: UITextField) { + if textField == nameCell?.contentTextField { + tableData[nameSection][0][PasswordEditorCellKey.content] = nameCell?.getContent() + } + } + + // update the data table after editing + func textViewDidEndEditing(_ textView: UITextView) { + if textView == additionsCell?.contentTextView { + tableData[additionsSection][0][PasswordEditorCellKey.content] = additionsCell?.getContent() } } } diff --git a/pass/Controllers/PasswordsViewController.swift b/pass/Controllers/PasswordsViewController.swift index c4bc68e..b0707e4 100644 --- a/pass/Controllers/PasswordsViewController.swift +++ b/pass/Controllers/PasswordsViewController.swift @@ -112,11 +112,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV SVProgressHUD.show(withStatus: "Saving") DispatchQueue.global(qos: .userInitiated).async { do { - try self.passwordStore.add(password: controller.password!, progressBlock: { progress in - DispatchQueue.main.async { - SVProgressHUD.showProgress(progress, status: "Encrypting") - } - }) + let _ = try self.passwordStore.add(password: controller.password!) DispatchQueue.main.async { // will trigger reloadTableView() by a notification SVProgressHUD.showSuccess(withStatus: "Done") @@ -132,19 +128,38 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } private func syncPasswords() { + guard passwordStore.repositoryExisted() else { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(800)) { + Utils.alert(title: "Error", message: "There is no password store right now.", controller: self, completion: nil) + } + return + } SVProgressHUD.setDefaultMaskType(.black) SVProgressHUD.setDefaultStyle(.light) SVProgressHUD.show(withStatus: "Sync Password Store") let numberOfLocalCommits = self.passwordStore.numberOfLocalCommits() + var gitCredential: GitCredential + if Defaults[.gitAuthenticationMethod] == "Password" { + gitCredential = GitCredential(credential: GitCredential.Credential.http(userName: Defaults[.gitUsername]!, controller: self)) + } else { + gitCredential = GitCredential( + credential: GitCredential.Credential.ssh( + userName: Defaults[.gitUsername]!, + publicKeyFile: Globals.gitSSHPublicKeyURL, + privateKeyFile: Globals.gitSSHPrivateKeyURL, + controller: self + ) + ) + } DispatchQueue.global(qos: .userInitiated).async { [unowned self] in do { - try self.passwordStore.pullRepository(transferProgressBlock: {(git_transfer_progress, stop) in + try self.passwordStore.pullRepository(credential: gitCredential, transferProgressBlock: {(git_transfer_progress, stop) in DispatchQueue.main.async { SVProgressHUD.showProgress(Float(git_transfer_progress.pointee.received_objects)/Float(git_transfer_progress.pointee.total_objects), status: "Pull Remote Repository") } }) if numberOfLocalCommits > 0 { - try self.passwordStore.pushRepository(transferProgressBlock: {(current, total, bytes, stop) in + try self.passwordStore.pushRepository(credential: gitCredential, transferProgressBlock: {(current, total, bytes, stop) in DispatchQueue.main.async { SVProgressHUD.showProgress(Float(current)/Float(total), status: "Push Remote Repository") } @@ -152,7 +167,6 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } DispatchQueue.main.async { self.reloadTableView(parent: nil) - Defaults[.gitPasswordAttempts] = 0 SVProgressHUD.showSuccess(withStatus: "Done") SVProgressHUD.dismiss(withDelay: 1) } @@ -323,15 +337,19 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } let password = getPasswordEntry(by: indexPath).passwordEntity! UIImpactFeedbackGenerator(style: .medium).impactOccurred() + decryptThenCopyPassword(passwordEntity: password) + } + + + + private func requestPGPKeyPassphrase() -> String { + let sem = DispatchSemaphore(value: 0) var passphrase = "" - if Defaults[.isRememberPassphraseOn] && self.passwordStore.pgpKeyPassphrase != nil { - passphrase = self.passwordStore.pgpKeyPassphrase! - self.decryptThenCopyPassword(passwordEntity: password, passphrase: passphrase) - } else { + DispatchQueue.main.async { 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 passphrase = alert.textFields!.first!.text! - self.decryptThenCopyPassword(passwordEntity: password, passphrase: passphrase) + sem.signal() })) alert.addTextField(configurationHandler: {(textField: UITextField!) in textField.text = "" @@ -339,17 +357,21 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV }) self.present(alert, animated: true, completion: nil) } - + let _ = sem.wait(timeout: DispatchTime.distantFuture) + if Defaults[.isRememberPassphraseOn] { + self.passwordStore.pgpKeyPassphrase = passphrase + } + return passphrase } - private func decryptThenCopyPassword(passwordEntity: PasswordEntity, passphrase: String) { + private func decryptThenCopyPassword(passwordEntity: PasswordEntity) { SVProgressHUD.setDefaultMaskType(.black) SVProgressHUD.setDefaultStyle(.dark) SVProgressHUD.show(withStatus: "Decrypting") DispatchQueue.global(qos: .userInteractive).async { var decryptedPassword: Password? do { - decryptedPassword = try passwordEntity.decrypt(passphrase: passphrase)! + decryptedPassword = try self.passwordStore.decrypt(passwordEntity: passwordEntity, requestPGPKeyPassphrase: self.requestPGPKeyPassphrase) DispatchQueue.main.async { Utils.copyToPasteboard(textToCopy: decryptedPassword?.password) SVProgressHUD.showSuccess(withStatus: "Password copied, and will be cleared in 45 seconds.") @@ -486,9 +508,9 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } func actOnReloadTableViewRelatedNotification() { - initPasswordsTableEntries(parent: nil) DispatchQueue.main.async { [weak weakSelf = self] in guard let strongSelf = weakSelf else { return } + strongSelf.initPasswordsTableEntries(parent: nil) strongSelf.reloadTableView(data: strongSelf.passwordsTableEntries) } } diff --git a/pass/Controllers/SSHKeySettingTableViewController.swift b/pass/Controllers/SSHKeySettingTableViewController.swift index 23f0c5c..615ef6c 100644 --- a/pass/Controllers/SSHKeySettingTableViewController.swift +++ b/pass/Controllers/SSHKeySettingTableViewController.swift @@ -43,32 +43,7 @@ class SSHKeySettingTableViewController: UITableViewController { Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil) } Defaults[.gitSSHKeySource] = "url" - let alert = UIAlertController( - title: "PGP Passphrase", - message: "Please fill in the passphrase for your Git Repository SSH key.", - preferredStyle: UIAlertControllerStyle.alert - ) - - alert.addAction( - UIAlertAction( - title: "OK", - style: UIAlertActionStyle.default, - handler: {_ in - Utils.addPasswordToKeychain( - name: "gitSSHPrivateKeyPassphrase", - password: alert.textFields!.first!.text! - ) - self.navigationController!.popViewController(animated: true) - } - ) - ) - - alert.addTextField( - configurationHandler: {(textField: UITextField!) in - textField.text = self.passwordStore.gitSSHPrivateKeyPassphrase - textField.isSecureTextEntry = true - }) - self.present(alert, animated: true, completion: nil) + self.navigationController!.popViewController(animated: true) } } diff --git a/pass/Controllers/SettingsTableViewController.swift b/pass/Controllers/SettingsTableViewController.swift index 764b688..8c0b38d 100644 --- a/pass/Controllers/SettingsTableViewController.swift +++ b/pass/Controllers/SettingsTableViewController.swift @@ -34,7 +34,9 @@ class SettingsTableViewController: UITableViewController { if let controller = segue.source as? PGPKeySettingTableViewController { Defaults[.pgpPrivateKeyURL] = URL(string: controller.pgpPrivateKeyURLTextField.text!) Defaults[.pgpPublicKeyURL] = URL(string: controller.pgpPublicKeyURLTextField.text!) - self.passwordStore.pgpKeyPassphrase = controller.pgpPassphrase + if Defaults[.isRememberPassphraseOn] { + self.passwordStore.pgpKeyPassphrase = controller.pgpPassphrase + } Defaults[.pgpKeySource] = "url" SVProgressHUD.setDefaultMaskType(.black) @@ -48,7 +50,7 @@ class SettingsTableViewController: UITableViewController { self.pgpKeyTableViewCell.detailTextLabel?.text = self.passwordStore.pgpKeyID SVProgressHUD.showSuccess(withStatus: "Success") SVProgressHUD.dismiss(withDelay: 1) - Utils.alert(title: "Rememver to Remove the Key", message: "Remember to remove the key from the server.", controller: self, completion: nil) + Utils.alert(title: "Remember to Remove the Key", message: "Remember to remove the key from the server.", controller: self, completion: nil) } } catch { DispatchQueue.main.async { @@ -60,9 +62,8 @@ class SettingsTableViewController: UITableViewController { } else if let controller = segue.source as? PGPKeyArmorSettingTableViewController { Defaults[.pgpKeySource] = "armor" - self.passwordStore.pgpKeyPassphrase = controller.pgpPassphrase if Defaults[.isRememberPassphraseOn] { - Utils.addPasswordToKeychain(name: "pgpKeyPassphrase", password: controller.pgpPassphrase!) + self.passwordStore.pgpKeyPassphrase = controller.pgpPassphrase } Defaults[.pgpPublicKeyArmor] = controller.armorPublicKeyTextView.text! @@ -97,64 +98,53 @@ class SettingsTableViewController: UITableViewController { if let controller = segue.source as? GitServerSettingTableViewController { let gitRepostiroyURL = controller.gitURLTextField.text! let username = controller.usernameTextField.text! - let password = controller.password let auth = controller.authenticationMethod - if Defaults[.gitURL] == nil || - Defaults[.gitURL]!.absoluteString != gitRepostiroyURL || - auth != Defaults[.gitAuthenticationMethod] || - username != Defaults[.gitUsername] || - password != self.passwordStore.gitPassword || - self.passwordStore.repositoryExisted() == false { - - SVProgressHUD.setDefaultMaskType(.black) - SVProgressHUD.setDefaultStyle(.light) - SVProgressHUD.show(withStatus: "Prepare Repository") - var gitCredential: GitCredential - if auth == "Password" { - gitCredential = GitCredential(credential: GitCredential.Credential.http(userName: username, password: password!)) - } else { - gitCredential = GitCredential( - credential: GitCredential.Credential.ssh( - userName: username, - password: Utils.getPasswordFromKeychain(name: "gitSSHPrivateKeyPassphrase") ?? "", - publicKeyFile: Globals.gitSSHPublicKeyURL, - privateKeyFile: Globals.gitSSHPrivateKeyURL, - passwordNotSetCallback: self.requestSshKeyPassword - ) + SVProgressHUD.setDefaultMaskType(.black) + SVProgressHUD.setDefaultStyle(.light) + SVProgressHUD.show(withStatus: "Prepare Repository") + var gitCredential: GitCredential + if auth == "Password" { + gitCredential = GitCredential(credential: GitCredential.Credential.http(userName: username, controller: self)) + } else { + gitCredential = GitCredential( + credential: GitCredential.Credential.ssh( + userName: username, + publicKeyFile: Globals.gitSSHPublicKeyURL, + privateKeyFile: Globals.gitSSHPrivateKeyURL, + controller: self ) - } - let dispatchQueue = DispatchQueue.global(qos: .userInitiated) - dispatchQueue.async { - do { - try self.passwordStore.cloneRepository(remoteRepoURL: URL(string: gitRepostiroyURL)!, - credential: gitCredential, - transferProgressBlock:{ (git_transfer_progress, stop) in - DispatchQueue.main.async { - SVProgressHUD.showProgress(Float(git_transfer_progress.pointee.received_objects)/Float(git_transfer_progress.pointee.total_objects), status: "Clone Remote Repository") - } - }, - checkoutProgressBlock: { (path, completedSteps, totalSteps) in - DispatchQueue.main.async { - SVProgressHUD.showProgress(Float(completedSteps)/Float(totalSteps), status: "Checkout Master Branch") - } - }) - DispatchQueue.main.async { - Defaults[.gitURL] = URL(string: gitRepostiroyURL) - Defaults[.gitUsername] = username - Defaults[.gitAuthenticationMethod] = auth - Defaults[.gitPasswordAttempts] = 0 - self.passwordRepositoryTableViewCell.detailTextLabel?.text = Defaults[.gitURL]?.host - SVProgressHUD.showSuccess(withStatus: "Done") - SVProgressHUD.dismiss(withDelay: 1) - } - } catch { - DispatchQueue.main.async { - Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil) - } + ) + } + let dispatchQueue = DispatchQueue.global(qos: .userInitiated) + dispatchQueue.async { + do { + try self.passwordStore.cloneRepository(remoteRepoURL: URL(string: gitRepostiroyURL)!, + credential: gitCredential, + transferProgressBlock:{ (git_transfer_progress, stop) in + DispatchQueue.main.async { + SVProgressHUD.showProgress(Float(git_transfer_progress.pointee.received_objects)/Float(git_transfer_progress.pointee.total_objects), status: "Clone Remote Repository") + } + }, + checkoutProgressBlock: { (path, completedSteps, totalSteps) in + DispatchQueue.main.async { + SVProgressHUD.showProgress(Float(completedSteps)/Float(totalSteps), status: "Checkout Master Branch") + } + }) + DispatchQueue.main.async { + Defaults[.gitURL] = URL(string: gitRepostiroyURL) + Defaults[.gitUsername] = username + Defaults[.gitAuthenticationMethod] = auth + self.passwordRepositoryTableViewCell.detailTextLabel?.text = Defaults[.gitURL]?.host + SVProgressHUD.showSuccess(withStatus: "Done") + SVProgressHUD.dismiss(withDelay: 1) + } + } catch { + DispatchQueue.main.async { + Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil) } - } + } } } @@ -203,32 +193,7 @@ class SettingsTableViewController: UITableViewController { touchIDSwitch.isOn = false } } - - func requestSshKeyPassword() -> String { - let sem = DispatchSemaphore(value: 0) - var newPassword = "" - - DispatchQueue.main.async { - SVProgressHUD.dismiss() - let alert = UIAlertController(title: "Password", message: "Please fill in the password of your SSH key.", preferredStyle: UIAlertControllerStyle.alert) - - alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in - newPassword = alert.textFields!.first!.text! - sem.signal() - })) - - alert.addTextField(configurationHandler: {(textField: UITextField!) in - textField.text = self.passwordStore.gitPassword - textField.isSecureTextEntry = true - }) - - self.present(alert, animated: true, completion: nil) - } - - let _ = sem.wait(timeout: DispatchTime.distantFuture) - return newPassword - } - + func actOnPasswordStoreErasedNotification() { setPGPKeyTableViewCellDetailText() setPasswordRepositoryTableViewCellDetailText() diff --git a/pass/Helpers/DefaultsKeys.swift b/pass/Helpers/DefaultsKeys.swift index 1dae85a..8ea41a9 100644 --- a/pass/Helpers/DefaultsKeys.swift +++ b/pass/Helpers/DefaultsKeys.swift @@ -20,14 +20,13 @@ extension DefaultsKeys { static let gitURL = DefaultsKey("gitURL") static let gitAuthenticationMethod = DefaultsKey("gitAuthenticationMethod") static let gitUsername = DefaultsKey("gitUsername") - static let gitPasswordAttempts = DefaultsKey("gitPasswordAttempts") static let gitSSHPublicKeyURL = DefaultsKey("gitSSHPublicKeyURL") static let gitSSHPrivateKeyURL = DefaultsKey("gitSSHPrivateKeyURL") static let gitSSHKeySource = DefaultsKey("gitSSHKeySource") - static let gitSSHPublicKeyArmor = DefaultsKey("gitSSHPublicKeyArmor") static let gitSSHPrivateKeyArmor = DefaultsKey("gitSSHPrivateKeyArmor") - + static let gitSignatureName = DefaultsKey("gitSignatureName") + static let gitSignatureEmail = DefaultsKey("gitSignatureEmail") static let lastSyncedTime = DefaultsKey("lastSyncedTime") diff --git a/pass/Helpers/Globals.swift b/pass/Helpers/Globals.swift index 0551b04..c4980f4 100644 --- a/pass/Helpers/Globals.swift +++ b/pass/Helpers/Globals.swift @@ -26,6 +26,9 @@ class Globals { static let passwordDefaultLength = ["Random": (min: 6, max: 24, def: 16), "Apple": (min: 15, max: 15, def: 15)] + static let gitSignatureDefaultName = "Pass for iOS" + static let gitSignatureDefaultEmail = "user@passforios" + static let passwordDots = "••••••••••••" static let oneTimePasswordDots = "••••••" static let passwordFonts = "Menlo" diff --git a/pass/Helpers/Utils.swift b/pass/Helpers/Utils.swift index daa4496..dee5411 100644 --- a/pass/Helpers/Utils.swift +++ b/pass/Helpers/Utils.swift @@ -238,3 +238,12 @@ extension FileManager { return accumulatedSize } } + +extension String { + func stringByAddingPercentEncodingForRFC3986() -> String? { + let unreserved = "-._~/?" + var allowed = CharacterSet.alphanumerics + allowed.insert(charactersIn: unreserved) + return addingPercentEncoding(withAllowedCharacters: allowed) + } +} diff --git a/pass/Info.plist b/pass/Info.plist index ebce541..a332db4 100644 --- a/pass/Info.plist +++ b/pass/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.2.4 + 0.2.5 CFBundleVersion 1 ITSAppUsesNonExemptEncryption diff --git a/pass/Models/Password.swift b/pass/Models/Password.swift index dd5106d..2ac0896 100644 --- a/pass/Models/Password.swift +++ b/pass/Models/Password.swift @@ -16,14 +16,29 @@ struct AdditionField { var content: String } +enum PasswordChange: Int { + case path = 0x01 + case content = 0x02 + case none = 0x00 +} + class Password { static let otpKeywords = ["otp_secret", "otp_type", "otp_algorithm", "otp_period", "otp_digits", "otp_counter", "otpauth"] - + var name = "" + var url: URL? + var namePath: String { + get { + if url == nil { + return "" + } + return url!.deletingPathExtension().path + } + } var password = "" var additions = [String: String]() var additionKeys = [String]() - var changed = false + var changed: Int = 0 var plainText = "" private var firstLineIsOTPField = false @@ -47,19 +62,25 @@ class Password { } } - init(name: String, plainText: String) { - self.initEverything(name: name, plainText: plainText) + init(name: String, url: URL?, plainText: String) { + self.initEverything(name: name, url: url, plainText: plainText) } - func updatePassword(name: String, plainText: String) { - if self.plainText != plainText { - self.initEverything(name: name, plainText: plainText) - changed = true + func updatePassword(name: String, url: URL?, plainText: String) { + if self.plainText != plainText || self.url != url { + if self.plainText != plainText { + changed = changed|PasswordChange.content.rawValue + } + if self.url != url { + changed = changed|PasswordChange.path.rawValue + } + self.initEverything(name: name, url: url, plainText: plainText) } } - private func initEverything(name: String, plainText: String) { + private func initEverything(name: String, url: URL?, plainText: String) { self.name = name + self.url = url self.plainText = plainText self.additions.removeAll() self.additionKeys.removeAll() @@ -322,7 +343,7 @@ class Password { if newOtpauth != nil { lines.append(newOtpauth!) } - self.updatePassword(name: self.name, plainText: lines.joined(separator: "\n")) + self.updatePassword(name: self.name, url: self.url, plainText: lines.joined(separator: "\n")) // get and return the password return self.otpToken?.currentPassword diff --git a/pass/Models/PasswordEntity.swift b/pass/Models/PasswordEntity.swift index f65d1e7..cda8389 100644 --- a/pass/Models/PasswordEntity.swift +++ b/pass/Models/PasswordEntity.swift @@ -21,24 +21,6 @@ extension PasswordEntity { } } - func decrypt(passphrase: String) throws -> Password? { - var password: Password? - let encryptedDataPath = URL(fileURLWithPath: "\(Globals.repositoryPath)/\(path!)") - let encryptedData = try Data(contentsOf: encryptedDataPath) - let decryptedData = try PasswordStore.shared.pgp.decryptData(encryptedData, passphrase: passphrase) - let plainText = String(data: decryptedData, encoding: .utf8) ?? "" - password = Password(name: name!, plainText: plainText) - return password - } - - func encrypt(password: Password) throws -> Data { - name = password.name - let plainData = password.getPlainData() - let pgp = PasswordStore.shared.pgp - let encryptedData = try pgp.encryptData(plainData, usingPublicKey: pgp.getKeysOf(.public)[0], armored: Defaults[.encryptInArmored]) - return encryptedData - } - func getCategoryText() -> String { var parentEntity = parent var passwordCategoryArray: [String] = [] @@ -49,4 +31,11 @@ extension PasswordEntity { passwordCategoryArray.reverse() return passwordCategoryArray.joined(separator: " > ") } + + func getURL() -> URL? { + if let p = path { + return URL(string: p.stringByAddingPercentEncodingForRFC3986()!) + } + return nil + } } diff --git a/pass/Models/PasswordStore.swift b/pass/Models/PasswordStore.swift index ff05a3b..a80ef27 100644 --- a/pass/Models/PasswordStore.swift +++ b/pass/Models/PasswordStore.swift @@ -14,81 +14,96 @@ import ObjectiveGit import SVProgressHUD struct GitCredential { + var credential: Credential enum Credential { - case http(userName: String, password: String) - case ssh(userName: String, password: String, publicKeyFile: URL, privateKeyFile: URL, passwordNotSetCallback: (() -> String)? ) + case http(userName: String, controller: UIViewController) + case ssh(userName: String, publicKeyFile: URL, privateKeyFile: URL, controller: UIViewController) + } + + init(credential: Credential) { + self.credential = credential } - var 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, password): - print(Defaults[.gitPasswordAttempts]) - var newPassword: String = password - if Defaults[.gitPasswordAttempts] != 0 { - let sem = DispatchSemaphore(value: 0) - DispatchQueue.main.async { - SVProgressHUD.dismiss() - if var topController = UIApplication.shared.keyWindow?.rootViewController { - while let presentedViewController = topController.presentedViewController { - topController = presentedViewController - } - let alert = UIAlertController(title: "Password", message: "Please fill in the password of your Git account.", preferredStyle: UIAlertControllerStyle.alert) - alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in - newPassword = alert.textFields!.first!.text! - PasswordStore.shared.gitPassword = newPassword - sem.signal() - })) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in - Defaults[.gitPasswordAttempts] = -1 - sem.signal() - }) - alert.addTextField(configurationHandler: {(textField: UITextField!) in - textField.text = PasswordStore.shared.gitPassword - textField.isSecureTextEntry = true - }) - topController.present(alert, animated: true, completion: nil) - } + 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 } - let _ = sem.wait(timeout: DispatchTime.distantFuture) } - if Defaults[.gitPasswordAttempts] == -1 { - Defaults[.gitPasswordAttempts] = 0 - 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 + } } - Defaults[.gitPasswordAttempts] += 1 - PasswordStore.shared.gitPassword = newPassword - credential = try? GTCredential(userName: userName, password: newPassword) - case let .ssh(userName, password, publicKeyFile, privateKeyFile, passwordNotSetCallback): - - var newPassword:String? = password - - // Check if the private key is encrypted - let encrypted = try? String(contentsOf: privateKeyFile).contains("ENCRYPTED") - - // Request password if not already set - if encrypted! && password == "" { - newPassword = passwordNotSetCallback!() - } - - // Save password for the future - Utils.addPasswordToKeychain(name: "gitSSHPrivateKeyPassphrase", password: newPassword!) - - // nil is expected in case of empty password - if newPassword == "" { - newPassword = nil - } - - - credential = try? GTCredential(userName: userName, publicKeyURL: publicKeyFile, privateKeyURL: privateKeyFile, passphrase: newPassword) + 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 { @@ -97,7 +112,6 @@ class PasswordStore { let tempStoreURL = URL(fileURLWithPath: "\(Globals.repositoryPath)-temp") var storeRepository: GTRepository? - var gitCredential: GitCredential? var pgpKeyID: String? var publicKey: PGPKey? { didSet { @@ -112,7 +126,9 @@ class PasswordStore { var gitSignatureForNow: GTSignature { get { - return GTSignature(name: Defaults[.gitUsername]!, email: Defaults[.gitUsername]!+"@passforios", time: Date())! + let gitSignatureName = Defaults[.gitSignatureName] ?? Globals.gitSignatureDefaultName + let gitSignatureEmail = Defaults[.gitSignatureEmail] ?? Globals.gitSignatureDefaultEmail + return GTSignature(name: gitSignatureName, email: gitSignatureEmail, time: Date())! } } @@ -126,6 +142,7 @@ class PasswordStore { return Utils.getPasswordFromKeychain(name: "pgpKeyPassphrase") } } + var gitPassword: String? { set { Utils.addPasswordToKeychain(name: "gitPassword", password: newValue) @@ -140,7 +157,7 @@ class PasswordStore { Utils.addPasswordToKeychain(name: "gitSSHPrivateKeyPassphrase", password: newValue) } get { - return Utils.getPasswordFromKeychain(name: "gitSSHPrivateKeyPassphrase") ?? "" + return Utils.getPasswordFromKeychain(name: "gitSSHPrivateKeyPassphrase") } } @@ -173,31 +190,12 @@ class PasswordStore { print(error) } initPGPKeys() - initGitCredential() } enum SSHKeyType { case `public`, secret } - public func initGitCredential() { - if Defaults[.gitAuthenticationMethod] == "Password" { - gitCredential = GitCredential(credential: GitCredential.Credential.http(userName: Defaults[.gitUsername]!, password: Utils.getPasswordFromKeychain(name: "gitPassword") ?? "")) - } else if Defaults[.gitAuthenticationMethod] == "SSH Key"{ - gitCredential = GitCredential( - credential: GitCredential.Credential.ssh( - userName: Defaults[.gitUsername]!, - password: gitSSHPrivateKeyPassphrase ?? "", - publicKeyFile: Globals.gitSSHPublicKeyURL, - privateKeyFile: Globals.gitSSHPrivateKeyURL, - passwordNotSetCallback: nil - ) - ) - } else { - gitCredential = nil - } - } - public func initGitSSHKey(with armorKey: String, _ keyType: SSHKeyType) throws { var keyPath = "" switch keyType { @@ -282,10 +280,9 @@ class PasswordStore { } func passwordExisted(password: Password) -> Bool { - print(password.name) let passwordEntityFetchRequest = NSFetchRequest(entityName: "PasswordEntity") do { - passwordEntityFetchRequest.predicate = NSPredicate(format: "name = %@", password.name) + passwordEntityFetchRequest.predicate = NSPredicate(format: "name = %@ and path = %@", password.name, password.url!.path) let count = try context.count(for: passwordEntityFetchRequest) if count > 0 { return true @@ -298,49 +295,79 @@ class PasswordStore { return true } + func passwordEntityExisted(path: String) -> Bool { + let passwordEntityFetchRequest = NSFetchRequest(entityName: "PasswordEntity") + do { + passwordEntityFetchRequest.predicate = NSPredicate(format: "path = %@", path) + let count = try context.count(for: passwordEntityFetchRequest) + if count > 0 { + return true + } else { + return false + } + } catch { + fatalError("Failed to fetch password entities: \(error)") + } + return true + } + + func getPasswordEntity(by path: String, isDir: Bool) -> PasswordEntity? { + let passwordEntityFetchRequest = NSFetchRequest(entityName: "PasswordEntity") + do { + passwordEntityFetchRequest.predicate = NSPredicate(format: "path = %@ and isDir = %@", path, isDir.description) + return try context.fetch(passwordEntityFetchRequest).first as? PasswordEntity + } catch { + fatalError("Failed to fetch password entities: \(error)") + } + } + func cloneRepository(remoteRepoURL: URL, credential: GitCredential, transferProgressBlock: @escaping (UnsafePointer, UnsafeMutablePointer) -> Void, checkoutProgressBlock: @escaping (String?, UInt, UInt) -> Void) throws { Utils.removeFileIfExists(at: storeURL) Utils.removeFileIfExists(at: tempStoreURL) - - let credentialProvider = try credential.credentialProvider() - let options: [String: Any] = [ - GTRepositoryCloneOptionsCredentialProvider: credentialProvider, - ] - storeRepository = try GTRepository.clone(from: remoteRepoURL, toWorkingDirectory: tempStoreURL, options: options, transferProgressBlock:transferProgressBlock) - let fm = FileManager.default do { + let credentialProvider = try credential.credentialProvider() + let options: [String: Any] = [ + GTRepositoryCloneOptionsCredentialProvider: credentialProvider, + ] + storeRepository = try GTRepository.clone(from: remoteRepoURL, toWorkingDirectory: tempStoreURL, options: options, transferProgressBlock:transferProgressBlock) + let fm = FileManager.default if fm.fileExists(atPath: storeURL.path) { try fm.removeItem(at: storeURL) } try fm.copyItem(at: tempStoreURL, to: storeURL) try fm.removeItem(at: tempStoreURL) + storeRepository = try GTRepository(url: storeURL) } catch { - print(error) + credential.delete() + throw(error) } - storeRepository = try GTRepository(url: storeURL) - gitCredential = credential - Defaults[.lastSyncedTime] = Date() DispatchQueue.main.async { + Defaults[.lastSyncedTime] = Date() self.updatePasswordEntityCoreData() NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil) } } - func pullRepository(transferProgressBlock: @escaping (UnsafePointer, UnsafeMutablePointer) -> Void) throws { - if gitCredential == nil { + 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."]) } - let credentialProvider = try gitCredential!.credentialProvider() - let options: [String: Any] = [ - GTRepositoryRemoteOptionsCredentialProvider: credentialProvider - ] - let remote = try GTRemote(name: "origin", in: storeRepository!) - try storeRepository?.pull((storeRepository?.currentBranch())!, from: remote, withOptions: options, progress: transferProgressBlock) - Defaults[.lastSyncedTime] = Date() + do { + let credentialProvider = try credential.credentialProvider() + let options: [String: Any] = [ + GTRepositoryRemoteOptionsCredentialProvider: credentialProvider + ] + let remote = try GTRemote(name: "origin", in: storeRepository!) + try storeRepository!.pull((storeRepository?.currentBranch())!, from: remote, withOptions: options, progress: transferProgressBlock) + } catch { + credential.delete() + throw(error) + } DispatchQueue.main.async { + Defaults[.lastSyncedTime] = Date() self.setAllSynced() self.updatePasswordEntityCoreData() NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil) @@ -509,45 +536,69 @@ class PasswordStore { func updateRemoteRepo() { } - func createAddCommitInRepository(message: String, fileData: Data, filename: String, progressBlock: (_ progress: Float) -> Void) -> GTCommit? { + private func gitAdd(path: String) throws { + if let repo = storeRepository { + try repo.index().addFile(path) + try repo.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() + } + + } + + private func deleteDirectoryTree(at url: URL) throws { + var tempURL = storeURL.appendingPathComponent(url.deletingLastPathComponent().path) + let fm = FileManager.default + var count = try fm.contentsOfDirectory(atPath: tempURL.path).count + while count == 0 { + try fm.removeItem(at: tempURL) + tempURL.deleteLastPathComponent() + count = try fm.contentsOfDirectory(atPath: tempURL.path).count + } + } + + private func createDirectoryTree(at url: URL) throws { + let tempURL = storeURL.appendingPathComponent(url.deletingLastPathComponent().path) + try FileManager.default.createDirectory(at: tempURL, withIntermediateDirectories: true, attributes: nil) + } + + private func gitMv(from: String, to: String) throws { + let fm = FileManager.default do { - try storeRepository?.index().add(fileData, withPath: filename) - try storeRepository?.index().write() - 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 - progressBlock(0.5) - let signature = gitSignatureForNow - let commit = try storeRepository!.createCommit(with: newTree, message: message, author: signature, committer: signature, parents: [parent], updatingReferenceNamed: headReference.name) - progressBlock(0.7) - return commit + 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) } - return nil + try gitAdd(path: to) + try gitRm(path: from) } - func createRemoveCommitInRepository(message: String, path: String) -> GTCommit? { - do { - try storeRepository?.index().removeFile(path) - try storeRepository?.index().write() - let newTree = try storeRepository!.index().writeTree() - let headReference = try storeRepository!.headReference() - let commitEnum = try GTEnumerator(repository: storeRepository!) + 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 storeRepository!.createCommit(with: newTree, message: message, author: signature, committer: signature, parents: [parent], updatingReferenceNamed: headReference.name) + let commit = try repo.createCommit(with: newTree, message: message, author: signature, committer: signature, parents: [parent], updatingReferenceNamed: headReference.name) return commit - } catch { - print(error) } return nil } - private func getLocalBranch(withName branchName: String) -> GTBranch? { do { let reference = GTBranch.localNamePrefix().appending(branchName) @@ -559,67 +610,133 @@ class PasswordStore { return nil } - func pushRepository(transferProgressBlock: @escaping (UInt32, UInt32, Int, UnsafeMutablePointer) -> Void) throws { - let credentialProvider = try gitCredential!.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) + func pushRepository(credential: GitCredential, transferProgressBlock: @escaping (UInt32, UInt32, Int, UnsafeMutablePointer) -> Void) throws { + 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) + } catch { + credential.delete() + throw(error) + } } - func add(password: Password, progressBlock: (_ progress: Float) -> Void) throws { - progressBlock(0.0) + 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."]) } - let passwordEntity = NSEntityDescription.insertNewObject(forEntityName: "PasswordEntity", into: context) as! PasswordEntity - do { - let encryptedData = try passwordEntity.encrypt(password: password) - progressBlock(0.3) - let saveURL = storeURL.appendingPathComponent("\(password.name).gpg") - try encryptedData.write(to: saveURL) - passwordEntity.name = password.name - passwordEntity.path = "\(password.name).gpg" - passwordEntity.parent = nil - passwordEntity.synced = false - passwordEntity.isDir = false - try context.save() - print(saveURL.path) - let _ = createAddCommitInRepository(message: "Add password for \(passwordEntity.nameWithCategory) to store using Pass for iOS.", fileData: encryptedData, filename: saveURL.lastPathComponent, progressBlock: progressBlock) - progressBlock(1.0) - NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil) - } catch { - print(error) + + var passwordURL = password.url! + var paths: [String] = [] + while passwordURL.path != "." { + paths.append(passwordURL.path) + passwordURL = passwordURL.deletingLastPathComponent() } + paths.reverse() + var parentPasswordEntity: PasswordEntity? = nil + for path in paths { + let isDir = !path.hasSuffix(".gpg") + if let passwordEntity = getPasswordEntity(by: path, isDir: isDir) { + print(passwordEntity.path!) + parentPasswordEntity = passwordEntity + } else { + if !isDir { + return insertPasswordEntity(name: URL(string: path.stringByAddingPercentEncodingForRFC3986()!)!.deletingPathExtension().lastPathComponent, path: path, parent: parentPasswordEntity, synced: false, isDir: false) + } else { + parentPasswordEntity = insertPasswordEntity(name: URL(string: path.stringByAddingPercentEncodingForRFC3986()!)!.lastPathComponent, path: path, parent: parentPasswordEntity, synced: false, isDir: true) + } + } + } + return nil } - func update(passwordEntity: PasswordEntity, password: Password, progressBlock: (_ progress: Float) -> Void) { - progressBlock(0.0) - do { - let encryptedData = try passwordEntity.encrypt(password: password) - let saveURL = storeURL.appendingPathComponent(passwordEntity.path!) - try encryptedData.write(to: saveURL) - progressBlock(0.3) - let _ = createAddCommitInRepository(message: "Edit password for \(passwordEntity.nameWithCategory) using Pass for iOS.", fileData: encryptedData, filename: saveURL.lastPathComponent, progressBlock: progressBlock) - progressBlock(1.0) - NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil) - } catch { - print(error) + private func insertPasswordEntity(name: String, path: String, parent: PasswordEntity?, synced: Bool = false, isDir: Bool = false) -> PasswordEntity? { + var ret: PasswordEntity? = nil + if let passwordEntity = NSEntityDescription.insertNewObject(forEntityName: "PasswordEntity", into: self.context) as? PasswordEntity { + passwordEntity.name = name + passwordEntity.path = path + passwordEntity.parent = parent + passwordEntity.synced = synced + passwordEntity.isDir = isDir + do { + try self.context.save() + ret = passwordEntity + } catch { + fatalError("Failed to insert a PasswordEntity: \(error)") + } } + return ret } - public func delete(passwordEntity: PasswordEntity) { - Utils.removeFileIfExists(at: storeURL.appendingPathComponent(passwordEntity.path!)) - let _ = createRemoveCommitInRepository(message: "Remove \(passwordEntity.nameWithCategory) from store using Pass for iOS", path: passwordEntity.path!) - context.delete(passwordEntity) - do { - try context.save() - } catch { - fatalError("Failed to delete a PasswordEntity: \(error)") + func add(password: Password) throws -> PasswordEntity? { + try createDirectoryTree(at: password.url!) + let newPasswordEntity = try addPasswordEntities(password: password) + let saveURL = storeURL.appendingPathComponent(password.url!.path) + try self.encrypt(password: password).write(to: saveURL) + try gitAdd(path: password.url!.path) + let _ = try gitCommit(message: "Add password for \(password.url!.deletingPathExtension().path) to store using Pass for iOS.") + NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil) + return newPasswordEntity + } + + public func delete(passwordEntity: PasswordEntity) throws { + let deletedFileURL = passwordEntity.getURL()! + try deleteDirectoryTree(at: passwordEntity.getURL()!) + try deletePasswordEntities(passwordEntity: passwordEntity) + try gitRm(path: deletedFileURL.path) + let _ = try gitCommit(message: "Remove \(deletedFileURL.deletingPathExtension().path.removingPercentEncoding!) from store using Pass for iOS.") + NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil) + } + + func edit(passwordEntity: PasswordEntity, password: Password) throws -> PasswordEntity? { + var newPasswordEntity: PasswordEntity? = passwordEntity + + if password.changed&PasswordChange.content.rawValue != 0 { + print("chagne content") + let saveURL = storeURL.appendingPathComponent(passwordEntity.getURL()!.path) + try self.encrypt(password: password).write(to: saveURL) + try gitAdd(path: passwordEntity.getURL()!.path) + let _ = try gitCommit(message: "Edit password for \(passwordEntity.getURL()!.deletingPathExtension().path.removingPercentEncoding!) to store using Pass for iOS.") + newPasswordEntity = passwordEntity + } + + if password.changed&PasswordChange.path.rawValue != 0 { + print("change path") + let deletedFileURL = passwordEntity.getURL()! + // add + try createDirectoryTree(at: password.url!) + newPasswordEntity = try addPasswordEntities(password: password) + + // mv + try gitMv(from: deletedFileURL.path, to: password.url!.path) + + // delete + try deleteDirectoryTree(at: deletedFileURL) + try deletePasswordEntities(passwordEntity: passwordEntity) + let _ = try gitCommit(message: "Rename \(deletedFileURL.deletingPathExtension().path.removingPercentEncoding!) to \(password.url!.deletingPathExtension().path.removingPercentEncoding!) using Pass for iOS.") + } NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil) + return newPasswordEntity + } + + 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!) + current = parent + do { + try self.context.save() + } catch { + fatalError("Failed to delete a PasswordEntity: \(error)") + } + } } func saveUpdated(passwordEntity: PasswordEntity) { @@ -703,7 +820,6 @@ class PasswordStore { try self.storeRepository?.reset(to: newHead, resetType: GTRepositoryResetType.hard) self.setAllSynced() self.updatePasswordEntityCoreData() - Defaults[.lastSyncedTime] = nil NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil) NotificationCenter.default.post(name: .passwordStoreChangeDiscarded, object: nil) @@ -739,4 +855,28 @@ class PasswordStore { // get a list of local commits 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 encryptedData = try Data(contentsOf: encryptedDataPath) + var passphrase = self.pgpKeyPassphrase + if passphrase == nil { + passphrase = requestPGPKeyPassphrase() + } + 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 + } + + func encrypt(password: Password) throws -> Data { + let plainData = password.getPlainData() + let pgp = PasswordStore.shared.pgp + let encryptedData = try pgp.encryptData(plainData, usingPublicKey: pgp.getKeysOf(.public)[0], armored: Defaults[.encryptInArmored]) + return encryptedData + } } diff --git a/screenshot/screenshot1.png b/screenshot/screenshot1.png index ad1bc91..7e2781a 100644 Binary files a/screenshot/screenshot1.png and b/screenshot/screenshot1.png differ diff --git a/screenshot/screenshot2.png b/screenshot/screenshot2.png index aab0d66..50f4b51 100644 Binary files a/screenshot/screenshot2.png and b/screenshot/screenshot2.png differ diff --git a/screenshot/screenshot3.png b/screenshot/screenshot3.png index ef2d07a..dfc3991 100644 Binary files a/screenshot/screenshot3.png and b/screenshot/screenshot3.png differ diff --git a/screenshot/screenshot4.png b/screenshot/screenshot4.png index cdbecc8..da81e1b 100644 Binary files a/screenshot/screenshot4.png and b/screenshot/screenshot4.png differ diff --git a/screenshot/screenshot5.png b/screenshot/screenshot5.png index 04b3cbc..f7f2506 100644 Binary files a/screenshot/screenshot5.png and b/screenshot/screenshot5.png differ