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 */; };
|
||||
A9FD375B2E143D77005319A8 /* GenericPasswordStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD375A2E143D77005319A8 /* GenericPasswordStore.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 */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -170,6 +171,7 @@
|
||||
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>"; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -366,6 +368,7 @@
|
||||
A96C6AFF2E0C45FE00F377FE /* KeyDetailView.swift */,
|
||||
A98554542E05535F009051BD /* KeyManagerView.swift */,
|
||||
A9D819302E102D8700442D38 /* HostkeysView.swift */,
|
||||
A9FD375E2E14648E005319A8 /* KeyImporterView.swift */,
|
||||
);
|
||||
path = Keys;
|
||||
sourceTree = "<group>";
|
||||
@@ -602,6 +605,7 @@
|
||||
A96BE6A62E113DB000C0FEE9 /* ColorCodable.swift in Sources */,
|
||||
A92538C82DEE0742007E0A18 /* ContentView.swift in Sources */,
|
||||
A96BE6A42E113D9400C0FEE9 /* ThemeCodable.swift in Sources */,
|
||||
A9FD375F2E14648E005319A8 /* KeyImporterView.swift in Sources */,
|
||||
A93143C02DF61B3200FCD5DB /* Host.swift in Sources */,
|
||||
A9B15A9A2E0ABA0400F66E02 /* DialogView.swift in Sources */,
|
||||
A92538C92DEE0742007E0A18 /* ShhShellApp.swift in Sources */,
|
||||
|
||||
@@ -39,6 +39,18 @@ struct Host: HostPr {
|
||||
var passphrase: 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(
|
||||
name: String = "",
|
||||
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) {
|
||||
hosts.move(fromOffsets: from, toOffset: to)
|
||||
saveHosts()
|
||||
|
||||
@@ -22,13 +22,14 @@ class KeyManager: ObservableObject {
|
||||
private let userdefaults = NSUbiquitousKeyValueStore.default
|
||||
|
||||
@Published var keypairs: [Keypair] = []
|
||||
var keyIDs: [UUID: KeyType] = [:]
|
||||
|
||||
var keyTypes: [UUID: KeyType] = [:]
|
||||
var keyNames: [UUID: String] = [:]
|
||||
private let baseTag = "com.neon443.ShhShell.keys".data(using: .utf8)!
|
||||
|
||||
init() {
|
||||
loadKeyIDs()
|
||||
for id in keyIDs.keys {
|
||||
for id in keyTypes.keys {
|
||||
guard let keypair = getFromKeychain(keyID: id) else { continue }
|
||||
keypairs.append(keypair)
|
||||
}
|
||||
@@ -39,7 +40,7 @@ class KeyManager: ObservableObject {
|
||||
let decoder = JSONDecoder()
|
||||
guard let data = userdefaults.data(forKey: "keyIDs") 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 decodedNames = try? decoder.decode([UUID:String].self, from: dataNames) else { return }
|
||||
@@ -48,7 +49,7 @@ class KeyManager: ObservableObject {
|
||||
|
||||
func saveKeyIDs() {
|
||||
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")
|
||||
|
||||
guard let encodedNames = try? encoder.encode(keyNames) else { return }
|
||||
@@ -58,7 +59,7 @@ class KeyManager: ObservableObject {
|
||||
|
||||
func saveToKeychain(_ keypair: Keypair) {
|
||||
withAnimation {
|
||||
keyIDs.updateValue(keypair.type, forKey: keypair.id)
|
||||
keyTypes.updateValue(keypair.type, forKey: keypair.id)
|
||||
keyNames.updateValue(keypair.name, forKey: keypair.id)
|
||||
}
|
||||
saveKeyIDs()
|
||||
@@ -77,7 +78,7 @@ class KeyManager: ObservableObject {
|
||||
}
|
||||
|
||||
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 }
|
||||
if keyType == .ed25519 {
|
||||
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
|
||||
func generateKey(type: KeyType, comment: String) {
|
||||
switch type {
|
||||
|
||||
@@ -25,7 +25,11 @@ struct Keypair: KeypairProtocol {
|
||||
var type: KeyType = .ed25519
|
||||
var name: String = ""
|
||||
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 passphrase: String = ""
|
||||
@@ -39,7 +43,12 @@ struct Keypair: KeypairProtocol {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -79,6 +79,13 @@ struct ConnectionView: View {
|
||||
|
||||
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")
|
||||
.onChange(of: pubkeyStr) { _ in
|
||||
let newStr = pubkeyStr.replacingOccurrences(of: "\r\n", with: "")
|
||||
|
||||
@@ -28,7 +28,7 @@ struct HostsView: View {
|
||||
} label: {
|
||||
SymbolPreview(symbol: host.symbol, label: host.label)
|
||||
.frame(width: 40, height: 40)
|
||||
Text(hostsManager.makeLabel(forHost: host))
|
||||
Text(host.description)
|
||||
}
|
||||
.id(host)
|
||||
.animation(.default, value: host)
|
||||
|
||||
@@ -24,7 +24,7 @@ struct KeyDetailView: View {
|
||||
HStack {
|
||||
SymbolPreview(symbol: host.symbol, label: host.label)
|
||||
.frame(width: 40, height: 40)
|
||||
Text(hostsManager.makeLabel(forHost: host))
|
||||
Text(host.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ struct KeyDetailView: View {
|
||||
}
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = String(data: KeyManager.makeSSHPubkey(keypair), encoding: .utf8) ?? ""
|
||||
UIPasteboard.general.string = keypair.openSshPubkey
|
||||
} label: {
|
||||
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 keyManager: KeyManager
|
||||
|
||||
@State var showImporter: Bool = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
hostsManager.selectedTheme.background.suiColor.opacity(0.7)
|
||||
@@ -47,9 +49,15 @@ 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)
|
||||
.navigationTitle("Keys")
|
||||
|
||||
@@ -29,7 +29,7 @@ struct SessionView: View {
|
||||
.foregroundStyle(.terminalGreen)
|
||||
SymbolPreview(symbol: host.symbol, label: host.label)
|
||||
.frame(width: 40, height: 40)
|
||||
Text(hostsManager.makeLabel(forHost: host))
|
||||
Text(host.description)
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $shellPresented) {
|
||||
|
||||
Reference in New Issue
Block a user