From 5c416bfb21995de15004715bbb0c3c0195e95774 Mon Sep 17 00:00:00 2001 From: Lysann Tranvouez Date: Thu, 12 Mar 2026 09:31:37 +0100 Subject: [PATCH] test .gpg-id support mostly using mocks --- passKitTests/Crypto/PGPAgentTest.swift | 4 +- passKitTests/Models/PasswordStoreTest.swift | 119 +++++++++++++++++--- 2 files changed, 105 insertions(+), 18 deletions(-) diff --git a/passKitTests/Crypto/PGPAgentTest.swift b/passKitTests/Crypto/PGPAgentTest.swift index 531ecc1..267742c 100644 --- a/passKitTests/Crypto/PGPAgentTest.swift +++ b/passKitTests/Crypto/PGPAgentTest.swift @@ -184,8 +184,8 @@ final class PGPAgentTest: XCTestCase { try importKeys(RSA2048_RSA4096.publicKeys | ED25519.publicKey, RSA2048_RSA4096.privateKeys) try pgpAgent.initKeys() - XCTAssertEqual(try pgpAgent.getKeyIDs(type: .PUBLIC).map(\.localizedLowercase).sorted(), (RSA2048_RSA4096.longFingerprints + [ED25519.longFingerprint]).sorted()) - XCTAssertEqual(try pgpAgent.getKeyIDs(type: .PRIVATE).map(\.localizedLowercase).sorted(), RSA2048_RSA4096.longFingerprints.sorted()) + XCTAssertEqual(try pgpAgent.getKeyIDs(type: .PUBLIC).map { $0.lowercased() }.sorted(), (RSA2048_RSA4096.longFingerprints + [ED25519.longFingerprint]).sorted()) + XCTAssertEqual(try pgpAgent.getKeyIDs(type: .PRIVATE).map { $0.lowercased() }.sorted(), RSA2048_RSA4096.longFingerprints.sorted()) let encryptedData = try pgpAgent.encrypt(plainData: testData, keyIDs: RSA2048_RSA4096.fingerprints + [ED25519.fingerprint]) diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift index e02039e..e9b3c44 100644 --- a/passKitTests/Models/PasswordStoreTest.swift +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -27,6 +27,18 @@ final class PasswordStoreTest: XCTestCase { passwordStore = PasswordStore(url: localRepoURL, pgpAgent: pgpAgent) } + private func setUpMockedPGPInterface() -> MockPGPInterface { + let mockPGPInterface = MockPGPInterface() + keyStore = DictBasedKeychain() + pgpAgent = PGPAgent(keyStore: keyStore, pgpInterface: mockPGPInterface) + passwordStore = PasswordStore(url: localRepoURL, pgpAgent: pgpAgent) + + // Set pgpKeyPassphrase key so checkAndInit() doesn't re-init and overwrite our mock. + keyStore.add(string: "dummy", for: Globals.pgpKeyPassphrase) + + return mockPGPInterface + } + override func tearDown() { passwordStore.erase() passwordStore = nil @@ -322,31 +334,106 @@ final class PasswordStoreTest: XCTestCase { // MARK: - .gpg-id support - func testCloneAndDecryptMultiKeys() throws { + func testReadGPGIDFile() throws { try cloneRepository(.withGPGID) - try importMultiplePGPKeys() - Defaults.isEnableGPGIDOn = true [ - ("work/github.com", ["4712286271220DB299883EA7062E678DA1024DAE"]), - ("personal/github.com", ["787EAE1A5FA3E749AA34CC6AA0645EBED862027E"]), - ("shared/github.com", ["4712286271220DB299883EA7062E678DA1024DAE", "787EAE1A5FA3E749AA34CC6AA0645EBED862027E"]), - ].forEach { path, keyIDs in + ("", [RSA4096.longFingerprint]), + ("family", [String(NISTP384.longFingerprint.suffix(16))]), + ("personal", [RSA4096.longFingerprint]), + ("shared", [RSA2048.longFingerprint, RSA4096.longFingerprint]), + ("work", [RSA2048.longFingerprint]), + ].forEach { path, expectedKeyIDs in let foundKeyIDs = findGPGIDs(from: localRepoURL.appendingPathComponent(path)) - XCTAssertEqual(foundKeyIDs, keyIDs) + XCTAssertEqual(foundKeyIDs, expectedKeyIDs.map { $0.uppercased() }) } + } - let personal = try decrypt(path: "personal/github.com.gpg") - XCTAssertEqual(personal.plainText, "passwordforpersonal\n") - - let work = try decrypt(path: "work/github.com.gpg") - XCTAssertEqual(work.plainText, "passwordforwork\n") + func testAddPasswordInRoot_WithSingleEntryInPGPIDFile_EncryptsWithThatKey() throws { + let mockPGPInterface = setUpMockedPGPInterface() + mockPGPInterface.publicKeyIDs = Set(RSA2048_RSA4096.fingerprints) + try cloneRepository(.withGPGID) + Defaults.isEnableGPGIDOn = true let testPassword = Password(name: "test", path: "test.gpg", plainText: "testpassword") - let testPasswordEntity = try passwordStore.add(password: testPassword)! - let testPasswordPlain = try passwordStore.decrypt(passwordEntity: testPasswordEntity, requestPGPKeyPassphrase: requestPGPKeyPassphrase) - XCTAssertEqual(testPasswordPlain.plainText, "testpassword") + _ = try passwordStore.add(password: testPassword) + + XCTAssertEqual(mockPGPInterface.encryptMultiKeyCalls.count, 1) + let encryptCall = mockPGPInterface.encryptMultiKeyCalls.first + XCTAssertEqual(encryptCall?.plainData, testPassword.plainData) + XCTAssertEqual(encryptCall?.keyIDs, [RSA4096.longFingerprint].map { $0.uppercased() }) + } + + func testEncryptWithSingleKeyViaGPGIDFileInSubDirectory() throws { + let mockPGPInterface = setUpMockedPGPInterface() + mockPGPInterface.publicKeyIDs = Set(RSA2048_RSA4096.fingerprints) + try cloneRepository(.withGPGID) + Defaults.isEnableGPGIDOn = true + + let testPassword = Password(name: "test", path: "family/test.gpg", plainText: "testpassword") + _ = try passwordStore.add(password: testPassword) + + XCTAssertEqual(mockPGPInterface.encryptMultiKeyCalls.count, 1) + let encryptCall = mockPGPInterface.encryptMultiKeyCalls.first + XCTAssertEqual(encryptCall?.plainData, testPassword.plainData) + XCTAssertEqual(encryptCall?.keyIDs, [String(NISTP384.longFingerprint.suffix(16))].map { $0.uppercased() }) + } + + func testEncryptWithSingleKeyViaGPGIDFileInParentDir() throws { + let mockPGPInterface = setUpMockedPGPInterface() + mockPGPInterface.publicKeyIDs = Set(RSA2048_RSA4096.fingerprints) + try cloneRepository(.withGPGID) + Defaults.isEnableGPGIDOn = true + + // /personal doesn't have its own .gpg-id file, but should inherit from the root .gpg-id file + let testPassword = Password(name: "test", path: "personal/test.gpg", plainText: "testpassword") + _ = try passwordStore.add(password: testPassword) + + XCTAssertEqual(mockPGPInterface.encryptMultiKeyCalls.count, 1) + let encryptCall = mockPGPInterface.encryptMultiKeyCalls.first + XCTAssertEqual(encryptCall?.plainData, testPassword.plainData) + XCTAssertEqual(encryptCall?.keyIDs, [RSA4096.longFingerprint].map { $0.uppercased() }) + } + + func testEncryptWithMultipleKeysViaGPGIDFile() throws { + let mockPGPInterface = setUpMockedPGPInterface() + mockPGPInterface.publicKeyIDs = Set(RSA2048_RSA4096.fingerprints) + try cloneRepository(.withGPGID) + Defaults.isEnableGPGIDOn = true + + // /shared uses both RSA2048 and RSA4096 + let testPassword = Password(name: "test", path: "shared/test.gpg", plainText: "testpassword") + _ = try passwordStore.add(password: testPassword) + + XCTAssertEqual(mockPGPInterface.encryptMultiKeyCalls.count, 1) + let encryptCall = mockPGPInterface.encryptMultiKeyCalls.first + XCTAssertEqual(encryptCall?.plainData, testPassword.plainData) + XCTAssertEqual(encryptCall?.keyIDs, RSA2048_RSA4096.longFingerprints.map { $0.uppercased() }) + } + + func testEncryptWithSingleKeyViaGPGFile_MissingKey() throws { + try cloneRepository(.withGPGID) + try importSinglePGPKey() // Only import RSA4096, but not RSA2048 + Defaults.isEnableGPGIDOn = true + + // /work uses RSA2048, but we didn't import that one + let testPassword = Password(name: "test", path: "work/test.gpg", plainText: "testpassword") + XCTAssertThrowsError(try passwordStore.add(password: testPassword)) { + XCTAssertEqual($0 as? AppError, .pgpPublicKeyNotFound(keyID: RSA2048.longFingerprint.uppercased())) + } + } + + func testEncryptWithMultipleKeysViaGPGFile_MissingKey() throws { + try cloneRepository(.withGPGID) + try importSinglePGPKey() // Only import RSA4096, but not RSA2048 + Defaults.isEnableGPGIDOn = true + + // /shared uses both RSA2048 and RSA4096, but we only imported RSA4096, so encryption should fail since one of the keys is missing + let testPassword = Password(name: "test", path: "shared/test.gpg", plainText: "testpassword") + XCTAssertThrowsError(try passwordStore.add(password: testPassword)) { + XCTAssertEqual($0 as? AppError, .pgpPublicKeyNotFound(keyID: RSA2048.longFingerprint.uppercased())) + } } // MARK: - Helpers