From 7f02bd5b729b751ce987916a9be285f8e7167d6d Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Tue, 28 May 2024 12:49:48 +0100 Subject: [PATCH] - new KeyPair functions to create objects from encrypted and unencrypted secret keys. Handling Tz1 and Tz2 - added new helpers - added new tests --- .../Extensions/Data+extensions.swift | 11 ++ Sources/KukaiCryptoSwift/KeyPair.swift | 123 ++++++++++++++++++ Sources/KukaiCryptoSwift/Prefix.swift | 8 +- .../KukaiCryptoSwiftTests/KeyPairTests.swift | 37 ++++++ 4 files changed, 176 insertions(+), 3 deletions(-) diff --git a/Sources/KukaiCryptoSwift/Extensions/Data+extensions.swift b/Sources/KukaiCryptoSwift/Extensions/Data+extensions.swift index 4f8b33f..edfaefe 100644 --- a/Sources/KukaiCryptoSwift/Extensions/Data+extensions.swift +++ b/Sources/KukaiCryptoSwift/Extensions/Data+extensions.swift @@ -69,6 +69,17 @@ public extension Data { try self.append(htoi(char1) << 4 + htoi(char2)) } } + + func bytes() -> [UInt8] { + return [UInt8](self) + } +} + +public extension [UInt8] { + + func data() -> Data { + return Data(self) + } } public extension DataProtocol { diff --git a/Sources/KukaiCryptoSwift/KeyPair.swift b/Sources/KukaiCryptoSwift/KeyPair.swift index 5703c26..c041423 100644 --- a/Sources/KukaiCryptoSwift/KeyPair.swift +++ b/Sources/KukaiCryptoSwift/KeyPair.swift @@ -8,6 +8,7 @@ import Foundation import Sodium import secp256k1 +import CommonCrypto import os.log /// Distingush between ed25519 (TZ1...) and secp256k1 (TZ2...) curves for creating and using wallet addresses @@ -82,6 +83,54 @@ public struct KeyPair { } } + /** + Create a `KeyPair` from a Base58 Check encoded secret key, optionaly encrypted with a passphrase. + Supports both Tz1 (edsk... edes...) and Tz2 (spsk... spes...) + */ + public static func regular(fromSecretKey secretKey: String, andPassphrase: String?) -> KeyPair? { + let first4 = secretKey.prefix(4) + + switch first4 { + case "edsk": + let is54Chars = (secretKey.count == 54) + let prefix = is54Chars ? Prefix.Keys.Ed25519.seed : Prefix.Keys.Ed25519.secret + guard let decoded = Base58Check.decode(string: secretKey, prefix: prefix), let keyPair = Sodium.shared.sign.keyPair(seed: Array(decoded.prefix(32))) else { + return nil + } + + return KeyPair(privateKey: PrivateKey(keyPair.secretKey), publicKey: PublicKey(keyPair.publicKey)) + + case "edes": + guard let password = andPassphrase else { + return nil + } + + return KeyPair.decryptSecretKey(secretKey, ellipticalCurve: .ed25519, passphrase: password) + + case "spsk": + guard let decoded = Base58Check.decode(string: secretKey, prefix: Prefix.Keys.Secp256k1.secret) else { + return nil + } + + let privateKey = PrivateKey(decoded, signingCurve: .secp256k1) + guard let publicKey = KeyPair.secp256k1PublicKey(fromPrivateKeyBytes: privateKey.bytes) else { + return nil + } + + return KeyPair(privateKey: privateKey, publicKey: publicKey) + + case "spes": + guard let password = andPassphrase else { + return nil + } + + return KeyPair.decryptSecretKey(secretKey, ellipticalCurve: .secp256k1, passphrase: password) + + default: + return nil + } + } + /** Create a HD `KeyPair` from a hex seed string and optional Derivation Path (defaults to m/44'/1729'/0'/0' ). Only TZ1 are produceable - parameter seedString: A hex string representing a cryptographic seed (can be created from `Mnemonic`) @@ -183,4 +232,78 @@ public struct KeyPair { return outputBytes } + + public static func decryptSecretKey(_ secretKey: String, ellipticalCurve: EllipticalCurve, passphrase: String) -> KeyPair? { + var decoded: [UInt8]? = nil + + switch ellipticalCurve { + case .ed25519: + decoded = Base58Check.decode(string: secretKey, prefix: Prefix.Keys.Ed25519.encrypted) + + case .secp256k1: + decoded = Base58Check.decode(string: secretKey, prefix: Prefix.Keys.Secp256k1.encrypted) + } + + guard let minusPrefix = decoded else { + return nil + } + + let salt = Array(minusPrefix.prefix(8)) + let encryptedSk = Array(minusPrefix.suffix(from: 8)) + guard let key = pbkdf2(password: passphrase, saltData: salt.data(), keyByteCount: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA512), rounds: 32768), + let box = Sodium.shared.secretBox.open(authenticatedCipherText: encryptedSk, secretKey: key.bytes(), nonce: Array(repeating: 0, count: 24)) else { + return nil + } + + + var keyPair: KeyPair? = nil + switch ellipticalCurve { + case .ed25519: + guard let res = Sodium.shared.sign.keyPair(seed: box) else { + return nil + } + + keyPair = KeyPair(privateKey: PrivateKey(res.secretKey), publicKey: PublicKey(res.publicKey)) + + case .secp256k1: + let privateKey = PrivateKey(box, signingCurve: .secp256k1) + guard let publicKey = KeyPair.secp256k1PublicKey(fromPrivateKeyBytes: privateKey.bytes) else { + return nil + } + + keyPair = KeyPair(privateKey: privateKey, publicKey: publicKey) + } + + return keyPair + } + + public static func pbkdf2(password: String, saltData: Data, keyByteCount: Int, prf: CCPseudoRandomAlgorithm, rounds: Int) -> Data? { + guard let passwordData = password.data(using: .utf8) else { return nil } + var derivedKeyData = Data(repeating: 0, count: keyByteCount) + let derivedCount = derivedKeyData.count + let derivationStatus: Int32 = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes in + let keyBuffer: UnsafeMutablePointer = + derivedKeyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + return saltData.withUnsafeBytes { saltBytes -> Int32 in + let saltBuffer: UnsafePointer = saltBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + return CCKeyDerivationPBKDF( + CCPBKDFAlgorithm(kCCPBKDF2), + password, + passwordData.count, + saltBuffer, + saltData.count, + prf, + UInt32(rounds), + keyBuffer, + derivedCount) + } + } + return derivationStatus == kCCSuccess ? derivedKeyData : nil + } + + public static func isSecretKeyEncrypted(_ secret: String) -> Bool { + let prefix = secret.prefix(4) + + return prefix == "edes" || prefix == "spes" + } } diff --git a/Sources/KukaiCryptoSwift/Prefix.swift b/Sources/KukaiCryptoSwift/Prefix.swift index 7fdbe1c..a758145 100644 --- a/Sources/KukaiCryptoSwift/Prefix.swift +++ b/Sources/KukaiCryptoSwift/Prefix.swift @@ -17,21 +17,23 @@ public enum Prefix { public enum Keys { public enum Ed25519 { public static let `public`: [UInt8] = [13, 15, 37, 217] // edpk - public static let secret: [UInt8] = [43, 246, 78, 7] // edsk + public static let secret: [UInt8] = [43, 246, 78, 7] // edsk public static let seed: [UInt8] = [13, 15, 58, 7] // edsk public static let signature: [UInt8] = [9, 245, 205, 134, 18] // edsig + public static let encrypted: [UInt8] = [7, 90, 60, 179, 41] // edesk } public enum P256 { - public static let secret: [UInt8] = [16, 81, 238, 189] // p2sk + public static let secret: [UInt8] = [16, 81, 238, 189] // p2sk public static let `public`: [UInt8] = [3, 178, 139, 127] // p2pk public static let signature: [UInt8] = [54, 240, 44, 52] // p2sig } public enum Secp256k1 { public static let `public`: [UInt8] = [3, 254, 226, 86] // sppk - public static let secret: [UInt8] = [17, 162, 224, 201] // spsk + public static let secret: [UInt8] = [17, 162, 224, 201] // spsk public static let signature: [UInt8] = [13, 115, 101, 19, 63] // spsig + public static let encrypted: [UInt8] = [9, 237, 241, 174, 150] // spesk } } diff --git a/Tests/KukaiCryptoSwiftTests/KeyPairTests.swift b/Tests/KukaiCryptoSwiftTests/KeyPairTests.swift index 6ebcd48..dec436c 100644 --- a/Tests/KukaiCryptoSwiftTests/KeyPairTests.swift +++ b/Tests/KukaiCryptoSwiftTests/KeyPairTests.swift @@ -189,4 +189,41 @@ final class KeyPairTests: XCTestCase { let hash4 = PublicKey.publicKeyHash(fromBase58EncodedKey: "sppk7Zzqz2AjP4yXqr5ys99gZkaPLFKfGKnUxn3u1T1xfNSArZ5CKX6") XCTAssert(hash4 == "tz2HpbGQcmU3UyusJ78Sbqeg9fYteamSMDGo", hash4 ?? "-") } + + func testKeyPairDecryption() { + let tz1Encrypted = "edesk1L8uVSYd3aug7jbeynzErQTnBxq6G6hJwmeue3yUBt11wp3ULXvcLwYRzDp4LWWvRFNJXRi3LaN7WGiEGhh" + let tz2Encrypted = "spesk1S5bMTCyH9z4mHSpnbn6DBY831DD6Rxgq7ANfEKkngoHSwy6B5odh942TKL6DtLbfTkpTHfSTAQu2d72Qd6" + let encryptedPassword = "pa55word" + + let tz1KeyPair = KeyPair.decryptSecretKey(tz1Encrypted, ellipticalCurve: .ed25519, passphrase: encryptedPassword) + XCTAssert(tz1KeyPair?.publicKey.publicKeyHash == "tz1XztestvvcXSQZUbZav5YgVLRQbxC4GuMF", tz1KeyPair?.publicKey.publicKeyHash ?? "-") + + let tz2KeyPair = KeyPair.decryptSecretKey(tz2Encrypted, ellipticalCurve: .secp256k1, passphrase: encryptedPassword) + XCTAssert(tz2KeyPair?.publicKey.publicKeyHash == "tz2C8APAjnQfffdkHssxdFRctkD1iPLGaGEg", tz2KeyPair?.publicKey.publicKeyHash ?? "-") + } + + func testImportingWalletFromPrivateKey() { + let tz1UnencryptedSeed = KeyPair.regular(fromSecretKey: "edsk3KvXD8SVD9GCyU4jbzaFba2HZRad5pQ7ajL79n7rUoc3nfHv5t", andPassphrase: nil) + XCTAssert(tz1UnencryptedSeed?.publicKey.publicKeyHash == "tz1Qvpsq7UZWyQ4yabf9wGpG97testZCjoCH", tz1UnencryptedSeed?.publicKey.publicKeyHash ?? "-") + + let tz1UnencryptedPk = KeyPair.regular(fromSecretKey: "edskRgQqEw17KMib89AzChu8DiJjmVeDfGmbCMpp7MpmhgTdNVvZ3TTaLfwNoux4hDDVeLxmEJxKiYE1cYp1Vgj6QATKaJa58L", andPassphrase: nil) + XCTAssert(tz1UnencryptedPk?.publicKey.publicKeyHash == "tz1Ue76bLW7boAcJEZf2kSGcamdBKVi4Kpss", tz1UnencryptedPk?.publicKey.publicKeyHash ?? "-") + + let tz1Encrypted = KeyPair.regular(fromSecretKey: "edesk1L8uVSYd3aug7jbeynzErQTnBxq6G6hJwmeue3yUBt11wp3ULXvcLwYRzDp4LWWvRFNJXRi3LaN7WGiEGhh", andPassphrase: "pa55word") + XCTAssert(tz1Encrypted?.publicKey.publicKeyHash == "tz1XztestvvcXSQZUbZav5YgVLRQbxC4GuMF", tz1Encrypted?.publicKey.publicKeyHash ?? "-") + + let tz2Unencrypted = KeyPair.regular(fromSecretKey: "spsk29hF9oJ6koNnnJMs1rXz4ynBs8hL8FyubTNPCu2tCVP5beGDbw", andPassphrase: nil) + XCTAssert(tz2Unencrypted?.publicKey.publicKeyHash == "tz2RbUirt95UQHa9YyxcLj9GusNctxwn3Xi1", tz2Unencrypted?.publicKey.publicKeyHash ?? "-") + + let tz2Encrypted = KeyPair.regular(fromSecretKey: "spesk1S5bMTCyH9z4mHSpnbn6DBY831DD6Rxgq7ANfEKkngoHSwy6B5odh942TKL6DtLbfTkpTHfSTAQu2d72Qd6", andPassphrase: "pa55word") + XCTAssert(tz2Encrypted?.publicKey.publicKeyHash == "tz2C8APAjnQfffdkHssxdFRctkD1iPLGaGEg", tz2Encrypted?.publicKey.publicKeyHash ?? "-") + } + + func testIsEncrypted() { + XCTAssert(KeyPair.isSecretKeyEncrypted("edsk3KvXD8SVD9GCyU4jbzaFba2HZRad5pQ7ajL79n7rUoc3nfHv5t") == false) + XCTAssert(KeyPair.isSecretKeyEncrypted("edskRgQqEw17KMib89AzChu8DiJjmVeDfGmbCMpp7MpmhgTdNVvZ3TTaLfwNoux4hDDVeLxmEJxKiYE1cYp1Vgj6QATKaJa58L") == false) + XCTAssert(KeyPair.isSecretKeyEncrypted("edesk1L8uVSYd3aug7jbeynzErQTnBxq6G6hJwmeue3yUBt11wp3ULXvcLwYRzDp4LWWvRFNJXRi3LaN7WGiEGhh") == true) + XCTAssert(KeyPair.isSecretKeyEncrypted("spsk29hF9oJ6koNnnJMs1rXz4ynBs8hL8FyubTNPCu2tCVP5beGDbw") == false) + XCTAssert(KeyPair.isSecretKeyEncrypted("spesk1S5bMTCyH9z4mHSpnbn6DBY831DD6Rxgq7ANfEKkngoHSwy6B5odh942TKL6DtLbfTkpTHfSTAQu2d72Qd6") == true) + } }