Skip to content

Commit 7afa750

Browse files
committed
feat: support age encryption
- Support decrypting / encrypting age secrets - Support generating age keys on device - Simplify copying age public keys from device, so age identities can be added as gopass recipeints with `gopass recipient add age1...`
1 parent 53ae642 commit 7afa750

35 files changed

+1371
-498
lines changed

Gemfile.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ GEM
3939
dotenv (2.8.1)
4040
emoji_regex (3.2.3)
4141
excon (0.112.0)
42-
faraday (1.10.5)
42+
faraday (1.10.4)
4343
faraday-em_http (~> 1.0)
4444
faraday-em_synchrony (~> 1.0)
4545
faraday-excon (~> 1.1)
@@ -55,11 +55,11 @@ GEM
5555
faraday (>= 0.8.0)
5656
http-cookie (~> 1.0.0)
5757
faraday-em_http (1.0.0)
58-
faraday-em_synchrony (1.0.1)
58+
faraday-em_synchrony (1.0.0)
5959
faraday-excon (1.1.0)
6060
faraday-httpclient (1.0.1)
61-
faraday-multipart (1.2.0)
62-
multipart-post (~> 2.0)
61+
faraday-multipart (1.0.4)
62+
multipart-post (~> 2)
6363
faraday-net_http (1.0.2)
6464
faraday-net_http_persistent (1.2.0)
6565
faraday-patron (1.0.0)

pass.xcodeproj/project.pbxproj

Lines changed: 77 additions & 26 deletions
Large diffs are not rendered by default.

pass.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 64 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pass/Base.lproj/Main.storyboard

Lines changed: 293 additions & 150 deletions
Large diffs are not rendered by default.

pass/Controllers/AddPasswordTableViewController.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ class AddPasswordTableViewController: PasswordEditorTableViewController {
2020
override func shouldPerformSegue(withIdentifier identifier: String, sender _: Any?) -> Bool {
2121
if identifier == "saveAddPasswordSegue" {
2222
// check PGP key
23-
guard PGPAgent.shared.isPrepared else {
23+
guard EncryptionManager.shared.isPrepared else {
2424
let alertTitle = "CannotAddPassword".localize()
25-
let alertMessage = "PgpKeyNotSet.".localize()
25+
let alertMessage = "EncryptionBackendNotConfigured.".localize()
2626
Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil)
2727
return false
2828
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import AgeKit
2+
import passKit
3+
import UIKit
4+
5+
class AgeKeyArmorImportTableViewController: AutoCellHeightUITableViewController, UITextViewDelegate {
6+
@IBOutlet var armorPrivateKeyTextView: UITextView!
7+
@IBOutlet var publicKeyLabel: UILabel!
8+
@IBOutlet var copyPublicKeyButton: UIButton!
9+
10+
private var armorPrivateKey: String?
11+
12+
override func viewDidLoad() {
13+
super.viewDidLoad()
14+
armorPrivateKeyTextView.delegate = self
15+
copyPublicKeyButton.setImage(UIImage(systemName: "doc.on.doc"), for: .normal)
16+
copyPublicKeyButton.isEnabled = false
17+
18+
// Pre-populate with existing private key from Keychain
19+
if let existingPrivateKey = AppKeychain.shared.get(for: AgeKey.PRIVATE.getKeychainKey()) {
20+
armorPrivateKeyTextView.text = existingPrivateKey
21+
updatePublicKey(from: existingPrivateKey)
22+
} else {
23+
publicKeyLabel.text = "PublicKeyAutomatic.".localize()
24+
}
25+
}
26+
27+
private func updatePublicKey(from privateKeyText: String?) {
28+
guard let privateKeyText = privateKeyText?.trimmed, !privateKeyText.isEmpty else {
29+
publicKeyLabel.text = "PublicKeyAutomatic.".localize()
30+
copyPublicKeyButton.isEnabled = false
31+
return
32+
}
33+
34+
do {
35+
let publicKey = try Age.derivePublicKey(from: privateKeyText)
36+
publicKeyLabel.text = publicKey
37+
copyPublicKeyButton.isEnabled = true
38+
} catch {
39+
publicKeyLabel.text = "PublicKeyAutomatic.".localize()
40+
copyPublicKeyButton.isEnabled = false
41+
}
42+
}
43+
44+
@IBAction
45+
private func copyPublicKey(_: Any) {
46+
guard copyPublicKeyButton.isEnabled, let publicKey = publicKeyLabel.text else {
47+
return
48+
}
49+
50+
SecurePasteboard.shared.copy(textToCopy: publicKey, expirationTime: 0)
51+
52+
let alert = UIAlertController(
53+
title: nil,
54+
message: "PublicKeyCopied.".localize(),
55+
preferredStyle: .alert
56+
)
57+
present(alert, animated: true)
58+
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
59+
alert.dismiss(animated: true)
60+
}
61+
}
62+
63+
@IBAction
64+
private func save(_: Any) {
65+
armorPrivateKey = armorPrivateKeyTextView.text
66+
saveImportedKeys()
67+
}
68+
69+
func textView(_: UITextView, shouldChangeTextIn _: NSRange, replacementText text: String) -> Bool {
70+
if text == UIPasteboard.general.string {
71+
// user pastes something, do the copy here again and clear the pasteboard in 45s
72+
SecurePasteboard.shared.copy(textToCopy: text)
73+
}
74+
return true
75+
}
76+
77+
func textViewDidChange(_ textView: UITextView) {
78+
updatePublicKey(from: textView.text)
79+
}
80+
}
81+
82+
extension AgeKeyArmorImportTableViewController: AgeKeyImporter {
83+
static let keySource = KeySource.ageArmor
84+
static let label = "AsciiArmorAgeKey".localize()
85+
86+
func isReadyToUse() -> Bool {
87+
let privateKey = armorPrivateKeyTextView.text.trimmed
88+
89+
// Check if private key is not empty
90+
guard !privateKey.isEmpty else {
91+
Utils.alert(title: "CannotSave".localize(), message: "SetAgePrivateKey.".localize(), controller: self, completion: nil)
92+
return false
93+
}
94+
95+
// Validate private key format
96+
guard privateKey.hasPrefix(Age.privateKeyPrefix) else {
97+
Utils.alert(
98+
title: "CannotSave".localize(),
99+
message: String(format: "AgeKeyInvalidPrefix.".localize(), Age.privateKeyPrefix),
100+
controller: self,
101+
completion: nil
102+
)
103+
return false
104+
}
105+
106+
// Validate that private key can be parsed and public key can be derived
107+
do {
108+
_ = try Age.derivePublicKey(from: privateKey)
109+
} catch {
110+
Utils.alert(
111+
title: "CannotSave".localize(),
112+
message: String(format: "AgeKeyInvalidFormat.".localize(), error.localizedDescription),
113+
controller: self,
114+
completion: nil
115+
)
116+
return false
117+
}
118+
119+
return true
120+
}
121+
122+
func importKeys() throws {
123+
// Only import private key - public key is derived
124+
try KeyFileManager.PrivateAge.importKey(from: armorPrivateKey ?? "")
125+
}
126+
127+
func saveImportedKeys() {
128+
performSegue(withIdentifier: "saveAgeKeySegue", sender: self)
129+
}
130+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import passKit
2+
3+
protocol AgeKeyImporter: KeyImporter {
4+
func doAfterImport()
5+
}
6+
7+
extension AgeKeyImporter {
8+
static var isCurrentKeySource: Bool {
9+
Defaults.ageKeySource == keySource
10+
}
11+
12+
func doAfterImport() {}
13+
}

pass/Controllers/GeneralSettingsTableViewController.swift

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,18 +81,28 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController {
8181
return uiSwitch
8282
}()
8383

84+
private lazy var encryptionBackendSegmentedControl: UISegmentedControl = {
85+
let segmentedControl = UISegmentedControl(items: ["GPG", "Age"])
86+
segmentedControl.sizeToFit()
87+
segmentedControl.addTarget(self, action: #selector(encryptionBackendSwitchAction), for: .valueChanged)
88+
return segmentedControl
89+
}()
90+
8491
override func viewDidLoad() {
8592
tableData = [
8693
// section 0
87-
[[.title: "AboutRepository".localize(), .action: "segue", .link: "showAboutRepositorySegue"]],
94+
[[.title: "Encryption", .action: "none"]],
8895

8996
// section 1
97+
[[.title: "AboutRepository".localize(), .action: "segue", .link: "showAboutRepositorySegue"]],
98+
99+
// section 2
90100
[
91101
[.title: "RememberPgpKeyPassphrase".localize(), .action: "none"],
92102
[.title: "RememberGitCredentialPassphrase".localize(), .action: "none"],
93103
],
94104

95-
// section 2
105+
// section 3
96106
[
97107
[.title: "EnableGPGID".localize(), .action: "none"],
98108
[.title: "ShowFolders".localize(), .action: "none"],
@@ -110,6 +120,10 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController {
110120
cell.accessoryType = .none
111121
cell.selectionStyle = .none
112122
switch cell.textLabel!.text! {
123+
case "Encryption":
124+
cell.accessoryView = encryptionBackendSegmentedControl
125+
let backend = Defaults.encryptionBackend
126+
encryptionBackendSegmentedControl.selectedSegmentIndex = backend == .gpg ? 0 : 1
113127
case "AboutRepository".localize():
114128
cell.accessoryType = .disclosureIndicator
115129
case "HideUnknownFields".localize():
@@ -138,6 +152,16 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController {
138152
return cell
139153
}
140154

155+
@objc
156+
func encryptionBackendSwitchAction(_ sender: UISegmentedControl) {
157+
if sender.selectedSegmentIndex == 0 {
158+
Defaults.encryptionBackend = .gpg
159+
} else {
160+
Defaults.encryptionBackend = .age
161+
}
162+
EncryptionManager.shared.uninitKeys()
163+
}
164+
141165
private func addDetailButton(to cell: UITableViewCell, for uiSwitch: UISwitch, with action: Selector) {
142166
let detailButton = UIButton(type: .detailDisclosure)
143167
uiSwitch.frame = CGRect(

pass/Controllers/GitRepositorySettingsTableViewController.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,8 @@ class GitRepositorySettingsTableViewController: UITableViewController, PasswordA
197197
)
198198

199199
let gpgIDFile = self.passwordStore.storeURL.appendingPathComponent(".gpg-id").path
200-
guard FileManager.default.fileExists(atPath: gpgIDFile) else {
200+
let ageRecipientsFile = self.passwordStore.storeURL.appendingPathComponent(".age-recipients").path
201+
guard FileManager.default.fileExists(atPath: gpgIDFile) || FileManager.default.fileExists(atPath: ageRecipientsFile) else {
201202
self.passwordStore.eraseStoreData()
202203
SVProgressHUD.dismiss {
203204
DispatchQueue.main.async {

pass/Controllers/PasswordDetailTableViewController.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
110110
}
111111
}
112112

113-
private func decryptThenShowPasswordLocalKey(keyID: String? = nil) {
113+
private func decryptThenShowPasswordLocalKey() {
114114
guard let passwordEntity else {
115115
Utils.alert(title: "CannotShowPassword".localize(), message: "PasswordDoesNotExist".localize(), controller: self, completion: {
116116
self.navigationController!.popViewController(animated: true)
@@ -121,15 +121,15 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
121121
// decrypt password
122122
do {
123123
let requestPGPKeyPassphrase = Utils.createRequestPGPKeyPassphraseHandler(controller: self)
124-
self.password = try self.passwordStore.decrypt(passwordEntity: passwordEntity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
124+
self.password = try self.passwordStore.decrypt(passwordEntity: passwordEntity, requestPassphrase: requestPGPKeyPassphrase)
125125
self.showPassword()
126126
} catch let AppError.pgpPrivateKeyNotFound(keyID: key) {
127127
DispatchQueue.main.async {
128128
// alert: cancel or try again
129129
let alert = UIAlertController(title: "CannotShowPassword".localize(), message: AppError.pgpPrivateKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert)
130130
alert.addAction(UIAlertAction.cancelAndPopView(controller: self))
131-
let selectKey = UIAlertAction.selectKey(controller: self) { action in
132-
self.decryptThenShowPasswordLocalKey(keyID: action.title)
131+
let selectKey = UIAlertAction.selectKey(controller: self) { _ in
132+
self.decryptThenShowPasswordLocalKey()
133133
}
134134
alert.addAction(selectKey)
135135

@@ -209,10 +209,10 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
209209
}
210210
}
211211

212-
private func saveEditPassword(password: Password, keyID: String? = nil) {
212+
private func saveEditPassword(password: Password) {
213213
SVProgressHUD.show(withStatus: "Saving".localize())
214214
do {
215-
passwordEntity = try passwordStore.edit(passwordEntity: passwordEntity!, password: password, keyID: keyID)
215+
passwordEntity = try passwordStore.edit(passwordEntity: passwordEntity!, password: password, path: password.path)
216216
setTableData()
217217
tableView.reloadData()
218218
SVProgressHUD.showSuccess(withStatus: "Success".localize())
@@ -223,8 +223,8 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
223223
SVProgressHUD.dismiss()
224224
let alert = UIAlertController(title: "Cannot Edit Password", message: AppError.pgpPublicKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert)
225225
alert.addAction(UIAlertAction.cancelAndPopView(controller: self))
226-
let selectKey = UIAlertAction.selectKey(controller: self) { action in
227-
self.saveEditPassword(password: password, keyID: action.title)
226+
let selectKey = UIAlertAction.selectKey(controller: self) { _ in
227+
self.saveEditPassword(password: password)
228228
}
229229
alert.addAction(selectKey)
230230

@@ -388,7 +388,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
388388
// commit the change of HOTP counter
389389
if password!.changed != 0 {
390390
do {
391-
passwordEntity = try passwordStore.edit(passwordEntity: passwordEntity!, password: password!)
391+
passwordEntity = try passwordStore.edit(passwordEntity: passwordEntity!, password: password!, path: password!.path)
392392
SVProgressHUD.showSuccess(withStatus: "PasswordCopied".localize() | "CounterUpdated".localize())
393393
SVProgressHUD.dismiss(withDelay: 1)
394394
} catch {

0 commit comments

Comments
 (0)