Skip to content

Commit

Permalink
- new KeyPair functions to create objects from encrypted and unencryp…
Browse files Browse the repository at this point in the history
…ted secret keys. Handling Tz1 and Tz2

- added new helpers
- added new tests
  • Loading branch information
simonmcl committed May 28, 2024
1 parent 203b661 commit 7f02bd5
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 3 deletions.
11 changes: 11 additions & 0 deletions Sources/KukaiCryptoSwift/Extensions/Data+extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
123 changes: 123 additions & 0 deletions Sources/KukaiCryptoSwift/KeyPair.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`)
Expand Down Expand Up @@ -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<UInt8> =
derivedKeyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
return saltData.withUnsafeBytes { saltBytes -> Int32 in
let saltBuffer: UnsafePointer<UInt8> = 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"
}
}
8 changes: 5 additions & 3 deletions Sources/KukaiCryptoSwift/Prefix.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
37 changes: 37 additions & 0 deletions Tests/KukaiCryptoSwiftTests/KeyPairTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

0 comments on commit 7f02bd5

Please sign in to comment.