diff --git a/.swiftlint.yml b/.swiftlint.yml index a9eaab6..b7c2931 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -3,3 +3,14 @@ excluded: disabled_rules: - cyclomatic_complexity - todo + - force_cast + +identifier_name: + min_length: 1 + +line_length: + warning: 200 + error: 250 + ignores_function_declarations: true + ignores_comments: true + ignores_urls: true \ No newline at end of file diff --git a/Sources/WalletSdk/DataConversions.swift b/Sources/WalletSdk/DataConversions.swift new file mode 100644 index 0000000..73d3e9a --- /dev/null +++ b/Sources/WalletSdk/DataConversions.swift @@ -0,0 +1,13 @@ +import Foundation + +extension Data { + var base64EncodedUrlSafe: String { + let string = self.base64EncodedString() + + // Make this URL safe and remove padding + return string + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } +} diff --git a/Sources/WalletSdk/KeyManager.swift b/Sources/WalletSdk/KeyManager.swift new file mode 100644 index 0000000..2c46c1e --- /dev/null +++ b/Sources/WalletSdk/KeyManager.swift @@ -0,0 +1,229 @@ +import CoreFoundation +import Foundation +import Security + +public class KeyManager: NSObject { + /** + * Resets the key store by removing all of the keys. + */ + static func reset() -> Bool { + let query: [String: Any] = [ + kSecClass as String: kSecClassKey + ] + + let ret = SecItemDelete(query as CFDictionary) + return ret == errSecSuccess + } + + /** + * Checks to see if a secret key exists based on the id/alias. + */ + static func keyExists(id: String) -> Bool { + let tag = id.data(using: .utf8)! + let query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: tag, + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, + kSecReturnRef as String: true + ] + + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + return status == errSecSuccess + } + + /** + * Returns a secret key - based on the id of the key. + */ + static func getSecretKey(id: String) -> SecKey? { + let tag = id.data(using: .utf8)! + let query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: tag, + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, + kSecReturnRef as String: true + ] + + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + + guard status == errSecSuccess else { return nil } + + let key = item as! SecKey + + return key + } + + /** + * Generates a secp256r1 signing key by id + */ + static func generateSigningKey(id: String) -> Bool { + let tag = id.data(using: .utf8)! + + let access = SecAccessControlCreateWithFlags( + kCFAllocatorDefault, + kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + .privateKeyUsage, + nil)! + + let attributes: [String: Any] = [ + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, + kSecAttrKeyClass as String: kSecAttrKeyClassPrivate, + kSecAttrKeySizeInBits as String: NSNumber(value: 256), + kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, + kSecPrivateKeyAttrs as String: [ + kSecAttrIsPermanent as String: true, + kSecAttrApplicationTag as String: tag, + kSecAttrAccessControl as String: access + ] + ] + + var error: Unmanaged? + SecKeyCreateRandomKey(attributes as CFDictionary, &error) + if error != nil { print(error!) } + return error == nil + } + + /** + * Returns a JWK for a particular secret key by key id. + */ + static func getJwk(id: String) -> String? { + guard let key = getSecretKey(id: id) else { return nil } + + guard let publicKey = SecKeyCopyPublicKey(key) else { + return nil + } + + var error: Unmanaged? + guard let data = SecKeyCopyExternalRepresentation(publicKey, &error) as? Data else { + return nil + } + + let fullData: Data = data.subdata(in: 1.. [UInt8]? { + guard let key = getSecretKey(id: id) else { return nil } + + guard let data = CFDataCreate(kCFAllocatorDefault, payload, payload.count) else { + return nil + } + + let algorithm: SecKeyAlgorithm = .ecdsaSignatureMessageX962SHA256 + var error: Unmanaged? + guard let signature = SecKeyCreateSignature( + key, + algorithm, + data, + &error + ) as Data? else { + print(error ?? "no error") + return nil + } + + return [UInt8](signature) + } + + /** + * Generates an encryption key with a provided id in the Secure Enclave. + */ + static func generateEncryptionKey(id: String) -> Bool { + let tag = id.data(using: .utf8)! + + let access = SecAccessControlCreateWithFlags( + kCFAllocatorDefault, + kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + .privateKeyUsage, + nil)! + + let attributes: [String: Any] = [ + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, + kSecAttrKeyClass as String: kSecAttrKeyClassPrivate, + kSecAttrKeySizeInBits as String: NSNumber(value: 256), + kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, + kSecPrivateKeyAttrs as String: [ + kSecAttrIsPermanent as String: true, + kSecAttrApplicationTag as String: tag, + kSecAttrAccessControl as String: access + ] + ] + + var error: Unmanaged? + SecKeyCreateRandomKey(attributes as CFDictionary, &error) + if error != nil { print(error ?? "no error") } + return error == nil + } + + /** + * Encrypts payload by a key referenced by key id. + */ + static func encryptPayload(id: String, payload: [UInt8]) -> ([UInt8], [UInt8])? { + guard let key = getSecretKey(id: id) else { return nil } + + guard let publicKey = SecKeyCopyPublicKey(key) else { + return nil + } + + guard let data = CFDataCreate(kCFAllocatorDefault, payload, payload.count) else { + return nil + } + + let algorithm: SecKeyAlgorithm = .eciesEncryptionCofactorX963SHA512AESGCM + var error: Unmanaged? + + guard let encrypted = SecKeyCreateEncryptedData( + publicKey, + algorithm, + data, + &error + ) as Data? else { + return nil + } + + return ([0], [UInt8](encrypted)) + } + + /** + * Decrypts the provided payload by a key id and initialization vector. + */ + static func decryptPayload(id: String, iv: [UInt8], payload: [UInt8]) -> [UInt8]? { + guard let key = getSecretKey(id: id) else { return nil } + + guard let data = CFDataCreate(kCFAllocatorDefault, payload, payload.count) else { + return nil + } + + let algorithm: SecKeyAlgorithm = .eciesEncryptionCofactorX963SHA512AESGCM + var error: Unmanaged? + guard let decrypted = SecKeyCreateDecryptedData( + key, + algorithm, + data, + &error + ) as Data? else { + return nil + } + + return [UInt8](decrypted) + } +} diff --git a/Tests/WalletSdkTests/DataConversions.swift b/Tests/WalletSdkTests/DataConversions.swift new file mode 100644 index 0000000..ee70884 --- /dev/null +++ b/Tests/WalletSdkTests/DataConversions.swift @@ -0,0 +1,27 @@ +// +// KeyManager.swift +// +// +// Created by Kuba on 6/4/24. +// + +import XCTest +@testable import SpruceIDWalletSdk + +final class DataConversions: XCTestCase { + + /** + * Tests to see if the base 64 url encoding correctly converts the sample data and + * replaces special characters. + */ + func testBase64EncodedUrlSafe() throws { + let staticString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=+ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=" + let sampleData: Data = staticString.data(using: .utf8)! + let base64 = sampleData.base64EncodedUrlSafe + + // Generated independently + let expectedBase64String = "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU2Nzg5Ky89K0FCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1Njc4OSsvPQ" + + XCTAssertEqual(base64, expectedBase64String) + } +}