diff --git a/Package.resolved b/Package.resolved index 322586ad..73a49b92 100644 --- a/Package.resolved +++ b/Package.resolved @@ -27,6 +27,15 @@ "version" : "1.18.0" } }, + { + "identity" : "bigint", + "kind" : "remoteSourceControl", + "location" : "https://github.com/attaswift/BigInt.git", + "state" : { + "revision" : "114343a705df4725dfe7ab8a2a326b8883cfd79c", + "version" : "5.5.1" + } + }, { "identity" : "console-kit", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index c1711a49..d70297a4 100644 --- a/Package.swift +++ b/Package.swift @@ -103,6 +103,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.0.0"), .package(url: "https://github.com/vapor/vapor.git", from: "4.101.3"), + .package(url: "https://github.com/attaswift/BigInt.git", from: "5.2.0"), ], targets: [ .target( @@ -128,6 +129,7 @@ let package = Package( .product(name: "GRPC", package: "grpc-swift"), .product(name: "Atomics", package: "swift-atomics"), .product(name: "secp256k1", package: "secp256k1.swift"), + .product(name: "BigInt", package: "BigInt"), "CryptoSwift", ] // todo: find some way to enable these locally. diff --git a/Sources/Hedera/Bip32Utils.swift b/Sources/Hedera/Bip32Utils.swift new file mode 100644 index 00000000..068764b8 --- /dev/null +++ b/Sources/Hedera/Bip32Utils.swift @@ -0,0 +1,39 @@ +/* + * ‌ + * Hedera Swift SDK + * + * Copyright (C) 2022 - 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +public class Bip32Utils { + static let hardenedMask: Int32 = 1 << 31 + + public init() {} + + /// Harden the index + public static func toHardenedIndex(_ index: UInt32) -> Int32 { + let index = Int32(bitPattern: index) + + return (index | hardenedMask) + } + + /// Check if the index is hardened + public static func isHardenedIndex(_ index: UInt32) -> Bool { + let index = Int32(bitPattern: index) + + return (index & hardenedMask) != 0 + } +} diff --git a/Sources/Hedera/Data+Extensions.swift b/Sources/Hedera/Data+Extensions.swift index 736798b4..0e106cc2 100644 --- a/Sources/Hedera/Data+Extensions.swift +++ b/Sources/Hedera/Data+Extensions.swift @@ -104,6 +104,30 @@ extension Data { } } +extension Data { + func leftPadded(to size: Int) -> Data { + if self.count >= size { return self } + return Data(repeating: 0, count: size - self.count) + self + } +} + +extension Data { + internal func hexEncodedString() -> String { + self.map { String(format: "%02x", $0) }.joined() + } +} + +extension Data { + func ensureSize(_ size: Int) -> Data { + if self.count > size { + return self.suffix(size) + } else if self.count < size { + return Data(repeating: 0, count: size - self.count) + self + } + return self + } +} + extension Data { internal func split(at middle: Index) -> (SubSequence, SubSequence)? { guard let index = index(startIndex, offsetBy: middle, limitedBy: endIndex) else { diff --git a/Sources/Hedera/Mnemonic/Mnemonic.swift b/Sources/Hedera/Mnemonic/Mnemonic.swift index 495c9a0a..9089d5a3 100644 --- a/Sources/Hedera/Mnemonic/Mnemonic.swift +++ b/Sources/Hedera/Mnemonic/Mnemonic.swift @@ -1,4 +1,4 @@ -/* +/* * ‌ * Hedera Swift SDK * ​ @@ -146,6 +146,19 @@ public struct Mnemonic: Equatable { String(describing: self) } + public func toStandardECDSAsecp256k1PrivateKey(_ passphrase: String = "", _ index: Int32) throws -> PrivateKey { + let seed = toSeed(passphrase: passphrase) + var derivedKey = PrivateKey.fromSeedECDSAsecp256k1(seed) + + for index: Int32 in [ + Bip32Utils.toHardenedIndex(44), Bip32Utils.toHardenedIndex(3030), Bip32Utils.toHardenedIndex(0), 0, index, + ] { + derivedKey = try! derivedKey.derive(index) + } + + return derivedKey + } + internal func toSeed(passphrase: S) -> Data { var salt = "mnemonic" salt += passphrase diff --git a/Sources/Hedera/PrivateKey.swift b/Sources/Hedera/PrivateKey.swift index a5edaadc..040d5e3f 100644 --- a/Sources/Hedera/PrivateKey.swift +++ b/Sources/Hedera/PrivateKey.swift @@ -18,6 +18,7 @@ * ‍ */ +import BigInt import CommonCrypto import CryptoKit import Foundation @@ -41,7 +42,7 @@ internal struct Keccak256Digest: Crypto.SecpDigest { } } -private struct ChainCode { +public struct ChainCode { let data: Data } @@ -55,6 +56,9 @@ private struct ChainCode { public struct PrivateKey: LosslessStringConvertible, ExpressibleByStringLiteral, CustomStringConvertible, CustomDebugStringConvertible { + + private let secp256k1Order = BigInt("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", radix: 16)! + /// Debug description for `PrivateKey` /// /// Please note that debugDescriptions of any kind should not be considered a stable format. @@ -113,7 +117,7 @@ public struct PrivateKey: LosslessStringConvertible, ExpressibleByStringLiteral, guts.kind } - private let chainCode: ChainCode? + public let chainCode: ChainCode? private static func decodeBytes(_ description: S) throws -> Data { let description = description.stripPrefix("0x") ?? description[...] @@ -197,11 +201,13 @@ public struct PrivateKey: LosslessStringConvertible, ExpressibleByStringLiteral, /// Generates a new Ed25519 private key. public static func generateEd25519() -> Self { + // PrivateKeyED25519.generateInternal() Self(kind: .ed25519(.init()), chainCode: .randomData(withLength: 32)) } /// Generates a new ECDSA(secp256k1) private key. public static func generateEcdsa() -> Self { + // PrivateKeyECDSA.generateInternal() .ecdsa(try! .init()) } @@ -217,7 +223,8 @@ public struct PrivateKey: LosslessStringConvertible, ExpressibleByStringLiteral, public var publicKey: PublicKey { switch kind { case .ed25519(let key): return .ed25519(key.publicKey) - case .ecdsa(let key): return .ecdsa(key.publicKey) + case .ecdsa(let key): + return .ecdsa(key.publicKey) } } @@ -446,7 +453,6 @@ public struct PrivateKey: LosslessStringConvertible, ExpressibleByStringLiteral, } public func derive(_ index: Int32) throws -> Self { - let hardenedMask: UInt32 = 1 << 31 let index = UInt32(bitPattern: index) guard let chainCode = chainCode else { @@ -454,9 +460,57 @@ public struct PrivateKey: LosslessStringConvertible, ExpressibleByStringLiteral, } switch kind { - case .ecdsa: throw HError(kind: .keyDerive, description: "ecdsa keys are currently underivable") + case .ecdsa(let key): + let isHardened = Bip32Utils.isHardenedIndex(index) + var data = Data() + let priv = toBytesRaw() + + if isHardened { + data.append(0x00) + data.append(priv) + } else { + data.append(key.publicKey.dataRepresentation) + } + + // Append the index bytes + data.append(index.bigEndianBytes) + + let hmac = HMAC.authenticationCode(for: data, using: SymmetricKey(data: chainCode.data)) + let il = Data(hmac.prefix(32)) + let newChainCode = Data(hmac.suffix(32)) + + let parentPrivateKeyBigInt = BigInt(priv.hexStringEncoded(), radix: 16)! + let ilBigInt = BigInt(il.hexStringEncoded(), radix: 16)! + + // Compute child key + let childPrivateKeyBigInt = (parentPrivateKeyBigInt + ilBigInt) % secp256k1Order + + var childPrivateKeyData = childPrivateKeyBigInt.serialize() + + // Convert to Data without leading zeros, left-pad to 32 bytes + if childPrivateKeyData.count > 32 { + childPrivateKeyData = childPrivateKeyData.suffix(32) + } else if childPrivateKeyData.count < 32 { + childPrivateKeyData = Data(repeating: 0, count: 32 - childPrivateKeyData.count) + childPrivateKeyData + } + + // Check if private key is valid + guard let childPrivateKey = try? secp256k1.Signing.PrivateKey(dataRepresentation: childPrivateKeyData) + else { + throw NSError( + domain: "InvalidPrivateKey", code: -1, + userInfo: [ + NSLocalizedDescriptionKey: "Failed to initialize secp256k1 private key. Key out of range." + ]) + } + + return Self( + kind: .ecdsa(try! .init(dataRepresentation: childPrivateKey.dataRepresentation)), + chainCode: Data(newChainCode) + ) + case .ed25519(let key): - let index = index | hardenedMask + let index = Bip32Utils.toHardenedIndex(index) var hmac = CryptoKit.HMAC(key: .init(data: chainCode.data)) @@ -476,7 +530,25 @@ public struct PrivateKey: LosslessStringConvertible, ExpressibleByStringLiteral, public func legacyDerive(_ index: Int64) throws -> Self { switch kind { - case .ecdsa: throw HError(kind: .keyDerive, description: "ecdsa keys are currently underivable") + case .ecdsa(let key): + var seed = key.dataRepresentation + + seed.append(contentsOf: index.bigEndianBytes) + + let salt = Data([0xff]) + let derivedKey = Pkcs5.pbkdf2( + variant: .sha2(.sha512), + password: seed, + salt: salt, + rounds: 2048, + keySize: 32 + ) + + guard let newKey = try? P256.Signing.PrivateKey(rawRepresentation: derivedKey) else { + throw KeyDerivationError.invalidDerivedKey + } + + return try .fromBytesEcdsa(newKey.rawRepresentation) case .ed25519(let key): var seed = key.rawRepresentation @@ -502,6 +574,47 @@ public struct PrivateKey: LosslessStringConvertible, ExpressibleByStringLiteral, } } + // Extract the ECDSA private key from a seed. + public static func fromSeedECDSAsecp256k1(_ seed: Data) -> Self { + var hmac = HMAC(key: .init(data: "Bitcoin seed".data(using: .utf8)!)) + hmac.update(data: seed) + + let output = hmac.finalize().bytes + + let (data, chainCode) = (output[..<32], output[32...]) + + // Create new private key + let key = Self( + kind: .ecdsa(try! .init(dataRepresentation: data)), + chainCode: Data(chainCode) + ) + + return key + } + + public static func fromSeedED25519(_ seed: Data) -> Self { + var hmac = HMAC(key: .init(data: "ed25519 seed".data(using: .utf8)!)) + + hmac.update(data: seed) + + let output = hmac.finalize().bytes + + let (data, chainCode) = (output[..<32], output[32...]) + + var key = Self( + kind: .ed25519(try! .init(rawRepresentation: data)), + chainCode: Data(chainCode) + ) + + for index: Int32 in [44, 3030, 0, 0] { + // an error here would be... Really weird because we just set chainCode. + // swiftlint:disable:next force_try + key = try! key.derive(index) + } + + return key + } + public static func fromMnemonic(_ mnemonic: Mnemonic, _ passphrase: String) -> Self { let seed = mnemonic.toSeed(passphrase: passphrase) @@ -567,6 +680,23 @@ extension PrivateKey { } } +extension UInt32 { + var bigEndianBytes: Data { + var value = self.bigEndian + return Data(bytes: &value, count: MemoryLayout.size) + } +} + +extension Int64 { + fileprivate var bigEndianBytes: [UInt8] { + withUnsafeBytes(of: self.bigEndian) { Array($0) } + } +} + +enum KeyDerivationError: Error { + case invalidDerivedKey +} + #if compiler(>=5.7) extension PrivateKey.Repr: Sendable {} #else diff --git a/Tests/HederaTests/MnemonicTests.swift b/Tests/HederaTests/MnemonicTests.swift index dfa5d35d..66a0046b 100644 --- a/Tests/HederaTests/MnemonicTests.swift +++ b/Tests/HederaTests/MnemonicTests.swift @@ -205,4 +205,119 @@ internal final class MnemonicTests: XCTestCase { "302e020100300506032b657004220420853f15aecd22706b105da1d709b4ac05b4906170c2b9c7495dff9af49e1391da" ) } + + internal func testToStandardECDSAsecp256k1PrivateKey() throws { + let chainCode = "7717bc71194c257d4b233e16cf48c24adef630052f874a262d19aeb2b527620d" + let privateKey = "0fde7bfd57ae6ec310bdd8b95967d98e8762a2c02da6f694b152cf9860860ab8" + let publicKey = "03b1c064b4d04d52e51f6c8e8bb1bff75d62fa7b1446412d5901d424f6aedd6fd4" + + let chainCode2 = "0ff552587f6baef1f0818136bacac0bb37236473f6ecb5a8c1cc68a716726ed1" + let privateKey2 = "6df5ed217cf6d5586fdf9c69d39c843eb9d152ca19d3e41f7bab483e62f6ac25" + let publicKey2 = "0357d69bb36fee569838fe7b325c07ca511e8c1b222873cde93fc6bb541eb7ecea" + + let chainCode3 = "e54254940db58ef4913a377062ac6e411daebf435ad592d262d5a66d808a8b94" + let privateKey3 = "60cb2496a623e1201d4e0e7ce5da3833cd4ec7d6c2c06bce2bcbcbc9dfef22d6" + let publicKey3 = "02b59f348a6b69bd97afa80115e2d5331749b3c89c61297255430c487d6677f404" + + let chainCode4 = "e333da4bd9e21b5dbd2b0f6d88bad02f0fa24cf4b70b2fb613368d0364cdf8af" + let privateKey4 = "aab7d720a32c2d1ea6123f58b074c865bb07f6c621f14cb012f66c08e64996bb" + let publicKey4 = "03a0ea31bb3562f8a309b1436bc4b2f537301778e8a5e12b68cec26052f567a235" + + let chainCode5 = "cb23165e9d2d798c85effddc901a248a1a273fab2a56fe7976df97b016e7bb77" + let privateKey5 = "100477c333028c8849250035be2a0a166a347a5074a8a727bce1db1c65181a50" + let publicKey5 = "03d10ebfa2d8ff2cd34aa96e5ef59ca2e69316b4c0996e6d5f54b6932fe51be560" + + // 24 word string Mnemonic + let mnemonic = try Mnemonic.fromString(knownGoodMnemonics[0]) + + // Chain m/44'/3030'/0'/0/0 + let key = try mnemonic.toStandardECDSAsecp256k1PrivateKey("", 0) + XCTAssertEqual(key.chainCode!.data.toHexString(), chainCode) + XCTAssertEqual(key.toStringRaw(), privateKey) + XCTAssert(publicKey.contains(key.publicKey.toStringRaw())) + + // Chain m/44'/3030'/0'/0/0; Passphrase "some pass" + let key2 = try mnemonic.toStandardECDSAsecp256k1PrivateKey("some pass", 0) + XCTAssertEqual(key2.chainCode!.data.toHexString(), chainCode2) + XCTAssertEqual(key2.toStringRaw(), privateKey2) + XCTAssert(publicKey2.contains(key2.publicKey.toStringRaw())) + + // Chain m/44'/3030'/0'/0/2147483647; Passphrase "some pass" + let key3 = try mnemonic.toStandardECDSAsecp256k1PrivateKey("some pass", 2_147_483_647) + XCTAssertEqual(key3.chainCode!.data.toHexString(), chainCode3) + XCTAssertEqual(key3.toStringRaw(), privateKey3) + XCTAssert(publicKey3.contains(key3.publicKey.toStringRaw())) + + // Chain m/44'/3030'/0'/0/0' + let key4 = try mnemonic.toStandardECDSAsecp256k1PrivateKey("", Bip32Utils.toHardenedIndex(0)) + XCTAssertEqual(key4.chainCode!.data.toHexString(), chainCode4) + XCTAssertEqual(key4.toStringRaw(), privateKey4) + XCTAssert(publicKey4.contains(key4.publicKey.toStringRaw())) + + // Chain m/44'/3030'/0'/0/2147483647'; Passphrase "some pass" + let key5 = try mnemonic.toStandardECDSAsecp256k1PrivateKey( + "some pass", Bip32Utils.toHardenedIndex(2_147_483_647)) + XCTAssertEqual(key5.chainCode!.data.toHexString(), chainCode5) + XCTAssertEqual(key5.toStringRaw(), privateKey5) + XCTAssert(publicKey5.contains(key5.publicKey.toStringRaw())) + } + + internal func testToStandardECDSAsecp256k1PrivateKey2() throws { + let chainCode = "e76e0480faf2790e62dc1a7bac9dce51db1b3571fd74d8e264abc0d240a55d09" + let privateKey = "f033824c20dd9949ad7a4440f67120ee02a826559ed5884077361d69b2ad51dd" + let publicKey = "0294bf84a54806989a74ca4b76291d386914610b40b610d303162b9e495bc06416" + + let chainCode2 = "911a1095b64b01f7f3a06198df3d618654e5ed65862b211997c67515e3167892" + let privateKey2 = "c139ebb363d7f441ccbdd7f58883809ec0cc3ee7a122ef67974eec8534de65e8" + let publicKey2 = "0293bdb1507a26542ed9c1ec42afe959cf8b34f39daab4bf842cdac5fa36d50ef7" + + let chainCode3 = "a7250c2b07b368a054f5c91e6a3dbe6ca3bbe01eb0489fe8778304bd0a19c711" + let privateKey3 = "2583170ee745191d2bb83474b1de41a1621c47f6e23db3f2bf413a1acb5709e4" + let publicKey3 = "03f9eb27cc73f751e8e476dd1db79037a7df2c749fa75b6cc6951031370d2f95a5" + + let chainCode4 = "60c39c6a77bd68c0aaabfe2f4711dc9c2247214c4f4dae15ad4cb76905f5f544" + let privateKey4 = "962f549dafe2d9c8091ac918cb4fc348ab0767353f37501067897efbc84e7651" + let publicKey4 = "027123855357fd41d28130fbc59053192b771800d28ef47319ef277a1a032af78f" + + let chainCode5 = "66a1175e7690e3714d53ffce16ee6bb4eb02065516be2c2ad6bf6c9df81ec394" + let privateKey5 = "f2d008cd7349bdab19ed85b523ba218048f35ca141a3ecbc66377ad50819e961" + let publicKey5 = "027b653d04958d4bf83dd913a9379b4f9a1a1e64025a691830a67383bc3157c044" + + let str = + "finish furnace tomorrow wine mass goose festival air palm easy region guilt" + + // 12 word string Mnemonic + let mnemonic = try Mnemonic.fromString(str) + + // Chain m/44'/3030'/0'/0 /0 + let key = try mnemonic.toStandardECDSAsecp256k1PrivateKey("", 0) + XCTAssertEqual(key.chainCode!.data.toHexString(), chainCode) + XCTAssertEqual(key.toStringRaw(), privateKey) + XCTAssert(publicKey.contains(key.publicKey.toStringRaw())) + + // Chain m/44'/3030'/0'/0 /0; Passphrase "some pass" + let key2 = try mnemonic.toStandardECDSAsecp256k1PrivateKey("some pass", 0) + XCTAssertEqual(key2.chainCode!.data.toHexString(), chainCode2) + XCTAssertEqual(key2.toStringRaw(), privateKey2) + XCTAssert(publicKey2.contains(key2.publicKey.toStringRaw())) + + // Chain m/44'/3030'/0'/0/2147483647; Passphrase "some pass" + let key3 = try mnemonic.toStandardECDSAsecp256k1PrivateKey("some pass", 2_147_483_647) + XCTAssertEqual(key3.chainCode!.data.toHexString(), chainCode3) + XCTAssertEqual(key3.toStringRaw(), privateKey3) + XCTAssert(publicKey3.contains(key3.publicKey.toStringRaw())) + + // Chain m/44'/3030'/0'/0/0' + let key4 = try mnemonic.toStandardECDSAsecp256k1PrivateKey("", Bip32Utils.toHardenedIndex(0)) + XCTAssertEqual(key4.chainCode!.data.toHexString(), chainCode4) + XCTAssertEqual(key4.toStringRaw(), privateKey4) + XCTAssert(publicKey4.contains(key4.publicKey.toStringRaw())) + + // Chain m/44'/3030'/0'/0/2147483647'; Passphrase "some pass" + let key5 = try mnemonic.toStandardECDSAsecp256k1PrivateKey( + "some pass", Bip32Utils.toHardenedIndex(2_147_483_647)) + XCTAssertEqual(key5.chainCode!.data.toHexString(), chainCode5) + XCTAssertEqual(key5.toStringRaw(), privateKey5) + XCTAssert(publicKey5.contains(key5.publicKey.toStringRaw())) + } } diff --git a/Tests/HederaTests/PrivateKeyTests.swift b/Tests/HederaTests/PrivateKeyTests.swift index f53c37fb..03faf612 100644 --- a/Tests/HederaTests/PrivateKeyTests.swift +++ b/Tests/HederaTests/PrivateKeyTests.swift @@ -403,4 +403,18 @@ internal final class PrivateKeyTests: XCTestCase { "03b69a75a5ddb1c0747e995d47555019e5d8a28003ab5202bd92f534361fb4ec8a" ) } + + internal func testslip10TestVector1() throws { + let chainCode1 = "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508" + let privateKey1 = "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35" + let publicKey1 = "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2" + + let seed = Data(hexEncoded: "000102030405060708090a0b0c0d0e0f")! + + let key1 = PrivateKey.fromSeedECDSAsecp256k1(seed) + XCTAssertEqual(key1.chainCode?.data.toHexString(), chainCode1) + XCTAssertEqual(key1.toStringRaw(), privateKey1) + XCTAssert(publicKey1.contains(key1.publicKey.toStringRaw())) + + } } diff --git a/hedera-sdk-swift/Package.swift b/hedera-sdk-swift/Package.swift new file mode 100644 index 00000000..7726d99c --- /dev/null +++ b/hedera-sdk-swift/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "hedera-sdk-swift", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "hedera-sdk-swift", + targets: ["hedera-sdk-swift"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "hedera-sdk-swift"), + .testTarget( + name: "hedera-sdk-swiftTests", + dependencies: ["hedera-sdk-swift"] + ), + ] +)