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)
This commit is contained in:
neon443
2025-07-01 17:41:42 +01:00
parent 15bcd2b7e9
commit 65eef7f29e
13 changed files with 457 additions and 135 deletions

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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<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:
print(status)
throw KeyStoreError.KeyStoreError("keychain read failed")
}
// return secKey
var error: Unmanaged<CFError>?
guard (SecKeyCopyExternalRepresentation(secKey, &error) as Data?) != nil else {
throw KeyStoreError.KeyStoreError(error.debugDescription)
}
// let key = try T(x963Representation: data)
return nil
}

View File

@@ -0,0 +1,84 @@
/*
See the LICENSE.txt file for this samples 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<D>(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<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)
}
}
}
// Ensure that SymmetricKey is generic password convertible.
extension SymmetricKey: GenericPasswordConvertible {
init<D>(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<D>(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<D>(genericKeyRepresentation data: D) throws where D: ContiguousBytes {
try self.init(dataRepresentation: data.withUnsafeBytes { Data($0) })
}
var genericKeyRepresentation: SymmetricKey {
return SymmetricKey(data: dataRepresentation)
}
}

View File

@@ -0,0 +1,83 @@
/*
See the LICENSE.txt file for this samples 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<T: GenericPasswordConvertible>(_ 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<T: GenericPasswordConvertible>(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<T: GenericPasswordConvertible>(_ 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)")
}
}
}

View File

@@ -0,0 +1,29 @@
/*
See the LICENSE.txt file for this samples 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)
}
}

View File

@@ -0,0 +1,36 @@
/*
See the LICENSE.txt file for this samples 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<Bytes>(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 {}

View File

@@ -0,0 +1,99 @@
/*
See the LICENSE.txt file for this samples 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<T: SecKeyConvertible>(_ 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<T: SecKeyConvertible>(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<CFError>?
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<T: SecKeyConvertible>(_ 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)")
}
}
}

View File

@@ -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()

View File

@@ -86,7 +86,7 @@ import CryptoKit
KeyDetailView(
hostsManager: HostsManager(),
keypair: Keypair(
type: .ecdsa,
type: .ed25519,
name: "previewKey",
privateKey: Curve25519.Signing.PrivateKey().rawRepresentation
)

View File

@@ -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") {
}
}