Skip to content

Commit

Permalink
feat: add Mnemonic.toStandardECDSAsecp256k1PrivateKey
Browse files Browse the repository at this point in the history
Signed-off-by: Ricky Saechao <[email protected]>
  • Loading branch information
RickyLB committed Dec 17, 2024
1 parent 4b445d3 commit 3938b77
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 5 deletions.
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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.
Expand Down
37 changes: 37 additions & 0 deletions Sources/Hedera/Bip32Utils.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
24 changes: 24 additions & 0 deletions Sources/Hedera/Data+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 19 additions & 1 deletion Sources/Hedera/Mnemonic/Mnemonic.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* ‌
* Hedera Swift SDK
* ​
Expand Down Expand Up @@ -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<S: StringProtocol>(passphrase: S) -> Data {
var salt = "mnemonic"
salt += passphrase
Expand Down
109 changes: 105 additions & 4 deletions Sources/Hedera/PrivateKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* ‍
*/

import BigInt
import CommonCrypto
import CryptoKit
import Foundation
Expand All @@ -41,7 +42,7 @@ internal struct Keccak256Digest: Crypto.SecpDigest {
}
}

private struct ChainCode {
public struct ChainCode {
let data: Data
}

Expand Down Expand Up @@ -113,7 +114,7 @@ public struct PrivateKey: LosslessStringConvertible, ExpressibleByStringLiteral,
guts.kind
}

private let chainCode: ChainCode?
public let chainCode: ChainCode?

private static func decodeBytes<S: StringProtocol>(_ description: S) throws -> Data {
let description = description.stripPrefix("0x") ?? description[...]
Expand Down Expand Up @@ -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())
}

Expand All @@ -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)

}
}
Expand Down Expand Up @@ -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)
Expand All @@ -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<SHA512>.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

Expand Down Expand Up @@ -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<SHA512>(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<SHA512>(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)

Expand Down Expand Up @@ -567,6 +661,13 @@ extension PrivateKey {
}
}

extension UInt32 {
var bigEndianBytes: Data {
var value = self.bigEndian
return Data(bytes: &value, count: MemoryLayout<UInt32>.size)
}
}

#if compiler(>=5.7)
extension PrivateKey.Repr: Sendable {}
#else
Expand Down
34 changes: 34 additions & 0 deletions Tests/HederaTests/MnemonicTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
}
}
14 changes: 14 additions & 0 deletions Tests/HederaTests/PrivateKeyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()))

}
}
24 changes: 24 additions & 0 deletions hedera-sdk-swift/Package.swift
Original file line number Diff line number Diff line change
@@ -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"]
),
]
)

0 comments on commit 3938b77

Please sign in to comment.