Store SSH private keys in Keychain instead of files
This commit is contained in:
parent
6b95e60ea1
commit
f1337622dc
9 changed files with 45 additions and 30 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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!
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue