fix picker on connectionview

using trimmingchars whitespaces and newlines to remove any trailing "\n"s
added padding to the button
publickey data uses a switch now
reorganised keymanager and used //MARKs
added header into KeyType for easier expansion of key types in the future
improved default key comment to use "at" between chunks and exclude time
This commit is contained in:
neon443
2025-07-03 10:13:57 +01:00
parent 3d9530f01b
commit c2c74248e2
7 changed files with 88 additions and 69 deletions

View File

@@ -46,11 +46,49 @@ class KeyManager: ObservableObject {
for keypair in keypairs { for keypair in keypairs {
saveToKeychain(keypair) saveToKeychain(keypair)
} }
}
func renameKey(keypair: Keypair, newName: String) {
guard !newName.isEmpty else { return }
let keyID = keypair.id
guard let index = keypairs.firstIndex(where: { $0.id == keyID }) else { return }
var keypairWithNewName = keypair
keypairWithNewName.name = newName
withAnimation { keypairs[index] = keypairWithNewName }
saveKeypairs()
}
func deleteKey(_ keypair: Keypair) {
removeFromKeycahin(keypair: keypair)
let keyID = keypair.id
withAnimation { keypairs.removeAll(where: { $0.id == keyID }) }
saveKeypairs()
}
func importKey(type: KeyType, priv: String, name: String) {
if type == .ed25519 {
guard let importedKeypair = KeyManager.importSSHPrivkey(priv: priv) else { return }
saveToKeychain(importedKeypair)
} else { fatalError() }
}
func generateKey(type: KeyType, comment: String) {
switch type {
case .ed25519:
let keypair = Keypair(
type: .ed25519,
name: comment,
privateKey: Curve25519.Signing.PrivateKey().rawRepresentation
)
saveToKeychain(keypair)
}
loadKeypairs() loadKeypairs()
} }
//MARK: keychain
func saveToKeychain(_ keypair: Keypair) { func saveToKeychain(_ keypair: Keypair) {
if keypair.type == .ed25519 { switch keypair.type {
case .ed25519:
let curve25519 = try! Curve25519.Signing.PrivateKey(rawRepresentation: keypair.privateKey) let curve25519 = try! Curve25519.Signing.PrivateKey(rawRepresentation: keypair.privateKey)
let readKey: Curve25519.Signing.PrivateKey? let readKey: Curve25519.Signing.PrivateKey?
readKey = try! passwordStore.readKey(account: keypair.id.uuidString) readKey = try! passwordStore.readKey(account: keypair.id.uuidString)
@@ -58,8 +96,10 @@ class KeyManager: ObservableObject {
try! passwordStore.deleteKey(account: keypair.id.uuidString) try! passwordStore.deleteKey(account: keypair.id.uuidString)
} }
try! passwordStore.storeKey(curve25519.genericKeyRepresentation, account: keypair.id.uuidString) try! passwordStore.storeKey(curve25519.genericKeyRepresentation, account: keypair.id.uuidString)
} else { }
if !keypairs.contains(keypair) {
keypairs.append(keypair)
saveKeypairs()
} }
} }
@@ -97,46 +137,7 @@ class KeyManager: ObservableObject {
saveKeypairs() saveKeypairs()
} }
func renameKey(keypair: Keypair, newName: String) { //MARK: openssh converters/importers
guard !newName.isEmpty else { return }
let keyID = keypair.id
guard let index = keypairs.firstIndex(where: { $0.id == keyID }) else { return }
var keypairWithNewName = keypair
keypairWithNewName.name = newName
withAnimation { keypairs[index] = keypairWithNewName }
saveKeypairs()
}
func deleteKey(_ keypair: Keypair) {
removeFromKeycahin(keypair: keypair)
let keyID = keypair.id
withAnimation { keypairs.removeAll(where: { $0.id == keyID }) }
saveKeypairs()
}
func importKey(type: KeyType, priv: String, name: String) {
if type == .ed25519 {
guard let importedKeypair = KeyManager.importSSHPrivkey(priv: priv) else { return }
saveToKeychain(importedKeypair)
saveKeypairs()
} else { fatalError() }
}
//MARK: generate keys
func generateKey(type: KeyType, comment: String) {
switch type {
case .ed25519:
let keypair = Keypair(
type: .ed25519,
name: comment,
privateKey: Curve25519.Signing.PrivateKey().rawRepresentation
)
saveToKeychain(keypair)
saveKeypairs()
}
loadKeypairs()
}
static func importSSHPubkey(pub: String) -> Data { static func importSSHPubkey(pub: String) -> Data {
let split = pub.split(separator: " ") let split = pub.split(separator: " ")
guard split.count == 3 else { return Data() } guard split.count == 3 else { return Data() }
@@ -150,15 +151,14 @@ class KeyManager: ObservableObject {
} }
static func makeSSHPubkey(_ keypair: Keypair) -> Data { static func makeSSHPubkey(_ keypair: Keypair) -> Data {
let header = "ssh-ed25519"
var keyBlob: Data = Data() var keyBlob: Data = Data()
//key type bit //key type bit
keyBlob += encode(str: header) keyBlob += encode(str: keypair.type.header)
//base64 blob bit //base64 blob bit
keyBlob += encode(data: keypair.publicKey) keyBlob += encode(data: keypair.publicKey)
let b64key = keyBlob.base64EncodedString() let b64key = keyBlob.base64EncodedString()
let pubkeyline = "\(header) \(b64key) \(keypair.name)\n" let pubkeyline = "\(keypair.type.header) \(b64key) \(keypair.name)\n"
return Data(pubkeyline.utf8) return Data(pubkeyline.utf8)
} }
@@ -254,6 +254,7 @@ class KeyManager: ObservableObject {
return content return content
} }
//MARK: openssh conversion helpers
static func encode(str: String) -> Data { static func encode(str: String) -> Data {
guard let utf8 = str.data(using: .utf8) else { guard let utf8 = str.data(using: .utf8) else {
return Data() return Data()

View File

@@ -16,4 +16,11 @@ enum KeyType: Codable, Equatable, Hashable, CustomStringConvertible, CaseIterabl
return "Ed25519" return "Ed25519"
} }
} }
var header: String {
switch self {
case .ed25519:
"ssh-ed25519"
}
}
} }

View File

@@ -25,10 +25,9 @@ struct Keypair: KeypairProtocol {
var type: KeyType = .ed25519 var type: KeyType = .ed25519
var name: String = "" var name: String = ""
var publicKey: Data { var publicKey: Data {
if privateKey.isEmpty { guard !privateKey.isEmpty else { return Data() }
print("not a valid ed25519 key") switch type {
fatalError() case .ed25519:
} else {
return (try? Curve25519.Signing.PrivateKey(rawRepresentation: privateKey).publicKey.rawRepresentation) ?? Data() return (try? Curve25519.Signing.PrivateKey(rawRepresentation: privateKey).publicKey.rawRepresentation) ?? Data()
} }
} }

View File

@@ -82,9 +82,10 @@ struct ConnectionView: View {
Picker("Private key", selection: $handler.host.privateKeyID) { Picker("Private key", selection: $handler.host.privateKeyID) {
Text("None") Text("None")
.tag(nil as UUID?) .tag(nil as UUID?)
Divider()
ForEach(keyManager.keypairs) { keypair in ForEach(keyManager.keypairs) { keypair in
Text(keypair.label) Text(keypair.label)
.tag(keypair.id) .tag(keypair.id as UUID?)
} }
} }
} }

View File

@@ -90,14 +90,14 @@ struct KeyDetailView: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Public key") Text("Public key")
.bold() .bold()
Text(keypair.openSshPubkey.dropLast(2)) Text(keypair.openSshPubkey.trimmingCharacters(in: .whitespacesAndNewlines))
} }
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Private key") Text("Private key")
.bold() .bold()
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
ZStack(alignment: .center) { ZStack(alignment: .center) {
Text(keypair.openSshPrivkey.dropLast(2)) Text(keypair.openSshPrivkey.trimmingCharacters(in: .whitespacesAndNewlines))
.blur(radius: reveal ? 0 : 5) .blur(radius: reveal ? 0 : 5)
VStack { VStack {
Image(systemName: "eye.slash.fill") Image(systemName: "eye.slash.fill")

View File

@@ -12,7 +12,7 @@ struct KeyImporterView: View {
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@State var keyName: String = UIDevice().model + " " + Date().formatted() @State var keyName: String = UIDevice().model + " at " + Date().formatted(date: .numeric, time: .omitted)
@State var privkeyStr: String = "" @State var privkeyStr: String = ""
@State var keyType: KeyType = .ed25519 @State var keyType: KeyType = .ed25519
@@ -24,25 +24,33 @@ struct KeyImporterView: View {
List { List {
TextBox(label: "Name", text: $keyName, prompt: "A name for your key") TextBox(label: "Name", text: $keyName, prompt: "A name for your key")
Picker("Key type", selection: $keyType) {
ForEach(KeyType.allCases, id: \.self) { type in
Text(type.description)
.tag(type)
}
}
.pickerStyle(SegmentedPickerStyle())
HStack { HStack {
Text("Private Key") Text("Key Type")
Spacer() Picker("Key type", selection: $keyType) {
Text("Required") ForEach(KeyType.allCases, id: \.self) { type in
.foregroundStyle(.red) Text(type.description)
.tag(type)
}
}
.pickerStyle(SegmentedPickerStyle())
} }
TextEditor(text: $privkeyStr) Section {
HStack {
Text("Private Key")
Spacer()
Text("Required")
.foregroundStyle(.red)
}
.listRowSeparator(.hidden)
TextEditor(text: $privkeyStr)
.background(.black)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
if !keypair.openSshPubkey.isEmpty { if !keypair.openSshPubkey.isEmpty {
TextEditor(text: .constant(keypair.openSshPubkey)) Text(keypair.openSshPubkey.trimmingCharacters(in: .whitespacesAndNewlines))
.foregroundStyle(.gray) .foregroundStyle(.gray)
} }
@@ -53,11 +61,14 @@ struct KeyImporterView: View {
dismiss() dismiss()
} label: { } label: {
Text("Import") Text("Import")
.font(.title)
.bold()
} }
.onTapGesture { .onTapGesture {
UINotificationFeedbackGenerator().notificationOccurred(.success) UINotificationFeedbackGenerator().notificationOccurred(.success)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.padding()
} }
} }

View File

@@ -52,7 +52,7 @@ struct KeyManagerView: View {
} }
Button("Generate a new Ed25519 Key") { Button("Generate a new Ed25519 Key") {
let comment = UIDevice().model + " " + Date().formatted() let comment = UIDevice().model + " at " + Date().formatted(date: .numeric, time: .omitted)
keyManager.generateKey(type: .ed25519, comment: comment) keyManager.generateKey(type: .ed25519, comment: comment)
} }