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..d8fef3fb --- /dev/null +++ b/Sources/Hedera/Bip32Utils.swift @@ -0,0 +1,37 @@ +/* + * ‌ + * 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: UInt32 = 1 << 31 + + static let hardenedIndex0: UInt32 = 0 | hardenedMask + + public init() {} + + /// Harden the index + public static func toHardenedIndex(_ index: UInt32) -> UInt64 { + UInt64(UInt32(truncatingIfNeeded: index) | hardenedMask) + } + + /// Check if the index is hardened + public static func isHardenedIndex(_ index: UInt32) -> Bool { + (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..bae75a91 100644 --- a/Sources/Hedera/Mnemonic/Mnemonic.swift +++ b/Sources/Hedera/Mnemonic/Mnemonic.swift @@ -1,4 +1,4 @@ -/* +/* * ‌ * Hedera Swift SDK * ​ @@ -146,6 +146,24 @@ 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) + + let hardenedMask: UInt32 = 1 << 31 + + let hardened44: UInt32 = 44 | hardenedMask + let hardened3030: UInt32 = 3030 | hardenedMask + let hardened0: UInt32 = 0 | hardenedMask + for index: Int32 in [ + Int32(bitPattern: hardened44), Int32(bitPattern: hardened3030), Int32(bitPattern: hardened0), 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..94405d0d 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 } @@ -113,7 +114,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 +198,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 +220,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) } } @@ -445,6 +449,8 @@ public struct PrivateKey: LosslessStringConvertible, ExpressibleByStringLiteral, isEd25519() && chainCode != nil } + let secp256k1Order = BigInt("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", radix: 16)! + public func derive(_ index: Int32) throws -> Self { let hardenedMask: UInt32 = 1 << 31 let index = UInt32(bitPattern: index) @@ -454,7 +460,54 @@ 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 = (index & hardenedMask) != 0 + 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 newPrivateKey = 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: newPrivateKey.dataRepresentation)), + chainCode: Data(newChainCode) + ) + case .ed25519(let key): let index = index | hardenedMask @@ -502,6 +555,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 +661,13 @@ extension PrivateKey { } } +extension UInt32 { + var bigEndianBytes: Data { + var value = self.bigEndian + return Data(bytes: &value, count: MemoryLayout.size) + } +} + #if compiler(>=5.7) extension PrivateKey.Repr: Sendable {} #else diff --git a/Tests/HederaTests/MnemonicTests.swift b/Tests/HederaTests/MnemonicTests.swift index dfa5d35d..87fe8a52 100644 --- a/Tests/HederaTests/MnemonicTests.swift +++ b/Tests/HederaTests/MnemonicTests.swift @@ -205,4 +205,38 @@ internal final class MnemonicTests: XCTestCase { "302e020100300506032b657004220420853f15aecd22706b105da1d709b4ac05b4906170c2b9c7495dff9af49e1391da" ) } + + internal func testToStandardECDSAsecp256k1PrivateKey() 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 str = + "finish furnace tomorrow wine mass goose festival air palm easy region guilt" + + let mnemonic = try Mnemonic.fromString(str) + + let key = try mnemonic.toStandardECDSAsecp256k1PrivateKey(passphrase: "", index: 0) + XCTAssertEqual(key.chainCode!.data.toHexString(), chainCode) + XCTAssertEqual(key.toStringRaw(), privateKey) + XCTAssert(publicKey.contains(key.publicKey.toStringRaw())) + + let key2 = try mnemonic.toStandardECDSAsecp256k1PrivateKey(passphrase: "some pass", index: 0) + XCTAssertEqual(key2.chainCode!.data.toHexString(), chainCode2) + XCTAssertEqual(key2.toStringRaw(), privateKey2) + XCTAssert(publicKey2.contains(key2.publicKey.toStringRaw())) + + let key3 = try mnemonic.toStandardECDSAsecp256k1PrivateKey(passphrase: "some pass", index: 2_147_483_647) + XCTAssertEqual(key3.chainCode!.data.toHexString(), chainCode3) + XCTAssertEqual(key3.toStringRaw(), privateKey3) + XCTAssert(publicKey3.contains(key3.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"] + ), + ] +)