diff --git a/.travis.yml b/.travis.yml index 5c209f3..17bf69d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ addons: homebrew: packages: - go + - gnupg2 + - pass before_install: - echo -e "machine github.com\n login $GITHUB_ACCESS_TOKEN" >> ~/.netrc install: diff --git a/gopenpgp_build.sh b/gopenpgp_build.sh index ff62018..89d499c 100755 --- a/gopenpgp_build.sh +++ b/gopenpgp_build.sh @@ -6,21 +6,16 @@ export GOPATH="$(pwd)/go" export PATH="$PATH:$GOPATH/bin" go get -u golang.org/x/mobile/cmd/gomobile || true -go get golang.org/x/tools/go/packages || true -go install golang.org/x/mobile/cmd/gobind -go get golang.org/x/mobile || true +gomobile init go get -u github.com/ProtonMail/gopenpgp || true PACKAGE_PATH="github.com/ProtonMail/gopenpgp" -GOPENPGP_REVISION="136c0a54956e0241ff1b0c31aa34f2042588f843" +GOPENPGP_REVISION="v2.0.0" ( cd "$GOPATH/src/$PACKAGE_PATH" && git checkout "$GOPENPGP_REVISION" && GO111MODULE=on go mod vendor ) -patch -p0 < $GOPATH/crypto.patch +#patch -p0 < $GOPATH/crypto.patch OUTPUT_PATH="$GOPATH/dist" mkdir -p "$OUTPUT_PATH" - -chmod -R u+w "$GOPATH/pkg/mod" - "$GOPATH/bin/gomobile" bind -v -ldflags="-s -w" -target ios -o "${OUTPUT_PATH}/Crypto.framework" \ "$PACKAGE_PATH"/{crypto,armor,constants,models,subtle} diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index bb43fa6..11f34d3 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -97,6 +97,7 @@ 8BA607EB4C9C8258741AC18C /* Pods_passExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E955B67C88672AA3A40BA0 /* Pods_passExtension.framework */; }; 9A8A8387402FCCCECB1232A4 /* Pods_passKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B2B2F844061EFA534FE9506 /* Pods_passKitTests.framework */; }; 9AA710CA23939C68009E3213 /* GitCredentialPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AA710C923939C68009E3213 /* GitCredentialPassword.swift */; }; + 9ADC954124418A5F0005402E /* PasswordStoreTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADC954024418A5F0005402E /* PasswordStoreTest.swift */; }; A20691F41F2A3D0E0096483D /* SecurePasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */; }; A217ACE41E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift */; }; A2367B9C1EEFE2E500C8FE8B /* SwiftyUserDefaults.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCA049951E3357E000522E8F /* SwiftyUserDefaults.framework */; }; @@ -347,6 +348,7 @@ 64AA8DF9E73F39CCC3317247 /* Pods-passKit.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-passKit.release.xcconfig"; path = "Pods/Target Support Files/Pods-passKit/Pods-passKit.release.xcconfig"; sourceTree = ""; }; 7CAD21E487234A0631B52E20 /* Pods-passKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-passKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-passKit/Pods-passKit.debug.xcconfig"; sourceTree = ""; }; 9AA710C923939C68009E3213 /* GitCredentialPassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitCredentialPassword.swift; sourceTree = ""; }; + 9ADC954024418A5F0005402E /* PasswordStoreTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordStoreTest.swift; sourceTree = ""; }; A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecurePasteboard.swift; sourceTree = ""; }; A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = GitConfigSettingsTableViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; A2367B9F1EF0387000C8FE8B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -630,6 +632,7 @@ children = ( A2699ACE24027D9500F36323 /* PasswordTableEntryTest.swift */, 30B0485F209A5141001013CA /* PasswordTest.swift */, + 9ADC954024418A5F0005402E /* PasswordStoreTest.swift */, ); path = Models; sourceTree = ""; @@ -1483,6 +1486,7 @@ 30A1D2AC21B32C2A00E2D1F7 /* TokenBuilderTest.swift in Sources */, 30DAFD4C240985E3002456E7 /* Array+SlicesTest.swift in Sources */, 301F646D216166AA0071A4CE /* AdditionFieldTest.swift in Sources */, + 9ADC954124418A5F0005402E /* PasswordStoreTest.swift in Sources */, 30BAC8CB22E3BB6C00438475 /* DictBasedKeychain.swift in Sources */, A2699ACF24027D9500F36323 /* PasswordTableEntryTest.swift in Sources */, 30FD2F78214D9E0E005E0A92 /* ParserTest.swift in Sources */, diff --git a/pass/Controllers/SettingsTableViewController.swift b/pass/Controllers/SettingsTableViewController.swift index 62ef47b..6edff60 100644 --- a/pass/Controllers/SettingsTableViewController.swift +++ b/pass/Controllers/SettingsTableViewController.swift @@ -83,7 +83,7 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele } private func setPGPKeyTableViewCellDetailText() { - pgpKeyTableViewCell.detailTextLabel?.text = try? PGPAgent.shared.getKeyId() ?? "NotSet".localize() + pgpKeyTableViewCell.detailTextLabel?.text = try? PGPAgent.shared.getShortKeyId() ?? "NotSet".localize() } private func setPasswordRepositoryTableViewCellDetailText() { diff --git a/passKit/Crypto/GopenPgp.swift b/passKit/Crypto/GopenPgp.swift index a14fb6e..58b179c 100644 --- a/passKit/Crypto/GopenPgp.swift +++ b/passKit/Crypto/GopenPgp.swift @@ -11,42 +11,56 @@ import Crypto struct GopenPgp: PgpInterface { private static let errorMapping: [String: Error] = [ - "openpgp: invalid data: private key checksum failure": AppError.WrongPassphrase, + "gopenpgp: error in unlocking key: openpgp: invalid data: private key checksum failure": AppError.WrongPassphrase, "openpgp: incorrect key": AppError.KeyExpiredOrIncompatible, ] - private let publicKey: CryptoKeyRing - private let privateKey: CryptoKeyRing + private let publicKey: CryptoKey + private let privateKey: CryptoKey init(publicArmoredKey: String, privateArmoredKey: String) throws { var error: NSError? - guard let publicKey = CryptoBuildKeyRingArmored(publicArmoredKey, &error), - let privateKey = CryptoBuildKeyRingArmored(privateArmoredKey, &error) else { + guard let publicKey = CryptoNewKeyFromArmored(publicArmoredKey, &error), + let privateKey = CryptoNewKeyFromArmored(privateArmoredKey, &error) else { + guard error == nil else { + throw error! + } throw AppError.KeyImport } - guard error == nil else { - throw error! - } self.publicKey = publicKey self.privateKey = privateKey } - func decrypt(encryptedData: Data, passphrase: String) throws -> Data? { + func decrypt(encryptedData: Data, keyID: String, passphrase: String) throws -> Data? { do { - try privateKey.unlock(withPassphrase: passphrase) - } catch { - throw Self.errorMapping[error.localizedDescription, default: error] - } - let message = createPgpMessage(from: encryptedData) - do { - return try privateKey.decrypt(message, verifyKey: nil, verifyTime: 0).data + let unlockedKey = try privateKey.unlock(passphrase.data(using: .utf8)) + var error: NSError? + + guard let keyRing = CryptoNewKeyRing(unlockedKey, &error) else { + guard error == nil else { + throw error! + } + throw AppError.Decryption + } + + let message = createPgpMessage(from: encryptedData) + return try keyRing.decrypt(message, verifyKey: nil, verifyTime: 0).data } catch { throw Self.errorMapping[error.localizedDescription, default: error] } } - func encrypt(plainData: Data) throws -> Data { - let encryptedData = try publicKey.encrypt(CryptoNewPlainMessage(plainData.mutable as Data), privateKey: nil) + func encrypt(plainData: Data, keyID: String) throws -> Data { + var error: NSError? + + guard let keyRing = CryptoNewKeyRing(publicKey, &error) else { + guard error == nil else { + throw error! + } + throw AppError.Encryption + } + + let encryptedData = try keyRing.encrypt(CryptoNewPlainMessage(plainData.mutable as Data), privateKey: nil) if Defaults.encryptInArmored { var error: NSError? let armor = encryptedData.getArmored(&error) @@ -60,8 +74,14 @@ struct GopenPgp: PgpInterface { var keyId: String { var error: NSError? - let fingerprint = publicKey.getFingerprint(&error) - return error == nil ? String(fingerprint.suffix(8)).uppercased() : "" + let fingerprint = publicKey.getHexKeyID() + return String(fingerprint).uppercased() + } + + var shortKeyId: String { + var error: NSError? + let fingerprint = publicKey.getHexKeyID() + return String(fingerprint.suffix(8)).uppercased() } private func createPgpMessage(from encryptedData: Data) -> CryptoPGPMessage? { diff --git a/passKit/Crypto/ObjectivePgp.swift b/passKit/Crypto/ObjectivePgp.swift index b1ea12d..58795f9 100644 --- a/passKit/Crypto/ObjectivePgp.swift +++ b/passKit/Crypto/ObjectivePgp.swift @@ -30,11 +30,11 @@ struct ObjectivePgp: PgpInterface { self.privateKey = privateKey } - func decrypt(encryptedData: Data, passphrase: String) throws -> Data? { + func decrypt(encryptedData: Data, keyID: String, passphrase: String) throws -> Data? { return try ObjectivePGP.decrypt(encryptedData, andVerifySignature: false, using: keyring.keys) { _ in passphrase } } - func encrypt(plainData: Data) throws -> Data { + 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)! @@ -43,6 +43,10 @@ struct ObjectivePgp: PgpInterface { } var keyId: String { + return publicKey.keyID.longIdentifier + } + + var shortKeyId: String { return publicKey.keyID.shortIdentifier } } diff --git a/passKit/Crypto/PGPAgent.swift b/passKit/Crypto/PGPAgent.swift index 2696a39..38c180e 100644 --- a/passKit/Crypto/PGPAgent.swift +++ b/passKit/Crypto/PGPAgent.swift @@ -40,6 +40,11 @@ public class PGPAgent { return pgpInterface?.keyId } + public func getShortKeyId() throws -> String? { + try checkAndInit() + return pgpInterface?.shortKeyId + } + public func decrypt(encryptedData: Data, requestPGPKeyPassphrase: () -> String) throws -> Data? { // Remember the previous status and set the current status let previousDecryptStatus = self.latestDecryptStatus @@ -54,7 +59,7 @@ public class PGPAgent { passphrase = keyStore.get(for: Globals.pgpKeyPassphrase) ?? requestPGPKeyPassphrase() } // Decrypt. - guard let result = try pgpInterface!.decrypt(encryptedData: encryptedData, passphrase: passphrase) else { + guard let result = try pgpInterface!.decrypt(encryptedData: encryptedData, keyID: "", passphrase: passphrase) else { return nil } // The decryption step has succeed. @@ -67,7 +72,7 @@ public class PGPAgent { guard let pgpInterface = pgpInterface else { throw AppError.Encryption } - return try pgpInterface.encrypt(plainData: plainData) + return try pgpInterface.encrypt(plainData: plainData, keyID: "") } public var isPrepared: Bool { diff --git a/passKit/Crypto/PgpInterface.swift b/passKit/Crypto/PgpInterface.swift index ff25666..3a0567e 100644 --- a/passKit/Crypto/PgpInterface.swift +++ b/passKit/Crypto/PgpInterface.swift @@ -8,9 +8,11 @@ protocol PgpInterface { - func decrypt(encryptedData: Data, passphrase: String) throws -> Data? + func decrypt(encryptedData: Data, keyID: String, passphrase: String) throws -> Data? - func encrypt(plainData: Data) throws -> Data + func encrypt(plainData: Data, keyID: String) throws -> Data var keyId: String { get } + + var shortKeyId: String { get } } diff --git a/passKit/Models/PasswordStore.swift b/passKit/Models/PasswordStore.swift index 1d5237b..ad665cd 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -21,9 +21,12 @@ public class PasswordStore { dateFormatter.timeStyle = .short return dateFormatter }() - - public let storeURL = URL(fileURLWithPath: "\(Globals.repositoryPath)") - public let tempStoreURL = URL(fileURLWithPath: "\(Globals.repositoryPath)-temp") + public var storeURL: URL + public var tempStoreURL: URL { + get { + URL(fileURLWithPath: "\(storeURL.path)-temp") + } + } public var storeRepository: GTRepository? @@ -100,11 +103,13 @@ public class PasswordStore { public var numberOfCommits: UInt? { return storeRepository?.numberOfCommits(inCurrentBranch: nil) } - - private init() { + + init(url: URL = URL(fileURLWithPath: "\(Globals.repositoryPath)")) { + storeURL = url + // Migration importExistingKeysIntoKeychain() - + do { if fm.fileExists(atPath: storeURL.path) { try storeRepository = GTRepository.init(url: storeURL) @@ -175,13 +180,26 @@ public class PasswordStore { requestCredentialPassword: @escaping (GitCredential.Credential, String?) -> String?, transferProgressBlock: @escaping (UnsafePointer, UnsafeMutablePointer) -> Void, checkoutProgressBlock: @escaping (String, UInt, UInt) -> Void) throws { + do { + let credentialProvider = try credential.credentialProvider(requestCredentialPassword: requestCredentialPassword) + let options = [GTRepositoryCloneOptionsCredentialProvider: credentialProvider] + try self.cloneRepository(remoteRepoURL: remoteRepoURL, options: options, branchName: branchName, transferProgressBlock: transferProgressBlock, checkoutProgressBlock: checkoutProgressBlock) + } catch { + credential.delete() + throw(error) + } + } + + public func cloneRepository(remoteRepoURL: URL, + options: [AnyHashable : Any]? = nil, + branchName: String, + transferProgressBlock: @escaping (UnsafePointer, UnsafeMutablePointer) -> Void, + checkoutProgressBlock: @escaping (String, UInt, UInt) -> Void) throws { try? fm.removeItem(at: storeURL) try? fm.removeItem(at: tempStoreURL) self.gitPassword = nil self.gitSSHPrivateKeyPassphrase = nil do { - let credentialProvider = try credential.credentialProvider(requestCredentialPassword: requestCredentialPassword) - let options = [GTRepositoryCloneOptionsCredentialProvider: credentialProvider] storeRepository = try GTRepository.clone(from: remoteRepoURL, toWorkingDirectory: tempStoreURL, options: options, @@ -192,7 +210,6 @@ public class PasswordStore { try checkoutAndChangeBranch(withName: branchName, progressBlock: checkoutProgressBlock) } } catch { - credential.delete() Defaults.lastSyncedTime = nil DispatchQueue.main.async { self.deleteCoreData(entityName: "PasswordEntity") diff --git a/passKitTests/Crypto/CryptoFrameworkTest.swift b/passKitTests/Crypto/CryptoFrameworkTest.swift index c4e6388..b4da262 100644 --- a/passKitTests/Crypto/CryptoFrameworkTest.swift +++ b/passKitTests/Crypto/CryptoFrameworkTest.swift @@ -38,26 +38,27 @@ class CryptoFrameworkTest: XCTestCase { private func testInternal(plainMessage: CryptoPlainMessage?, messageConverter: MessageConverter) throws { try [ RSA2048, - RSA2048_SUB, + RSA4096, + //RSA2048_SUB, ED25519, - ED25519_SUB, + //ED25519_SUB, ].forEach { keyTriple in var error: NSError? - guard let publicKey = CryptoBuildKeyRingArmored(keyTriple.publicKey, &error), - let privateKey = CryptoBuildKeyRingArmored(keyTriple.privateKey, &error) else { + guard let publicKey = CryptoNewKeyFromArmored(keyTriple.publicKey, &error), + let privateKey = CryptoNewKeyFromArmored(keyTriple.privateKey, &error) else { XCTFail("Keys cannot be initialized.") return } XCTAssertNil(error) - XCTAssert(publicKey.getFingerprint(&error).hasSuffix(keyTriple.fingerprint)) + XCTAssert(publicKey.getHexKeyID().hasSuffix(keyTriple.fingerprint)) XCTAssertNil(error) - try privateKey.unlock(withPassphrase: keyTriple.passphrase) - let encryptedMessage = try publicKey.encrypt(plainMessage, privateKey: nil) - let decryptedData = try privateKey.decrypt(messageConverter(encryptedMessage, &error), verifyKey: nil, verifyTime: 0) + let unlockedKey = try privateKey.unlock(keyTriple.passphrase.data(using: .utf8)) + let encryptedMessage = try CryptoNewKeyRing(publicKey, &error)?.encrypt(plainMessage, privateKey: nil) + let decryptedData = try CryptoNewKeyRing(unlockedKey, &error)?.decrypt(messageConverter(encryptedMessage!, &error), verifyKey: nil, verifyTime: 0) XCTAssertNil(error) - XCTAssertEqual(testText, decryptedData.getString()) + XCTAssertEqual(testText, decryptedData!.getString()) } } } diff --git a/passKitTests/Crypto/PGPAgentTest.swift b/passKitTests/Crypto/PGPAgentTest.swift index 7e56e56..bb923b9 100644 --- a/passKitTests/Crypto/PGPAgentTest.swift +++ b/passKitTests/Crypto/PGPAgentTest.swift @@ -41,9 +41,10 @@ class PGPAgentTest: XCTestCase { func testBasicEncryptDecrypt() throws { try [ RSA2048, - RSA2048_SUB, + RSA4096, + //RSA2048_SUB, ED25519, - ED25519_SUB, + //ED25519_SUB, ].forEach { keyTriple in let keychain = DictBasedKeychain() let pgpAgent = PGPAgent(keyStore: keychain) @@ -75,7 +76,7 @@ class PGPAgentTest: XCTestCase { try importKeys(RSA2048.privateKey, RSA2048.publicKey) XCTAssert(pgpAgent.isPrepared) XCTAssertThrowsError(try basicEncryptDecrypt(using: pgpAgent)) { - XCTAssert($0.localizedDescription.contains("gopenpgp: cannot unlock key ring, no private key available")) + XCTAssert($0.localizedDescription.contains("gopenpgp: unable to add locked key to a keyring")) } } diff --git a/passKitTests/Models/PasswordStoreTest.swift b/passKitTests/Models/PasswordStoreTest.swift new file mode 100644 index 0000000..acf2e28 --- /dev/null +++ b/passKitTests/Models/PasswordStoreTest.swift @@ -0,0 +1,36 @@ +// +// PasswordStoreTest.swift +// passKitTests +// +// Copyright © 2020 Bob Sun. All rights reserved. +// + +import Foundation +import XCTest +import ObjectiveGit + +@testable import passKit + +class PasswordStoreTest: XCTestCase { +// let cloneOptions: [String : GTCredentialProvider] = { +// let credentialProvider = GTCredentialProvider { (_, _, _) -> (GTCredential?) in +// try? GTCredential(userName: "", password: "") +// } +// return [GTRepositoryCloneOptionsCredentialProvider: credentialProvider] +// }() +// let remoteRepoURL = URL(string: "git://localhost/")! +// +// func testClone() throws { +// let url = URL(fileURLWithPath: "\(Globals.repositoryPath)-test") +// let passwordStore = PasswordStore(url: url) +// +// try passwordStore.cloneRepository( +// remoteRepoURL: remoteRepoURL, +// options: cloneOptions, +// branchName: "master", +// transferProgressBlock: { _, _ in }, +// checkoutProgressBlock: { _, _, _ in } +// ) +// passwordStore.erase() +// } +} diff --git a/passKitTests/Testbase/TestPGPKeys.swift b/passKitTests/Testbase/TestPGPKeys.swift index aa53c79..2fcaa7f 100644 --- a/passKitTests/Testbase/TestPGPKeys.swift +++ b/passKitTests/Testbase/TestPGPKeys.swift @@ -30,6 +30,12 @@ let RSA2048_SUB = PGPKeyTestTriple( fingerprint: "a1024dae" ) +let RSA4096 = PGPKeyTestTriple( + publicKey: PGP_RSA4096_PUBLIC_KEY, + privateKey: PGP_RSA4096_PRIVATE_KEY, + fingerprint: "d862027e" +) + let ED25519 = PGPKeyTestTriple( publicKey: PGP_ED25519_PUBLIC_KEY, privateKey: PGP_ED25519_PRIVATE_KEY, @@ -189,6 +195,171 @@ XY0AlWAbvH1ytWboh+CgS493JfZbNRCXTWA/BDE= -----END PGP PRIVATE KEY BLOCK----- """ +let PGP_RSA4096_PUBLIC_KEY = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +ERIDrUdG +=N0dj +-----END PGP PUBLIC KEY BLOCK----- +""" + +let PGP_RSA4096_PRIVATE_KEY = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=Ql28 +-----END PGP PRIVATE KEY BLOCK----- +""" + let PGP_ED25519_PUBLIC_KEY = """ -----BEGIN PGP PUBLIC KEY BLOCK-----