Skip to content

Commit

Permalink
added key managet and a test
Browse files Browse the repository at this point in the history
  • Loading branch information
jszersze committed Jun 5, 2024
1 parent aca8397 commit ce2b497
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 1 deletion.
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import PackageDescription
let package = Package(
name: "SpruceIDWalletSdk",
platforms: [
.iOS(.v14)
.iOS(.v13),
.macOS(.v10_15)
],
products: [
.library(
Expand Down
13 changes: 13 additions & 0 deletions Sources/WalletSdk/DataConversions.swift
Original file line number Diff line number Diff line change
@@ -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: "")
}
}
229 changes: 229 additions & 0 deletions Sources/WalletSdk/KeyManager.swift
Original file line number Diff line number Diff line change
@@ -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<CFError>?
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<CFError>?
guard let data = SecKeyCopyExternalRepresentation(publicKey, &error) as? Data else {
return nil
}

let fullData: Data = data.subdata(in: 1..<data.count)
let xDataRaw: Data = fullData.subdata(in: 0..<32)
let yDataRaw: Data = fullData.subdata(in: 32..<64)

let x = xDataRaw.base64EncodedUrlSafe
let y = yDataRaw.base64EncodedUrlSafe

let jsonObject: [String: Any] = [
"kty": "EC",
"crv": "P-256",
"x": x,
"y": y
]

guard let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject, options: []) else { return nil }
let jsonString = String(data: jsonData, encoding: String.Encoding.ascii)!

return jsonString
}

/**
* Signs the provided payload with a ecdsaSignatureMessageX962SHA256 private key.
*/
static func signPayload(id: String, 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 = .ecdsaSignatureMessageX962SHA256
var error: Unmanaged<CFError>?
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<CFError>?
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<CFError>?

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<CFError>?
guard let decrypted = SecKeyCreateDecryptedData(
key,
algorithm,
data,
&error
) as Data? else {
return nil
}

return [UInt8](decrypted)
}
}
27 changes: 27 additions & 0 deletions Tests/WalletSdkTests/DataConversions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// KeyManager.swift
//
//
// Created by Kuba on 6/4/24.
//

import XCTest
@testable import CADMVWalletSdk

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

0 comments on commit ce2b497

Please sign in to comment.