Skip to content

Commit

Permalink
toECDSAsecp256k1PrivateKey & derive legacy ecdsa private key
Browse files Browse the repository at this point in the history
  • Loading branch information
RickyLB authored Dec 18, 2024
1 parent 87926b5 commit c65028c
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 8 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
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

0 comments on commit c65028c

Please sign in to comment.