Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions pass.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
301F646D216166AA0071A4CE /* AdditionFieldTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 301F646C216166AA0071A4CE /* AdditionFieldTest.swift */; };
302269B323E634B000F843A3 /* PGPKeyFIleImportTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302269B223E634B000F843A3 /* PGPKeyFIleImportTableViewController.swift */; };
302B2C9822C2BDE700D831EE /* AppKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302B2C9722C2BDE700D831EE /* AppKeychain.swift */; };
8A7150122F60000100C7A64D /* ObjectiveCExceptionCatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A7150102F60000100C7A64D /* ObjectiveCExceptionCatcher.h */; settings = {ATTRIBUTES = (Public, ); }; };
8A7150132F60000100C7A64D /* ObjectiveCExceptionCatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A7150112F60000100C7A64D /* ObjectiveCExceptionCatcher.m */; };
302E85612125ECC70031BA64 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302E85602125ECC70031BA64 /* Parser.swift */; };
302E85632125EE550031BA64 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302E85622125EE550031BA64 /* Constants.swift */; };
3032327422C7F710009EBD9C /* KeyFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3032327322C7F710009EBD9C /* KeyFileManager.swift */; };
Expand Down Expand Up @@ -343,6 +345,8 @@
302202EE222F14E400555236 /* SearchBarScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarScope.swift; sourceTree = "<group>"; };
302269B223E634B000F843A3 /* PGPKeyFIleImportTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PGPKeyFIleImportTableViewController.swift; sourceTree = "<group>"; };
302B2C9722C2BDE700D831EE /* AppKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppKeychain.swift; sourceTree = "<group>"; };
8A7150102F60000100C7A64D /* ObjectiveCExceptionCatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ObjectiveCExceptionCatcher.h; sourceTree = "<group>"; };
8A7150112F60000100C7A64D /* ObjectiveCExceptionCatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ObjectiveCExceptionCatcher.m; sourceTree = "<group>"; };
302E85602125ECC70031BA64 /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
302E85622125EE550031BA64 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
3032327322C7F710009EBD9C /* KeyFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyFileManager.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -976,6 +980,8 @@
30BAC8CC22E3BB9700438475 /* KeyStore.swift */,
30B331762704DBEE00D64A99 /* NotificationCenterDispatcher.swift */,
30697C2321F63C580064FCAC /* NotificationNames.swift */,
8A7150102F60000100C7A64D /* ObjectiveCExceptionCatcher.h */,
8A7150112F60000100C7A64D /* ObjectiveCExceptionCatcher.m */,
302202EE222F14E400555236 /* SearchBarScope.swift */,
30697C2721F63C590064FCAC /* Utils.swift */,
);
Expand Down Expand Up @@ -1132,6 +1138,7 @@
buildActionMask = 2147483647;
files = (
A260758A1EEC6F34005DB03E /* passKit.h in Headers */,
8A7150122F60000100C7A64D /* ObjectiveCExceptionCatcher.h in Headers */,
DCB0EC272846857E00EFEE10 /* Objective-CBridgingHeader.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -1603,6 +1610,7 @@
buildActionMask = 2147483647;
files = (
30A1D2A221B2BC6F00E2D1F7 /* TokenBuilder.swift in Sources */,
8A7150132F60000100C7A64D /* ObjectiveCExceptionCatcher.m in Sources */,
30697C3C21F63C990064FCAC /* UITextFieldExtension.swift in Sources */,
302E85632125EE550031BA64 /* Constants.swift in Sources */,
9A652414244BB33300DA0A41 /* UIAlertActionExtension.swift in Sources */,
Expand Down
10 changes: 9 additions & 1 deletion pass/Controllers/SettingsTableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele
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(
Expand Down Expand Up @@ -194,12 +198,16 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele
)
}

if Defaults.pgpKeySource != nil {
// 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
Expand Down
36 changes: 29 additions & 7 deletions passKit/Crypto/ObjectivePGPInterface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ struct ObjectivePGPInterface: PGPInterface {
guard let publicKeyData = publicArmoredKey.data(using: .ascii), let privateKeyData = privateArmoredKey.data(using: .ascii) else {
throw AppError.keyImport
}
let publicKeys = try ObjectivePGP.readKeys(from: publicKeyData)
let privateKeys = try ObjectivePGP.readKeys(from: privateKeyData)
let publicKeys = try catchingObjectiveCException(orThrow: .keyImport) { try ObjectivePGP.readKeys(from: publicKeyData) }
let privateKeys = try catchingObjectiveCException(orThrow: .keyImport) { try ObjectivePGP.readKeys(from: privateKeyData) }
keyring.import(keys: publicKeys)
keyring.import(keys: privateKeys)
guard publicKeys.first != nil, privateKeys.first != nil else {
Expand All @@ -25,15 +25,19 @@ struct ObjectivePGPInterface: PGPInterface {
}

func decrypt(encryptedData: Data, keyID _: String?, passphrase: String) throws -> Data? {
try ObjectivePGP.decrypt(encryptedData, andVerifySignature: false, using: keyring.keys) { _ in passphrase }
try catchingObjectiveCException(orThrow: .decryption) {
try ObjectivePGP.decrypt(encryptedData, andVerifySignature: false, using: keyring.keys) { _ in passphrase }
}
}

func encrypt(plainData: Data, keyID _: String?) throws -> Data {
let encryptedData = try ObjectivePGP.encrypt(plainData, addSignature: false, using: keyring.keys, passphraseForKey: nil)
if Defaults.encryptInArmored {
return Armor.armored(encryptedData, as: .message).data(using: .ascii)!
try catchingObjectiveCException(orThrow: .encryption) {
let encryptedData = try ObjectivePGP.encrypt(plainData, addSignature: false, using: keyring.keys, passphraseForKey: nil)
if Defaults.encryptInArmored {
return Armor.armored(encryptedData, as: .message).data(using: .ascii)!
}
return encryptedData
}
return encryptedData
}

func containsPublicKey(with keyID: String) -> Bool {
Expand All @@ -52,3 +56,21 @@ struct ObjectivePGPInterface: PGPInterface {
keyring.keys.map(\.keyID.shortIdentifier)
}
}

/// Runs a block which may raise an Objective-C exception, e.g. when ObjectivePGP is fed a malformed
/// key. Such an exception cannot be caught in Swift and terminates the app, so it is replaced by the
/// given error here.
private func catchingObjectiveCException<T>(orThrow appError: AppError, _ block: () throws -> T) throws -> T {
var result: Result<T, Error>?
do {
try ObjectiveCExceptionCatcher.catchException {
result = Result { try block() }
}
} catch {
throw appError
}
guard let result else {
throw appError
}
return try result.get()
}
23 changes: 23 additions & 0 deletions passKit/Helpers/ObjectiveCExceptionCatcher.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// ObjectiveCExceptionCatcher.h
// passKit
//
// Copyright 漏 2026 Bob Sun. All rights reserved.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

extern NSString *const ObjectiveCExceptionErrorDomain;

/// Runs a block and turns any Objective-C exception raised by it into an `NSError`. Swift cannot
/// catch `NSException`s, so calls into Objective-C libraries which raise them (like ObjectivePGP on
/// malformed input) have to be routed through this class to not terminate the app.
@interface ObjectiveCExceptionCatcher : NSObject

+ (BOOL)catchException:(NS_NOESCAPE void (^)(void))block error:(NSError **)error;

@end

NS_ASSUME_NONNULL_END
29 changes: 29 additions & 0 deletions passKit/Helpers/ObjectiveCExceptionCatcher.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// ObjectiveCExceptionCatcher.m
// passKit
//
// Copyright 漏 2026 Bob Sun. All rights reserved.
//

#import "ObjectiveCExceptionCatcher.h"

NSString *const ObjectiveCExceptionErrorDomain = @"ObjectiveCExceptionErrorDomain";

@implementation ObjectiveCExceptionCatcher

+ (BOOL)catchException:(NS_NOESCAPE void (^)(void))block error:(NSError **)error {
@try {
block();
return YES;
} @catch (NSException *exception) {
if (error != NULL) {
NSMutableDictionary<NSErrorUserInfoKey, id> *userInfo = [NSMutableDictionary dictionary];
userInfo[NSLocalizedDescriptionKey] = exception.reason ?: exception.name;
userInfo[NSDebugDescriptionErrorKey] = exception.description;
*error = [NSError errorWithDomain:ObjectiveCExceptionErrorDomain code:0 userInfo:userInfo];
}
return NO;
}
}

@end
2 changes: 2 additions & 0 deletions passKit/passKit.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

#import <UIKit/UIKit.h>

#import <passKit/ObjectiveCExceptionCatcher.h>

//! Project version number for passKit.
FOUNDATION_EXPORT double passKitVersionNumber;

Expand Down
31 changes: 31 additions & 0 deletions passKitTests/Crypto/PGPAgentTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,37 @@ final class PGPAgentTest: XCTestCase {
}
}

func testEmptyArmoredKey() throws {
// Reading an armored block without a body raises an Objective-C exception in ObjectivePGP.
// As it cannot be caught in Swift, it used to terminate the app as soon as a key ID was
// requested, e.g. when opening the settings screen.
try importKeys(
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n-----END PGP PUBLIC KEY BLOCK-----\n",
"-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n-----END PGP PRIVATE KEY BLOCK-----\n"
)
XCTAssert(pgpAgent.isPrepared)
XCTAssertThrowsError(try pgpAgent.initKeys()) {
XCTAssertEqual($0 as! AppError, AppError.keyImport)
}
XCTAssertThrowsError(try pgpAgent.getShortKeyID()) {
XCTAssertEqual($0 as! AppError, AppError.keyImport)
}
XCTAssertThrowsError(try basicEncryptDecrypt(using: pgpAgent, keyID: RSA2048.fingerprint)) {
XCTAssertEqual($0 as! AppError, AppError.keyImport)
}
}

func testTruncatedKey() throws {
try importKeys(
String(RSA2048.publicKey.prefix(300)) + "\n-----END PGP PUBLIC KEY BLOCK-----\n",
String(RSA2048.privateKey.prefix(300)) + "\n-----END PGP PRIVATE KEY BLOCK-----\n"
)
XCTAssert(pgpAgent.isPrepared)
XCTAssertThrowsError(try pgpAgent.getShortKeyID()) {
XCTAssert($0.localizedDescription.contains("Can't read keys. Invalid input."))
}
}

func testUnsetKeys() throws {
try importKeys(ED25519.publicKey, ED25519.privateKey)
XCTAssert(pgpAgent.isPrepared)
Expand Down
Loading