Skip to content

Commit c559e8a

Browse files
committed
Add bip39 mnemonic support
1 parent 6fa4db2 commit c559e8a

File tree

3 files changed

+149
-14
lines changed

3 files changed

+149
-14
lines changed
Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,106 @@
11
import Foundation
2+
import CryptoKit
23

34
public enum Ed25519 {
4-
public enum Error: Swift.Error {
5-
case sharedSecretError(Swift.Error)
6-
}
7-
8-
public static func getSharedSecret(privateKey: PrivateKey, publicKey: PublicKey) throws -> Data {
9-
do {
10-
let xPrivateKey = try privateKey.toX25519
11-
let xPublicKey = try publicKey.toX25519
12-
return try X25519.getSharedSecret(privateKey: xPrivateKey, publicKey: xPublicKey)
13-
} catch {
14-
throw Error.sharedSecretError(error)
15-
}
16-
}
5+
6+
public enum Error: Swift.Error {
7+
case sharedSecretError(Swift.Error)
8+
case derivePathError(String)
9+
}
10+
11+
static let ED25519_CURVE = "ed25519 seed"
12+
public static let HARDENED_OFFSET: UInt32 = 0x80000000
13+
14+
public struct Keys {
15+
var key: Data
16+
var chainCode: Data
17+
}
18+
19+
public static func getSharedSecret(privateKey: PrivateKey, publicKey: PublicKey) throws -> Data {
20+
do {
21+
let xPrivateKey = try privateKey.toX25519
22+
let xPublicKey = try publicKey.toX25519
23+
return try X25519.getSharedSecret(privateKey: xPrivateKey, publicKey: xPublicKey)
24+
} catch {
25+
throw Error.sharedSecretError(error)
26+
27+
}
28+
}
29+
30+
public static func getMasterKeyFromSeed(seed: String) throws -> Keys {
31+
guard let seedData = Data(hexString: seed) else {
32+
throw Error.derivePathError("Invalid seed hex string")
33+
}
34+
35+
let hmac = HMAC<SHA512>.authenticationCode(for: seedData, using: SymmetricKey(data: ED25519_CURVE.data(using: .utf8)!))
36+
let I = Data(hmac)
37+
let IL = I.prefix(32)
38+
let IR = I.suffix(from: 32)
39+
40+
return Keys(key: IL, chainCode: IR)
41+
}
42+
43+
public static func CKDPriv(keys: Keys, index: UInt32) -> Keys {
44+
var indexData = Data(count: 4)
45+
indexData.withUnsafeMutableBytes { $0.bindMemory(to: UInt8.self).baseAddress?.withMemoryRebound(to: UInt32.self, capacity: 1) {
46+
$0.pointee = index.bigEndian
47+
}}
48+
49+
let data = Data([0]) + keys.key + indexData
50+
let hmacValue = HMAC<SHA512>.authenticationCode(for: data, using: SymmetricKey(data: keys.chainCode))
51+
let I = Data(hmacValue)
52+
let IL = I.prefix(32)
53+
let IR = I.suffix(from: 32)
54+
55+
return Keys(key: IL, chainCode: IR)
56+
}
57+
58+
public static func isValidPath(path: String) -> Bool {
59+
let pathRegex = #"^m(\/[0-9]+')+$"#
60+
let regex = try? NSRegularExpression(pattern: pathRegex)
61+
62+
let range = NSRange(location: 0, length: path.utf16.count)
63+
guard regex?.firstMatch(in: path, options: [], range: range) != nil else {
64+
return false
65+
}
66+
67+
return !path.split(separator: "/").dropFirst().map { $0.replacingOccurrences(of: "'", with: "") }.contains { Int($0) == nil }
68+
}
69+
70+
public static func derivePath(path: String, seed: String, offset: UInt32 = HARDENED_OFFSET) throws -> Keys {
71+
guard isValidPath(path: path) else {
72+
throw Error.derivePathError("Invalid derivation path")
73+
}
74+
75+
var keys = try getMasterKeyFromSeed(seed: seed)
76+
77+
let segments = path
78+
.split(separator: "/")
79+
.dropFirst()
80+
.map { $0.replacingOccurrences(of: "'", with: "") }
81+
.compactMap { UInt32($0) }
82+
83+
for segment in segments {
84+
keys = CKDPriv(keys: keys, index: segment + offset)
85+
}
86+
87+
return keys
88+
}
89+
}
90+
91+
extension Data {
92+
init?(hexString: String) {
93+
let length = hexString.count / 2
94+
var data = Data(capacity: length)
95+
var index = hexString.startIndex
96+
for _ in 0..<length {
97+
let nextIndex = hexString.index(index, offsetBy: 2)
98+
guard let byte = UInt8(hexString[index..<nextIndex], radix: 16) else {
99+
return nil
100+
}
101+
data.append(byte)
102+
index = nextIndex
103+
}
104+
self = data
105+
}
17106
}

Source/TonSwift/Mnemonic/Mnemonic.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,44 @@ public enum Mnemonic {
146146
return src.map({ $0.lowercased() })
147147
}
148148

149+
public static func isValidBip39Mnemonic(mnemonicArray: [String]) -> Bool {
150+
guard !mnemonicArray.isEmpty else { return false }
151+
guard mnemonicArray.allSatisfy({ words.contains($0) }) else { return false }
152+
return mnemonicArray.count % 3 == 0
153+
}
154+
155+
public static func bip39MnemonicToSeed(mnemonicArray: [String], password: String = "") -> Data {
156+
let salt: (_ password: String) -> String = { password in
157+
let salt = "mnemonic" + password
158+
return salt
159+
}
160+
161+
let mnemonicBuffer = Data(normalizeMnemonic(src: mnemonicArray).joined(separator: " ").utf8)
162+
let saltBuffer = Data(salt(password).utf8)
163+
164+
let res = pbkdf2Sha512(phrase: mnemonicBuffer, salt: saltBuffer, iterations: 2048, keyLength: 64)
165+
166+
return Data(res)
167+
}
168+
169+
public static func bip39MnemonicToPrivateKey(mnemonicArray: [String]) throws -> KeyPair {
170+
guard isValidBip39Mnemonic(mnemonicArray: mnemonicArray) else {
171+
throw NSError(domain: "Mnemonic", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid mnemonic"])
172+
}
173+
174+
let seed = bip39MnemonicToSeed(mnemonicArray: mnemonicArray)
175+
176+
do {
177+
let derived = try Ed25519.derivePath(path: "m/44'/607'/0'", seed: seed.hexString())
178+
179+
let keyPair = try TweetNacl.NaclSign.KeyPair.keyPair(fromSeed: derived.key)
180+
return KeyPair(publicKey: .init(data: keyPair.publicKey), privateKey: .init(data: keyPair.secretKey))
181+
182+
} catch {
183+
throw error
184+
}
185+
}
186+
149187
/**
150188
Extract private key from mnemonic
151189

@@ -165,4 +203,12 @@ public enum Mnemonic {
165203
throw error
166204
}
167205
}
206+
207+
public static func anyMnemonicToPrivateKey(mnemonicArray: [String], password: String = "") throws -> KeyPair {
208+
if(mnemonicValidate(mnemonicArray: mnemonicArray)) {
209+
return try mnemonicToPrivateKey(mnemonicArray: mnemonicArray)
210+
} else {
211+
return try bip39MnemonicToPrivateKey(mnemonicArray: mnemonicArray)
212+
}
213+
}
168214
}

Tests/TonSwiftTests/Mnemonic/MnemonicTest.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ final class MnemonicTest: XCTestCase {
1515

1616
// should create valid key pair
1717

18-
let keyPair = try Mnemonic.mnemonicToPrivateKey(mnemonicArray: mnemonicArray)
18+
let keyPair = try Mnemonic.anyMnemonicToPrivateKey(mnemonicArray: mnemonicArray)
1919
XCTAssertEqual(keyPair.publicKey.hexString, "34eb4b67d64f74d989ce2bc2e3dfddb7ed4cb0eec92f29fbecd05b1eabab0254")
2020
XCTAssertEqual(keyPair.privateKey.hexString, "c893fc0b676782a5c157ad8fddb389f75caba6eea1c198d8075a8a43afce70a934eb4b67d64f74d989ce2bc2e3dfddb7ed4cb0eec92f29fbecd05b1eabab0254")
2121
}

0 commit comments

Comments
 (0)