mirror of
https://github.com/neon443/ShhShell.git
synced 2026-03-11 13:26:16 +00:00
hostsview - list of hosts
hostmanager for loading and saving hosts to defaults host.debug/.blank as static vars fix crash on disconnect by checking for connection in timer before checking if channel is open keychain layer? to convert ed25519 to seckey and back for keychain storage generate ed25519 via cryptokit
This commit is contained in:
@@ -32,6 +32,9 @@
|
||||
A98554552E05535F009051BD /* KeyManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554542E05535F009051BD /* KeyManagerView.swift */; };
|
||||
A98554592E0553AA009051BD /* KeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554582E0553AA009051BD /* KeyManager.swift */; };
|
||||
A985545D2E055D4D009051BD /* ConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985545C2E055D4D009051BD /* ConnectionView.swift */; };
|
||||
A985545F2E056EDD009051BD /* KeychainLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985545E2E056EDD009051BD /* KeychainLayer.swift */; };
|
||||
A98554612E058433009051BD /* HostsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554602E058433009051BD /* HostsManager.swift */; };
|
||||
A98554632E0587DF009051BD /* HostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554622E0587DF009051BD /* HostsView.swift */; };
|
||||
A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -93,6 +96,9 @@
|
||||
A98554542E05535F009051BD /* KeyManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManagerView.swift; sourceTree = "<group>"; };
|
||||
A98554582E0553AA009051BD /* KeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManager.swift; sourceTree = "<group>"; };
|
||||
A985545C2E055D4D009051BD /* ConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionView.swift; sourceTree = "<group>"; };
|
||||
A985545E2E056EDD009051BD /* KeychainLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainLayer.swift; sourceTree = "<group>"; };
|
||||
A98554602E058433009051BD /* HostsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsManager.swift; sourceTree = "<group>"; };
|
||||
A98554622E0587DF009051BD /* HostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsView.swift; sourceTree = "<group>"; };
|
||||
A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHHandler.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -189,6 +195,7 @@
|
||||
children = (
|
||||
A98554532E05534F009051BD /* Keys */,
|
||||
A92538C52DEE0742007E0A18 /* ContentView.swift */,
|
||||
A98554622E0587DF009051BD /* HostsView.swift */,
|
||||
A985545C2E055D4D009051BD /* ConnectionView.swift */,
|
||||
A98554522E055347009051BD /* Terminal */,
|
||||
A93143C52DF61FE300FCD5DB /* ViewModifiers.swift */,
|
||||
@@ -243,6 +250,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A93143BF2DF61B3200FCD5DB /* Host.swift */,
|
||||
A98554602E058433009051BD /* HostsManager.swift */,
|
||||
);
|
||||
path = Host;
|
||||
sourceTree = "<group>";
|
||||
@@ -251,6 +259,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A98554582E0553AA009051BD /* KeyManager.swift */,
|
||||
A985545E2E056EDD009051BD /* KeychainLayer.swift */,
|
||||
);
|
||||
path = Keys;
|
||||
sourceTree = "<group>";
|
||||
@@ -413,10 +422,13 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A985545F2E056EDD009051BD /* KeychainLayer.swift in Sources */,
|
||||
A93143C62DF61FE300FCD5DB /* ViewModifiers.swift in Sources */,
|
||||
A98554632E0587DF009051BD /* HostsView.swift in Sources */,
|
||||
A92538C82DEE0742007E0A18 /* ContentView.swift in Sources */,
|
||||
A93143C02DF61B3200FCD5DB /* Host.swift in Sources */,
|
||||
A92538C92DEE0742007E0A18 /* ShhShellApp.swift in Sources */,
|
||||
A98554612E058433009051BD /* HostsManager.swift in Sources */,
|
||||
A985545D2E055D4D009051BD /* ConnectionView.swift in Sources */,
|
||||
A91AE3B22DF73E0900FF3537 /* TerminalView.swift in Sources */,
|
||||
A98554592E0553AA009051BD /* KeyManager.swift in Sources */,
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol HostPr: Codable {
|
||||
protocol HostPr: Codable, Identifiable {
|
||||
var id: UUID { get set }
|
||||
var address: String { get set }
|
||||
var port: Int { get set }
|
||||
var username: String { get set }
|
||||
@@ -16,7 +17,8 @@ protocol HostPr: Codable {
|
||||
}
|
||||
|
||||
struct Host: HostPr {
|
||||
var address: String = "address"
|
||||
var id = UUID()
|
||||
var address: String = ""
|
||||
var port: Int
|
||||
var username: String
|
||||
var password: String
|
||||
@@ -37,18 +39,11 @@ struct Host: HostPr {
|
||||
}
|
||||
}
|
||||
|
||||
struct blankHost: HostPr {
|
||||
var address: String = ""
|
||||
var port: Int = 22
|
||||
var username: String = ""
|
||||
var password: String = ""
|
||||
var key: Data? = nil
|
||||
}
|
||||
|
||||
struct debugHost: HostPr {
|
||||
var address: String = "localhost"
|
||||
var port: Int = 22
|
||||
var username: String = "default"
|
||||
var password: String = ""
|
||||
var key: Data? = nil
|
||||
extension Host {
|
||||
static var blank: Host {
|
||||
Host(address: "", port: 22, username: "", password: "")
|
||||
}
|
||||
static var debug: Host {
|
||||
Host(address: "localhost", port: 22, username: "default", password: "")
|
||||
}
|
||||
}
|
||||
|
||||
35
ShhShell/Host/HostsManager.swift
Normal file
35
ShhShell/Host/HostsManager.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// HostsManager.swift
|
||||
// ShhShell
|
||||
//
|
||||
// Created by neon443 on 20/06/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class HostsManager: ObservableObject {
|
||||
private let userDefaults = NSUbiquitousKeyValueStore.default
|
||||
|
||||
@Published var savedHosts: [Host] = []
|
||||
|
||||
init() {
|
||||
loadSavedHosts()
|
||||
}
|
||||
|
||||
func loadSavedHosts() {
|
||||
let decoder = JSONDecoder()
|
||||
guard let data = userDefaults.data(forKey: "savedHosts") else { return }
|
||||
|
||||
if let decoded = try? decoder.decode([Host].self, from: data) {
|
||||
self.savedHosts = decoded
|
||||
}
|
||||
}
|
||||
|
||||
func saveSavedHosts() {
|
||||
let encoder = JSONEncoder()
|
||||
if let encoded = try? encoder.encode(savedHosts) {
|
||||
userDefaults.set(encoded, forKey: "savedHosts")
|
||||
userDefaults.synchronize()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
struct Key: Identifiable, Hashable {
|
||||
var id = UUID()
|
||||
@@ -16,6 +17,13 @@ struct Key: Identifiable, Hashable {
|
||||
}
|
||||
|
||||
class KeyManager: ObservableObject {
|
||||
func generateEd25519() {
|
||||
let privateKey = Curve25519.Signing.PrivateKey()
|
||||
let publicKeyData = privateKey.publicKey
|
||||
dump(privateKey.rawRepresentation)
|
||||
print(publicKeyData.rawRepresentation)
|
||||
}
|
||||
|
||||
func generateRSA() throws {
|
||||
let type = kSecAttrKeyTypeRSA
|
||||
let tag = "com.neon443.ShhSell.keys.\(Date().timeIntervalSince1970)".data(using: .utf8)!
|
||||
|
||||
95
ShhShell/Keys/KeychainLayer.swift
Normal file
95
ShhShell/Keys/KeychainLayer.swift
Normal file
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// KeychainLayer.swift
|
||||
// ShhShell
|
||||
//
|
||||
// Created by neon443 on 20/06/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
//https://developer.apple.com/documentation/cryptokit/storing-cryptokit-keys-in-the-keychain
|
||||
protocol SecKeyConvertible: CustomStringConvertible {
|
||||
// cretes a ket from an x9.63 represenation
|
||||
init<Bytes>(x963Representation: Bytes) throws where Bytes: ContiguousBytes
|
||||
|
||||
//an x9.63 representation of the key
|
||||
var x963Representation: Data { get }
|
||||
}
|
||||
|
||||
protocol GenericPasswordConvertible {
|
||||
//creates key from generic rep
|
||||
init<D>(genericKeyRepresentation data: D) throws where D: ContiguousBytes
|
||||
|
||||
//generic rep of key
|
||||
var genericKeyRepresentation: SymmetricKey { get }
|
||||
}
|
||||
|
||||
extension Curve25519.KeyAgreement.PrivateKey: GenericPasswordConvertible {
|
||||
init<D>(genericKeyRepresentation data: D) throws where D: ContiguousBytes {
|
||||
try self.init(rawRepresentation: data)
|
||||
}
|
||||
|
||||
var genericKeyRepresentation: SymmetricKey {
|
||||
self.rawRepresentation.withUnsafeBytes {
|
||||
SymmetricKey(data: $0)
|
||||
}
|
||||
}
|
||||
}
|
||||
extension Curve25519.Signing.PrivateKey: GenericPasswordConvertible {
|
||||
init<D>(genericKeyRepresentation data: D) throws where D: ContiguousBytes {
|
||||
try self.init(rawRepresentation: data)
|
||||
}
|
||||
|
||||
var genericKeyRepresentation: SymmetricKey {
|
||||
self.rawRepresentation.withUnsafeBytes {
|
||||
SymmetricKey(data: $0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum KeyStoreError: Error {
|
||||
case KeyStoreError(String)
|
||||
}
|
||||
|
||||
func storeKey<T: SecKeyConvertible>(_ key: T, label: String) throws {
|
||||
let attributes = [kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
|
||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate] as [String: Any]
|
||||
|
||||
guard let secKey = SecKeyCreateWithData(
|
||||
key.x963Representation as CFData,
|
||||
attributes as CFDictionary,
|
||||
nil
|
||||
) else {
|
||||
throw KeyStoreError.KeyStoreError("unable to create SecKey represntation")
|
||||
}
|
||||
|
||||
let query = [kSecClass: kSecClassKey,
|
||||
kSecAttrApplicationLabel: label,
|
||||
kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,
|
||||
kSecUseDataProtectionKeychain: true,
|
||||
kSecValueRef: secKey] as [String: Any]
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
guard status == errSecSuccess else {
|
||||
throw KeyStoreError.KeyStoreError("unable to sstore item \(status)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func retrieveKey(label: String) throws -> SecKey? {
|
||||
let query = [kSecClass: kSecClassKey,
|
||||
kSecAttrApplicationLabel: label,
|
||||
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
|
||||
kSecUseDataProtectionKeychain: true,
|
||||
kSecReturnRef: true] as [String: Any]
|
||||
|
||||
var item: CFTypeRef?
|
||||
var secKey: SecKey
|
||||
switch SecItemCopyMatching(query as CFDictionary, &item) {
|
||||
case errSecSuccess: secKey = item as! SecKey
|
||||
case errSecItemNotFound: return nil
|
||||
case let status: throw KeyStoreError.KeyStoreError("keychain read failed")
|
||||
}
|
||||
return secKey
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class SSHHandler: ObservableObject {
|
||||
) {
|
||||
self.host = host
|
||||
#if DEBUG
|
||||
self.host = debugHost()
|
||||
self.host = Host.debug
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -87,9 +87,9 @@ class SSHHandler: ObservableObject {
|
||||
}
|
||||
ssh_disconnect(session)
|
||||
ssh_free(session)
|
||||
session = nil
|
||||
withAnimation { authorized = false }
|
||||
withAnimation { connected = false }
|
||||
session = nil
|
||||
host.key = nil
|
||||
}
|
||||
|
||||
@@ -296,12 +296,12 @@ class SSHHandler: ObservableObject {
|
||||
guard status == SSH_OK else { return }
|
||||
|
||||
self.readTimer = Timer(timeInterval: 0.1, repeats: true) { timer in
|
||||
guard ssh_channel_is_open(self.channel) != 0 else {
|
||||
guard self.connected else {
|
||||
timer.invalidate()
|
||||
self.readTimer = nil
|
||||
return
|
||||
}
|
||||
guard ssh_channel_is_eof(self.channel) == 0 else {
|
||||
guard ssh_channel_is_open(self.channel) != 0 && ssh_channel_is_eof(self.channel) == 0 else {
|
||||
timer.invalidate()
|
||||
self.readTimer = nil
|
||||
return
|
||||
|
||||
@@ -9,14 +9,16 @@ import SwiftUI
|
||||
|
||||
@main
|
||||
struct ShhShellApp: App {
|
||||
@StateObject var sshHandler: SSHHandler = SSHHandler(host: blankHost())
|
||||
@StateObject var sshHandler: SSHHandler = SSHHandler(host: Host.blank)
|
||||
@StateObject var keyManager: KeyManager = KeyManager()
|
||||
@StateObject var hostsManager: HostsManager = HostsManager()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView(
|
||||
handler: sshHandler,
|
||||
keyManager: keyManager
|
||||
keyManager: keyManager,
|
||||
hostsManager: hostsManager
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
struct ConnectionView: View {
|
||||
@StateObject var handler: SSHHandler
|
||||
@StateObject var keyManager: KeyManager
|
||||
@StateObject var hostsManager: HostsManager
|
||||
|
||||
@State var passphrase: String = ""
|
||||
|
||||
@@ -156,7 +157,8 @@ struct ConnectionView: View {
|
||||
|
||||
#Preview {
|
||||
ConnectionView(
|
||||
handler: SSHHandler(host: debugHost()),
|
||||
keyManager: KeyManager()
|
||||
handler: SSHHandler(host: Host.debug),
|
||||
keyManager: KeyManager(),
|
||||
hostsManager: HostsManager()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,15 +10,16 @@ import SwiftUI
|
||||
struct ContentView: View {
|
||||
@ObservedObject var handler: SSHHandler
|
||||
@ObservedObject var keyManager: KeyManager
|
||||
@ObservedObject var hostsManager: HostsManager
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
ConnectionView(
|
||||
handler: handler,
|
||||
keyManager: keyManager
|
||||
HostsView(
|
||||
keyManager: keyManager,
|
||||
hostsManager: hostsManager
|
||||
)
|
||||
.tabItem {
|
||||
Label("Connection", systemImage: "powerplug.portrait")
|
||||
Label("Hosts", systemImage: "server.rack")
|
||||
}
|
||||
KeyManagerView(keyManager: keyManager)
|
||||
.tabItem {
|
||||
@@ -30,7 +31,8 @@ struct ContentView: View {
|
||||
|
||||
#Preview {
|
||||
ContentView(
|
||||
handler: SSHHandler(host: debugHost()),
|
||||
keyManager: KeyManager()
|
||||
handler: SSHHandler(host: Host.debug),
|
||||
keyManager: KeyManager(),
|
||||
hostsManager: HostsManager()
|
||||
)
|
||||
}
|
||||
|
||||
45
ShhShell/Views/HostsView.swift
Normal file
45
ShhShell/Views/HostsView.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// HostsView.swift
|
||||
// ShhShell
|
||||
//
|
||||
// Created by neon443 on 20/06/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HostsView: View {
|
||||
@ObservedObject var keyManager: KeyManager
|
||||
@ObservedObject var hostsManager: HostsManager
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Text("hi")
|
||||
ForEach(hostsManager.savedHosts) { host in
|
||||
NavigationLink() {
|
||||
ConnectionView(
|
||||
handler: SSHHandler(host: host),
|
||||
keyManager: keyManager,
|
||||
hostsManager: hostsManager
|
||||
)
|
||||
} label: {
|
||||
Text(host.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button {
|
||||
hostsManager.savedHosts.append(Host.blank)
|
||||
} label: {
|
||||
Label("Add", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
HostsView(keyManager: KeyManager(), hostsManager: HostsManager())
|
||||
}
|
||||
@@ -11,20 +11,18 @@ struct KeyManagerView: View {
|
||||
@ObservedObject var keyManager: KeyManager
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Button("ed25519") {
|
||||
keyManager.generateEd25519()
|
||||
}
|
||||
Button("rsa") {
|
||||
do {
|
||||
try keyManager.generateEd25519()
|
||||
try keyManager.generateRSA()
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
// Button("rsa") {
|
||||
// do {
|
||||
// try keyManager.generateRSA()
|
||||
// } catch {
|
||||
// print(error.localizedDescription)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,5 +39,5 @@ struct TerminalView: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
TerminalView(handler: SSHHandler(host: debugHost()))
|
||||
TerminalView(handler: SSHHandler(host: Host.debug))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user