// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation /// Based on [mnemonic.js](https://github.com/loki-project/loki-messenger/blob/development/libloki/modules/mnemonic.js) . public enum Mnemonic { /// This implementation was sourced from https://gist.github.com/antfarm/695fa78e0730b67eb094c77d53942216 enum CRC32 { static let table: [UInt32] = { (0...255).map { i -> UInt32 in (0..<8).reduce(UInt32(i), { c, _ in ((0xEDB88320 * (c % 2)) ^ (c >> 1)) }) } }() static func checksum(bytes: [UInt8]) -> UInt32 { return ~(bytes.reduce(~UInt32(0), { crc, byte in (crc >> 8) ^ table[(Int(crc) ^ Int(byte)) & 0xFF] })) } } public struct Language: Hashable { fileprivate let filename: String fileprivate let prefixLength: UInt public static let english = Language(filename: "english", prefixLength: 3) public static let japanese = Language(filename: "japanese", prefixLength: 3) public static let portuguese = Language(filename: "portuguese", prefixLength: 4) public 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: UInt) { self.filename = filename self.prefixLength = prefixLength } fileprivate func loadWordSet() -> [String] { if let cachedResult = Language.wordSetCache[self] { return cachedResult } let url = Bundle.main.url(forResource: filename, withExtension: "txt")! let contents = try! String(contentsOf: url) let result = contents.split(separator: ",").map { String($0) } Language.wordSetCache[self] = result return result } fileprivate func loadTruncatedWordSet() -> [String] { if let cachedResult = Language.truncatedWordSetCache[self] { return cachedResult } let result = loadWordSet().map { $0.prefix(length: prefixLength) } Language.truncatedWordSetCache[self] = result return result } } public enum DecodingError : LocalizedError { case generic, inputTooShort, missingLastWord, invalidWord, verificationFailed public var errorDescription: String? { switch self { case .generic: return "RECOVERY_PHASE_ERROR_GENERIC".localized() case .inputTooShort: return "RECOVERY_PHASE_ERROR_LENGTH".localized() case .missingLastWord: return "RECOVERY_PHASE_ERROR_LAST_WORD".localized() case .invalidWord: return "RECOVERY_PHASE_ERROR_INVALID_WORD".localized() case .verificationFailed: return "RECOVERY_PHASE_ERROR_FAILED".localized() } } } public static func hash(hexEncodedString string: String, language: Language = .english) -> String { return encode(hexEncodedString: string).split(separator: " ")[0..<3].joined(separator: " ") } public 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 n = wordSet.count let characterCount = string.indices.count // Safe for this particular case 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) let p1 = string[string.startIndex.. 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.isMultiple(of: 3) 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 = CRC32.checksum(bytes: Array(x.map { $0.prefix(length: prefixLength) }.joined().utf8)) return Int(checksum) % x.count } } private extension String { func prefix(length: UInt) -> String { return String(self[startIndex..