import CryptoSwift
// / B a s e d o n [ m n e m o n i c . j s ] ( h t t p s : / / g i t h u b . c o m / l o k i - p r o j e c t / l o k i - m e s s e n g e r / b l o b / d e v e l o p m e n t / l i b l o k i / m o d u l e s / m n e m o n i c . j s ) .
public enum Mnemonic {
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
} else {
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
} else {
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 NSLocalizedString ( " Something went wrong. Please check your recovery phrase and try again. " , comment : " " )
case . inputTooShort : return NSLocalizedString ( " Looks like you didn't enter enough words. Please check your recovery phrase and try again. " , comment : " " )
case . missingLastWord : return NSLocalizedString ( " You seem to be missing the last word of your recovery phrase. Please check what you entered and try again. " , comment : " " )
case . invalidWord : return NSLocalizedString ( " There appears to be an invalid word in your recovery phrase. Please check what you entered and try again. " , comment : " " )
case . verificationFailed : return NSLocalizedString ( " Your recovery phrase couldn't be verified. Please check what you entered and try again. " , comment : " " )
}
}
}
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 // S a f e f o r t h i s p a r t i c u l a r c a s e
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 = mnemonic . split ( separator : " " ) . map { String ( $0 ) }
let truncatedWordSet = language . loadTruncatedWordSet ( )
let prefixLength = language . prefixLength
var result = " "
let n = truncatedWordSet . count
// C h e c k p r e c o n d i t i o n s
guard words . count >= 12 else { throw DecodingError . inputTooShort }
guard ! words . count . isMultiple ( of : 3 ) else { throw DecodingError . missingLastWord }
// G e t c h e c k s u m w o r d
let checksumWord = words . popLast ( ) !
// D e c o d e
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 ] ) )
}
// V e r i f y c h e c k s u m
let checksumIndex = determineChecksumIndex ( for : words , prefixLength : prefixLength )
let expectedChecksumWord = words [ checksumIndex ]
guard expectedChecksumWord . prefix ( length : prefixLength ) = = checksumWord . prefix ( length : prefixLength ) else { throw DecodingError . verificationFailed }
// R e t u r n
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 : UInt ) -> Int {
let checksum = Array ( x . map { $0 . prefix ( length : prefixLength ) } . joined ( ) . utf8 ) . crc32 ( )
return Int ( checksum ) % x . count
}
}
private extension String {
func prefix ( length : UInt ) -> String {
return String ( self [ startIndex . . < index ( startIndex , offsetBy : Int ( length ) ) ] )
}
}
@objc ( SNMnemonic )
public final class ObjCMnemonic : NSObject {
override private init ( ) { }
@objc ( hashHexEncodedString : )
public static func hash ( hexEncodedString string : String ) -> String {
return Mnemonic . hash ( hexEncodedString : string )
}
@objc ( encodeHexEncodedString : )
public static func encode ( hexEncodedString string : String ) -> String {
return Mnemonic . encode ( hexEncodedString : string )
}
}