diff --git a/Signal/src/util/Mnemonic.swift b/Signal/src/util/Mnemonic.swift index db997de25..0ef519d7d 100644 --- a/Signal/src/util/Mnemonic.swift +++ b/Signal/src/util/Mnemonic.swift @@ -1,5 +1,6 @@ import CryptoSwift +/// Based on [mnemonic.js](https://github.com/loki-project/loki-messenger/blob/development/libloki/modules/mnemonic.js) . enum Mnemonic { struct Language : Hashable { @@ -12,6 +13,7 @@ enum Mnemonic { static let spanish = Language(filename: "spanish", prefixLength: 4) private static var wordSetCache: [Language:[String]] = [:] + private static var truncatedWordSetCache: [Language:[String]] = [:] private init(filename: String, prefixLength: Int) { self.filename = filename @@ -29,29 +31,32 @@ enum Mnemonic { return result } } + + func loadTruncatedWordSet() -> [String] { + if let cachedResult = Language.truncatedWordSetCache[self] { + return cachedResult + } else { + let result = loadWordSet().map { $0.prefix(length: prefixLength) } + Language.truncatedWordSetCache[self] = result + return result + } + } + } + + enum DecodingError : Error { + case generic, inputTooShort, missingLastWord, invalidWord, verificationFailed } - /// Based on [mnemonic.js](https://github.com/loki-project/loki-messenger/blob/development/libloki/modules/mnemonic.js) . static func encode(hexEncodedString string: String, language: Language = .english) -> String { var string = string let wordSet = language.loadWordSet() let prefixLength = language.prefixLength var result: [String] = [] - let wordCount = wordSet.count - let characterCount = string.indices.count // Safe for this particular case + let n = wordSet.count + let characterCount = string.indices.count for chunkStartIndexAsInt in stride(from: 0, to: characterCount, by: 8) { let chunkStartIndex = string.index(string.startIndex, offsetBy: chunkStartIndexAsInt) let chunkEndIndex = string.index(chunkStartIndex, offsetBy: 8) - func swap(_ chunk: String) -> String { - func toStringIndex(_ indexAsInt: Int) -> String.Index { - return chunk.index(chunk.startIndex, offsetBy: indexAsInt) - } - let p1 = chunk[toStringIndex(6).. String { + var words = mnemonic.split(separator: " ").map { String($0) } + let truncatedWordSet = language.loadTruncatedWordSet() + let prefixLength = language.prefixLength + var result = "" + let n = truncatedWordSet.count + // Check preconditions + guard words.count >= 12 else { throw DecodingError.inputTooShort } + guard words.count % 3 != 0 else { throw DecodingError.missingLastWord } + // Get checksum word + let checksumWord = words.popLast()! + // Decode + for chunkStartIndex in stride(from: 0, to: words.count, by: 3) { + guard let w1 = truncatedWordSet.firstIndex(of: words[chunkStartIndex].prefix(length: prefixLength)), + let w2 = truncatedWordSet.firstIndex(of: words[chunkStartIndex + 1].prefix(length: prefixLength)), + let w3 = truncatedWordSet.firstIndex(of: words[chunkStartIndex + 2].prefix(length: prefixLength)) else { throw DecodingError.invalidWord } + let x = w1 + n * ((n - w1 + w2) % n) + n * n * ((n - w2 + w3) % n) + guard x % n == w1 else { throw DecodingError.generic } + let string = "0000000" + String(x, radix: 16) + result += swap(String(string[string.index(string.endIndex, offsetBy: -8).. String { + func toStringIndex(_ indexAsInt: Int) -> String.Index { + return x.index(x.startIndex, offsetBy: indexAsInt) + } + let p1 = x[toStringIndex(6).. Int { + let checksum = Array(x.map { $0.prefix(length: prefixLength) }.joined().utf8).crc32() + return Int(checksum) % x.count + } +} + +private extension String { + + func prefix(length: Int) -> String { + return String(self[startIndex..