Skip to content

Commit ffe292a

Browse files
authored
Merge pull request #80 from novasamatech/feature/trust-wallet-derivation
Hierarchical wallet derivations for Ed25519
2 parents 89d66f2 + 3f934fe commit ffe292a

11 files changed

+321
-64
lines changed

Package.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,9 @@ enum Resources {
173173
]
174174
case .hdkd:
175175
paths = [
176-
"../Resources/BIP32HDKD.json",
177-
"../Resources/BIP32HDKDEtalon.json",
176+
"../Resources/BIP32Secp256HDKD.json",
177+
"../Resources/BIP32Secp256HDKDEtalon.json",
178+
"../Resources/BIP32Ed25519HDKDEtalon.json",
178179
"../Resources/ecdsaHDKD.json",
179180
"../Resources/ed25519HDKD.json",
180181
"../Resources/sr25519HDKD.json"

SubstrateSdk.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
Pod::Spec.new do |s|
1010
s.name = 'SubstrateSdk'
11-
s.version = '4.3.0'
11+
s.version = '4.4.0'
1212
s.summary = 'Utility library that implements clients specific logic to interact with substrate based networks'
1313

1414
s.homepage = 'https://github.com/nova-wallet/substrate-sdk-ios'
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import NovaCrypto
2+
import BigInt
3+
4+
public final class BIP32Ed25519KeyFactory: BIP32KeypairFactory {
5+
private static let initialSeed = "ed25519 seed"
6+
let internalFactory = EDKeyFactory()
7+
8+
public override init() {
9+
super.init()
10+
}
11+
12+
override func deriveFromSeed(_ seed: Data) throws -> BIP32ExtendedKeypair {
13+
let hmacResult = try generateHMAC512(
14+
from: seed,
15+
secretKeyData: Data(Self.initialSeed.utf8)
16+
)
17+
18+
let privateKeySeed = hmacResult[...31]
19+
let chainCode = hmacResult[32...]
20+
21+
// we are returning seed as private key as both further derivation and signature requires it
22+
let publicKey = try internalFactory.derive(fromSeed: privateKeySeed).publicKey()
23+
let secretKey = try EDPrivateKey(rawData: privateKeySeed)
24+
25+
let keypair = IRCryptoKeypair(publicKey: publicKey, privateKey: secretKey)
26+
27+
return BIP32ExtendedKeypair(keypair: keypair, chaincode: chainCode)
28+
}
29+
30+
override func createKeypairFrom(
31+
_ parentKeypair: BIP32ExtendedKeypair,
32+
chaincode: Chaincode
33+
) throws -> BIP32ExtendedKeypair {
34+
let sourceData: Data = try {
35+
switch chaincode.type {
36+
case .hard:
37+
let padding = Data(repeating: 0, count: 1)
38+
let privateKeyData = parentKeypair.privateKey().rawData()
39+
40+
return padding + privateKeyData + chaincode.data
41+
42+
case .soft:
43+
throw BIP32KeypairFactoryError.unsupportedSoftDerivation
44+
}
45+
}()
46+
47+
let hmacResult = try generateHMAC512(
48+
from: sourceData,
49+
secretKeyData: parentKeypair.chaincode
50+
)
51+
52+
let childPrivateKeySeed = hmacResult[...31]
53+
let childChaincode = hmacResult[32...]
54+
55+
// we are returning seed as private key as both further derivation and signature requires it
56+
let publicKey = try internalFactory.derive(fromSeed: childPrivateKeySeed).publicKey()
57+
let secretKey = try EDPrivateKey(rawData: childPrivateKeySeed)
58+
59+
let keypair = IRCryptoKeypair(publicKey: publicKey, privateKey: secretKey)
60+
61+
return BIP32ExtendedKeypair(keypair: keypair, chaincode: childChaincode)
62+
}
63+
}

SubstrateSdk/Classes/Crypto/KeypairFactory/BIP32KeypairFactory.swift

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,52 @@
1-
import Foundation
1+
import CommonCrypto
22
import NovaCrypto
33

4-
public struct BIP32KeypairFactory {
5-
let internalFactory = BIP32KeyFactory()
4+
public class BIP32KeypairFactory {
5+
func deriveFromSeed(_ seed: Data) throws -> BIP32ExtendedKeypair {
6+
fatalError("Must be overriden by subsclass")
7+
}
8+
9+
func createKeypairFrom(_ parentKeypair: BIP32ExtendedKeypair, chaincode: Chaincode) throws -> BIP32ExtendedKeypair {
10+
fatalError("Must be overriden by subsclass")
11+
}
12+
13+
func generateHMAC512(
14+
from originalData: Data,
15+
secretKeyData: Data
16+
) throws -> Data {
17+
let digestLength = Int(CC_SHA512_DIGEST_LENGTH)
18+
let algorithm = CCHmacAlgorithm(kCCHmacAlgSHA512)
19+
20+
var buffer = [UInt8](repeating: 0, count: digestLength)
21+
22+
originalData.withUnsafeBytes {
23+
let rawOriginalDataPtr = $0.baseAddress!
24+
25+
secretKeyData.withUnsafeBytes {
26+
let rawSecretKeyPtr = $0.baseAddress!
627

7-
public init() {}
28+
CCHmac(
29+
algorithm,
30+
rawSecretKeyPtr,
31+
secretKeyData.count,
32+
rawOriginalDataPtr,
33+
originalData.count,
34+
&buffer
35+
)
36+
}
37+
}
38+
39+
return Data(bytes: buffer, count: digestLength)
40+
}
41+
}
842

9-
private func deriveChildKeypairFromMaster(
43+
private extension BIP32KeypairFactory {
44+
func deriveChildKeypairFromMaster(
1045
_ masterKeypair: BIP32ExtendedKeypair,
1146
chainIndexList: [Chaincode]
1247
) throws -> IRCryptoKeypairProtocol {
1348
let childExtendedKeypair = try chainIndexList.reduce(masterKeypair) { parentKeypair, chainIndex in
14-
try internalFactory.createKeypairFrom(parentKeypair, chaincode: chainIndex)
49+
try createKeypairFrom(parentKeypair, chaincode: chainIndex)
1550
}
1651

1752
return childExtendedKeypair.keypair
@@ -23,7 +58,7 @@ extension BIP32KeypairFactory: KeypairFactoryProtocol {
2358
_ seed: Data,
2459
chaincodeList: [Chaincode]
2560
) throws -> IRCryptoKeypairProtocol {
26-
let masterKeypair = try internalFactory.deriveFromSeed(seed)
61+
let masterKeypair = try deriveFromSeed(seed)
2762

2863
return try deriveChildKeypairFromMaster(
2964
masterKeypair,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import NovaCrypto
2+
3+
public enum BIP32KeypairFactoryError: Error {
4+
case invalidMasterKey
5+
case invalidChildKey
6+
case unsupportedSoftDerivation
7+
}
Lines changed: 23 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,37 @@
1-
import CommonCrypto
21
import NovaCrypto
32
import BigInt
43

5-
public enum BIP32KeyFactoryError: Error {
6-
case invalidChildKey
7-
}
8-
9-
protocol BIP32KeyFactoryProtocol {
10-
func deriveFromSeed(_ seed: Data) throws -> BIP32ExtendedKeypair
11-
func createKeypairFrom(_ parentKeypair: BIP32ExtendedKeypair, chaincode: Chaincode) throws -> BIP32ExtendedKeypair
12-
}
13-
14-
public struct BIP32KeyFactory {
4+
public final class BIP32Secp256KeypairFactory: BIP32KeypairFactory {
155
private static let initialSeed = "Bitcoin seed"
166
let internalFactory = SECKeyFactory()
17-
18-
private func generateHMAC512(
19-
from originalData: Data,
20-
secretKeyData: Data
21-
) throws -> Data {
22-
let digestLength = Int(CC_SHA512_DIGEST_LENGTH)
23-
let algorithm = CCHmacAlgorithm(kCCHmacAlgSHA512)
24-
25-
var buffer = [UInt8](repeating: 0, count: digestLength)
26-
27-
originalData.withUnsafeBytes {
28-
let rawOriginalDataPtr = $0.baseAddress!
29-
30-
secretKeyData.withUnsafeBytes {
31-
let rawSecretKeyPtr = $0.baseAddress!
32-
33-
CCHmac(
34-
algorithm,
35-
rawSecretKeyPtr,
36-
secretKeyData.count,
37-
rawOriginalDataPtr,
38-
originalData.count,
39-
&buffer
40-
)
41-
}
42-
}
43-
44-
return Data(bytes: buffer, count: digestLength)
7+
8+
public override init() {
9+
super.init()
4510
}
46-
}
47-
48-
extension BIP32KeyFactory: BIP32KeyFactoryProtocol {
49-
func deriveFromSeed(_ seed: Data) throws -> BIP32ExtendedKeypair {
11+
12+
override func deriveFromSeed(_ seed: Data) throws -> BIP32ExtendedKeypair {
5013
let hmacResult = try generateHMAC512(
5114
from: seed,
5215
secretKeyData: Data(Self.initialSeed.utf8)
5316
)
5417

55-
let privateKey = try SECPrivateKey(rawData: hmacResult[...31])
18+
let privateKeyData = hmacResult[...31]
19+
20+
let privateKeyInt = BigUInt(privateKeyData)
21+
22+
guard privateKeyInt < .secp256k1CurveOrder, privateKeyInt > 0 else {
23+
throw BIP32KeypairFactoryError.invalidMasterKey
24+
}
25+
26+
let privateKey = try SECPrivateKey(rawData: privateKeyData)
5627
let chainCode = hmacResult[32...]
5728

5829
let keypair = try internalFactory.derive(fromPrivateKey: privateKey)
5930

6031
return BIP32ExtendedKeypair(keypair: keypair, chaincode: chainCode)
6132
}
6233

63-
func createKeypairFrom(
34+
override func createKeypairFrom(
6435
_ parentKeypair: BIP32ExtendedKeypair,
6536
chaincode: Chaincode
6637
) throws -> BIP32ExtendedKeypair {
@@ -69,7 +40,7 @@ extension BIP32KeyFactory: BIP32KeyFactoryProtocol {
6940
case .hard:
7041
let padding = Data(repeating: 0, count: 1)
7142
let privateKeyData = parentKeypair.privateKey().rawData()
72-
43+
7344
return padding + privateKeyData + chaincode.data
7445

7546
case .soft:
@@ -88,14 +59,14 @@ extension BIP32KeyFactory: BIP32KeyFactoryProtocol {
8859
var privateKeyInt = BigUInt(privateKeySourceData.rawData())
8960

9061
guard privateKeyInt < .secp256k1CurveOrder else {
91-
throw BIP32KeyFactoryError.invalidChildKey
62+
throw BIP32KeypairFactoryError.invalidChildKey
9263
}
9364

9465
privateKeyInt += BigUInt(parentKeypair.privateKey().rawData())
9566
privateKeyInt %= .secp256k1CurveOrder
9667

9768
guard privateKeyInt > 0 else {
98-
throw BIP32KeyFactoryError.invalidChildKey
69+
throw BIP32KeypairFactoryError.invalidChildKey
9970
}
10071

10172
var privateKeyData = privateKeyInt.serialize()
@@ -113,6 +84,9 @@ extension BIP32KeyFactory: BIP32KeyFactoryProtocol {
11384
let privateKey = try SECPrivateKey(rawData: privateKeyData)
11485
let keypair = try internalFactory.derive(fromPrivateKey: privateKey)
11586

116-
return BIP32ExtendedKeypair(keypair: keypair, chaincode: childChaincode)
87+
return BIP32ExtendedKeypair(
88+
keypair: keypair,
89+
chaincode: childChaincode
90+
)
11791
}
11892
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import XCTest
2+
@testable import SubstrateSdk
3+
import NovaCrypto
4+
#if canImport(TestHelpers)
5+
import TestHelpers
6+
#endif
7+
8+
final class BIP32Ed25519KeypairDerivationTests: XCTestCase {
9+
struct TestVectorItem: Decodable {
10+
let seed: String
11+
let derivationPath: String?
12+
let privateKey: String
13+
let publicKey: String
14+
15+
func getPublicKeyWithoutPrefix() throws -> Data {
16+
// drop "00" prefix which is present in slip10 vectors
17+
// It is present in the test vectors ensure unified format with other vectors
18+
// But we do not need it
19+
try Data(hexString: publicKey).dropFirst(1)
20+
}
21+
}
22+
23+
func testSoftDerivationNotSupported() throws {
24+
let factory = BIP32Ed25519KeyFactory()
25+
26+
let junction = try BIP32JunctionFactory().parse(path: "/0")
27+
28+
let seed = try Data(hexString: "000102030405060708090a0b0c0d0e0f")
29+
30+
XCTAssertThrowsError(
31+
try factory.createKeypairFromSeed(seed, chaincodeList: junction.chaincodes),
32+
"Error is expected"
33+
) { error in
34+
guard let bip32Error = error as? BIP32KeypairFactoryError else {
35+
XCTFail("Keypair derivation error is expected")
36+
return
37+
}
38+
39+
XCTAssert(bip32Error == .unsupportedSoftDerivation)
40+
}
41+
}
42+
43+
// SLIP-0010 Official Test Vectors: https://github.com/satoshilabs/slips/blob/master/slip-0010.md
44+
func testDerivationFromSeed() throws {
45+
try performTest(filename: "BIP32Ed25519HDKDEtalon", keypairFactory: BIP32Ed25519KeyFactory())
46+
}
47+
48+
private func performTest(filename: String, keypairFactory: KeypairFactoryProtocol) throws {
49+
let bundle: Bundle
50+
#if SWIFT_PACKAGE
51+
bundle = Bundle.module
52+
#else
53+
bundle = Bundle(for: KeypairDeriviationTests.self)
54+
#endif
55+
guard let url = bundle
56+
.url(forResource: filename, withExtension: "json") else {
57+
XCTFail("Can't find resource")
58+
return
59+
}
60+
61+
do {
62+
let testData = try Data(contentsOf: url)
63+
let decoder = JSONDecoder()
64+
decoder.keyDecodingStrategy = .convertFromSnakeCase
65+
let items = try decoder.decode([TestVectorItem].self, from: testData)
66+
67+
let junctionFactory = BIP32JunctionFactory()
68+
69+
for item in items {
70+
let result: JunctionResult
71+
72+
if let derivationPath = item.derivationPath, !derivationPath.isEmpty {
73+
result = try junctionFactory.parse(path: derivationPath)
74+
} else {
75+
result = JunctionResult(chaincodes: [], password: nil)
76+
}
77+
78+
let seed = try Data(hexString: item.seed)
79+
80+
let keypair = try keypairFactory.createKeypairFromSeed(seed, chaincodeList: result.chaincodes)
81+
82+
let expectedPrivateKey = try Data(hexString: item.privateKey)
83+
let expectedPublicKey = try item.getPublicKeyWithoutPrefix()
84+
85+
let actualPrivateKey = keypair.privateKey().rawData()
86+
let actualPublicKey = keypair.publicKey().rawData()
87+
88+
XCTAssertEqual(
89+
expectedPrivateKey,
90+
actualPrivateKey,
91+
"Expected private key \(expectedPrivateKey.toHex()) but received \(actualPrivateKey.toHex())"
92+
)
93+
94+
XCTAssertEqual(
95+
expectedPublicKey,
96+
actualPublicKey,
97+
"Expected public key \(expectedPublicKey.toHex()) but received \(actualPublicKey.toHex())"
98+
)
99+
}
100+
} catch {
101+
XCTFail("Unexpected error \(error)")
102+
}
103+
}
104+
105+
}

0 commit comments

Comments
 (0)