session-ios/SessionUtilitiesKit/Crypto/Mnemonic.swift

177 lines
7.6 KiB
Swift
Raw Normal View History

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
2019-04-30 09:09:44 +02:00
/// Based on [mnemonic.js](https://github.com/loki-project/loki-messenger/blob/development/libloki/modules/mnemonic.js) .
2019-05-03 03:42:56 +02:00
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 {
2019-05-03 05:44:30 +02:00
fileprivate let filename: String
2019-05-07 07:33:08 +02:00
fileprivate let prefixLength: UInt
2019-05-03 03:42:56 +02:00
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]] = [:]
2019-05-07 07:33:08 +02:00
private init(filename: String, prefixLength: UInt) {
self.filename = filename
self.prefixLength = prefixLength
}
2019-05-03 05:44:30 +02:00
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
}
2019-04-30 09:09:44 +02:00
2019-05-03 05:44:30 +02:00
fileprivate func loadTruncatedWordSet() -> [String] {
2019-04-30 09:09:44 +02:00
if let cachedResult = Language.truncatedWordSetCache[self] {
return cachedResult
}
let result = loadWordSet().map { $0.prefix(length: prefixLength) }
Language.truncatedWordSetCache[self] = result
return result
2019-04-30 09:09:44 +02:00
}
}
2019-05-03 03:42:56 +02:00
public enum DecodingError : LocalizedError {
2019-04-30 09:09:44 +02:00
case generic, inputTooShort, missingLastWord, invalidWord, verificationFailed
2019-05-01 05:58:35 +02:00
2019-05-06 01:45:52 +02:00
public var errorDescription: String? {
2019-05-01 05:58:35 +02:00
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()
2019-05-01 05:58:35 +02:00
}
}
}
public static func hash(hexEncodedString string: String, language: Language = .english) -> String {
return encode(hexEncodedString: string).split(separator: " ")[0..<3].joined(separator: " ")
}
2019-05-03 03:42:56 +02:00
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] = []
2019-04-30 09:09:44 +02:00
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..<chunkStartIndex]
let p2 = swap(String(string[chunkStartIndex..<chunkEndIndex]))
let p3 = string[chunkEndIndex..<string.endIndex]
string = String(p1 + p2 + p3)
}
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 x = Int(string[chunkStartIndex..<chunkEndIndex], radix: 16)!
2019-04-30 09:09:44 +02:00
let w1 = x % n
let w2 = ((x / n) + w1) % n
let w3 = (((x / n) / n) + w2) % n
result += [ wordSet[w1], wordSet[w2], wordSet[w3] ]
}
2019-04-30 09:09:44 +02:00
let checksumIndex = determineChecksumIndex(for: result, prefixLength: prefixLength)
let checksumWord = result[checksumIndex]
result.append(checksumWord)
return result.joined(separator: " ")
}
2019-04-30 09:09:44 +02:00
2019-05-03 03:42:56 +02:00
public static func decode(mnemonic: String, language: Language = .english) throws -> String {
2019-04-30 09:09:44 +02:00
var words = mnemonic.split(separator: " ").map { String($0) }
let truncatedWordSet = language.loadTruncatedWordSet()
let prefixLength = language.prefixLength
var result = ""
let n = truncatedWordSet.count
2019-04-30 09:09:44 +02:00
// Check preconditions
guard words.count >= 12 else { throw DecodingError.inputTooShort }
guard !words.count.isMultiple(of: 3) else { throw DecodingError.missingLastWord }
2019-04-30 09:09:44 +02:00
// Get checksum word
let checksumWord = words.popLast()!
2019-04-30 09:09:44 +02:00
// 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.endIndex]))
}
2019-04-30 09:09:44 +02:00
// Verify checksum
let checksumIndex = determineChecksumIndex(for: words, prefixLength: prefixLength)
let expectedChecksumWord = words[checksumIndex]
guard expectedChecksumWord.prefix(length: prefixLength) == checksumWord.prefix(length: prefixLength) else { throw DecodingError.verificationFailed }
2019-04-30 09:09:44 +02:00
// Return
return result
}
private static func swap(_ x: String) -> String {
func toStringIndex(_ indexAsInt: Int) -> String.Index {
return x.index(x.startIndex, offsetBy: indexAsInt)
}
2019-04-30 09:09:44 +02:00
let p1 = x[toStringIndex(6)..<toStringIndex(8)]
let p2 = x[toStringIndex(4)..<toStringIndex(6)]
let p3 = x[toStringIndex(2)..<toStringIndex(4)]
let p4 = x[toStringIndex(0)..<toStringIndex(2)]
2019-04-30 09:09:44 +02:00
return String(p1 + p2 + p3 + p4)
}
2019-05-07 07:33:08 +02:00
private static func determineChecksumIndex(for x: [String], prefixLength: UInt) -> Int {
let checksum = CRC32.checksum(bytes: Array(x.map { $0.prefix(length: prefixLength) }.joined().utf8))
2019-04-30 09:09:44 +02:00
return Int(checksum) % x.count
}
}
private extension String {
2019-05-07 07:33:08 +02:00
func prefix(length: UInt) -> String {
return String(self[startIndex..<index(startIndex, offsetBy: Int(length))])
2019-04-30 09:09:44 +02:00
}
}