From 65eef7f29ef090ac44e33f523851a2e1589977c5 Mon Sep 17 00:00:00 2001 From: neon443 <69979447+neon443@users.noreply.github.com> Date: Tue, 1 Jul 2025 17:41:42 +0100 Subject: [PATCH] added support for saving keys in the keychain load and save yk added savetokeychain to save a key to the keychain added getfromkeycahin to get a key from the keychain fix generatekey added apple's cryptokit in the keychain sample code (keychain layer dir) --- ShhShell.xcodeproj/project.pbxproj | 32 +++++- ShhShell/Host/HostsManager.swift | 2 +- ShhShell/Keys/KeyManager.swift | 99 ++++++++++++++--- ShhShell/Keys/KeyType.swift | 10 +- ShhShell/Keys/KeychainLayer.swift | 104 ------------------ .../GenericPasswordConvertible.swift | 84 ++++++++++++++ .../KeychainLayer/GenericPasswordStore.swift | 83 ++++++++++++++ .../Keys/KeychainLayer/KeyStoreError.swift | 29 +++++ .../KeychainLayer/SecKeyConvertible.swift | 36 ++++++ ShhShell/Keys/KeychainLayer/SecKeyStore.swift | 99 +++++++++++++++++ ShhShell/Keys/Keypair.swift | 2 +- ShhShell/Views/Keys/KeyDetailView.swift | 2 +- ShhShell/Views/Keys/KeyManagerView.swift | 10 +- 13 files changed, 457 insertions(+), 135 deletions(-) delete mode 100644 ShhShell/Keys/KeychainLayer.swift create mode 100644 ShhShell/Keys/KeychainLayer/GenericPasswordConvertible.swift create mode 100644 ShhShell/Keys/KeychainLayer/GenericPasswordStore.swift create mode 100644 ShhShell/Keys/KeychainLayer/KeyStoreError.swift create mode 100644 ShhShell/Keys/KeychainLayer/SecKeyConvertible.swift create mode 100644 ShhShell/Keys/KeychainLayer/SecKeyStore.swift diff --git a/ShhShell.xcodeproj/project.pbxproj b/ShhShell.xcodeproj/project.pbxproj index 7049005..5d9bad7 100644 --- a/ShhShell.xcodeproj/project.pbxproj +++ b/ShhShell.xcodeproj/project.pbxproj @@ -52,7 +52,6 @@ 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 */; }; A9A587202E0BF220006B31E6 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = A9A5871F2E0BF220006B31E6 /* SwiftTerm */; }; @@ -65,6 +64,11 @@ A9D819312E102D8700442D38 /* HostkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D819302E102D8700442D38 /* HostkeysView.swift */; }; A9DA97712E0D30ED00142DDC /* HostSymbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DA97702E0D30ED00142DDC /* HostSymbol.swift */; }; A9DA97732E0D40C100142DDC /* SymbolPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DA97722E0D40C100142DDC /* SymbolPreview.swift */; }; + A9FD37552E143D23005319A8 /* SecKeyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD37542E143D23005319A8 /* SecKeyConvertible.swift */; }; + A9FD37572E143D5A005319A8 /* SecKeyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD37562E143D5A005319A8 /* SecKeyStore.swift */; }; + 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 */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -150,7 +154,6 @@ A98554542E05535F009051BD /* KeyManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManagerView.swift; sourceTree = ""; }; A98554582E0553AA009051BD /* KeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManager.swift; sourceTree = ""; }; A985545C2E055D4D009051BD /* ConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionView.swift; sourceTree = ""; }; - A985545E2E056EDD009051BD /* KeychainLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainLayer.swift; sourceTree = ""; }; A98554602E058433009051BD /* HostsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsManager.swift; sourceTree = ""; }; A98554622E0587DF009051BD /* HostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsView.swift; sourceTree = ""; }; A9B15A992E0ABA0400F66E02 /* DialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogView.swift; sourceTree = ""; }; @@ -162,6 +165,11 @@ A9D819302E102D8700442D38 /* HostkeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostkeysView.swift; sourceTree = ""; }; A9DA97702E0D30ED00142DDC /* HostSymbol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostSymbol.swift; sourceTree = ""; }; A9DA97722E0D40C100142DDC /* SymbolPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolPreview.swift; sourceTree = ""; }; + A9FD37542E143D23005319A8 /* SecKeyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecKeyConvertible.swift; sourceTree = ""; }; + A9FD37562E143D5A005319A8 /* SecKeyStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecKeyStore.swift; sourceTree = ""; }; + A9FD37582E143D74005319A8 /* GenericPasswordConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPasswordConvertible.swift; sourceTree = ""; }; + A9FD375A2E143D77005319A8 /* GenericPasswordStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPasswordStore.swift; sourceTree = ""; }; + A9FD375C2E143D7E005319A8 /* KeyStoreError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyStoreError.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -376,9 +384,9 @@ A98554572E055398009051BD /* Keys */ = { isa = PBXGroup; children = ( + A9FD37532E143D11005319A8 /* KeychainLayer */, A96C90A22E12D53900724253 /* KeyType.swift */, A98554582E0553AA009051BD /* KeyManager.swift */, - A985545E2E056EDD009051BD /* KeychainLayer.swift */, A96C6AFD2E0C43B600F377FE /* Keypair.swift */, ); path = Keys; @@ -414,6 +422,18 @@ path = Themes; sourceTree = ""; }; + A9FD37532E143D11005319A8 /* KeychainLayer */ = { + isa = PBXGroup; + children = ( + A9FD37542E143D23005319A8 /* SecKeyConvertible.swift */, + A9FD37562E143D5A005319A8 /* SecKeyStore.swift */, + A9FD37582E143D74005319A8 /* GenericPasswordConvertible.swift */, + A9FD375A2E143D77005319A8 /* GenericPasswordStore.swift */, + A9FD375C2E143D7E005319A8 /* KeyStoreError.swift */, + ); + path = KeychainLayer; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -572,9 +592,9 @@ A9D8192F2E0F1BEE00442D38 /* ThemePreview.swift in Sources */, A96C6B022E0C49E800F377FE /* CenteredLabel.swift in Sources */, A923172F2E08851200ECE1E6 /* ShellView.swift in Sources */, - A985545F2E056EDD009051BD /* KeychainLayer.swift in Sources */, A9D819292E0E904200442D38 /* Theme.swift in Sources */, A9D8192D2E0E9EB500442D38 /* ThemeManagerView.swift in Sources */, + A9FD375D2E143D7E005319A8 /* KeyStoreError.swift in Sources */, A93143C62DF61FE300FCD5DB /* ViewModifiers.swift in Sources */, A98554632E0587DF009051BD /* HostsView.swift in Sources */, A96C6A8A2E0C0B1100F377FE /* SSHState.swift in Sources */, @@ -593,14 +613,18 @@ A96C90A32E12D53B00724253 /* KeyType.swift in Sources */, A98554612E058433009051BD /* HostsManager.swift in Sources */, A985545D2E055D4D009051BD /* ConnectionView.swift in Sources */, + A9FD37592E143D74005319A8 /* GenericPasswordConvertible.swift in Sources */, A98554592E0553AA009051BD /* KeyManager.swift in Sources */, A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */, A9D819312E102D8700442D38 /* HostkeysView.swift in Sources */, A98554552E05535F009051BD /* KeyManagerView.swift in Sources */, A923172D2E07138000ECE1E6 /* SSHTerminalDelegate.swift in Sources */, + A9FD37552E143D23005319A8 /* SecKeyConvertible.swift in Sources */, A96C6AFE2E0C43B600F377FE /* Keypair.swift in Sources */, A9C4140C2E096DB7005E3047 /* SSHError.swift in Sources */, A96BE6AA2E116EC000C0FEE9 /* TerminalViewContainer.swift in Sources */, + A9FD375B2E143D77005319A8 /* GenericPasswordStore.swift in Sources */, + A9FD37572E143D5A005319A8 /* SecKeyStore.swift in Sources */, A923172A2E07113100ECE1E6 /* TerminalController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ShhShell/Host/HostsManager.swift b/ShhShell/Host/HostsManager.swift index e695e53..d0b8189 100644 --- a/ShhShell/Host/HostsManager.swift +++ b/ShhShell/Host/HostsManager.swift @@ -191,7 +191,7 @@ class HostsManager: ObservableObject, @unchecked Sendable { string.contains("-----") { keypair = KeyManager.importSSHPrivkey(priv: string) } else { - keypair = Keypair(type: .ecdsa, name: UUID().uuidString, privateKey: privateKey) + keypair = Keypair(type: .ed25519, name: UUID().uuidString, privateKey: privateKey) } if !result.contains(keypair) { result.append(keypair) diff --git a/ShhShell/Keys/KeyManager.swift b/ShhShell/Keys/KeyManager.swift index d7a1205..9930d8e 100644 --- a/ShhShell/Keys/KeyManager.swift +++ b/ShhShell/Keys/KeyManager.swift @@ -8,6 +8,7 @@ import Foundation import CryptoKit import Security +import SwiftUI struct Key: Identifiable, Hashable { var id = UUID() @@ -20,28 +21,96 @@ struct Key: Identifiable, Hashable { class KeyManager: ObservableObject { private let userdefaults = NSUbiquitousKeyValueStore.default - var tags: [String] = [] + @Published var keypairs: [Keypair] = [] + var keyIDs: [UUID: KeyType] = [:] + var keyNames: [UUID: String] = [:] + private let baseTag = "com.neon443.ShhShell.keys".data(using: .utf8)! - func loadTags() { - userdefaults.synchronize() - let decoder = JSONDecoder() - guard let data = userdefaults.data(forKey: "keyTags") else { return } - guard let decoded = try? decoder.decode([String].self, from: data) else { return } - tags = decoded + init() { + loadKeyIDs() + for id in keyIDs.keys { + guard let keypair = getFromKeychain(keyID: id) else { continue } + keypairs.append(keypair) + } } - func saveTags() { - let encoder = JSONEncoder() - guard let encoded = try? encoder.encode(tags) else { return } - userdefaults.set(encoded, forKey: "keyTags") + func loadKeyIDs() { userdefaults.synchronize() + 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 + + guard let dataNames = userdefaults.data(forKey: "keyNames") else { return } + guard let decodedNames = try? decoder.decode([UUID:String].self, from: dataNames) else { return } + keyNames = decodedNames + } + + func saveKeyIDs() { + let encoder = JSONEncoder() + guard let encoded = try? encoder.encode(keyIDs) else { return } + userdefaults.set(encoded, forKey: "keyIDs") + + guard let encodedNames = try? encoder.encode(keyNames) else { return } + userdefaults.set(encodedNames, forKey: "keyNames") + userdefaults.synchronize() + } + + func saveToKeychain(_ keypair: Keypair) { + withAnimation { + keyIDs.updateValue(keypair.type, forKey: keypair.id) + keyNames.updateValue(keypair.name, forKey: keypair.id) + } + saveKeyIDs() + if keypair.type == .ed25519 { + let curve25519 = try! Curve25519.Signing.PrivateKey(rawRepresentation: keypair.privateKey) + try! GenericPasswordStore().storeKey(curve25519.genericKeyRepresentation, account: keypair.id.uuidString) + } else { + let tag = baseTag+keypair.id.uuidString.data(using: .utf8)! + let addQuery: [String: Any] = [kSecClass as String: kSecClassGenericPassword, + kSecAttrApplicationTag as String: tag, + kSecValueRef as String: keypair.privateKey, + kSecAttrSynchronizable as String: kCFBooleanTrue!] + let status = SecItemAdd(addQuery as CFDictionary, nil) + guard status == errSecSuccess else { fatalError() } + } + } + + func getFromKeychain(keyID: UUID) -> Keypair? { + guard let keyType = keyIDs[keyID] else { return nil } + guard let keyName = keyNames[keyID] else { return nil } + if keyType == .ed25519 { + var key: Curve25519.Signing.PrivateKey? + key = try? GenericPasswordStore().readKey(account: keyID.uuidString) + guard let key else { return nil } + return Keypair(type: keyType, name: keyName, privateKey: key.rawRepresentation) + } else { + let tag = baseTag+keyID.uuidString.data(using: .utf8)! + let getQuery: [String: Any] = [kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: tag, + kSecAttrKeyType as String: kSecAttrKeyTypeEC, + kSecReturnRef as String: true] + var item: CFTypeRef? + let status = SecItemCopyMatching(getQuery as CFDictionary, &item) + guard status == errSecSuccess else { fatalError() } + return Keypair( + type: keyType, + name: keyName, + privateKey: item as! Data + ) + } } //MARK: generate keys - func generateKey(type: KeyType, SEPKeyTag: String, comment: String, passphrase: String) -> Keypair? { + func generateKey(type: KeyType, comment: String) { switch type { - case .ecdsa: - Keypair(type: .ecdsa, name: comment, privateKey: Curve25519.Signing.PrivateKey().rawRepresentation) + case .ed25519: + let keypair = Keypair( + type: .ed25519, + name: comment, + privateKey: Curve25519.Signing.PrivateKey().rawRepresentation + ) + saveToKeychain(keypair) case .rsa: fatalError("unimplemented") } @@ -109,7 +178,7 @@ class KeyManager: ObservableObject { let comment = String(data: extractField(&dataBlob), encoding: .utf8)! - return Keypair(type: .ecdsa, name: comment, privateKey: privatekeyData) + return Keypair(type: .ed25519, name: comment, privateKey: privatekeyData) } static func makeSSHPrivkey(_ keypair: Keypair) -> Data { diff --git a/ShhShell/Keys/KeyType.swift b/ShhShell/Keys/KeyType.swift index d007f5a..2d23c26 100644 --- a/ShhShell/Keys/KeyType.swift +++ b/ShhShell/Keys/KeyType.swift @@ -8,15 +8,15 @@ import Foundation enum KeyType: Codable, Equatable, Hashable, CustomStringConvertible { + case ed25519 + case rsa + var description: String { switch self { - case .ecdsa: - return "ECDSA" + case .ed25519: + return "Ed25519" case .rsa: return "RSA" } } - - case ecdsa - case rsa } diff --git a/ShhShell/Keys/KeychainLayer.swift b/ShhShell/Keys/KeychainLayer.swift deleted file mode 100644 index 5522859..0000000 --- a/ShhShell/Keys/KeychainLayer.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// 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(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(genericKeyRepresentation data: D) throws where D: ContiguousBytes - - //generic rep of key - var genericKeyRepresentation: SymmetricKey { get } -} - -extension Curve25519.KeyAgreement.PrivateKey: GenericPasswordConvertible { - init(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(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(_ 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: - print(status) - throw KeyStoreError.KeyStoreError("keychain read failed") - } -// return secKey - - var error: Unmanaged? - guard (SecKeyCopyExternalRepresentation(secKey, &error) as Data?) != nil else { - throw KeyStoreError.KeyStoreError(error.debugDescription) - } -// let key = try T(x963Representation: data) - return nil -} diff --git a/ShhShell/Keys/KeychainLayer/GenericPasswordConvertible.swift b/ShhShell/Keys/KeychainLayer/GenericPasswordConvertible.swift new file mode 100644 index 0000000..077b970 --- /dev/null +++ b/ShhShell/Keys/KeychainLayer/GenericPasswordConvertible.swift @@ -0,0 +1,84 @@ +/* +See the LICENSE.txt file for this sample’s licensing information. + +Abstract: +The interface required for conversion to a generic password keychain item. +*/ + +import Foundation +import CryptoKit + +/// The interface needed for SecKey conversion. +protocol GenericPasswordConvertible: CustomStringConvertible { + /// Creates a key from a generic key representation. + init(genericKeyRepresentation data: D) throws where D: ContiguousBytes + + /// A generic representation of the key. + var genericKeyRepresentation: SymmetricKey { get } +} + +extension GenericPasswordConvertible { + /// A string version of the key for visual inspection. + /// IMPORTANT: Never log the actual key data. + public var description: String { + return self.genericKeyRepresentation.withUnsafeBytes { bytes in + return "Key representation contains \(bytes.count) bytes." + } + } +} + +// Declare that the Curve25519 keys are generic passord convertible. +extension Curve25519.KeyAgreement.PrivateKey: GenericPasswordConvertible { + init(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(genericKeyRepresentation data: D) throws where D: ContiguousBytes { + try self.init(rawRepresentation: data) + } + + var genericKeyRepresentation: SymmetricKey { + self.rawRepresentation.withUnsafeBytes { + SymmetricKey(data: $0) + } + } +} + +// Ensure that SymmetricKey is generic password convertible. +extension SymmetricKey: GenericPasswordConvertible { + init(genericKeyRepresentation data: D) throws where D: ContiguousBytes { + self.init(data: data) + } + + var genericKeyRepresentation: SymmetricKey { + self + } +} + +// Ensure that Secure Enclave keys are generic password convertible. +extension SecureEnclave.P256.KeyAgreement.PrivateKey: GenericPasswordConvertible { + init(genericKeyRepresentation data: D) throws where D: ContiguousBytes { + try self.init(dataRepresentation: data.withUnsafeBytes { Data($0) }) + } + + var genericKeyRepresentation: SymmetricKey { + return SymmetricKey(data: dataRepresentation) + } +} + +extension SecureEnclave.P256.Signing.PrivateKey: GenericPasswordConvertible { + init(genericKeyRepresentation data: D) throws where D: ContiguousBytes { + try self.init(dataRepresentation: data.withUnsafeBytes { Data($0) }) + } + + var genericKeyRepresentation: SymmetricKey { + return SymmetricKey(data: dataRepresentation) + } +} diff --git a/ShhShell/Keys/KeychainLayer/GenericPasswordStore.swift b/ShhShell/Keys/KeychainLayer/GenericPasswordStore.swift new file mode 100644 index 0000000..ef8adc7 --- /dev/null +++ b/ShhShell/Keys/KeychainLayer/GenericPasswordStore.swift @@ -0,0 +1,83 @@ +/* +See the LICENSE.txt file for this sample’s licensing information. + +Abstract: +Methods for storing generic password convertible items in the keychain. +*/ + +import Foundation +import CryptoKit +import Security + +struct GenericPasswordStore { + + /// Stores a CryptoKit key in the keychain as a generic password. + func storeKey(_ key: T, account: String) throws { + + // Treat the key data as a generic password. + try key.genericKeyRepresentation.withUnsafeBytes { keyBytes in + let cfd = Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: keyBytes.baseAddress!), count: keyBytes.count, deallocator: .none) + let query = [kSecClass: kSecClassGenericPassword, + kSecAttrAccount: account, + kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked, + kSecUseDataProtectionKeychain: true, + kSecValueData: cfd] as [String: Any] + + // Add the key data. + let status = SecItemAdd(query as CFDictionary, nil) + guard status == errSecSuccess else { + throw KeyStoreError("Unable to store item: \(status.message)") + } + } + + } + + /// Reads a CryptoKit key from the keychain as a generic password. + func readKey(account: String) throws -> T? { + + // Seek a generic password with the given account. + let query = [kSecClass: kSecClassGenericPassword, + kSecAttrAccount: account, + kSecUseDataProtectionKeychain: true, + kSecReturnData: true] as [String: Any] + + // Find and cast the result as data. + var item: CFTypeRef? + switch SecItemCopyMatching(query as CFDictionary, &item) { + case errSecSuccess: + guard let data = item as? Data else { return nil } + return try T(genericKeyRepresentation: data) // Convert back to a key. + case errSecItemNotFound: return nil + case let status: throw KeyStoreError("Keychain read failed: \(status.message)") + } + } + + /// Stores a key in the keychain and then reads it back. + func roundTrip(_ key: T) throws -> T { + + // An account name for the key in the keychain. + let account = "com.example.genericpassword.key" + + // Start fresh. + try deleteKey(account: account) + + // Store and read it back. + try storeKey(key, account: account) + guard let key: T = try readKey(account: account) else { + throw KeyStoreError("Failed to locate stored key.") + } + return key + } + + /// Removes any existing key with the given account. + func deleteKey(account: String) throws { + let query = [kSecClass: kSecClassGenericPassword, + kSecUseDataProtectionKeychain: true, + kSecAttrAccount: account] as [String: Any] + switch SecItemDelete(query as CFDictionary) { + case errSecItemNotFound, errSecSuccess: break // Okay to ignore + case let status: + throw KeyStoreError("Unexpected deletion error: \(status.message)") + } + } +} diff --git a/ShhShell/Keys/KeychainLayer/KeyStoreError.swift b/ShhShell/Keys/KeychainLayer/KeyStoreError.swift new file mode 100644 index 0000000..f2b58fe --- /dev/null +++ b/ShhShell/Keys/KeychainLayer/KeyStoreError.swift @@ -0,0 +1,29 @@ +/* +See the LICENSE.txt file for this sample’s licensing information. + +Abstract: +Errors that can be generated as a result of attempting to store keys. +*/ + +import Foundation + +/// An error we can throw when something goes wrong. +struct KeyStoreError: Error, CustomStringConvertible { + var message: String + + init(_ message: String) { + self.message = message + } + + public var description: String { + return message + } +} + +extension OSStatus { + + /// A human readable message for the status. + var message: String { + return (SecCopyErrorMessageString(self, nil) as String?) ?? String(self) + } +} diff --git a/ShhShell/Keys/KeychainLayer/SecKeyConvertible.swift b/ShhShell/Keys/KeychainLayer/SecKeyConvertible.swift new file mode 100644 index 0000000..585f39a --- /dev/null +++ b/ShhShell/Keys/KeychainLayer/SecKeyConvertible.swift @@ -0,0 +1,36 @@ +/* +See the LICENSE.txt file for this sample’s licensing information. + +Abstract: +The interface required for conversion to a SecKey instance. +*/ + +import Foundation +import CryptoKit + +/// The interface needed for SecKey conversion. +protocol SecKeyConvertible: CustomStringConvertible { + /// Creates a key from an X9.63 representation. + init(x963Representation: Bytes) throws where Bytes: ContiguousBytes + + /// An X9.63 representation of the key. + var x963Representation: Data { get } +} + +extension SecKeyConvertible { + /// A string version of the key for visual inspection. + /// IMPORTANT: Never log the actual key data. + public var description: String { + return self.x963Representation.withUnsafeBytes { bytes in + return "Key representation contains \(bytes.count) bytes." + } + } +} + +// Assert that the NIST keys are convertible. +extension P256.Signing.PrivateKey: SecKeyConvertible {} +extension P256.KeyAgreement.PrivateKey: SecKeyConvertible {} +extension P384.Signing.PrivateKey: SecKeyConvertible {} +extension P384.KeyAgreement.PrivateKey: SecKeyConvertible {} +extension P521.Signing.PrivateKey: SecKeyConvertible {} +extension P521.KeyAgreement.PrivateKey: SecKeyConvertible {} diff --git a/ShhShell/Keys/KeychainLayer/SecKeyStore.swift b/ShhShell/Keys/KeychainLayer/SecKeyStore.swift new file mode 100644 index 0000000..e093c15 --- /dev/null +++ b/ShhShell/Keys/KeychainLayer/SecKeyStore.swift @@ -0,0 +1,99 @@ +/* +See the LICENSE.txt file for this sample’s licensing information. + +Abstract: +Methods for storing SecKey convertible items in the keychain. +*/ + +import Foundation +import CryptoKit +import Security + +struct SecKeyStore { + + /// Stores a CryptoKit key in the keychain as a SecKey instance. + func storeKey(_ key: T, label: String) throws { + + // Describe the key. + let attributes = [kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom, + kSecAttrKeyClass: kSecAttrKeyClassPrivate] as [String: Any] + + // Get a SecKey representation. + guard let secKey = SecKeyCreateWithData(key.x963Representation as CFData, + attributes as CFDictionary, + nil) + else { + throw KeyStoreError("Unable to create SecKey representation.") + } + + // Describe the add operation. + let query = [kSecClass: kSecClassKey, + kSecAttrApplicationLabel: label, + kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked, + kSecUseDataProtectionKeychain: true, + kSecValueRef: secKey] as [String: Any] + + // Add the key to the keychain. + let status = SecItemAdd(query as CFDictionary, nil) + guard status == errSecSuccess else { + throw KeyStoreError("Unable to store item: \(status.message)") + } + } + + /// Reads a CryptoKit key from the keychain as a SecKey instance. + func readKey(label: String) throws -> T? { + + // Seek an elliptic-curve key with a given label. + let query = [kSecClass: kSecClassKey, + kSecAttrApplicationLabel: label, + kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom, + kSecUseDataProtectionKeychain: true, + kSecReturnRef: true] as [String: Any] + + // Find and cast the result as a SecKey instance. + 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("Keychain read failed: \(status.message)") + } + + // Convert the SecKey into a CryptoKit key. + var error: Unmanaged? + guard let data = SecKeyCopyExternalRepresentation(secKey, &error) as Data? else { + throw KeyStoreError(error.debugDescription) + } + let key = try T(x963Representation: data) + + return key + } + + /// Stores a key in the keychain and then reads it back. + func roundTrip(_ key: T) throws -> T { + // A label for the key in the keychain. + let label = "com.example.seckey.key" + + // Start fresh. + try deleteKey(label: label) + + // Store it and then get it back. + try storeKey(key, label: label) + guard let key: T = try readKey(label: label) else { + throw KeyStoreError("Failed to locate stored key.") + } + return key + } + + /// Removes any existing key with the given label. + func deleteKey(label: String) throws { + let query = [kSecClass: kSecClassKey, + kSecUseDataProtectionKeychain: true, + kSecAttrApplicationLabel: label] as [String: Any] + switch SecItemDelete(query as CFDictionary) { + case errSecItemNotFound, errSecSuccess: break // Ignore these. + case let status: + throw KeyStoreError("Unexpected deletion error: \(status.message)") + } + } +} diff --git a/ShhShell/Keys/Keypair.swift b/ShhShell/Keys/Keypair.swift index 89ac164..424f93a 100644 --- a/ShhShell/Keys/Keypair.swift +++ b/ShhShell/Keys/Keypair.swift @@ -22,7 +22,7 @@ protocol KeypairProtocol: Identifiable, Equatable, Codable, Hashable { struct Keypair: KeypairProtocol { var id = UUID() - var type: KeyType = .ecdsa + var type: KeyType = .ed25519 var name: String = "" var publicKey: Data { (try? Curve25519.Signing.PrivateKey(rawRepresentation: privateKey).publicKey.rawRepresentation) ?? Data() diff --git a/ShhShell/Views/Keys/KeyDetailView.swift b/ShhShell/Views/Keys/KeyDetailView.swift index a425e93..a8154a6 100644 --- a/ShhShell/Views/Keys/KeyDetailView.swift +++ b/ShhShell/Views/Keys/KeyDetailView.swift @@ -86,7 +86,7 @@ import CryptoKit KeyDetailView( hostsManager: HostsManager(), keypair: Keypair( - type: .ecdsa, + type: .ed25519, name: "previewKey", privateKey: Curve25519.Signing.PrivateKey().rawRepresentation ) diff --git a/ShhShell/Views/Keys/KeyManagerView.swift b/ShhShell/Views/Keys/KeyManagerView.swift index 4728fa8..5cfca5b 100644 --- a/ShhShell/Views/Keys/KeyManagerView.swift +++ b/ShhShell/Views/Keys/KeyManagerView.swift @@ -22,16 +22,18 @@ struct KeyManagerView: View { NavigationLink { KeyDetailView(hostsManager: hostsManager, keypair: keypair) } label: { - Text(String(data: keypair.publicKey, encoding: .utf8) ?? "nil") + Text(keypair.openSshPubkey) } } } - Button("ed25519") { - + Section() { + ForEach(keyManager.keypairs) { kp in + Text(kp.openSshPubkey) + } } - Button("genereate rsa") { + Button("ed25519") { } }