Skip to content

Commit 241e009

Browse files
feat: parsing mnemonic phrase from [String]
Add Support For The Mnemonic Phrase To Be An Array
2 parents 2357868 + 5205653 commit 241e009

File tree

8 files changed

+531
-82
lines changed

8 files changed

+531
-82
lines changed

Sources/Web3Core/KeystoreManager/BIP32HDNode.swift

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,31 @@ extension UInt32 {
2222
}
2323

2424
public class HDNode {
25+
private static var maxIterationIndex = UInt32(1) << 31
26+
27+
/// Contains private and public prefixes for serialization.
28+
/// See [BIP-32's serialization format](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#serialization-format) for more info.
2529
public struct HDversion {
26-
// swiftlint:disable force_unwrapping
27-
public var privatePrefix: Data = Data.fromHex("0x0488ADE4")!
28-
public var publicPrefix: Data = Data.fromHex("0x0488B21E")!
29-
// swiftlint:enable force_unwrapping
30-
public init() {}
30+
/// Mainnet public key prefix.
31+
/// Value `0x0488B21E` is a string `xpub` encoded as Base-58 and later as hexadecimal.
32+
public static let publicPrefix: Data! = Data.fromHex("0x0488B21E")
33+
34+
/// Mainnet private key prefix.
35+
/// Value `0x0488ADE4` is a string `xprv` encoded as Base-58 and later as hexadecimal.
36+
public static let privatePrefix: Data! = Data.fromHex("0x0488ADE4")
37+
38+
public let publicPrefix: Data
39+
public let privatePrefix: Data
40+
41+
/// Default values for `publicPrefix` and `privatePrefix` are
42+
/// `HDversion.publicPrefix` and `HDversion.privatePrefix` respectively.
43+
public init(public publicPrefix: Data = HDversion.publicPrefix,
44+
private privatePrefix: Data = HDversion.privatePrefix) {
45+
self.publicPrefix = publicPrefix
46+
self.privatePrefix = privatePrefix
47+
}
3148
}
49+
3250
public var path: String? = "m"
3351
public var privateKey: Data?
3452
public var publicKey: Data
@@ -37,14 +55,10 @@ public class HDNode {
3755
public var parentFingerprint: Data = Data(repeating: 0, count: 4)
3856
public var childNumber: UInt32 = UInt32(0)
3957
public var isHardened: Bool {
40-
childNumber >= (UInt32(1) << 31)
58+
childNumber >= Self.maxIterationIndex
4159
}
4260
public var index: UInt32 {
43-
if self.isHardened {
44-
return childNumber - (UInt32(1) << 31)
45-
} else {
46-
return childNumber
47-
}
61+
childNumber - (isHardened ? Self.maxIterationIndex : 0)
4862
}
4963
public var hasPrivate: Bool {
5064
privateKey != nil
@@ -65,7 +79,7 @@ public class HDNode {
6579
guard data.count == 82 else { return nil }
6680
let header = data[0..<4]
6781
var serializePrivate = false
68-
if header == HDNode.HDversion().privatePrefix {
82+
if header == HDversion.privatePrefix {
6983
serializePrivate = true
7084
}
7185
depth = data[4..<5].bytes[0]
@@ -90,30 +104,30 @@ public class HDNode {
90104

91105
public init?(seed: Data) {
92106
guard seed.count >= 16 else { return nil }
93-
// swiftlint:disable force_unwrapping
94-
let hmacKey = "Bitcoin seed".data(using: .ascii)!
95-
let hmac = HMAC(key: hmacKey.bytes, variant: HMAC.Variant.sha2(.sha512))
107+
108+
guard let hmacKey = "Bitcoin seed".data(using: .ascii) else { return nil }
109+
let hmac = HMAC(key: hmacKey.bytes, variant: .sha2(.sha512))
110+
96111
guard let entropy = try? hmac.authenticate(seed.bytes), entropy.count == 64 else { return nil }
97112
let I_L = entropy[0..<32]
98113
let I_R = entropy[32..<64]
99114
chaincode = Data(I_R)
100115
let privKeyCandidate = Data(I_L)
101116
guard SECP256K1.verifyPrivateKey(privateKey: privKeyCandidate) else { return nil }
102117
guard let pubKeyCandidate = SECP256K1.privateToPublic(privateKey: privKeyCandidate, compressed: true) else { return nil }
103-
guard pubKeyCandidate.bytes[0] == 0x02 || pubKeyCandidate.bytes[0] == 0x03 else { return nil }
118+
guard pubKeyCandidate.bytes.first == 0x02 || pubKeyCandidate.bytes.first == 0x03 else { return nil }
104119
publicKey = pubKeyCandidate
105120
privateKey = privKeyCandidate
106121
depth = 0x00
107122
childNumber = UInt32(0)
108123
}
109124

110-
private static var curveOrder = BigUInt("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", radix: 16)!
111-
// swiftlint:enable force_unwrapping
112-
public static var defaultPath: String = "m/44'/60'/0'/0"
113-
public static var defaultPathPrefix: String = "m/44'/60'/0'"
114-
public static var defaultPathMetamask: String = "m/44'/60'/0'/0/0"
115-
public static var defaultPathMetamaskPrefix: String = "m/44'/60'/0'/0"
116-
public static var hardenedIndexPrefix: UInt32 = (UInt32(1) << 31)
125+
private static let curveOrder = BigUInt("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", radix: 16)!
126+
public static let defaultPath = "m/44'/60'/0'/0"
127+
public static let defaultPathPrefix = "m/44'/60'/0'"
128+
public static let defaultPathMetamask = "m/44'/60'/0'/0/0"
129+
public static let defaultPathMetamaskPrefix = "m/44'/60'/0'/0"
130+
public static var hardenedIndexPrefix: UInt32 { Self.maxIterationIndex }
117131
}
118132

119133
extension HDNode {

Sources/Web3Core/KeystoreManager/BIP32Keystore.swift

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ public class BIP32Keystore: AbstractKeystore {
4444
guard let decryptedRootNode = try? self.getPrefixNodeData(password) else {throw AbstractKeystoreError.encryptionError("Failed to decrypt a keystore")}
4545
guard let rootNode = HDNode(decryptedRootNode) else {throw AbstractKeystoreError.encryptionError("Failed to deserialize a root node")}
4646
guard rootNode.depth == (self.rootPrefix.components(separatedBy: "/").count - 1) else {throw AbstractKeystoreError.encryptionError("Derivation depth mismatch")}
47-
// guard rootNode.depth == HDNode.defaultPathPrefix.components(separatedBy: "/").count - 1 else {throw AbstractKeystoreError.encryptionError("Derivation depth mismatch")}
4847
guard let index = UInt32(key.components(separatedBy: "/").last!) else {
4948
throw AbstractKeystoreError.encryptionError("Derivation depth mismatch")
5049
}
@@ -98,7 +97,17 @@ public class BIP32Keystore: AbstractKeystore {
9897
try self.init(seed: seed, password: password, prefixPath: prefixPath, aesMode: aesMode)
9998
}
10099

101-
public init? (seed: Data, password: String, prefixPath: String = HDNode.defaultPathMetamaskPrefix, aesMode: String = "aes-128-cbc") throws {
100+
public convenience init?(mnemonicsPhrase: [String], password: String, mnemonicsPassword: String = "", language: BIP39Language = .english, prefixPath: String = HDNode.defaultPathMetamaskPrefix, aesMode: String = "aes-128-cbc") throws {
101+
guard var seed = BIP39.seedFromMmemonics(mnemonicsPhrase, password: mnemonicsPassword, language: language) else {
102+
throw AbstractKeystoreError.noEntropyError
103+
}
104+
defer {
105+
Data.zero(&seed)
106+
}
107+
try self.init(seed: seed, password: password, prefixPath: prefixPath, aesMode: aesMode)
108+
}
109+
110+
public init?(seed: Data, password: String, prefixPath: String = HDNode.defaultPathMetamaskPrefix, aesMode: String = "aes-128-cbc") throws {
102111
addressStorage = PathAddressStorage()
103112
guard let rootNode = HDNode(seed: seed)?.derive(path: prefixPath, derivePrivateKey: true) else { return nil }
104113
self.rootPrefix = prefixPath
@@ -127,13 +136,18 @@ public class BIP32Keystore: AbstractKeystore {
127136
try encryptDataToStorage(password, data: serializedRootNode, aesMode: self.keystoreParams!.crypto.cipher)
128137
}
129138

130-
func createNewAccount(parentNode: HDNode, password: String ) throws {
131-
var newIndex = UInt32(0)
132-
for p in addressStorage.paths {
133-
guard let idx = UInt32(p.components(separatedBy: "/").last!) else {continue}
134-
if idx >= newIndex {
135-
newIndex = idx + 1
136-
}
139+
func createNewAccount(parentNode: HDNode, password: String = "web3swift") throws {
140+
let maxIndex = addressStorage.paths
141+
.compactMap { $0.components(separatedBy: "/").last }
142+
.compactMap { UInt32($0) }
143+
.max()
144+
145+
let newIndex: UInt32
146+
147+
if let idx = maxIndex {
148+
newIndex = idx + 1
149+
} else {
150+
newIndex = UInt32.zero
137151
}
138152
guard let newNode = parentNode.derive(index: newIndex, derivePrivateKey: true, hardened: false) else {
139153
throw AbstractKeystoreError.keyDerivationError
@@ -151,7 +165,8 @@ public class BIP32Keystore: AbstractKeystore {
151165
addressStorage.add(address: newAddress, for: newPath)
152166
}
153167

154-
public func createNewCustomChildAccount(password: String, path: String) throws {guard let decryptedRootNode = try? self.getPrefixNodeData(password) else {
168+
public func createNewCustomChildAccount(password: String, path: String) throws {
169+
guard let decryptedRootNode = try getPrefixNodeData(password) else {
155170
throw AbstractKeystoreError.encryptionError("Failed to decrypt a keystore")
156171
}
157172
guard let rootNode = HDNode(decryptedRootNode) else {
@@ -201,15 +216,11 @@ public class BIP32Keystore: AbstractKeystore {
201216
try encryptDataToStorage(password, data: serializedRootNode, aesMode: self.keystoreParams!.crypto.cipher)
202217
}
203218

204-
fileprivate func encryptDataToStorage(_ password: String, data: Data?, dkLen: Int = 32, N: Int = 4096, R: Int = 6, P: Int = 1, aesMode: String = "aes-128-cbc") throws {
205-
if data == nil {
206-
throw AbstractKeystoreError.encryptionError("Encryption without key data")
207-
}
208-
if data!.count != 82 {
219+
fileprivate func encryptDataToStorage(_ password: String, data: Data, dkLen: Int = 32, N: Int = 4096, R: Int = 6, P: Int = 1, aesMode: String = "aes-128-cbc") throws {
220+
guard data.count == 82 else {
209221
throw AbstractKeystoreError.encryptionError("Invalid expected data length")
210222
}
211-
let saltLen = 32
212-
guard let saltData = Data.randomBytes(length: saltLen) else {
223+
guard let saltData = Data.randomBytes(length: 32) else {
213224
throw AbstractKeystoreError.noEntropyError
214225
}
215226
guard let derivedKey = scrypt(password: password, salt: saltData, length: dkLen, N: N, R: R, P: P) else {
@@ -232,7 +243,7 @@ public class BIP32Keystore: AbstractKeystore {
232243
if aesCipher == nil {
233244
throw AbstractKeystoreError.aesError
234245
}
235-
guard let encryptedKey = try aesCipher?.encrypt(data!.bytes) else {
246+
guard let encryptedKey = try aesCipher?.encrypt(data.bytes) else {
236247
throw AbstractKeystoreError.aesError
237248
}
238249
let encryptedKeyData = Data(encryptedKey)

Sources/Web3Core/KeystoreManager/BIP39.swift

Lines changed: 93 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public enum BIP39Language {
1515
case french
1616
case italian
1717
case spanish
18+
1819
public var words: [String] {
1920
switch self {
2021
case .english:
@@ -70,49 +71,94 @@ public enum BIP39Language {
7071

7172
public class BIP39 {
7273

73-
static public func generateMnemonicsFromEntropy(entropy: Data, language: BIP39Language = BIP39Language.english) -> String? {
74-
guard entropy.count >= 16, entropy.count & 4 == 0 else { return nil }
75-
let checksum = entropy.sha256()
76-
let checksumBits = entropy.count*8/32
77-
var fullEntropy = Data()
78-
fullEntropy.append(entropy)
79-
fullEntropy.append(checksum[0 ..< (checksumBits+7)/8 ])
80-
var wordList = [String]()
81-
for i in 0 ..< fullEntropy.count*8/11 {
82-
guard let bits = fullEntropy.bitsInRange(i*11, 11) else { return nil }
83-
let index = Int(bits)
84-
guard language.words.count > index else { return nil }
85-
let word = language.words[index]
86-
wordList.append(word)
74+
/// Generates a mnemonic phrase length of which depends on the provided `bitsOfEntropy`.
75+
/// Returned value is a single string where words are joined by ``BIP39Language/separator``.
76+
/// Keep in mind that different languages may have different separators.
77+
/// - Parameters:
78+
/// - bitsOfEntropy: 128 - 12 words, 160 - 15 words, and up to 256 - 24 words as output. The value must be a multiple of 32.
79+
/// - language: words language, default is set to english.
80+
/// - Returns: mnemonic phrase as a single string containing 12, 15, 18, 21 or 24 words.
81+
public static func generateMnemonics(bitsOfEntropy: Int, language: BIP39Language = .english) throws -> String? {
82+
let entropy = try entropyOf(size: bitsOfEntropy)
83+
return generateMnemonicsFromEntropy(entropy: entropy, language: language)
84+
}
85+
86+
/// Generates a mnemonic phrase length of which depends on the provided `entropy`.
87+
/// - Parameters:
88+
/// - entropy: 128 - 12 words, 192 - 18 words, 256 - 24 words in output.
89+
/// - language: words language, default is set to english.
90+
/// - Returns: mnemonic phrase as an array containing 12, 15, 18, 21 or 24 words.
91+
/// `nil` is returned in cases like wrong `entropy` value (e.g. `entropy` is not a multiple of 32).
92+
public static func generateMnemonics(entropy: Int, language: BIP39Language = .english) throws -> [String] {
93+
let entropy = try entropyOf(size: entropy)
94+
return generateMnemonicsFrom(entropy: entropy, language: language)
95+
}
96+
97+
private static func entropyOf(size: Int) throws -> Data {
98+
guard
99+
size >= 128 && size <= 256 && size.isMultiple(of: 32),
100+
let entropy = Data.randomBytes(length: size/8)
101+
else {
102+
throw AbstractKeystoreError.noEntropyError
103+
}
104+
return entropy
105+
}
106+
107+
static func bitarray(from data: Data) -> String {
108+
data.map {
109+
let binary = String($0, radix: 2)
110+
let padding = String(repeating: "0", count: 8 - binary.count)
111+
return padding + binary
112+
}.joined()
113+
}
114+
115+
static func generateChecksum(entropyBytes inputData: Data, checksumLength: Int) -> String? {
116+
guard let checksumData = inputData.sha256().bitsInRange(0, checksumLength) else {
117+
return nil
87118
}
119+
return String(checksumData, radix: 2).leftPadding(toLength: checksumLength, withPad: "0")
120+
}
121+
122+
public static func generateMnemonicsFromEntropy(entropy: Data, language: BIP39Language = .english) -> String? {
123+
guard entropy.count >= 16, entropy.count & 4 == 0 else { return nil }
88124
let separator = language.separator
125+
let wordList = generateMnemonicsFrom(entropy: entropy)
89126
return wordList.joined(separator: separator)
90127
}
91128

92-
/// Initializes a new mnemonics set with the provided bitsOfEntropy.
93-
/// - Parameters:
94-
/// - bitsOfEntropy: 128 - 12 words, 192 - 18 words , 256 - 24 words in output.
95-
/// - language: words language, default english
96-
/// - Returns: random 12-24 words, that represent new Mnemonic phrase.
97-
static public func generateMnemonics(bitsOfEntropy: Int, language: BIP39Language = BIP39Language.english) throws -> String? {
98-
guard bitsOfEntropy >= 128 && bitsOfEntropy <= 256 && bitsOfEntropy.isMultiple(of: 32) else { return nil }
99-
guard let entropy = Data.randomBytes(length: bitsOfEntropy/8) else {throw AbstractKeystoreError.noEntropyError}
100-
return BIP39.generateMnemonicsFromEntropy(entropy: entropy, language:
101-
language)
129+
public static func generateMnemonicsFrom(entropy: Data, language: BIP39Language = .english) -> [String] {
130+
let entropyBitSize = entropy.count * 8
131+
let checksum_length = entropyBitSize / 32
132+
133+
var entropy_bits = bitarray(from: entropy)
134+
135+
guard let checksumTest = generateChecksum(entropyBytes: entropy, checksumLength: checksum_length) else {
136+
return []
137+
}
138+
entropy_bits += checksumTest
139+
return entropy_bits
140+
.split(intoChunksOf: 11)
141+
.compactMap { binary in
142+
Int(binary, radix: 2)
143+
}
144+
.map { index in
145+
language.words[index]
146+
}
147+
}
102148

149+
public static func mnemonicsToEntropy(_ mnemonics: String, language: BIP39Language = .english) -> Data? {
150+
let wordList = mnemonics.components(separatedBy: language.separator)
151+
return mnemonicsToEntropy(wordList, language: language)
103152
}
104153

105-
static public func mnemonicsToEntropy(_ mnemonics: String, language: BIP39Language = BIP39Language.english) -> Data? {
106-
let wordList = mnemonics.components(separatedBy: " ")
107-
guard wordList.count >= 12 && wordList.count.isMultiple(of: 3) && wordList.count <= 24 else { return nil }
154+
public static func mnemonicsToEntropy(_ mnemonics: [String], language: BIP39Language = .english) -> Data? {
155+
guard 12...24 ~= mnemonics.count && mnemonics.count.isMultiple(of: 3) else { return nil }
108156
var bitString = ""
109-
for word in wordList {
110-
let idx = language.words.firstIndex(of: word)
111-
if idx == nil {
157+
for word in mnemonics {
158+
guard let idx = language.words.firstIndex(of: word) else {
112159
return nil
113160
}
114-
let idxAsInt = language.words.startIndex.distance(to: idx!)
115-
let stringForm = String(UInt16(idxAsInt), radix: 2).leftPadding(toLength: 11, withPad: "0")
161+
let stringForm = String(UInt16(idx), radix: 2).leftPadding(toLength: 11, withPad: "0")
116162
bitString.append(stringForm)
117163
}
118164
let stringCount = bitString.count
@@ -131,23 +177,30 @@ public class BIP39 {
131177
return entropy
132178
}
133179

134-
static public func seedFromMmemonics(_ mnemonics: String, password: String = "", language: BIP39Language = BIP39Language.english) -> Data? {
135-
let valid = BIP39.mnemonicsToEntropy(mnemonics, language: language) != nil
136-
if !valid {
180+
public static func seedFromMmemonics(_ mnemonics: [String], password: String = "", language: BIP39Language = .english) -> Data? {
181+
let wordList = mnemonics.joined(separator: language.separator)
182+
return seedFromMmemonics(wordList, password: password, language: language)
183+
}
184+
185+
public static func seedFromMmemonics(_ mnemonics: String, password: String = "", language: BIP39Language = .english) -> Data? {
186+
guard mnemonicsToEntropy(mnemonics, language: language) != nil else {
137187
return nil
138188
}
189+
return dataFrom(mnemonics: mnemonics, password: password)
190+
}
191+
192+
private static func dataFrom(mnemonics: String, password: String) -> Data? {
139193
guard let mnemData = mnemonics.decomposedStringWithCompatibilityMapping.data(using: .utf8) else { return nil }
140194
let salt = "mnemonic" + password
141195
guard let saltData = salt.decomposedStringWithCompatibilityMapping.data(using: .utf8) else { return nil }
142196
guard let seedArray = try? PKCS5.PBKDF2(password: mnemData.bytes, salt: saltData.bytes, iterations: 2048, keyLength: 64, variant: HMAC.Variant.sha2(.sha512)).calculate() else { return nil }
143-
let seed = Data(seedArray)
144-
return seed
197+
return Data(seedArray)
145198
}
146199

147-
static public func seedFromEntropy(_ entropy: Data, password: String = "", language: BIP39Language = BIP39Language.english) -> Data? {
148-
guard let mnemonics = BIP39.generateMnemonicsFromEntropy(entropy: entropy, language: language) else {
200+
public static func seedFromEntropy(_ entropy: Data, password: String = "", language: BIP39Language = .english) -> Data? {
201+
guard let mnemonics = generateMnemonicsFromEntropy(entropy: entropy, language: language) else {
149202
return nil
150203
}
151-
return BIP39.seedFromMmemonics(mnemonics, password: password, language: language)
204+
return seedFromMmemonics(mnemonics, password: password, language: language)
152205
}
153206
}

0 commit comments

Comments
 (0)