mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			178 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			178 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			Swift
		
	
| // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | |
| //
 | |
| // stringlint:disable
 | |
| 
 | |
| 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: Int
 | |
|         
 | |
|         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: Int) {
 | |
|             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 { String($0.prefix(prefixLength)) }
 | |
|             Language.truncatedWordSetCache[self] = result
 | |
|             
 | |
|             return result
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     public enum DecodingError : LocalizedError {
 | |
|         case generic, inputTooShort, invalidWord, verificationFailed
 | |
|     }
 | |
|     
 | |
|     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..<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)!
 | |
|             let w1 = x % n
 | |
|             let w2 = ((x / n) + w1) % n
 | |
|             let w3 = (((x / n) / n) + w2) % n
 | |
|             result += [ wordSet[w1], wordSet[w2], wordSet[w3] ]
 | |
|         }
 | |
|         
 | |
|         let checksumIndex = determineChecksumIndex(for: result, prefixLength: prefixLength)
 | |
|         let checksumWord = result[checksumIndex]
 | |
|         result.append(checksumWord)
 | |
|         
 | |
|         return result.joined(separator: " ")
 | |
|     }
 | |
|     
 | |
|     public static func decode(mnemonic: String, language: Language = .english) throws -> String {
 | |
|         var words: [String] = mnemonic
 | |
|             .components(separatedBy: .whitespacesAndNewlines)
 | |
|             .filter { !$0.isEmpty }
 | |
|         let truncatedWordSet: [String] = language.loadTruncatedWordSet()
 | |
|         let prefixLength: Int = language.prefixLength
 | |
|         var result = ""
 | |
|         let n = truncatedWordSet.count
 | |
|         
 | |
|         // Check preconditions
 | |
|         guard words.count > 12 else { throw DecodingError.inputTooShort }
 | |
|         guard words.count < 14 else { throw DecodingError.generic }
 | |
|         
 | |
|         // Get checksum word
 | |
|         let checksumWord = words.popLast()!
 | |
|         
 | |
|         // Limit the words to a multiple of 3 to avoid index-out-of-bounds issues
 | |
|         let remainder: Int = (words.count % 3)
 | |
|         
 | |
|         if remainder > 0 {
 | |
|             words.removeLast(remainder)
 | |
|         }
 | |
|         
 | |
|         // Decode
 | |
|         for chunkStartIndex in stride(from: 0, to: words.count, by: 3) {
 | |
|             guard
 | |
|                 let w1 = truncatedWordSet.firstIndex(of: String(words[chunkStartIndex].prefix(prefixLength))),
 | |
|                 let w2 = truncatedWordSet.firstIndex(of: String(words[chunkStartIndex + 1].prefix(prefixLength))),
 | |
|                 let w3 = truncatedWordSet.firstIndex(of: String(words[chunkStartIndex + 2].prefix(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]))
 | |
|         }
 | |
|         
 | |
|         // Verify checksum
 | |
|         let checksumIndex = determineChecksumIndex(for: words, prefixLength: prefixLength)
 | |
|         let expectedChecksumWord = words[checksumIndex]
 | |
|         
 | |
|         guard expectedChecksumWord.prefix(prefixLength) == checksumWord.prefix(prefixLength) else {
 | |
|             throw DecodingError.verificationFailed
 | |
|         }
 | |
|         
 | |
|         // Return
 | |
|         return result
 | |
|     }
 | |
|     
 | |
|     private static func swap(_ x: String) -> String {
 | |
|         func toStringIndex(_ indexAsInt: Int) -> String.Index {
 | |
|             return x.index(x.startIndex, offsetBy: indexAsInt)
 | |
|         }
 | |
|         
 | |
|         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)]
 | |
|         
 | |
|         return String(p1 + p2 + p3 + p4)
 | |
|     }
 | |
|     
 | |
|     private static func determineChecksumIndex(for x: [String], prefixLength: Int) -> Int {
 | |
|         let checksum = CRC32.checksum(bytes: Array(x.map { $0.prefix(prefixLength) }.joined().utf8))
 | |
|         
 | |
|         return Int(checksum) % x.count
 | |
|     }
 | |
| }
 |