2020-04-11 23:23:38 -07:00
|
|
|
//
|
|
|
|
|
// PasswordStoreTest.swift
|
|
|
|
|
// passKitTests
|
|
|
|
|
//
|
2021-09-03 02:50:40 +02:00
|
|
|
// Created by Mingshen Sun on 13/4/2020.
|
2020-04-11 23:23:38 -07:00
|
|
|
// Copyright © 2020 Bob Sun. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
import ObjectiveGit
|
2020-06-28 21:25:40 +02:00
|
|
|
import XCTest
|
2020-04-11 23:23:38 -07:00
|
|
|
|
|
|
|
|
@testable import passKit
|
|
|
|
|
|
2024-11-24 13:14:38 +01:00
|
|
|
final class PasswordStoreTest: XCTestCase {
|
2026-03-09 00:17:31 +01:00
|
|
|
private let localRepoURL: URL = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/")
|
|
|
|
|
|
|
|
|
|
private var passwordStore: PasswordStore! = nil
|
|
|
|
|
|
|
|
|
|
override func setUp() {
|
|
|
|
|
passwordStore = PasswordStore(url: localRepoURL)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override func tearDown() {
|
|
|
|
|
passwordStore.erase()
|
|
|
|
|
passwordStore = nil
|
2026-03-09 10:57:39 +01:00
|
|
|
|
|
|
|
|
Defaults.removeAll()
|
2026-03-09 00:17:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testInitPasswordEntityCoreData() throws {
|
2026-03-09 10:57:39 +01:00
|
|
|
try cloneRepository(.withGPGID)
|
2026-03-09 00:17:31 +01:00
|
|
|
|
|
|
|
|
XCTAssertEqual(passwordStore.numberOfPasswords, 4)
|
|
|
|
|
|
|
|
|
|
let entity = passwordStore.fetchPasswordEntity(with: "personal/github.com.gpg")
|
|
|
|
|
XCTAssertEqual(entity!.path, "personal/github.com.gpg")
|
|
|
|
|
XCTAssertEqual(entity!.name, "github.com")
|
|
|
|
|
XCTAssertTrue(entity!.isSynced)
|
|
|
|
|
XCTAssertEqual(entity!.parent!.name, "personal")
|
|
|
|
|
|
|
|
|
|
XCTAssertNotNil(passwordStore.fetchPasswordEntity(with: "family/amazon.com.gpg"))
|
|
|
|
|
XCTAssertNotNil(passwordStore.fetchPasswordEntity(with: "work/github.com.gpg"))
|
|
|
|
|
XCTAssertNotNil(passwordStore.fetchPasswordEntity(with: "shared/github.com.gpg"))
|
|
|
|
|
|
|
|
|
|
let dirEntity = passwordStore.fetchPasswordEntity(with: "shared")
|
|
|
|
|
XCTAssertNotNil(dirEntity)
|
|
|
|
|
XCTAssertTrue(dirEntity!.isDir)
|
|
|
|
|
XCTAssertEqual(dirEntity!.name, "shared")
|
|
|
|
|
XCTAssertEqual(dirEntity!.children.count, 1)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 00:34:34 +01:00
|
|
|
func testEraseStoreData() throws {
|
2026-03-09 10:57:39 +01:00
|
|
|
try cloneRepository(.withGPGID)
|
2026-03-09 00:34:34 +01:00
|
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: localRepoURL.path))
|
|
|
|
|
XCTAssertGreaterThan(passwordStore.numberOfPasswords, 0)
|
|
|
|
|
XCTAssertNotNil(passwordStore.gitRepository)
|
|
|
|
|
|
|
|
|
|
passwordStore.eraseStoreData()
|
|
|
|
|
|
|
|
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: localRepoURL.path))
|
|
|
|
|
XCTAssertEqual(passwordStore.numberOfPasswords, 0)
|
|
|
|
|
XCTAssertNil(passwordStore.gitRepository)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testErase() throws {
|
2026-03-09 10:57:39 +01:00
|
|
|
try cloneRepository(.withGPGID)
|
|
|
|
|
try importSinglePGPKey()
|
2026-03-09 00:34:34 +01:00
|
|
|
Defaults.gitSignatureName = "Test User"
|
|
|
|
|
PasscodeLock.shared.save(passcode: "1234")
|
|
|
|
|
|
|
|
|
|
XCTAssertGreaterThan(passwordStore.numberOfPasswords, 0)
|
|
|
|
|
XCTAssertTrue(AppKeychain.shared.contains(key: PGPKey.PUBLIC.getKeychainKey()))
|
|
|
|
|
XCTAssertEqual(Defaults.gitSignatureName, "Test User")
|
|
|
|
|
XCTAssertTrue(PasscodeLock.shared.hasPasscode)
|
|
|
|
|
XCTAssertTrue(PGPAgent.shared.isInitialized())
|
|
|
|
|
|
|
|
|
|
passwordStore.erase()
|
|
|
|
|
|
|
|
|
|
XCTAssertEqual(passwordStore.numberOfPasswords, 0)
|
|
|
|
|
XCTAssertFalse(AppKeychain.shared.contains(key: PGPKey.PUBLIC.getKeychainKey()))
|
|
|
|
|
XCTAssertFalse(Defaults.hasKey(\.gitSignatureName))
|
|
|
|
|
XCTAssertFalse(PasscodeLock.shared.hasPasscode)
|
|
|
|
|
XCTAssertFalse(PGPAgent.shared.isInitialized())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testFetchPasswordEntityCoreDataByParent() throws {
|
2026-03-09 10:57:39 +01:00
|
|
|
try cloneRepository(.withGPGID)
|
2026-03-09 00:34:34 +01:00
|
|
|
|
|
|
|
|
let rootChildren = passwordStore.fetchPasswordEntityCoreData(parent: nil)
|
|
|
|
|
XCTAssertGreaterThan(rootChildren.count, 0)
|
|
|
|
|
rootChildren.forEach { entity in
|
|
|
|
|
XCTAssertTrue(entity.isDir)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let personalDir = passwordStore.fetchPasswordEntity(with: "personal")
|
|
|
|
|
let personalChildren = passwordStore.fetchPasswordEntityCoreData(parent: personalDir)
|
|
|
|
|
XCTAssertEqual(personalChildren.count, 1)
|
|
|
|
|
XCTAssertEqual(personalChildren.first?.name, "github.com")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testFetchPasswordEntityCoreDataWithDir() throws {
|
2026-03-09 10:57:39 +01:00
|
|
|
try cloneRepository(.withGPGID)
|
2026-03-09 00:34:34 +01:00
|
|
|
|
|
|
|
|
let allPasswords = passwordStore.fetchPasswordEntityCoreData(withDir: false)
|
|
|
|
|
XCTAssertEqual(allPasswords.count, 4)
|
|
|
|
|
allPasswords.forEach { entity in
|
|
|
|
|
XCTAssertFalse(entity.isDir)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 10:57:39 +01:00
|
|
|
func testEncryptSaveDecryptMultiline() throws {
|
|
|
|
|
try cloneRepository(.empty)
|
|
|
|
|
try importSinglePGPKey()
|
|
|
|
|
|
|
|
|
|
let password = Password(name: "test", path: "test.gpg", plainText: "foobar\nwith\nmultiple\nlines")
|
|
|
|
|
_ = try passwordStore.add(password: password)
|
|
|
|
|
let decryptedPassword = try decrypt(path: "test.gpg")
|
|
|
|
|
XCTAssertEqual(decryptedPassword.plainText, "foobar\nwith\nmultiple\nlines")
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-13 01:30:00 -07:00
|
|
|
func testCloneAndDecryptMultiKeys() throws {
|
2026-03-09 10:57:39 +01:00
|
|
|
try cloneRepository(.withGPGID)
|
|
|
|
|
try importMultiplePGPKeys()
|
2025-01-25 15:40:12 -08:00
|
|
|
|
2021-01-10 20:28:20 -08:00
|
|
|
Defaults.isEnableGPGIDOn = true
|
2020-04-13 01:30:00 -07:00
|
|
|
|
|
|
|
|
[
|
|
|
|
|
("work/github.com", "4712286271220DB299883EA7062E678DA1024DAE"),
|
2020-06-28 21:25:40 +02:00
|
|
|
("personal/github.com", "787EAE1A5FA3E749AA34CC6AA0645EBED862027E"),
|
|
|
|
|
].forEach { path, id in
|
2026-03-09 00:17:31 +01:00
|
|
|
let keyID = findGPGID(from: localRepoURL.appendingPathComponent(path))
|
2020-04-13 01:30:00 -07:00
|
|
|
XCTAssertEqual(keyID, id)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 00:34:34 +01:00
|
|
|
let personal = try decrypt(path: "personal/github.com.gpg")
|
2020-04-13 01:30:00 -07:00
|
|
|
XCTAssertEqual(personal.plainText, "passwordforpersonal\n")
|
|
|
|
|
|
2026-03-09 00:34:34 +01:00
|
|
|
let work = try decrypt(path: "work/github.com.gpg")
|
2020-04-13 01:30:00 -07:00
|
|
|
XCTAssertEqual(work.plainText, "passwordforwork\n")
|
|
|
|
|
|
2025-01-25 15:40:12 -08:00
|
|
|
let testPassword = Password(name: "test", path: "test.gpg", plainText: "testpassword")
|
2020-04-13 10:25:01 -07:00
|
|
|
let testPasswordEntity = try passwordStore.add(password: testPassword)!
|
2020-06-28 21:25:40 +02:00
|
|
|
let testPasswordPlain = try passwordStore.decrypt(passwordEntity: testPasswordEntity, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
|
2020-04-13 10:25:01 -07:00
|
|
|
XCTAssertEqual(testPasswordPlain.plainText, "testpassword")
|
2020-04-12 19:32:58 -07:00
|
|
|
}
|
2020-04-13 01:30:00 -07:00
|
|
|
|
2026-03-09 00:34:34 +01:00
|
|
|
// MARK: - Helpers
|
|
|
|
|
|
2026-03-09 10:57:39 +01:00
|
|
|
private enum RemoteRepo {
|
|
|
|
|
case empty
|
|
|
|
|
case withGPGID
|
|
|
|
|
|
|
|
|
|
var url: URL {
|
|
|
|
|
switch self {
|
|
|
|
|
case .empty:
|
|
|
|
|
Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store-empty.git")
|
|
|
|
|
case .withGPGID:
|
|
|
|
|
Bundle(for: PasswordStoreTest.self).resourceURL!.appendingPathComponent("Fixtures/password-store-with-gpgid.git")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var branchName: String {
|
|
|
|
|
switch self {
|
|
|
|
|
case .empty:
|
|
|
|
|
"main"
|
|
|
|
|
case .withGPGID:
|
|
|
|
|
"master"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func cloneRepository(_ remote: RemoteRepo) throws {
|
|
|
|
|
try passwordStore.cloneRepository(remoteRepoURL: remote.url, branchName: remote.branchName)
|
2026-03-09 00:17:31 +01:00
|
|
|
expectation(for: NSPredicate { _, _ in FileManager.default.fileExists(atPath: self.localRepoURL.path) }, evaluatedWith: nil)
|
|
|
|
|
waitForExpectations(timeout: 3, handler: nil)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 10:57:39 +01:00
|
|
|
private func importSinglePGPKey() throws {
|
|
|
|
|
let keychain = AppKeychain.shared
|
|
|
|
|
try KeyFileManager(keyType: PGPKey.PUBLIC, keyPath: "", keyHandler: keychain.add).importKey(from: RSA4096.publicKey)
|
|
|
|
|
try KeyFileManager(keyType: PGPKey.PRIVATE, keyPath: "", keyHandler: keychain.add).importKey(from: RSA4096.privateKey)
|
|
|
|
|
try PGPAgent.shared.initKeys()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func importMultiplePGPKeys() throws {
|
2026-03-09 00:34:34 +01:00
|
|
|
let keychain = AppKeychain.shared
|
|
|
|
|
try KeyFileManager(keyType: PGPKey.PUBLIC, keyPath: "", keyHandler: keychain.add).importKey(from: RSA2048_RSA4096.publicKeys)
|
|
|
|
|
try KeyFileManager(keyType: PGPKey.PRIVATE, keyPath: "", keyHandler: keychain.add).importKey(from: RSA2048_RSA4096.privateKeys)
|
|
|
|
|
try PGPAgent.shared.initKeys()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 10:57:39 +01:00
|
|
|
private func decrypt(path: String, keyID: String? = nil) throws -> Password {
|
2025-02-08 14:26:45 -08:00
|
|
|
let entity = passwordStore.fetchPasswordEntity(with: path)!
|
2026-03-09 10:57:39 +01:00
|
|
|
return try passwordStore.decrypt(passwordEntity: entity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
|
2020-04-13 01:30:00 -07:00
|
|
|
}
|
2020-04-11 23:23:38 -07:00
|
|
|
}
|