diff --git a/ShhShell/Keys/KeyManager.swift b/ShhShell/Keys/KeyManager.swift index 893afe5..c35e0ed 100644 --- a/ShhShell/Keys/KeyManager.swift +++ b/ShhShell/Keys/KeyManager.swift @@ -19,8 +19,6 @@ class KeyManager: ObservableObject { var keyIDs: [UUID] { keypairs.map { $0.id } } -// @Published var keyTypes: [UUID: KeyType] = [:] -// @Published var keyNames: [UUID: String] = [:] private let baseTag = "com.neon443.ShhShell.keys".data(using: .utf8)! init() { @@ -59,9 +57,8 @@ class KeyManager: ObservableObject { } func deleteKey(_ keypair: Keypair) { + withAnimation { keypairs.removeAll(where: { $0.id == keypair.id }) } removeFromKeycahin(keypair: keypair) - let keyID = keypair.id - withAnimation { keypairs.removeAll(where: { $0.id == keyID }) } saveKeypairs() } diff --git a/ShhShell/Keys/KeychainLayer/GenericPasswordConvertible.swift b/ShhShell/Keys/KeychainLayer/GenericPasswordConvertible.swift index 077b970..1e9bbe3 100644 --- a/ShhShell/Keys/KeychainLayer/GenericPasswordConvertible.swift +++ b/ShhShell/Keys/KeychainLayer/GenericPasswordConvertible.swift @@ -1,84 +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. -*/ + 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 } + /// 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." - } - } + /// 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) - } - } + 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) - } - } + 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 - } + 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) - } + 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) - } + 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 index ef8adc7..a0f1386 100644 --- a/ShhShell/Keys/KeychainLayer/GenericPasswordStore.swift +++ b/ShhShell/Keys/KeychainLayer/GenericPasswordStore.swift @@ -1,83 +1,86 @@ /* -See the LICENSE.txt file for this sample’s licensing information. - -Abstract: -Methods for storing generic password convertible items in the keychain. -*/ + 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)") - } - } + + /// 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, + kSecAttrSynchronizable: 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, + kSecAttrSynchronizable: 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, + kSecAttrSynchronizable: kSecAttrSynchronizableAny, + 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 index f2b58fe..2970c51 100644 --- a/ShhShell/Keys/KeychainLayer/KeyStoreError.swift +++ b/ShhShell/Keys/KeychainLayer/KeyStoreError.swift @@ -1,29 +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. -*/ + 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 - } + 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) - } + + /// 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 index 585f39a..e661f5b 100644 --- a/ShhShell/Keys/KeychainLayer/SecKeyConvertible.swift +++ b/ShhShell/Keys/KeychainLayer/SecKeyConvertible.swift @@ -1,30 +1,30 @@ /* -See the LICENSE.txt file for this sample’s licensing information. - -Abstract: -The interface required for conversion to a SecKey instance. -*/ + 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 } + /// 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." - } - } + /// 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. diff --git a/ShhShell/Keys/KeychainLayer/SecKeyStore.swift b/ShhShell/Keys/KeychainLayer/SecKeyStore.swift index e093c15..efc5b4a 100644 --- a/ShhShell/Keys/KeychainLayer/SecKeyStore.swift +++ b/ShhShell/Keys/KeychainLayer/SecKeyStore.swift @@ -1,99 +1,99 @@ /* -See the LICENSE.txt file for this sample’s licensing information. - -Abstract: -Methods for storing SecKey convertible items in the keychain. -*/ + 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)") - } - } + + /// 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)") + } + } }