Do not forget pgp passphrase on decryption error #296
This commit is contained in:
parent
6b00d4911b
commit
e332b1f3be
6 changed files with 46 additions and 35 deletions
|
|
@ -18,6 +18,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
||||||
private var oneTimePasswordIndexPath : IndexPath?
|
private var oneTimePasswordIndexPath : IndexPath?
|
||||||
private var shouldPopCurrentView = false
|
private var shouldPopCurrentView = false
|
||||||
private let passwordStore = PasswordStore.shared
|
private let passwordStore = PasswordStore.shared
|
||||||
|
private let keychain = AppKeychain.shared
|
||||||
|
|
||||||
private lazy var editUIBarButtonItem: UIBarButtonItem = {
|
private lazy var editUIBarButtonItem: UIBarButtonItem = {
|
||||||
let uiBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(pressEdit(_:)))
|
let uiBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(pressEdit(_:)))
|
||||||
|
|
@ -96,14 +97,14 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
||||||
sem.signal()
|
sem.signal()
|
||||||
}))
|
}))
|
||||||
alert.addTextField(configurationHandler: {(textField: UITextField!) in
|
alert.addTextField(configurationHandler: {(textField: UITextField!) in
|
||||||
textField.text = ""
|
textField.text = self.keychain.get(for: Globals.pgpKeyPassphrase) ?? ""
|
||||||
textField.isSecureTextEntry = true
|
textField.isSecureTextEntry = true
|
||||||
})
|
})
|
||||||
self.present(alert, animated: true, completion: nil)
|
self.present(alert, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
let _ = sem.wait(timeout: DispatchTime.distantFuture)
|
let _ = sem.wait(timeout: DispatchTime.distantFuture)
|
||||||
if SharedDefaults[.isRememberPGPPassphraseOn] {
|
if SharedDefaults[.isRememberPGPPassphraseOn] {
|
||||||
AppKeychain.shared.add(string: passphrase, for: Globals.pgpKeyPassphrase)
|
self.keychain.add(string: passphrase, for: Globals.pgpKeyPassphrase)
|
||||||
}
|
}
|
||||||
return passphrase
|
return passphrase
|
||||||
}
|
}
|
||||||
|
|
@ -121,8 +122,6 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
||||||
self.password = try self.passwordStore.decrypt(passwordEntity: passwordEntity, requestPGPKeyPassphrase: self.requestPGPKeyPassphrase)
|
self.password = try self.passwordStore.decrypt(passwordEntity: passwordEntity, requestPGPKeyPassphrase: self.requestPGPKeyPassphrase)
|
||||||
} catch {
|
} catch {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
// remove the wrong passphrase so that users could enter it next time
|
|
||||||
AppKeychain.shared.removeContent(for: Globals.pgpKeyPassphrase)
|
|
||||||
// alert: cancel or try again
|
// alert: cancel or try again
|
||||||
let alert = UIAlertController(title: "CannotShowPassword".localize(), message: error.localizedDescription, preferredStyle: UIAlertController.Style.alert)
|
let alert = UIAlertController(title: "CannotShowPassword".localize(), message: error.localizedDescription, preferredStyle: UIAlertController.Style.alert)
|
||||||
alert.addAction(UIAlertAction(title: "Cancel".localize(), style: UIAlertAction.Style.default) { _ in
|
alert.addAction(UIAlertAction(title: "Cancel".localize(), style: UIAlertAction.Style.default) { _ in
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
||||||
SVProgressHUD.setDefaultStyle(.light)
|
SVProgressHUD.setDefaultStyle(.light)
|
||||||
SVProgressHUD.show(withStatus: "SyncingPasswordStore".localize())
|
SVProgressHUD.show(withStatus: "SyncingPasswordStore".localize())
|
||||||
var gitCredential: GitCredential
|
var gitCredential: GitCredential
|
||||||
let privateKey: String? = AppKeychain.shared.get(for: SshKey.PRIVATE.getKeychainKey())
|
let privateKey: String? = self.keychain.get(for: SshKey.PRIVATE.getKeychainKey())
|
||||||
if SharedDefaults[.gitAuthenticationMethod] == "Password" || privateKey == nil {
|
if SharedDefaults[.gitAuthenticationMethod] == "Password" || privateKey == nil {
|
||||||
gitCredential = GitCredential(credential: GitCredential.Credential.http(userName: SharedDefaults[.gitUsername]!))
|
gitCredential = GitCredential(credential: GitCredential.Credential.http(userName: SharedDefaults[.gitUsername]!))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -406,20 +406,14 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
||||||
sem.signal()
|
sem.signal()
|
||||||
}))
|
}))
|
||||||
alert.addTextField(configurationHandler: {(textField: UITextField!) in
|
alert.addTextField(configurationHandler: {(textField: UITextField!) in
|
||||||
textField.text = ""
|
textField.text = self.keychain.get(for: Globals.pgpKeyPassphrase) ?? ""
|
||||||
textField.isSecureTextEntry = true
|
textField.isSecureTextEntry = true
|
||||||
})
|
})
|
||||||
// hide it so that alert is on the top of the view
|
|
||||||
SVProgressHUD.dismiss()
|
|
||||||
self.present(alert, animated: true, completion: nil)
|
self.present(alert, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
let _ = sem.wait(timeout: DispatchTime.distantFuture)
|
let _ = sem.wait(timeout: DispatchTime.distantFuture)
|
||||||
DispatchQueue.main.async {
|
|
||||||
// bring back
|
|
||||||
SVProgressHUD.show(withStatus: "Decrypting".localize())
|
|
||||||
}
|
|
||||||
if SharedDefaults[.isRememberPGPPassphraseOn] {
|
if SharedDefaults[.isRememberPGPPassphraseOn] {
|
||||||
keychain.add(string: passphrase, for: Globals.pgpKeyPassphrase)
|
self.keychain.add(string: passphrase, for: Globals.pgpKeyPassphrase)
|
||||||
}
|
}
|
||||||
return passphrase
|
return passphrase
|
||||||
}
|
}
|
||||||
|
|
@ -431,22 +425,20 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
||||||
}
|
}
|
||||||
let passwordEntity = getPasswordEntry(by: indexPath).passwordEntity!
|
let passwordEntity = getPasswordEntry(by: indexPath).passwordEntity!
|
||||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||||
SVProgressHUD.setDefaultMaskType(.black)
|
SVProgressHUD.dismiss()
|
||||||
SVProgressHUD.setDefaultStyle(.dark)
|
|
||||||
SVProgressHUD.show(withStatus: "Decrypting".localize())
|
|
||||||
DispatchQueue.global(qos: .userInteractive).async {
|
DispatchQueue.global(qos: .userInteractive).async {
|
||||||
var decryptedPassword: Password?
|
var decryptedPassword: Password?
|
||||||
do {
|
do {
|
||||||
decryptedPassword = try self.passwordStore.decrypt(passwordEntity: passwordEntity, requestPGPKeyPassphrase: self.requestPGPKeyPassphrase)
|
decryptedPassword = try self.passwordStore.decrypt(passwordEntity: passwordEntity, requestPGPKeyPassphrase: self.requestPGPKeyPassphrase)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
SecurePasteboard.shared.copy(textToCopy: decryptedPassword?.password)
|
SecurePasteboard.shared.copy(textToCopy: decryptedPassword?.password)
|
||||||
|
SVProgressHUD.setDefaultMaskType(.black)
|
||||||
|
SVProgressHUD.setDefaultStyle(.dark)
|
||||||
SVProgressHUD.showSuccess(withStatus: "PasswordCopiedToPasteboard.".localize())
|
SVProgressHUD.showSuccess(withStatus: "PasswordCopiedToPasteboard.".localize())
|
||||||
SVProgressHUD.dismiss(withDelay: 0.6)
|
SVProgressHUD.dismiss(withDelay: 0.6)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
// remove the wrong passphrase so that users could enter it next time
|
|
||||||
self.keychain.removeContent(for: Globals.pgpKeyPassphrase)
|
|
||||||
Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self, completion: nil)
|
Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController, UITa
|
||||||
@IBOutlet weak var tableView: UITableView!
|
@IBOutlet weak var tableView: UITableView!
|
||||||
|
|
||||||
private let passwordStore = PasswordStore.shared
|
private let passwordStore = PasswordStore.shared
|
||||||
|
private let keychain = AppKeychain.shared
|
||||||
|
|
||||||
private var searchActive = false
|
private var searchActive = false
|
||||||
private var passwordsTableEntries: [PasswordsTableEntry] = []
|
private var passwordsTableEntries: [PasswordsTableEntry] = []
|
||||||
|
|
@ -164,8 +165,6 @@ class CredentialProviderViewController: ASCredentialProviderViewController, UITa
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
// remove the wrong passphrase so that users could enter it next time
|
|
||||||
AppKeychain.shared.removeContent(for: Globals.pgpKeyPassphrase)
|
|
||||||
Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self, completion: nil)
|
Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -193,14 +192,14 @@ class CredentialProviderViewController: ASCredentialProviderViewController, UITa
|
||||||
sem.signal()
|
sem.signal()
|
||||||
}))
|
}))
|
||||||
alert.addTextField(configurationHandler: {(textField: UITextField!) in
|
alert.addTextField(configurationHandler: {(textField: UITextField!) in
|
||||||
textField.text = ""
|
textField.text = self.keychain.get(for: Globals.pgpKeyPassphrase) ?? ""
|
||||||
textField.isSecureTextEntry = true
|
textField.isSecureTextEntry = true
|
||||||
})
|
})
|
||||||
self.present(alert, animated: true, completion: nil)
|
self.present(alert, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
let _ = sem.wait(timeout: DispatchTime.distantFuture)
|
let _ = sem.wait(timeout: DispatchTime.distantFuture)
|
||||||
if SharedDefaults[.isRememberPGPPassphraseOn] {
|
if SharedDefaults[.isRememberPGPPassphraseOn] {
|
||||||
AppKeychain.shared.add(string: passphrase, for: Globals.pgpKeyPassphrase)
|
self.keychain.add(string: passphrase, for: Globals.pgpKeyPassphrase)
|
||||||
}
|
}
|
||||||
return passphrase
|
return passphrase
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -191,8 +191,6 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
// remove the wrong passphrase so that users could enter it next time
|
|
||||||
self.keychain.removeContent(for: Globals.pgpKeyPassphrase)
|
|
||||||
Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self, completion: nil)
|
Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -221,7 +219,7 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV
|
||||||
sem.signal()
|
sem.signal()
|
||||||
}))
|
}))
|
||||||
alert.addTextField(configurationHandler: {(textField: UITextField!) in
|
alert.addTextField(configurationHandler: {(textField: UITextField!) in
|
||||||
textField.text = ""
|
textField.text = self.keychain.get(for: Globals.pgpKeyPassphrase) ?? ""
|
||||||
textField.isSecureTextEntry = true
|
textField.isSecureTextEntry = true
|
||||||
})
|
})
|
||||||
self.present(alert, animated: true, completion: nil)
|
self.present(alert, animated: true, completion: nil)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ public class PGPAgent {
|
||||||
|
|
||||||
private let keyStore: KeyStore
|
private let keyStore: KeyStore
|
||||||
private var pgpInterface: PgpInterface?
|
private var pgpInterface: PgpInterface?
|
||||||
|
private var latestDecryptStatus: Bool = true
|
||||||
|
|
||||||
public init(keyStore: KeyStore = AppKeychain.shared) {
|
public init(keyStore: KeyStore = AppKeychain.shared) {
|
||||||
self.keyStore = keyStore
|
self.keyStore = keyStore
|
||||||
|
|
@ -38,9 +39,25 @@ public class PGPAgent {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func decrypt(encryptedData: Data, requestPGPKeyPassphrase: () -> String) throws -> Data? {
|
public func decrypt(encryptedData: Data, requestPGPKeyPassphrase: () -> String) throws -> Data? {
|
||||||
|
// Remember the previous status and set the current status
|
||||||
|
let previousDecryptStatus = self.latestDecryptStatus
|
||||||
|
self.latestDecryptStatus = false
|
||||||
|
// Init keys.
|
||||||
try checkAndInit()
|
try checkAndInit()
|
||||||
let passphrase = keyStore.get(for: Globals.pgpKeyPassphrase) ?? requestPGPKeyPassphrase()
|
// Get the PGP key passphrase.
|
||||||
return try pgpInterface!.decrypt(encryptedData: encryptedData, passphrase: passphrase)
|
var passphrase = ""
|
||||||
|
if previousDecryptStatus == false {
|
||||||
|
passphrase = requestPGPKeyPassphrase()
|
||||||
|
} else {
|
||||||
|
passphrase = keyStore.get(for: Globals.pgpKeyPassphrase) ?? requestPGPKeyPassphrase()
|
||||||
|
}
|
||||||
|
// Decrypt.
|
||||||
|
guard let result = try pgpInterface!.decrypt(encryptedData: encryptedData, passphrase: passphrase) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// The decryption step has succeed.
|
||||||
|
self.latestDecryptStatus = true
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
public func encrypt(plainData: Data) throws -> Data {
|
public func encrypt(plainData: Data) throws -> Data {
|
||||||
|
|
|
||||||
|
|
@ -112,23 +112,29 @@ class PGPAgentTest: XCTestCase {
|
||||||
func testNoDecryptionWithIncorrectPassphrase() throws {
|
func testNoDecryptionWithIncorrectPassphrase() throws {
|
||||||
try importKeys(RSA2048.publicKey, RSA2048.privateKey)
|
try importKeys(RSA2048.publicKey, RSA2048.privateKey)
|
||||||
|
|
||||||
var passphraseRequestCalled = false
|
var passphraseRequestCalledCount = 0
|
||||||
let provideCorrectPassphrase: () -> String = {
|
let provideCorrectPassphrase: () -> String = {
|
||||||
passphraseRequestCalled = true
|
passphraseRequestCalledCount = passphraseRequestCalledCount + 1
|
||||||
return requestPGPKeyPassphrase()
|
return requestPGPKeyPassphrase()
|
||||||
}
|
}
|
||||||
XCTAssertEqual(try basicEncryptDecrypt(using: pgpAgent, requestPassphrase: provideCorrectPassphrase), testData)
|
|
||||||
XCTAssert(passphraseRequestCalled)
|
|
||||||
|
|
||||||
passphraseRequestCalled = false
|
|
||||||
let provideIncorrectPassphrase: () -> String = {
|
let provideIncorrectPassphrase: () -> String = {
|
||||||
passphraseRequestCalled = true
|
passphraseRequestCalledCount = passphraseRequestCalledCount + 1
|
||||||
return "incorrect passphrase"
|
return "incorrect passphrase"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provide the correct passphrase.
|
||||||
|
XCTAssertEqual(try basicEncryptDecrypt(using: pgpAgent, requestPassphrase: provideCorrectPassphrase), testData)
|
||||||
|
XCTAssertEqual(passphraseRequestCalledCount, 1)
|
||||||
|
|
||||||
|
// Provide the wrong passphrase.
|
||||||
XCTAssertThrowsError(try basicEncryptDecrypt(using: pgpAgent, requestPassphrase: provideIncorrectPassphrase)) {
|
XCTAssertThrowsError(try basicEncryptDecrypt(using: pgpAgent, requestPassphrase: provideIncorrectPassphrase)) {
|
||||||
XCTAssert($0.localizedDescription.contains("openpgp: invalid data: private key checksum failure"))
|
XCTAssert($0.localizedDescription.contains("openpgp: invalid data: private key checksum failure"))
|
||||||
}
|
}
|
||||||
XCTAssert(passphraseRequestCalled)
|
XCTAssertEqual(passphraseRequestCalledCount, 2)
|
||||||
|
|
||||||
|
// Ask for the passphrase because the previous decryption has failed.
|
||||||
|
XCTAssertEqual(try basicEncryptDecrypt(using: pgpAgent, requestPassphrase: provideCorrectPassphrase), testData)
|
||||||
|
XCTAssertEqual(passphraseRequestCalledCount, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func importKeys(_ publicKey: String, _ privateKey: String) throws {
|
private func importKeys(_ publicKey: String, _ privateKey: String) throws {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue