Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

toECDSAsecp256k1PrivateKey & derive legacy ecdsa private key #418

Merged
merged 2 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
39 changes: 39 additions & 0 deletions Sources/Hedera/Bip32Utils.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
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
15 changes: 14 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,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<S: StringProtocol>(passphrase: S) -> Data {
var salt = "mnemonic"
salt += passphrase
Expand Down
144 changes: 137 additions & 7 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 All @@ -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.
Expand Down Expand Up @@ -113,7 +117,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 +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())
}

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

}
}
Expand Down Expand Up @@ -446,17 +453,64 @@ 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 {
throw HError(kind: .keyDerive, description: "key is underivable")
}

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<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 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<CryptoKit.SHA512>(key: .init(data: chainCode.data))

Expand All @@ -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
Expand All @@ -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<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 +680,23 @@ extension PrivateKey {
}
}

extension UInt32 {
var bigEndianBytes: Data {
var value = self.bigEndian
return Data(bytes: &value, count: MemoryLayout<UInt32>.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
Expand Down
Loading