Store SSH private keys in Keychain instead of files

This commit is contained in:
Danny Moesch 2019-07-02 20:28:47 +02:00 committed by Mingshen Sun
parent 6b95e60ea1
commit f1337622dc
9 changed files with 45 additions and 30 deletions

View file

@ -71,7 +71,7 @@ class GitSSHKeyArmorSettingTableViewController: AutoCellHeightUITableViewControl
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
armorPrivateKeyTextView.text = SharedDefaults[.gitSSHPrivateKeyArmor] armorPrivateKeyTextView.text = AppKeychain.get(for: SshKey.PRIVATE.getKeychainKey())
armorPrivateKeyTextView.delegate = self armorPrivateKeyTextView.delegate = self
scanPrivateKeyCell?.textLabel?.text = "ScanPrivateKeyQrCodes".localize() scanPrivateKeyCell?.textLabel?.text = "ScanPrivateKeyQrCodes".localize()
@ -81,7 +81,6 @@ class GitSSHKeyArmorSettingTableViewController: AutoCellHeightUITableViewControl
} }
@IBAction func doneButtonTapped(_ sender: Any) { @IBAction func doneButtonTapped(_ sender: Any) {
SharedDefaults[.gitSSHPrivateKeyArmor] = armorPrivateKeyTextView.text
do { do {
try passwordStore.initGitSSHKey(with: armorPrivateKeyTextView.text) try passwordStore.initGitSSHKey(with: armorPrivateKeyTextView.text)
} catch { } catch {

View file

@ -43,7 +43,7 @@ class GitServerSettingTableViewController: UITableViewController {
super.viewWillAppear(animated) super.viewWillAppear(animated)
// Grey out ssh option if ssh_key is not present // Grey out ssh option if ssh_key is not present
if let sshLabel = sshLabel { if let sshLabel = sshLabel {
sshLabel.isEnabled = passwordStore.gitSSHKeyExists() sshLabel.isEnabled = AppKeychain.contains(key: SshKey.PRIVATE.getKeychainKey())
} }
} }
override func viewDidLoad() { override func viewDidLoad() {
@ -86,13 +86,14 @@ class GitServerSettingTableViewController: UITableViewController {
SVProgressHUD.setDefaultStyle(.light) SVProgressHUD.setDefaultStyle(.light)
SVProgressHUD.show(withStatus: "PrepareRepository".localize()) SVProgressHUD.show(withStatus: "PrepareRepository".localize())
var gitCredential: GitCredential var gitCredential: GitCredential
if auth == "Password" { let privateKey: String? = AppKeychain.get(for: SshKey.PRIVATE.getKeychainKey())
if auth == "Password" || privateKey == nil {
gitCredential = GitCredential(credential: GitCredential.Credential.http(userName: username)) gitCredential = GitCredential(credential: GitCredential.Credential.http(userName: username))
} else { } else {
gitCredential = GitCredential( gitCredential = GitCredential(
credential: GitCredential.Credential.ssh( credential: GitCredential.Credential.ssh(
userName: username, userName: username,
privateKeyFile: Globals.gitSSHPrivateKeyURL privateKey: privateKey!
) )
) )
} }
@ -159,7 +160,7 @@ class GitServerSettingTableViewController: UITableViewController {
authenticationMethod = "Password" authenticationMethod = "Password"
} else if cell == authSSHKeyCell { } else if cell == authSSHKeyCell {
if !passwordStore.gitSSHKeyExists() { if !AppKeychain.contains(key: SshKey.PRIVATE.getKeychainKey()) {
Utils.alert(title: "CannotSelectSshKey".localize(), message: "PleaseSetupSshKeyFirst.".localize(), controller: self, completion: nil) Utils.alert(title: "CannotSelectSshKey".localize(), message: "PleaseSetupSshKeyFirst.".localize(), controller: self, completion: nil)
authenticationMethod = "Password" authenticationMethod = "Password"
} else { } else {
@ -235,7 +236,7 @@ class GitServerSettingTableViewController: UITableViewController {
optionMenu.addAction(urlAction) optionMenu.addAction(urlAction)
optionMenu.addAction(armorAction) optionMenu.addAction(armorAction)
if passwordStore.gitSSHKeyExists(inFileSharing: true) { if KeyFileManager.PrivateSsh.doesKeyFileExist() {
// might keys updated via iTunes, or downloaded/pasted inside the app // might keys updated via iTunes, or downloaded/pasted inside the app
fileActionTitle.append(" (\("Import".localize()))") fileActionTitle.append(" (\("Import".localize()))")
let fileAction = UIAlertAction(title: fileActionTitle, style: .default) { _ in let fileAction = UIAlertAction(title: fileActionTitle, style: .default) { _ in

View file

@ -139,13 +139,14 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
SVProgressHUD.setDefaultStyle(.light) SVProgressHUD.setDefaultStyle(.light)
SVProgressHUD.show(withStatus: "SyncingPasswordStore".localize()) SVProgressHUD.show(withStatus: "SyncingPasswordStore".localize())
var gitCredential: GitCredential var gitCredential: GitCredential
if SharedDefaults[.gitAuthenticationMethod] == "Password" { let privateKey: String? = AppKeychain.get(for: SshKey.PRIVATE.getKeychainKey())
if SharedDefaults[.gitAuthenticationMethod] == "Password" || privateKey == nil {
gitCredential = GitCredential(credential: GitCredential.Credential.http(userName: SharedDefaults[.gitUsername]!)) gitCredential = GitCredential(credential: GitCredential.Credential.http(userName: SharedDefaults[.gitUsername]!))
} else { } else {
gitCredential = GitCredential( gitCredential = GitCredential(
credential: GitCredential.Credential.ssh( credential: GitCredential.Credential.ssh(
userName: SharedDefaults[.gitUsername]!, userName: SharedDefaults[.gitUsername]!,
privateKeyFile: Globals.gitSSHPrivateKeyURL privateKey: privateKey!
) )
) )
} }

View file

@ -22,6 +22,10 @@ public class AppKeychain {
keychain[key] = string keychain[key] = string
} }
public static func contains(key: String) -> Bool {
return (try? keychain.contains(key)) ?? false
}
public static func get(for key: String) -> Data? { public static func get(for key: String) -> Data? {
return try? keychain.getData(key) return try? keychain.getData(key)
} }

View file

@ -34,3 +34,20 @@ public enum PgpKey: CryptographicKey {
} }
} }
public enum SshKey: CryptographicKey {
case PRIVATE
public func getKeychainKey() -> String {
switch self {
case .PRIVATE:
return "sshPrivateKey"
}
}
public func getFileSharingPath() -> String {
switch self {
case .PRIVATE:
return Globals.iTunesFileSharingSSHPrivate
}
}
}

View file

@ -19,6 +19,7 @@ public extension DefaultsKeys {
// Keep them for legacy reasons. // Keep them for legacy reasons.
static let pgpPublicKeyArmor = DefaultsKey<String?>("pgpPublicKeyArmor") static let pgpPublicKeyArmor = DefaultsKey<String?>("pgpPublicKeyArmor")
static let pgpPrivateKeyArmor = DefaultsKey<String?>("pgpPrivateKeyArmor") static let pgpPrivateKeyArmor = DefaultsKey<String?>("pgpPrivateKeyArmor")
static let gitSSHPrivateKeyArmor = DefaultsKey<String?>("gitSSHPrivateKeyArmor")
static let gitURL = DefaultsKey<URL?>("gitURL") static let gitURL = DefaultsKey<URL?>("gitURL")
static let gitAuthenticationMethod = DefaultsKey<String?>("gitAuthenticationMethod") static let gitAuthenticationMethod = DefaultsKey<String?>("gitAuthenticationMethod")
@ -26,7 +27,6 @@ public extension DefaultsKeys {
static let gitBranchName = DefaultsKey<String>("gitBranchName", defaultValue: "master") static let gitBranchName = DefaultsKey<String>("gitBranchName", defaultValue: "master")
static let gitSSHPrivateKeyURL = DefaultsKey<URL?>("gitSSHPrivateKeyURL") static let gitSSHPrivateKeyURL = DefaultsKey<URL?>("gitSSHPrivateKeyURL")
static let gitSSHKeySource = DefaultsKey<String?>("gitSSHKeySource") static let gitSSHKeySource = DefaultsKey<String?>("gitSSHKeySource")
static let gitSSHPrivateKeyArmor = DefaultsKey<String?>("gitSSHPrivateKeyArmor")
static let gitSignatureName = DefaultsKey<String?>("gitSignatureName") static let gitSignatureName = DefaultsKey<String?>("gitSignatureName")
static let gitSignatureEmail = DefaultsKey<String?>("gitSignatureEmail") static let gitSignatureEmail = DefaultsKey<String?>("gitSignatureEmail")

View file

@ -11,6 +11,7 @@ public class KeyFileManager {
public static let PublicPgp = KeyFileManager(keyType: PgpKey.PUBLIC) public static let PublicPgp = KeyFileManager(keyType: PgpKey.PUBLIC)
public static let PrivatePgp = KeyFileManager(keyType: PgpKey.PRIVATE) public static let PrivatePgp = KeyFileManager(keyType: PgpKey.PRIVATE)
public static let PrivateSsh = KeyFileManager(keyType: SshKey.PRIVATE)
private let keyType: CryptographicKey private let keyType: CryptographicKey
private let keyPath: String private let keyPath: String

View file

@ -17,7 +17,7 @@ public struct GitCredential {
public enum Credential { public enum Credential {
case http(userName: String) case http(userName: String)
case ssh(userName: String, privateKeyFile: URL) case ssh(userName: String, privateKey: String)
} }
public init(credential: Credential) { public init(credential: Credential) {
@ -48,7 +48,7 @@ public struct GitCredential {
} }
attempts += 1 attempts += 1
credential = try? GTCredential(userName: userName, password: lastPassword!) credential = try? GTCredential(userName: userName, password: lastPassword!)
case let .ssh(userName, privateKeyFile): case let .ssh(userName, privateKey):
if attempts > 0 { if attempts > 0 {
// The passphrase seems correct, but the previous authentification failed. // The passphrase seems correct, but the previous authentification failed.
return nil return nil
@ -65,7 +65,7 @@ public struct GitCredential {
} }
} }
attempts += 1 attempts += 1
credential = try? GTCredential(userName: userName, publicKeyURL: nil, privateKeyURL: privateKeyFile, passphrase: lastPassword!) credential = try? GTCredential(userName: userName, publicKeyString: nil, privateKeyString: privateKey, passphrase: lastPassword!)
} }
return credential return credential
} }

View file

@ -142,7 +142,7 @@ public class PasswordStore {
private func migrateIfNeeded() { private func migrateIfNeeded() {
// migrate happens only if the repository was cloned and pgp keys were set up using earlier versions // migrate happens only if the repository was cloned and pgp keys were set up using earlier versions
let needMigration = !pgpKeyExists() && !gitSSHKeyExists() && !fm.fileExists(atPath: Globals.repositoryPath) && fm.fileExists(atPath: Globals.repositoryPathLegacy) let needMigration = !pgpKeyExists() && !fm.fileExists(atPath: Globals.gitSSHPrivateKeyPath) && !fm.fileExists(atPath: Globals.repositoryPath) && fm.fileExists(atPath: Globals.repositoryPathLegacy)
guard needMigration == true else { guard needMigration == true else {
return return
} }
@ -190,21 +190,19 @@ public class PasswordStore {
do { do {
try KeyFileManager(keyType: PgpKey.PUBLIC, keyPath: Globals.pgpPublicKeyPath).importKeyAndDeleteFile() try KeyFileManager(keyType: PgpKey.PUBLIC, keyPath: Globals.pgpPublicKeyPath).importKeyAndDeleteFile()
try KeyFileManager(keyType: PgpKey.PRIVATE, keyPath: Globals.pgpPrivateKeyPath).importKeyAndDeleteFile() try KeyFileManager(keyType: PgpKey.PRIVATE, keyPath: Globals.pgpPrivateKeyPath).importKeyAndDeleteFile()
try KeyFileManager(keyType: SshKey.PRIVATE, keyPath: Globals.gitSSHPrivateKeyPath).importKeyAndDeleteFile()
SharedDefaults.remove(.pgpPublicKeyArmor) SharedDefaults.remove(.pgpPublicKeyArmor)
SharedDefaults.remove(.pgpPrivateKeyArmor) SharedDefaults.remove(.pgpPrivateKeyArmor)
SharedDefaults.remove(.gitSSHPrivateKeyArmor)
SharedDefaults[.pgpKeySource] = "file" SharedDefaults[.pgpKeySource] = "file"
SharedDefaults[.gitSSHKeySource] = "file"
} catch { } catch {
print("MigrationError".localize(error)) print("MigrationError".localize(error))
} }
} }
enum SSHKeyType {
case `public`, secret
}
public func initGitSSHKey(with armorKey: String) throws { public func initGitSSHKey(with armorKey: String) throws {
let keyPath = Globals.gitSSHPrivateKeyPath AppKeychain.add(string: armorKey, for: SshKey.PRIVATE.getKeychainKey())
try armorKey.write(toFile: keyPath, atomically: true, encoding: .ascii)
} }
public func initPGPKeys() throws { public func initPGPKeys() throws {
@ -851,17 +849,11 @@ public class PasswordStore {
public func removeGitSSHKeys() { public func removeGitSSHKeys() {
try? fm.removeItem(atPath: Globals.gitSSHPrivateKeyPath) try? fm.removeItem(atPath: Globals.gitSSHPrivateKeyPath)
Defaults.remove(.gitSSHKeySource)
Defaults.remove(.gitSSHPrivateKeyArmor) Defaults.remove(.gitSSHPrivateKeyArmor)
Defaults.remove(.gitSSHPrivateKeyURL) Defaults.remove(.gitSSHPrivateKeyURL)
self.gitSSHPrivateKeyPassphrase = nil AppKeychain.removeContent(for: SshKey.PRIVATE.getKeychainKey())
} gitSSHPrivateKeyPassphrase = nil
public func gitSSHKeyExists(inFileSharing: Bool = false) -> Bool {
if inFileSharing == false {
return fm.fileExists(atPath: Globals.gitSSHPrivateKeyPath)
} else {
return fm.fileExists(atPath: Globals.iTunesFileSharingSSHPrivate)
}
} }
public func pgpKeyExists(inFileSharing: Bool = false) -> Bool { public func pgpKeyExists(inFileSharing: Bool = false) -> Bool {
@ -873,7 +865,7 @@ public class PasswordStore {
} }
public func gitSSHKeyImportFromFileSharing() throws { public func gitSSHKeyImportFromFileSharing() throws {
try fm.moveItem(atPath: Globals.iTunesFileSharingSSHPrivate, toPath: Globals.gitSSHPrivateKeyPath) try KeyFileManager.PrivateSsh.importKeyAndDeleteFile()
} }
public func pgpKeyImportFromFileSharing() throws { public func pgpKeyImportFromFileSharing() throws {