Skip to content

Commit f1c50ca

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 f1c50ca

35 files changed

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

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 {

0 commit comments

Comments
 (0)