Skip to content

Update Mnemonic creation to support arrays #687

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

Closed
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
280 changes: 145 additions & 135 deletions Sources/Core/KeystoreManager/BIP32HDNode.swift

Large diffs are not rendered by default.

48 changes: 31 additions & 17 deletions Sources/Core/KeystoreManager/BIP32Keystore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ public class BIP32Keystore: AbstractKeystore {
guard let decryptedRootNode = try? self.getPrefixNodeData(password) else {throw AbstractKeystoreError.encryptionError("Failed to decrypt a keystore")}
guard let rootNode = HDNode(decryptedRootNode) else {throw AbstractKeystoreError.encryptionError("Failed to deserialize a root node")}
guard rootNode.depth == (self.rootPrefix.components(separatedBy: "/").count - 1) else {throw AbstractKeystoreError.encryptionError("Derivation depth mismatch")}
// guard rootNode.depth == HDNode.defaultPathPrefix.components(separatedBy: "/").count - 1 else {throw AbstractKeystoreError.encryptionError("Derivation depth mismatch")}
guard let index = UInt32(key.components(separatedBy: "/").last!) else {
throw AbstractKeystoreError.encryptionError("Derivation depth mismatch")
}
Expand Down Expand Up @@ -98,6 +97,18 @@ public class BIP32Keystore: AbstractKeystore {
try self.init(seed: seed, password: password, prefixPath: prefixPath, aesMode: aesMode)
}

//TODO: Unit Test
//TODO: merge and cleanup with above code
public convenience init?(mnemonicsPhrase: [String], password: String, mnemonicsPassword: String = "", language: BIP39Language = .english, prefixPath: String = HDNode.defaultPathMetamaskPrefix, aesMode: String = "aes-128-cbc") throws {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: add newline characters after each comma so that it's easier to read.

guard var seed = BIP39.seedFromMmemonics(mnemonicsPhrase, password: mnemonicsPassword, language: language) else {
throw AbstractKeystoreError.noEntropyError
}
defer {
Data.zero(&seed)
}
try self.init(seed: seed, password: password, prefixPath: prefixPath, aesMode: aesMode)
}

public init? (seed: Data, password: String, prefixPath: String = HDNode.defaultPathMetamaskPrefix, aesMode: String = "aes-128-cbc") throws {
addressStorage = PathAddressStorage()
guard let rootNode = HDNode(seed: seed)?.derive(path: prefixPath, derivePrivateKey: true) else {return nil}
Expand Down Expand Up @@ -127,13 +138,19 @@ public class BIP32Keystore: AbstractKeystore {
try encryptDataToStorage(password, data: serializedRootNode, aesMode: self.keystoreParams!.crypto.cipher)
}

func createNewAccount(parentNode: HDNode, password: String ) throws {
var newIndex = UInt32(0)
for p in addressStorage.paths {
guard let idx = UInt32(p.components(separatedBy: "/").last!) else {continue}
if idx >= newIndex {
newIndex = idx + 1
}
func createNewAccount(parentNode: HDNode, password: String = "web3swift") throws {

let maxIndex = addressStorage.paths
.compactMap { $0.components(separatedBy: "/").last }
.compactMap { UInt32($0) }
.max()

let newIndex: UInt32

if let idx = maxIndex {
newIndex = idx + 1
} else {
newIndex = UInt32.zero
}
guard let newNode = parentNode.derive(index: newIndex, derivePrivateKey: true, hardened: false) else {
throw AbstractKeystoreError.keyDerivationError
Expand All @@ -151,7 +168,8 @@ public class BIP32Keystore: AbstractKeystore {
addressStorage.add(address: newAddress, for: newPath)
}

public func createNewCustomChildAccount(password: String, path: String) throws {guard let decryptedRootNode = try? self.getPrefixNodeData(password) else {
public func createNewCustomChildAccount(password: String, path: String) throws {
guard let decryptedRootNode = try? getPrefixNodeData(password) else {
throw AbstractKeystoreError.encryptionError("Failed to decrypt a keystore")
}
guard let rootNode = HDNode(decryptedRootNode) else {
Expand Down Expand Up @@ -201,15 +219,11 @@ public class BIP32Keystore: AbstractKeystore {
try encryptDataToStorage(password, data: serializedRootNode, aesMode: self.keystoreParams!.crypto.cipher)
}

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 {
if data == nil {
throw AbstractKeystoreError.encryptionError("Encryption without key data")
}
if data!.count != 82 {
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 {
guard data.count == 82 else {
throw AbstractKeystoreError.encryptionError("Invalid expected data length")
}
let saltLen = 32
guard let saltData = Data.randomBytes(length: saltLen) else {
guard let saltData = Data.randomBytes(length: 32) else {
throw AbstractKeystoreError.noEntropyError
}
guard let derivedKey = scrypt(password: password, salt: saltData, length: dkLen, N: N, R: R, P: P) else {
Expand All @@ -232,7 +246,7 @@ public class BIP32Keystore: AbstractKeystore {
if aesCipher == nil {
throw AbstractKeystoreError.aesError
}
guard let encryptedKey = try aesCipher?.encrypt(data!.bytes) else {
guard let encryptedKey = try aesCipher?.encrypt(data.bytes) else {
throw AbstractKeystoreError.aesError
}
let encryptedKeyData = Data(encryptedKey)
Expand Down
129 changes: 89 additions & 40 deletions Sources/Core/KeystoreManager/BIP39.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,50 +69,88 @@ public enum BIP39Language {
}

public class BIP39 {
/**
Initializes a new mnemonics set with the provided bitsOfEntropy.
**/
Comment on lines +72 to +74
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something to remove.

/// Initializes a new mnemonics set with the provided bitsOfEntropy.
/// - Parameters:
/// - bitsOfEntropy: 128 - 12 words, 192 - 18 words , 256 - 24 words in output.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

18 words , 256 - redundant space.

/// - language: words language, default english
/// - Returns: random 12-24 words, that represent new Mnemonic phrase.
static public func generateMnemonics(bitsOfEntropy: Int, language: BIP39Language = .english) throws -> String? {
guard let entropy = entropyOf(size: bitsOfEntropy) else { throw AbstractKeystoreError.noEntropyError }
return generateMnemonicsFromEntropy(entropy: entropy, language: language)
}

static public func generateMnemonicsFromEntropy(entropy: Data, language: BIP39Language = BIP39Language.english) -> String? {
guard entropy.count >= 16, entropy.count & 4 == 0 else {return nil}
let checksum = entropy.sha256()
let checksumBits = entropy.count*8/32
var fullEntropy = Data()
fullEntropy.append(entropy)
fullEntropy.append(checksum[0 ..< (checksumBits+7)/8 ])
var wordList = [String]()
for i in 0 ..< fullEntropy.count*8/11 {
guard let bits = fullEntropy.bitsInRange(i*11, 11) else {return nil}
let index = Int(bits)
guard language.words.count > index else {return nil}
let word = language.words[index]
wordList.append(word)
static public func generateMnemonics(entropy: Int, language: BIP39Language = .english) -> [String]? {
guard let entropy = entropyOf(size: entropy) else { return nil }
return generateMnemonicsFrom(entropy: entropy, language: language)
}

static private func entropyOf(size: Int) -> Data? {
guard size >= 128 && size <= 256 && size.isMultiple(of: 32) else {
return nil
}

return Data.randomBytes(length: size/8)
}

static func bitarray(from data: Data) -> String {
data.map {
let binary = String($0, radix: 2)
let padding = String(repeating: "0", count: 8 - binary.count)
return padding + binary
}.joined()
}

static func generateChecksum(entropyBytes inputData: Data, checksumLength: Int) -> String? {
guard let checksumData = inputData.sha256().bitsInRange(0, checksumLength) else {
return nil
}
let checksum = String(checksumData, radix: 2).leftPadding(toLength: checksumLength, withPad: "0")
return checksum
}

static public func generateMnemonicsFromEntropy(entropy: Data, language: BIP39Language = .english) -> String? {
guard entropy.count >= 16, entropy.count & 4 == 0 else {return nil}
let separator = language.separator
let wordList = generateMnemonicsFrom(entropy: entropy)
return wordList.joined(separator: separator)
}

/// Initializes a new mnemonics set with the provided bitsOfEntropy.
/// - Parameters:
/// - bitsOfEntropy: 128 - 12 words, 192 - 18 words , 256 - 24 words in output.
/// - language: words language, default english
/// - Returns: random 12-24 words, that represent new Mnemonic phrase.
static public func generateMnemonics(bitsOfEntropy: Int, language: BIP39Language = BIP39Language.english) throws -> String? {
guard bitsOfEntropy >= 128 && bitsOfEntropy <= 256 && bitsOfEntropy.isMultiple(of: 32) else {return nil}
guard let entropy = Data.randomBytes(length: bitsOfEntropy/8) else {throw AbstractKeystoreError.noEntropyError}
return BIP39.generateMnemonicsFromEntropy(entropy: entropy, language:
language)
static public func generateMnemonicsFrom(entropy: Data, language: BIP39Language = .english) -> [String] {
let entropyBitSize = entropy.count * 8
let checksum_length = entropyBitSize / 32

var entropy_bits = bitarray(from: entropy)

guard let checksumTest = generateChecksum(entropyBytes: entropy, checksumLength: checksum_length) else {
return []
}
entropy_bits += checksumTest
return entropy_bits
.split(every: 11)
.compactMap { binary in
Int(binary, radix: 2)
}
.map { index in
language.words[index]
}
Comment on lines +133 to +138
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting is required.
Please make sure all files you worked with are formatted.

}

static public func mnemonicsToEntropy(_ mnemonics: String, language: BIP39Language = BIP39Language.english) -> Data? {
let wordList = mnemonics.components(separatedBy: " ")
guard wordList.count >= 12 && wordList.count.isMultiple(of: 3) && wordList.count <= 24 else {return nil}
static public func mnemonicsToEntropy(_ mnemonics: String, language: BIP39Language = .english) -> Data? {
let wordList = mnemonics.components(separatedBy: language.separator)
return mnemonicsToEntropy(wordList, language: language)
}

static public func mnemonicsToEntropy(_ mnemonics: [String], language: BIP39Language = .english) -> Data? {
guard mnemonics.count >= 12 && mnemonics.count.isMultiple(of: 3) && mnemonics.count <= 24 else {return nil}
var bitString = ""
for word in wordList {
let idx = language.words.firstIndex(of: word)
if idx == nil {
for word in mnemonics {
guard let idx = language.words.firstIndex(of: word) else {
return nil
}
let idxAsInt = language.words.startIndex.distance(to: idx!)
let stringForm = String(UInt16(idxAsInt), radix: 2).leftPadding(toLength: 11, withPad: "0")
let stringForm = String(UInt16(idx), radix: 2).leftPadding(toLength: 11, withPad: "0")
bitString.append(stringForm)
}
let stringCount = bitString.count
Expand All @@ -131,23 +169,34 @@ public class BIP39 {
return entropy
}

static public func seedFromMmemonics(_ mnemonics: String, password: String = "", language: BIP39Language = BIP39Language.english) -> Data? {
let valid = BIP39.mnemonicsToEntropy(mnemonics, language: language) != nil
if !valid {

//=========================================

static public func seedFromMmemonics(_ mnemonics: [String], password: String = "", language: BIP39Language = .english) -> Data? {
let wordList = mnemonics.joined(separator: language.separator)
return seedFromMmemonics(wordList, password: password, language: language)
}

static public func seedFromMmemonics(_ mnemonics: String, password: String = "", language: BIP39Language = .english) -> Data? {
if mnemonicsToEntropy(mnemonics, language: language) == nil {
return nil
}
return dataFrom(mnemonics: mnemonics, password: password)
}

static private func dataFrom(mnemonics: String, password: String) -> Data? {
guard let mnemData = mnemonics.decomposedStringWithCompatibilityMapping.data(using: .utf8) else {return nil}
let salt = "mnemonic" + password
guard let saltData = salt.decomposedStringWithCompatibilityMapping.data(using: .utf8) else {return nil}
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}
let seed = Data(seedArray)
return seed
return Data(seedArray)
}

static public func seedFromEntropy(_ entropy: Data, password: String = "", language: BIP39Language = BIP39Language.english) -> Data? {
guard let mnemonics = BIP39.generateMnemonicsFromEntropy(entropy: entropy, language: language) else {
static public func seedFromEntropy(_ entropy: Data, password: String = "", language: BIP39Language = .english) -> Data? {
guard let mnemonics = generateMnemonicsFromEntropy(entropy: entropy, language: language) else {
return nil
}
return BIP39.seedFromMmemonics(mnemonics, password: password, language: language)
return seedFromMmemonics(mnemonics, password: password, language: language)
}
}

44 changes: 26 additions & 18 deletions Sources/Core/Utility/Data+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
//

import Foundation
import Metal

public extension Data {

extension Data {
init<T>(fromArray values: [T]) {
let values = values
let ptrUB = values.withUnsafeBufferPointer { (ptr: UnsafeBufferPointer) in return ptr }
Expand Down Expand Up @@ -33,32 +35,38 @@ extension Data {
return difference == UInt8(0x00)
}

public static func zero(_ data: inout Data) {
static func zero(_ data: inout Data) {
let count = data.count
data.withUnsafeMutableBytes { (body: UnsafeMutableRawBufferPointer) in
body.baseAddress?.assumingMemoryBound(to: UInt8.self).initialize(repeating: 0, count: count)
}
}

public static func randomBytes(length: Int) -> Data? {
for _ in 0...1024 {
var data = Data(repeating: 0, count: length)
let result = data.withUnsafeMutableBytes { (body: UnsafeMutableRawBufferPointer) -> Int32? in
if let bodyAddress = body.baseAddress, body.count > 0 {
let pointer = bodyAddress.assumingMemoryBound(to: UInt8.self)
return SecRandomCopyBytes(kSecRandomDefault, length, pointer)
} else {
return nil
}
}
if let notNilResult = result, notNilResult == errSecSuccess {
return data
}
static func randomBytes(length: Int) -> Data? {
let entropy_bit_size = length//128
//# valid_entropy_bit_sizes = [128, 160, 192, 224, 256], count: [12, 15, 18, 21, 24]
var entropy_bytes = [UInt8](repeating: 0, count: entropy_bit_size)// / 8)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more code to remove // / 8)


let status = SecRandomCopyBytes(kSecRandomDefault, entropy_bytes.count, &entropy_bytes)

if status != errSecSuccess { // Always test the status.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least return nil is missing.
I'd also add NSLog with some Error: .... reason here (based on status) ....

} else {
entropy_bytes = [UInt8](repeating: 0, count: entropy_bit_size)// / 8)
arc4random_buf(&entropy_bytes, entropy_bytes.count)
}
return nil

let source1 = MTLCreateSystemDefaultDevice()?.makeBuffer(length: length)?.hash.description.data(using: .utf8)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure but maybe you can answer: will it be compatible with all platforms we support?
Except for Linux of course.

I suppose it should be but can say for sure.


let entropyData = entropy_bytes.shuffled().map{ bit in
return bit ^ (source1?.randomElement() ?? 0)

}

return Data(entropyData)
}

public func bitsInRange(_ startingBit: Int, _ length: Int) -> UInt64? { // return max of 8 bytes for simplicity, non-public
func bitsInRange(_ startingBit: Int, _ length: Int) -> UInt64? { // return max of 8 bytes for simplicity, non-public
if startingBit + length / 8 > self.count, length > 64, startingBit > 0, length >= 1 {return nil}
let bytes = self[(startingBit/8) ..< (startingBit+length+7)/8]
let padding = Data(repeating: 0, count: 8 - bytes.count)
Expand Down
24 changes: 22 additions & 2 deletions Sources/Core/Utility/String+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ extension String {
return String(self[start..<end])
}

func leftPadding(toLength: Int, withPad character: Character) -> String {
public func leftPadding(toLength: Int, withPad character: Character) -> String {

let stringLength = self.count
if stringLength < toLength {
return String(repeatElement(character, count: toLength - stringLength)) + self
Expand Down Expand Up @@ -92,7 +93,6 @@ extension String {
guard let matcher = try? NSRegularExpression(pattern: "^(?<prefix>0x)(?<leadingZeroes>0+)(?<end>[0-9a-fA-F]*)$",
options: .dotMatchesLineSeparators)
else {
NSLog("stripLeadingZeroes(): failed to parse regex pattern.")
return self
}
let match = matcher.captureGroups(string: hex, options: .anchored)
Expand Down Expand Up @@ -130,6 +130,26 @@ extension String {
return Int(s[s.startIndex].value)
}
}

/// Splits a string into groups of `every` n characters, grouping from left-to-right by default. If `backwards` is true, right-to-left.
public func split(every: Int, backwards: Bool = false) -> [String] {
var result = [String]()

for i in stride(from: 0, to: self.count, by: every) {
switch backwards {
case true:
let endIndex = self.index(self.endIndex, offsetBy: -i)
let startIndex = self.index(endIndex, offsetBy: -every, limitedBy: self.startIndex) ?? self.startIndex
result.insert(String(self[startIndex..<endIndex]), at: 0)
case false:
let startIndex = self.index(self.startIndex, offsetBy: i)
let endIndex = self.index(startIndex, offsetBy: every, limitedBy: self.endIndex) ?? self.endIndex
result.append(String(self[startIndex..<endIndex]))
}
}

return result
}
}

extension Character {
Expand Down
Loading