Files
passforios-mirror/pass/Controllers/SettingsTableViewController.swift
Mingshen SunandClaude Opus 5 60ec70ffce handle malformed PGP keys without crashing (fix #715)
ObjectivePGP raises an Objective-C exception when reading an armored block
without a body. Swift cannot catch it, so the app terminated as soon as a key
ID was requested, e.g. when opening the settings screen. As the keys live in the
keychain, they survive reinstalling the app and the crash persisted.

Route the calls into ObjectivePGP through an Objective-C exception catcher which
turns exceptions into regular errors. Additionally, offer the removal of PGP
keys whenever keys are stored, not only when a key source is set. The key source
is gone after a reinstall while the keys are not, which left no way to get rid
of unusable keys.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:25:23 -07:00

314 lines
13 KiB
Swift

//
// SettingsTableViewController.swift
// pass
//
// Created by Mingshen Sun on 18/1/2017.
// Copyright © 2017 Bob Sun. All rights reserved.
//
import CoreData
import passKit
import SVProgressHUD
import UIKit
import YubiKit
class SettingsTableViewController: UITableViewController, UITabBarControllerDelegate {
@IBOutlet var pgpKeyTableViewCell: UITableViewCell!
@IBOutlet var passcodeTableViewCell: UITableViewCell!
@IBOutlet var passwordRepositoryTableViewCell: UITableViewCell!
var setPasscodeLockAlert: UIAlertController?
let keychain = AppKeychain.shared
var passcodeLock = PasscodeLock.shared
func tabBarController(_: UITabBarController, didSelect _: UIViewController) {
navigationController?.popViewController(animated: true)
}
@IBAction
private func savePGPKey(segue: UIStoryboardSegue) {
guard let sourceController = segue.source as? PGPKeyImporter, sourceController.isReadyToUse() else {
return
}
savePGPKey(using: sourceController)
}
private func savePGPKey(using keyImporter: PGPKeyImporter) {
SVProgressHUD.setDefaultMaskType(.black)
SVProgressHUD.setDefaultStyle(.light)
SVProgressHUD.show(withStatus: "FetchingPgpKey".localize())
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
Defaults.pgpKeySource = type(of: keyImporter).keySource
do {
// Remove exiting passphrase
AppKeychain.shared.removeAllContent(withPrefix: Globals.pgpKeyPassphrase)
try keyImporter.importKeys()
try PGPAgent.shared.initKeys()
DispatchQueue.main.async {
self.setPGPKeyTableViewCellDetailText()
SVProgressHUD.showSuccess(withStatus: "Success".localize())
SVProgressHUD.dismiss(withDelay: 1)
keyImporter.doAfterImport()
}
} catch {
DispatchQueue.main.async {
self.pgpKeyTableViewCell.detailTextLabel?.text = "NotSet".localize()
Utils.alert(title: "Error".localize(), message: error.localizedDescription, controller: self, completion: nil)
}
}
}
}
@IBAction
private func saveGitServerSetting(segue _: UIStoryboardSegue) {
passwordRepositoryTableViewCell.detailTextLabel?.text = Defaults.gitURL.host
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(actOnPasswordStoreErasedNotification), name: .passwordStoreErased, object: nil)
passwordRepositoryTableViewCell.detailTextLabel?.text = Defaults.gitURL.host
setPGPKeyTableViewCellDetailText()
setPasscodeLockCell()
}
override func viewWillAppear(_: Bool) {
super.viewWillAppear(true)
tabBarController!.delegate = self
setPasswordRepositoryTableViewCellDetailText()
}
private func setPasscodeLockCell() {
if passcodeLock.hasPasscode {
passcodeTableViewCell.detailTextLabel?.text = "On".localize()
} else {
passcodeTableViewCell.detailTextLabel?.text = "Off".localize()
}
}
private func setPGPKeyTableViewCellDetailText() {
var label = "NotSet".localize()
let keyID = (try? PGPAgent.shared.getShortKeyID()) ?? []
if keyID.count == 1 {
label = keyID.first ?? ""
} else if keyID.count > 1 {
label = "Multiple"
}
if Defaults.isYubiKeyEnabled {
label += "+YubiKey"
}
pgpKeyTableViewCell.detailTextLabel?.text = label
}
private func setPasswordRepositoryTableViewCellDetailText() {
let host: String? = {
let gitURL = Defaults.gitURL
if gitURL.scheme == nil {
return URL(string: "scheme://" + gitURL.absoluteString)?.host
}
return gitURL.host
}()
passwordRepositoryTableViewCell.detailTextLabel?.text = host
}
@objc
func actOnPasswordStoreErasedNotification() {
setPGPKeyTableViewCellDetailText()
setPasswordRepositoryTableViewCellDetailText()
setPasscodeLockCell()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = super.tableView(tableView, cellForRowAt: indexPath)
cell.textLabel?.font = UIFont.preferredFont(forTextStyle: .body)
cell.detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .body)
cell.textLabel?.adjustsFontForContentSizeCategory = true
cell.detailTextLabel?.adjustsFontForContentSizeCategory = true
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
if cell == passcodeTableViewCell {
if passcodeLock.hasPasscode {
showPasscodeActionSheet()
} else {
setPasscodeLock()
}
} else if cell == pgpKeyTableViewCell {
showPGPKeyActionSheet()
}
tableView.deselectRow(at: indexPath, animated: true)
}
override func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat {
UITableView.automaticDimension
}
override func tableView(_: UITableView, estimatedHeightForRowAt _: IndexPath) -> CGFloat {
UITableView.automaticDimension
}
private var hasStoredPGPKeys: Bool {
keychain.contains(key: PGPKey.PUBLIC.getKeychainKey()) || keychain.contains(key: PGPKey.PRIVATE.getKeychainKey())
}
func showPGPKeyActionSheet() {
let optionMenu = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
optionMenu.addAction(
UIAlertAction(title: PGPKeyURLImportTableViewController.menuLabel, style: .default) { _ in
self.performSegue(withIdentifier: "setPGPKeyByURLSegue", sender: self)
}
)
optionMenu.addAction(
UIAlertAction(title: PGPKeyArmorImportTableViewController.menuLabel, style: .default) { _ in
self.performSegue(withIdentifier: "setPGPKeyByASCIISegue", sender: self)
}
)
optionMenu.addAction(
UIAlertAction(title: PGPKeyFileImportTableViewController.menuLabel, style: .default) { _ in
self.performSegue(withIdentifier: "setPGPKeyByFileSegue", sender: self)
}
)
if isReadyToUse() {
optionMenu.addAction(
UIAlertAction(title: "\(Self.menuLabel) (\("Import".localize()))", style: .default) { _ in
self.saveImportedKeys()
}
)
} else {
optionMenu.addAction(
UIAlertAction(title: "\(Self.menuLabel) (\("Tips".localize()))", style: .default) { _ in
let title = "Tips".localize()
let message = "PgpCopyPublicAndPrivateKeyToPass.".localize()
Utils.alert(title: title, message: message, controller: self)
}
)
}
if YubiKitDeviceCapabilities.supportsISO7816NFCTags {
optionMenu.addAction(
UIAlertAction(title: Defaults.isYubiKeyEnabled ? "✓ YubiKey" : "YubiKey", style: .default) { _ in
Defaults.isYubiKeyEnabled.toggle()
self.setPGPKeyTableViewCellDetailText()
}
)
}
// Offer the removal also when no key source is set but keys are still around. The key source
// is gone after reinstalling the app while the keys survive in the keychain. Without this,
// there would be no way to get rid of keys which cannot be used anymore.
if Defaults.pgpKeySource != nil || hasStoredPGPKeys {
optionMenu.addAction(
UIAlertAction(title: "RemovePgpKeys".localize(), style: .destructive) { _ in
let alert = UIAlertController.removeConfirmationAlert(title: "RemovePgpKeys".localize(), message: "") { _ in
self.keychain.removeContent(for: PGPKey.PUBLIC.getKeychainKey())
self.keychain.removeContent(for: PGPKey.PRIVATE.getKeychainKey())
self.keychain.removeAllContent(withPrefix: Globals.pgpKeyPassphrase)
PGPAgent.shared.uninitKeys()
self.pgpKeyTableViewCell.detailTextLabel?.text = "NotSet".localize()
Defaults.pgpKeySource = nil
}
self.present(alert, animated: true, completion: nil)
}
)
}
optionMenu.addAction(UIAlertAction.cancel())
optionMenu.popoverPresentationController?.sourceView = pgpKeyTableViewCell
optionMenu.popoverPresentationController?.sourceRect = pgpKeyTableViewCell.bounds
present(optionMenu, animated: true)
}
func showPasscodeActionSheet() {
let optionMenu = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let passcodeRemoveViewController = PasscodeLockViewController()
let removePasscodeAction = UIAlertAction(title: "RemovePasscode".localize(), style: .destructive) { [weak self] _ in
passcodeRemoveViewController.successCallback = {
self?.passcodeLock.delete()
self?.setPasscodeLockCell()
}
self?.present(passcodeRemoveViewController, animated: true, completion: nil)
}
let changePasscodeAction = UIAlertAction(title: "ChangePasscode".localize(), style: .default) { [weak self] _ in
self?.setPasscodeLock()
}
optionMenu.addAction(removePasscodeAction)
optionMenu.addAction(changePasscodeAction)
optionMenu.addAction(UIAlertAction.cancel())
optionMenu.popoverPresentationController?.sourceView = passcodeTableViewCell
optionMenu.popoverPresentationController?.sourceRect = passcodeTableViewCell.bounds
present(optionMenu, animated: true, completion: nil)
}
@objc
func alertTextFieldDidChange(_ sender: UITextField) {
// check whether we should enable the Save button in setPasscodeLockAlert
if let setPasscodeLockAlert,
let setPasscodeLockAlertTextFields0 = setPasscodeLockAlert.textFields?[0],
let setPasscodeLockAlertTextFields1 = setPasscodeLockAlert.textFields?[1] {
if sender == setPasscodeLockAlertTextFields0 || sender == setPasscodeLockAlertTextFields1 {
// two passwords should be the same, and length >= 4
let passcodeText = setPasscodeLockAlertTextFields0.text!
let passcodeConfirmationText = setPasscodeLockAlertTextFields1.text!
setPasscodeLockAlert.actions[0].isEnabled = passcodeText == passcodeConfirmationText && passcodeText.count >= 4
}
}
}
func setPasscodeLock() {
// prepare the alert for setting the passcode
setPasscodeLockAlert = UIAlertController(title: "SetPasscode".localize(), message: "FillInAppPasscode.".localize(), preferredStyle: .alert)
setPasscodeLockAlert?.addTextField { textField in
textField.placeholder = "Passcode".localize()
textField.isSecureTextEntry = true
textField.addTarget(self, action: #selector(self.alertTextFieldDidChange), for: UIControl.Event.editingChanged)
}
setPasscodeLockAlert?.addTextField { textField in
textField.placeholder = "PasswordConfirmation".localize()
textField.isSecureTextEntry = true
textField.addTarget(self, action: #selector(self.alertTextFieldDidChange), for: UIControl.Event.editingChanged)
}
// save action
let saveAction = UIAlertAction(title: "Save".localize(), style: .default) { (_: UIAlertAction) in
let passcode: String = self.setPasscodeLockAlert!.textFields![0].text!
self.passcodeLock.save(passcode: passcode)
// refresh the passcode lock cell ("On")
self.setPasscodeLockCell()
}
saveAction.isEnabled = false // disable the Save button by default
// cancel action
let cancelAction = UIAlertAction.cancel()
// present
setPasscodeLockAlert?.addAction(saveAction)
setPasscodeLockAlert?.addAction(cancelAction)
present(setPasscodeLockAlert!, animated: true, completion: nil)
}
}
extension SettingsTableViewController: PGPKeyImporter {
static let keySource = KeySource.itunes
static let label = "ITunesFileSharing".localize()
func isReadyToUse() -> Bool {
KeyFileManager.PublicPGP.doesKeyFileExist() && KeyFileManager.PrivatePGP.doesKeyFileExist()
}
func importKeys() throws {
try KeyFileManager.PublicPGP.importKeyFromFileSharing()
try KeyFileManager.PrivatePGP.importKeyFromFileSharing()
}
func saveImportedKeys() {
savePGPKey(using: self)
}
}