Skip to content

Commit

Permalink
IOS-3059 Add seed generation and parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
tureck1y committed Mar 7, 2023
1 parent 28b3998 commit 22b4481
Show file tree
Hide file tree
Showing 11 changed files with 349 additions and 44 deletions.
16 changes: 12 additions & 4 deletions TangemSdk/TangemSdk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,9 @@
DC1244B329B60B6F0037BC05 /* BIP39.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244B229B60B6F0037BC05 /* BIP39.swift */; };
DC1244B529B60E480037BC05 /* english.txt in Resources */ = {isa = PBXBuildFile; fileRef = DC1244B429B60E480037BC05 /* english.txt */; };
DC1244B929B610550037BC05 /* MnemonicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244B829B610550037BC05 /* MnemonicTests.swift */; };
DC1244BD29B61DCB0037BC05 /* seed_test_vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = DC1244BC29B61DCB0037BC05 /* seed_test_vectors.json */; };
DC1244C129B766920037BC05 /* seed_test_vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = DC1244C029B766920037BC05 /* seed_test_vectors.json */; };
DC1244C329B766B70037BC05 /* mnemonic_valid_test_vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = DC1244C229B766B70037BC05 /* mnemonic_valid_test_vectors.json */; };
DC1244C529B769400037BC05 /* mnemonic_invalid_test_vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = DC1244C429B769400037BC05 /* mnemonic_invalid_test_vectors.json */; };
DC59CB0429AF597900EC14E1 /* Wordlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0329AF597900EC14E1 /* Wordlist.swift */; };
DC59CB0A29AF6F9C00EC14E1 /* EntropyLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0929AF6F9C00EC14E1 /* EntropyLength.swift */; };
DC59CB0C29AF706100EC14E1 /* MnemonicError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0B29AF706100EC14E1 /* MnemonicError.swift */; };
Expand Down Expand Up @@ -616,7 +618,9 @@
DC1244B229B60B6F0037BC05 /* BIP39.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BIP39.swift; sourceTree = "<group>"; };
DC1244B429B60E480037BC05 /* english.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = english.txt; sourceTree = "<group>"; };
DC1244B829B610550037BC05 /* MnemonicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicTests.swift; sourceTree = "<group>"; };
DC1244BC29B61DCB0037BC05 /* seed_test_vectors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = seed_test_vectors.json; sourceTree = "<group>"; };
DC1244C029B766920037BC05 /* seed_test_vectors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = seed_test_vectors.json; sourceTree = "<group>"; };
DC1244C229B766B70037BC05 /* mnemonic_valid_test_vectors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = mnemonic_valid_test_vectors.json; sourceTree = "<group>"; };
DC1244C429B769400037BC05 /* mnemonic_invalid_test_vectors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = mnemonic_invalid_test_vectors.json; sourceTree = "<group>"; };
DC59CB0329AF597900EC14E1 /* Wordlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wordlist.swift; sourceTree = "<group>"; };
DC59CB0929AF6F9C00EC14E1 /* EntropyLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntropyLength.swift; sourceTree = "<group>"; };
DC59CB0B29AF706100EC14E1 /* MnemonicError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicError.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1401,7 +1405,9 @@
isa = PBXGroup;
children = (
DC1244B829B610550037BC05 /* MnemonicTests.swift */,
DC1244BC29B61DCB0037BC05 /* seed_test_vectors.json */,
DC1244C029B766920037BC05 /* seed_test_vectors.json */,
DC1244C229B766B70037BC05 /* mnemonic_valid_test_vectors.json */,
DC1244C429B769400037BC05 /* mnemonic_invalid_test_vectors.json */,
);
path = Seed;
sourceTree = "<group>";
Expand Down Expand Up @@ -1587,8 +1593,8 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
DC1244BD29B61DCB0037BC05 /* seed_test_vectors.json in Resources */,
5D54408A2682260000F7D05B /* SetAccessCode.json in Resources */,
DC1244C529B769400037BC05 /* mnemonic_invalid_test_vectors.json in Resources */,
5D46F537274D68010004681F /* DeriveWalletPublicKey.json in Resources */,
5D54408E268226B600F7D05B /* Depersonalize.json in Resources */,
5D3217462684B1DB000C3AAF /* v3.05ada.json in Resources */,
Expand All @@ -1601,8 +1607,10 @@
5D544094268243F700F7D05B /* Card.json in Resources */,
5D5440982682497100F7D05B /* v4.json in Resources */,
5D4B127726D3CEE7006E173C /* ReadFiles.json in Resources */,
DC1244C129B766920037BC05 /* seed_test_vectors.json in Resources */,
5D3B9F0326BD678500532CC7 /* SignHashes.json in Resources */,
5D2BDF8626DD4869002F7E19 /* TestParseRequest.json in Resources */,
DC1244C329B766B70037BC05 /* mnemonic_valid_test_vectors.json in Resources */,
5D5440922682297A00F7D05B /* CreateWallet.json in Resources */,
5D54408C2682269400F7D05B /* SetPasscode.json in Resources */,
5D4B127D26D3D351006E173C /* WriteFiles.json in Resources */,
Expand Down
34 changes: 31 additions & 3 deletions TangemSdk/TangemSdk/Common/Extensions/Data+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,30 @@ extension Data {
public init(_ byte: Byte) {
self = Data([byte])
}


init?(bitsString: String) {
let byteLength = 8

guard bitsString.count % byteLength == 0 else {
return nil
}

let binaryBytes = Array(bitsString).chunked(into: byteLength)

var bytes = [UInt8]()
bytes.reserveCapacity(bitsString.count / byteLength)

for binaryByte in binaryBytes {
guard let byte = UInt8(String(binaryByte), radix: 2) else {
return nil
}

bytes.append(byte)
}

self = Data(bytes)
}

@available(iOS 13.0, *)
public func getSha256() -> Data {
let digest = SHA256.hash(data: self)
Expand Down Expand Up @@ -149,8 +172,13 @@ extension Data {
}

@available(iOS 13.0, *)
public func pbkdf2sha256(salt: Data, rounds: Int) throws -> Data {
return try pbkdf2(hash: CCPBKDFAlgorithm(kCCPRFHmacAlgSHA256), salt: salt, keyByteCount: 32, rounds: rounds)
public func pbkdf2sha256(salt: Data, rounds: Int, keyByteCount: Int = 32) throws -> Data {
return try pbkdf2(hash: CCPBKDFAlgorithm(kCCPRFHmacAlgSHA256), salt: salt, keyByteCount: keyByteCount, rounds: rounds)
}

@available(iOS 13.0, *)
public func pbkdf2sha512(salt: Data, rounds: Int, keyByteCount: Int = 64) throws -> Data {
return try pbkdf2(hash: CCPBKDFAlgorithm(kCCPRFHmacAlgSHA512), salt: salt, keyByteCount: keyByteCount, rounds: rounds)
}

//SO14443A
Expand Down
18 changes: 6 additions & 12 deletions TangemSdk/TangemSdk/Common/Extensions/String+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,12 @@ public extension String {
internal func trim() -> String {
return trimmingCharacters(in: .whitespacesAndNewlines)
}

internal func camelCaseToSnakeCase() -> String {
let acronymPattern = "([A-Z]+)([A-Z][a-z]|[0-9])"
let normalPattern = "([a-z0-9])([A-Z])"
return self.processCamelCaseRegex(pattern: acronymPattern)?
.processCamelCaseRegex(pattern: normalPattern)?.lowercased() ?? self.lowercased()
}

private func processCamelCaseRegex(pattern: String) -> String? {
let regex = try? NSRegularExpression(pattern: pattern, options: [])
let range = NSRange(location: 0, length: count)
return regex?.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "$1_$2")

internal func zeroPadding(toLength newLength: Int) -> String {
guard count < newLength else { return self }

let prefix = Array(repeating: "0", count: newLength - count).joined()
return prefix + self
}
}

Expand Down
167 changes: 156 additions & 11 deletions TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import Foundation
public struct BIP39 {
/// Generate a mnemonic.
/// - Parameters:
/// - entropyLength: An entropy length to use. Default is 128 bit.
/// - wordlist: A wordlist to use. Default is english.
/// - Returns: Generated mnemonic
/// - entropyLength: The entropy length to use. Default is 128 bit.
/// - wordlist: The wordlist to use. Default is english.
/// - Returns: The generated mnemonic splitted to components
public func generateMnemonic(entropyLength: EntropyLength = .bits128, wordlist: Wordlist = .en) throws -> [String] {
guard entropyLength.rawValue % 32 == 0 else {
throw MnemonicError.mnenmonicCreationFailed
Expand All @@ -25,19 +25,129 @@ public struct BIP39 {
return try generateMnemonic(from: entropyData, wordlist: wordlist)
}

/// Generate a determenistic seed
/// - Parameters:
/// - mnemonic: The mnemonic to use
/// - passphrase: The passphrase to use. Default is no passphrase (empty).
/// - Returns: The generated seed
public func generateSeed(from mnemonicComponents: [String], passphrase: String = "") throws -> Data {
try validate(mnemonicComponents: mnemonicComponents)

let mnemonicString = convertToMnemonicString(mnemonicComponents)
let normalizedMnemonic = try normalizedData(from: mnemonicString)
let normalizedSalt = try normalizedData(from: Constants.seedSaltPrefix + passphrase)
let seed = try normalizedMnemonic.pbkdf2sha512(salt: normalizedSalt, rounds: 2048)
return seed
}

public func convertToMnemonicString(_ mnemonicComponents: [String]) -> String {
return mnemonicComponents.joined(separator: " ")
}

public func parse(mnemonicString: String) throws -> [String] {
let regex = try NSRegularExpression(pattern: "[a-zA-Z]+")
let range = NSRange(location: 0, length: mnemonicString.count)
let matches = regex.matches(in: mnemonicString, range: range)
let components = matches.compactMap { result -> String? in
guard result.numberOfRanges > 0,
let stringRange = Range(result.range(at: 0), in: mnemonicString) else {
return nil
}

return String(mnemonicString[stringRange]).trim().lowercased()
}

try validate(mnemonicComponents: components)
return components
}

private static func extractCaptureGroupString(
from result: NSTextCheckingResult,
at index: Int,
in text: String
) -> String? {
guard index < result.numberOfRanges,
let stringRange = Range(
result.range(at: index),
in: text
) else {
return nil
}

return String(text[stringRange])
}

public func validate(mnemonicComponents: [String]) throws {
// Validate words count
guard !mnemonicComponents.isEmpty else {
throw MnemonicError.wrongWordCount
}

guard let entropyLength = EntropyLength.allCases.first(where: { $0.wordsCount == mnemonicComponents.count }) else {
throw MnemonicError.wrongWordCount
}

// Validate wordlist by the first word
let wordlist = try getWordlist(by: mnemonicComponents[0])

// Validate all the words
var invalidWords = Set<String>()

// Generate an indices array inplace
var concatenatedBits = ""

for word in mnemonicComponents {
guard let wordIndex = wordlist.firstIndex(of: word) else {
invalidWords.insert(word)
continue
}

let indexBits = String(wordIndex, radix: 2).zeroPadding(toLength: 11)
concatenatedBits.append(contentsOf: indexBits)
}

guard invalidWords.isEmpty else {
throw MnemonicError.invalidWords(words: Array(invalidWords))
}

// Validate checksum

let checksumBitsCount = mnemonicComponents.count / 3
guard checksumBitsCount == entropyLength.cheksumBitsCount else {
throw MnemonicError.invalidCheksum
}

let entropyBitsCount = concatenatedBits.count - checksumBitsCount
let entropyBits = String(concatenatedBits.prefix(entropyBitsCount))
let checksumBits = String(concatenatedBits.suffix(checksumBitsCount))

guard let entropyData = Data(bitsString: entropyBits) else {
throw MnemonicError.invalidCheksum
}

let calculatedChecksumBits = entropyData
.getSha256()
.toBits()
.prefix(entropyLength.cheksumBitsCount)
.joined()

guard calculatedChecksumBits == checksumBits else {
throw MnemonicError.invalidCheksum
}
}

/// Generate a mnemonic from data. Useful for testing purposes.
/// - Parameters:
/// - data: Entropy data in hex format
/// - wordlist: A wordlist to use.
/// - Returns: Generated mnemonic
/// - data: The entropy data in hex format
/// - wordlist: The wordlist to use.
/// - Returns: The generated mnemonic
func generateMnemonic(from entropyData: Data, wordlist: Wordlist) throws -> [String] {
guard let entropyLength = EntropyLength(rawValue: entropyData.count * 8) else {
throw MnemonicError.invalidEntropyLength
}

let entropyHashBits = entropyData.getSha256().toBits()
let checksumBitLength = entropyLength.rawValue / 32
let entropyChecksumBits = entropyHashBits.prefix(checksumBitLength)
let entropyChecksumBits = entropyHashBits.prefix(entropyLength.cheksumBitsCount)

let entropyBits = entropyData.toBits()
let concatenatedBits = entropyBits + entropyChecksumBits
Expand All @@ -51,11 +161,46 @@ public struct BIP39 {
let allWords = wordlist.words
let maxWordIndex = allWords.count

guard indexes.allSatisfy({ $0 < maxWordIndex }) else {
throw MnemonicError.mnenmonicCreationFailed
let words = try indexes.map { index in
guard index < maxWordIndex else {
throw MnemonicError.mnenmonicCreationFailed
}

return allWords[index]

}

let words = indexes.map { allWords[$0] }
return words
}

private func normalizedData(from string: String) throws -> Data {
let normalizedString = string.decomposedStringWithCompatibilityMapping

guard let data = normalizedString.data(using: .utf8) else {
throw MnemonicError.normalizationFailed
}

return data
}

private func getWordlist(by word: String) throws -> [String] {
for list in Wordlist.allCases {
let words = list.words

if words.contains(word) {
return words
}
}

throw MnemonicError.unsupportedLanguage
}
}

// MARK: - Constants

@available(iOS 13.0, *)
private extension BIP39 {
enum Constants {
static let seedSaltPrefix = "mnemonic"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import Foundation

public enum EntropyLength: Int {
public enum EntropyLength: Int, CaseIterable {
case bits128 = 128
case bits160 = 160
case bits192 = 192
Expand All @@ -24,4 +24,8 @@ public enum EntropyLength: Int {
case .bits256: return 24
}
}

var cheksumBitsCount: Int {
rawValue / 32
}
}
5 changes: 5 additions & 0 deletions TangemSdk/TangemSdk/Common/HDWallet/BIP39/MnemonicError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,10 @@ public enum MnemonicError: Error {
case invalidEntropyLength
case invalidWordCount
case invalidWordsFile
case invalidCheksum
case mnenmonicCreationFailed
case normalizationFailed
case wrongWordCount
case unsupportedLanguage
case invalidWords(words: [String])
}
2 changes: 1 addition & 1 deletion TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import Foundation

@available(iOS 13.0, *)
public enum Wordlist {
public enum Wordlist: CaseIterable {
case en

/// This var reads a big array from a file
Expand Down
Loading

0 comments on commit 22b4481

Please sign in to comment.