mirror of
https://github.com/neon443/ShhShell.git
synced 2026-03-11 13:26:16 +00:00
added keyimporter view to import keys
added importkey to import keys and save to keychain added a picker to select a key id in connection view fix publickey and opensshpublickey returning a pubkey when privatekey is empty added generate key button and import button change HostManager.makeLabel() to a computed property on Host
This commit is contained in:
@@ -69,6 +69,7 @@
|
|||||||
A9FD37592E143D74005319A8 /* GenericPasswordConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD37582E143D74005319A8 /* GenericPasswordConvertible.swift */; };
|
A9FD37592E143D74005319A8 /* GenericPasswordConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD37582E143D74005319A8 /* GenericPasswordConvertible.swift */; };
|
||||||
A9FD375B2E143D77005319A8 /* GenericPasswordStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD375A2E143D77005319A8 /* GenericPasswordStore.swift */; };
|
A9FD375B2E143D77005319A8 /* GenericPasswordStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD375A2E143D77005319A8 /* GenericPasswordStore.swift */; };
|
||||||
A9FD375D2E143D7E005319A8 /* KeyStoreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD375C2E143D7E005319A8 /* KeyStoreError.swift */; };
|
A9FD375D2E143D7E005319A8 /* KeyStoreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD375C2E143D7E005319A8 /* KeyStoreError.swift */; };
|
||||||
|
A9FD375F2E14648E005319A8 /* KeyImporterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD375E2E14648E005319A8 /* KeyImporterView.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -170,6 +171,7 @@
|
|||||||
A9FD37582E143D74005319A8 /* GenericPasswordConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPasswordConvertible.swift; sourceTree = "<group>"; };
|
A9FD37582E143D74005319A8 /* GenericPasswordConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPasswordConvertible.swift; sourceTree = "<group>"; };
|
||||||
A9FD375A2E143D77005319A8 /* GenericPasswordStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPasswordStore.swift; sourceTree = "<group>"; };
|
A9FD375A2E143D77005319A8 /* GenericPasswordStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPasswordStore.swift; sourceTree = "<group>"; };
|
||||||
A9FD375C2E143D7E005319A8 /* KeyStoreError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyStoreError.swift; sourceTree = "<group>"; };
|
A9FD375C2E143D7E005319A8 /* KeyStoreError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyStoreError.swift; sourceTree = "<group>"; };
|
||||||
|
A9FD375E2E14648E005319A8 /* KeyImporterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyImporterView.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -366,6 +368,7 @@
|
|||||||
A96C6AFF2E0C45FE00F377FE /* KeyDetailView.swift */,
|
A96C6AFF2E0C45FE00F377FE /* KeyDetailView.swift */,
|
||||||
A98554542E05535F009051BD /* KeyManagerView.swift */,
|
A98554542E05535F009051BD /* KeyManagerView.swift */,
|
||||||
A9D819302E102D8700442D38 /* HostkeysView.swift */,
|
A9D819302E102D8700442D38 /* HostkeysView.swift */,
|
||||||
|
A9FD375E2E14648E005319A8 /* KeyImporterView.swift */,
|
||||||
);
|
);
|
||||||
path = Keys;
|
path = Keys;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -602,6 +605,7 @@
|
|||||||
A96BE6A62E113DB000C0FEE9 /* ColorCodable.swift in Sources */,
|
A96BE6A62E113DB000C0FEE9 /* ColorCodable.swift in Sources */,
|
||||||
A92538C82DEE0742007E0A18 /* ContentView.swift in Sources */,
|
A92538C82DEE0742007E0A18 /* ContentView.swift in Sources */,
|
||||||
A96BE6A42E113D9400C0FEE9 /* ThemeCodable.swift in Sources */,
|
A96BE6A42E113D9400C0FEE9 /* ThemeCodable.swift in Sources */,
|
||||||
|
A9FD375F2E14648E005319A8 /* KeyImporterView.swift in Sources */,
|
||||||
A93143C02DF61B3200FCD5DB /* Host.swift in Sources */,
|
A93143C02DF61B3200FCD5DB /* Host.swift in Sources */,
|
||||||
A9B15A9A2E0ABA0400F66E02 /* DialogView.swift in Sources */,
|
A9B15A9A2E0ABA0400F66E02 /* DialogView.swift in Sources */,
|
||||||
A92538C92DEE0742007E0A18 /* ShhShellApp.swift in Sources */,
|
A92538C92DEE0742007E0A18 /* ShhShellApp.swift in Sources */,
|
||||||
|
|||||||
@@ -39,6 +39,18 @@ struct Host: HostPr {
|
|||||||
var passphrase: String
|
var passphrase: String
|
||||||
var key: String?
|
var key: String?
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
if name.isEmpty && address.isEmpty {
|
||||||
|
return id.uuidString
|
||||||
|
} else if name.isEmpty {
|
||||||
|
return address
|
||||||
|
} else if address.isEmpty {
|
||||||
|
return name
|
||||||
|
} else {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
name: String = "",
|
name: String = "",
|
||||||
symbol: HostSymbol = .genericServer,
|
symbol: HostSymbol = .genericServer,
|
||||||
|
|||||||
@@ -133,19 +133,6 @@ class HostsManager: ObservableObject, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeLabel(forHost: Host?) -> String {
|
|
||||||
guard let forHost else { return "" }
|
|
||||||
if forHost.name.isEmpty && forHost.address.isEmpty {
|
|
||||||
return forHost.id.uuidString
|
|
||||||
} else if forHost.name.isEmpty {
|
|
||||||
return forHost.address
|
|
||||||
} else if forHost.address.isEmpty {
|
|
||||||
return forHost.name
|
|
||||||
} else {
|
|
||||||
return forHost.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func moveHost(from: IndexSet, to: Int) {
|
func moveHost(from: IndexSet, to: Int) {
|
||||||
hosts.move(fromOffsets: from, toOffset: to)
|
hosts.move(fromOffsets: from, toOffset: to)
|
||||||
saveHosts()
|
saveHosts()
|
||||||
|
|||||||
@@ -22,13 +22,14 @@ class KeyManager: ObservableObject {
|
|||||||
private let userdefaults = NSUbiquitousKeyValueStore.default
|
private let userdefaults = NSUbiquitousKeyValueStore.default
|
||||||
|
|
||||||
@Published var keypairs: [Keypair] = []
|
@Published var keypairs: [Keypair] = []
|
||||||
var keyIDs: [UUID: KeyType] = [:]
|
|
||||||
|
var keyTypes: [UUID: KeyType] = [:]
|
||||||
var keyNames: [UUID: String] = [:]
|
var keyNames: [UUID: String] = [:]
|
||||||
private let baseTag = "com.neon443.ShhShell.keys".data(using: .utf8)!
|
private let baseTag = "com.neon443.ShhShell.keys".data(using: .utf8)!
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
loadKeyIDs()
|
loadKeyIDs()
|
||||||
for id in keyIDs.keys {
|
for id in keyTypes.keys {
|
||||||
guard let keypair = getFromKeychain(keyID: id) else { continue }
|
guard let keypair = getFromKeychain(keyID: id) else { continue }
|
||||||
keypairs.append(keypair)
|
keypairs.append(keypair)
|
||||||
}
|
}
|
||||||
@@ -39,7 +40,7 @@ class KeyManager: ObservableObject {
|
|||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
guard let data = userdefaults.data(forKey: "keyIDs") else { return }
|
guard let data = userdefaults.data(forKey: "keyIDs") else { return }
|
||||||
guard let decoded = try? decoder.decode([UUID:KeyType].self, from: data) else { return }
|
guard let decoded = try? decoder.decode([UUID:KeyType].self, from: data) else { return }
|
||||||
keyIDs = decoded
|
keyTypes = decoded
|
||||||
|
|
||||||
guard let dataNames = userdefaults.data(forKey: "keyNames") else { return }
|
guard let dataNames = userdefaults.data(forKey: "keyNames") else { return }
|
||||||
guard let decodedNames = try? decoder.decode([UUID:String].self, from: dataNames) else { return }
|
guard let decodedNames = try? decoder.decode([UUID:String].self, from: dataNames) else { return }
|
||||||
@@ -48,7 +49,7 @@ class KeyManager: ObservableObject {
|
|||||||
|
|
||||||
func saveKeyIDs() {
|
func saveKeyIDs() {
|
||||||
let encoder = JSONEncoder()
|
let encoder = JSONEncoder()
|
||||||
guard let encoded = try? encoder.encode(keyIDs) else { return }
|
guard let encoded = try? encoder.encode(keyTypes) else { return }
|
||||||
userdefaults.set(encoded, forKey: "keyIDs")
|
userdefaults.set(encoded, forKey: "keyIDs")
|
||||||
|
|
||||||
guard let encodedNames = try? encoder.encode(keyNames) else { return }
|
guard let encodedNames = try? encoder.encode(keyNames) else { return }
|
||||||
@@ -58,7 +59,7 @@ class KeyManager: ObservableObject {
|
|||||||
|
|
||||||
func saveToKeychain(_ keypair: Keypair) {
|
func saveToKeychain(_ keypair: Keypair) {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
keyIDs.updateValue(keypair.type, forKey: keypair.id)
|
keyTypes.updateValue(keypair.type, forKey: keypair.id)
|
||||||
keyNames.updateValue(keypair.name, forKey: keypair.id)
|
keyNames.updateValue(keypair.name, forKey: keypair.id)
|
||||||
}
|
}
|
||||||
saveKeyIDs()
|
saveKeyIDs()
|
||||||
@@ -77,7 +78,7 @@ class KeyManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getFromKeychain(keyID: UUID) -> Keypair? {
|
func getFromKeychain(keyID: UUID) -> Keypair? {
|
||||||
guard let keyType = keyIDs[keyID] else { return nil }
|
guard let keyType = keyTypes[keyID] else { return nil }
|
||||||
guard let keyName = keyNames[keyID] else { return nil }
|
guard let keyName = keyNames[keyID] else { return nil }
|
||||||
if keyType == .ed25519 {
|
if keyType == .ed25519 {
|
||||||
var key: Curve25519.Signing.PrivateKey?
|
var key: Curve25519.Signing.PrivateKey?
|
||||||
@@ -101,6 +102,12 @@ class KeyManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func importKey(type: KeyType, priv: String, name: String) {
|
||||||
|
if type == .ed25519 {
|
||||||
|
saveToKeychain(KeyManager.importSSHPrivkey(priv: priv))
|
||||||
|
} else { fatalError() }
|
||||||
|
}
|
||||||
|
|
||||||
//MARK: generate keys
|
//MARK: generate keys
|
||||||
func generateKey(type: KeyType, comment: String) {
|
func generateKey(type: KeyType, comment: String) {
|
||||||
switch type {
|
switch type {
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ struct Keypair: KeypairProtocol {
|
|||||||
var type: KeyType = .ed25519
|
var type: KeyType = .ed25519
|
||||||
var name: String = ""
|
var name: String = ""
|
||||||
var publicKey: Data {
|
var publicKey: Data {
|
||||||
(try? Curve25519.Signing.PrivateKey(rawRepresentation: privateKey).publicKey.rawRepresentation) ?? Data()
|
if privateKey.isEmpty {
|
||||||
|
return Data()
|
||||||
|
} else {
|
||||||
|
return (try? Curve25519.Signing.PrivateKey(rawRepresentation: privateKey).publicKey.rawRepresentation) ?? Data()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var privateKey: Data
|
var privateKey: Data
|
||||||
var passphrase: String = ""
|
var passphrase: String = ""
|
||||||
@@ -39,7 +43,12 @@ struct Keypair: KeypairProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var openSshPubkey: String {
|
var openSshPubkey: String {
|
||||||
String(data: KeyManager.makeSSHPubkey(self), encoding: .utf8) ?? "OpenSSH key format error"
|
if privateKey.isEmpty {
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
return String(data: KeyManager.makeSSHPubkey(self), encoding: .utf8) ?? "OpenSSH key format error"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var openSshPrivkey: String {
|
var openSshPrivkey: String {
|
||||||
|
|||||||
@@ -79,6 +79,13 @@ struct ConnectionView: View {
|
|||||||
|
|
||||||
TextBox(label: "Password", text: $handler.host.password, prompt: "not required if using publickeys", secure: true)
|
TextBox(label: "Password", text: $handler.host.password, prompt: "not required if using publickeys", secure: true)
|
||||||
|
|
||||||
|
Picker("Private key", selection: $handler.host.privateKeyID) {
|
||||||
|
ForEach(keyManager.keypairs) { keypair in
|
||||||
|
Text(keypair.label)
|
||||||
|
.tag(keypair.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TextBox(label: "Publickey", text: $pubkeyStr, prompt: "in openssh format")
|
TextBox(label: "Publickey", text: $pubkeyStr, prompt: "in openssh format")
|
||||||
.onChange(of: pubkeyStr) { _ in
|
.onChange(of: pubkeyStr) { _ in
|
||||||
let newStr = pubkeyStr.replacingOccurrences(of: "\r\n", with: "")
|
let newStr = pubkeyStr.replacingOccurrences(of: "\r\n", with: "")
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ struct HostsView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
SymbolPreview(symbol: host.symbol, label: host.label)
|
SymbolPreview(symbol: host.symbol, label: host.label)
|
||||||
.frame(width: 40, height: 40)
|
.frame(width: 40, height: 40)
|
||||||
Text(hostsManager.makeLabel(forHost: host))
|
Text(host.description)
|
||||||
}
|
}
|
||||||
.id(host)
|
.id(host)
|
||||||
.animation(.default, value: host)
|
.animation(.default, value: host)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ struct KeyDetailView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
SymbolPreview(symbol: host.symbol, label: host.label)
|
SymbolPreview(symbol: host.symbol, label: host.label)
|
||||||
.frame(width: 40, height: 40)
|
.frame(width: 40, height: 40)
|
||||||
Text(hostsManager.makeLabel(forHost: host))
|
Text(host.description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ struct KeyDetailView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
UIPasteboard.general.string = String(data: KeyManager.makeSSHPubkey(keypair), encoding: .utf8) ?? ""
|
UIPasteboard.general.string = keypair.openSshPubkey
|
||||||
} label: {
|
} label: {
|
||||||
CenteredLabel(title: "Copy private key", systemName: "document.on.document")
|
CenteredLabel(title: "Copy private key", systemName: "document.on.document")
|
||||||
}
|
}
|
||||||
|
|||||||
49
ShhShell/Views/Keys/KeyImporterView.swift
Normal file
49
ShhShell/Views/Keys/KeyImporterView.swift
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//
|
||||||
|
// KeyImporterView.swift
|
||||||
|
// ShhShell
|
||||||
|
//
|
||||||
|
// Created by neon443 on 01/07/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct KeyImporterView: View {
|
||||||
|
@ObservedObject var keyManager: KeyManager
|
||||||
|
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
|
@State var keyName: String = UIDevice().model + " " + Date().formatted()
|
||||||
|
@State var privkeyStr: String = ""
|
||||||
|
@State var keyType: KeyType = .ed25519
|
||||||
|
|
||||||
|
var keypair: Keypair {
|
||||||
|
Keypair(type: keyType, name: keyName, privateKey: privkeyStr.data(using: .utf8) ?? Data())
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
TextBox(label: "Name", text: $keyName, prompt: "A name for your key")
|
||||||
|
HStack {
|
||||||
|
Text("Private Key")
|
||||||
|
Spacer()
|
||||||
|
Text("Required")
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
TextEditor(text: $privkeyStr)
|
||||||
|
|
||||||
|
TextEditor(text: .constant(keypair.openSshPubkey))
|
||||||
|
|
||||||
|
Button() {
|
||||||
|
keyManager.importKey(type: keyType, priv: privkeyStr, name: keyName)
|
||||||
|
dismiss()
|
||||||
|
} label: {
|
||||||
|
Label("Import", systemImage: "key.horizontal")
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
KeyImporterView(keyManager: KeyManager())
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ struct KeyManagerView: View {
|
|||||||
@ObservedObject var hostsManager: HostsManager
|
@ObservedObject var hostsManager: HostsManager
|
||||||
@ObservedObject var keyManager: KeyManager
|
@ObservedObject var keyManager: KeyManager
|
||||||
|
|
||||||
|
@State var showImporter: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
hostsManager.selectedTheme.background.suiColor.opacity(0.7)
|
hostsManager.selectedTheme.background.suiColor.opacity(0.7)
|
||||||
@@ -47,8 +49,14 @@ struct KeyManagerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button("ed25519") {
|
Button("Generate a new Ed25519 Key") {
|
||||||
|
let comment = UIDevice().model + " " + Date().formatted()
|
||||||
|
keyManager.generateKey(type: .ed25519, comment: comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Import Key") { showImporter.toggle() }
|
||||||
|
.sheet(isPresented: $showImporter) {
|
||||||
|
KeyImporterView(keyManager: keyManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ struct SessionView: View {
|
|||||||
.foregroundStyle(.terminalGreen)
|
.foregroundStyle(.terminalGreen)
|
||||||
SymbolPreview(symbol: host.symbol, label: host.label)
|
SymbolPreview(symbol: host.symbol, label: host.label)
|
||||||
.frame(width: 40, height: 40)
|
.frame(width: 40, height: 40)
|
||||||
Text(hostsManager.makeLabel(forHost: host))
|
Text(host.description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $shellPresented) {
|
.fullScreenCover(isPresented: $shellPresented) {
|
||||||
|
|||||||
Reference in New Issue
Block a user