Skip to content

Commit 05b8b40

Browse files
committed
add methods necessary to create shifted Mnemonics, and reverse
1 parent 9c07785 commit 05b8b40

File tree

3 files changed

+168
-0
lines changed

3 files changed

+168
-0
lines changed

Sources/KukaiCryptoSwift/Mnemonic/EntropyGenerator.swift

+6
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ extension Int: EntropyGenerator {
3737
public static var strongest: Int { 256 }
3838
}
3939

40+
extension Data: EntropyGenerator {
41+
public func entropy() -> Result<Data, Error> {
42+
return Result.success(self)
43+
}
44+
}
45+
4046
extension EntropyGenerator where Self: StringProtocol {
4147
/**
4248
* Interprets `self` as a string of pre-computed _entropy_, at least if its

Sources/KukaiCryptoSwift/Mnemonic/Mnemonic.swift

+122
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ public enum MnemonicError: Swift.Error {
99
case seedDerivationFailed
1010
case seedPhraseInvalid(String)
1111
case error(Swift.Error)
12+
case invalidWordCount
13+
case invalidWordToShift
14+
case invalidMnemonic
1215
}
1316

1417
/**
@@ -181,4 +184,123 @@ public struct Mnemonic: Equatable, Codable {
181184

182185
return Mnemonic.isValidChecksum(phrase: words, wordlist: vocabulary)
183186
}
187+
188+
/**
189+
Modifed from: https://github.com/pengpengliu/BIP39/blob/master/Sources/BIP39/Mnemonic.swift
190+
Convert the current Mnemonic back into entropy
191+
*/
192+
public func toEntropy(ignoreChecksum: Bool, wordlist: WordList = WordList.english) throws -> [UInt8] {
193+
let wordListWords = wordlist.words
194+
195+
let bits = try words.map { (word) -> String in
196+
guard let index = wordListWords.firstIndex(of: word) else {
197+
throw MnemonicError.invalidMnemonic
198+
}
199+
200+
var str = String(index, radix:2)
201+
while str.count < 11 {
202+
str = "0" + str
203+
}
204+
return str
205+
}.joined(separator: "")
206+
207+
let dividerIndex = Int(Double(bits.count / 33).rounded(.down) * 32)
208+
let entropyBits = String(bits.prefix(dividerIndex))
209+
let checksumBits = String(bits.suffix(bits.count - dividerIndex))
210+
211+
let regex = try! NSRegularExpression(pattern: "[01]{1,8}", options: .caseInsensitive)
212+
let entropyBytes = regex.matches(in: entropyBits, options: [], range: NSRange(location: 0, length: entropyBits.count)).map {
213+
UInt8(strtoul(String(entropyBits[Range($0.range, in: entropyBits)!]), nil, 2))
214+
}
215+
216+
if !ignoreChecksum && (checksumBits != Mnemonic.deriveChecksumBits(entropyBytes)) {
217+
throw MnemonicError.invalidMnemonic
218+
}
219+
220+
return entropyBytes
221+
}
222+
223+
/**
224+
Take a `PrivateKey` from a TorusWallet and generate a custom "shifted checksum" mnemonic, so that we can recover wallets that previously had no seed words
225+
*/
226+
public static func shiftedMnemonic(fromSpskPrivateKey pk: PrivateKey) -> Mnemonic? {
227+
guard let entropy = Base58Check.decode(string: pk.base58CheckRepresentation, prefix: Prefix.Keys.Secp256k1.secret) else {
228+
return nil
229+
}
230+
231+
let data = Data(entropy)
232+
guard let mnemonic = try? Mnemonic(entropy: data) else {
233+
return nil
234+
}
235+
236+
return try? shiftChecksum(mnemonic: mnemonic)
237+
}
238+
239+
/**
240+
Shift the checksum of of a `Mnemonic` so that it won't be accepted by tradtional improts
241+
*/
242+
public static func shiftChecksum(mnemonic: Mnemonic, wordList: WordList = WordList.english) throws -> Mnemonic {
243+
var mutableMnemonic = mnemonic
244+
guard mutableMnemonic.words.count == 24,
245+
let lastWord = mutableMnemonic.words.last,
246+
let shiftedWord = try? Mnemonic.getShiftedWord(word: lastWord, wordList: wordList) else {
247+
throw MnemonicError.invalidWordCount
248+
}
249+
250+
var isValidMnemonic = (mutableMnemonic.isValid() ? 1 : 0)
251+
mutableMnemonic.phrase = mutableMnemonic.phrase.replacingOccurrences(of: lastWord, with: shiftedWord)
252+
isValidMnemonic += (mutableMnemonic.isValid() ? 1 : 0)
253+
254+
if isValidMnemonic != 1 {
255+
throw MnemonicError.invalidMnemonic
256+
} else {
257+
return mutableMnemonic
258+
}
259+
}
260+
261+
/**
262+
Return a shifted word to replace the last word in a mnemonic
263+
*/
264+
public static func getShiftedWord(word: String, wordList: WordList = WordList.english) throws -> String {
265+
let words = wordList.words
266+
guard let wordIndex = words.firstIndex(of: word) else {
267+
throw MnemonicError.invalidWordToShift
268+
}
269+
270+
let checksumByte = wordIndex % 256
271+
let newIndex = wordIndex - checksumByte + ((checksumByte + 128) % 256)
272+
273+
if words.count > newIndex {
274+
return words[newIndex]
275+
} else {
276+
throw MnemonicError.invalidWordToShift
277+
}
278+
}
279+
280+
/**
281+
Convert a mnemonic to a Base58 encoded private key string. Helpful when determining if a shifted mnemonic is valid
282+
*/
283+
public static func mnemonicToSpsk(mnemonic: Mnemonic, wordList: WordList = WordList.english) -> String? {
284+
guard let bytes = try? mnemonic.toEntropy(ignoreChecksum: true, wordlist: wordList) else {
285+
return nil
286+
}
287+
288+
return Base58Check.encode(message: bytes, prefix: Prefix.Keys.Secp256k1.secret)
289+
}
290+
291+
/**
292+
Convert a shifted Mnemoinc back to normal
293+
*/
294+
public static func shiftedMnemonicToMnemonic(mnemonic: Mnemonic) -> Mnemonic? {
295+
return try? shiftChecksum(mnemonic: mnemonic)
296+
}
297+
298+
/**
299+
Check if a supplied Spsk string is valid
300+
*/
301+
public static func validSpsk(_ sk: String) -> Bool {
302+
let canDecode = Base58Check.decode(string: sk, prefix: Prefix.Keys.Secp256k1.secret)
303+
304+
return sk.count == 54 && canDecode != nil
305+
}
184306
}

Tests/KukaiCryptoSwiftTests/MnemonicTests.swift

+40
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,44 @@ final class MnemonicTests: XCTestCase {
8282
let mnemonic8 = try Mnemonic(seedPhrase: "Kit trigger pledge excess payment sentence dutch mandate start sense seed venture")
8383
XCTAssert(mnemonic8.isValid() == false)
8484
}
85+
86+
func testShifting() throws {
87+
let privateKeyBytes: [UInt8] = [125, 133, 194, 84, 250, 98, 79, 41, 174, 84, 233, 129, 41, 85, 148, 33, 44, 186, 87, 103, 235, 213, 247, 99, 133, 29, 151, 197, 91, 106, 136, 214]
88+
let privateKey = PrivateKey(privateKeyBytes, signingCurve: .secp256k1)
89+
90+
let expectedShiftedWords = "laugh come news visit ceiling network rich outdoor license enjoy govern drastic slight close panic kingdom wash bring electric convince fiber relief cash siren"
91+
let expectedNormalWords = "laugh come news visit ceiling network rich outdoor license enjoy govern drastic slight close panic kingdom wash bring electric convince fiber relief cash sunny"
92+
let expectedTz2Address = "tz2HpbGQcmU3UyusJ78Sbqeg9fYteamSMDGo"
93+
94+
95+
// Test shift
96+
guard let shiftedMnemonic = Mnemonic.shiftedMnemonic(fromSpskPrivateKey: privateKey) else {
97+
XCTFail("Couldn't create shifted Mnemonic")
98+
return
99+
}
100+
101+
let joinedWords = shiftedMnemonic.words.joined(separator: " ")
102+
XCTAssert(joinedWords == expectedShiftedWords, joinedWords)
103+
104+
// Test unshift
105+
let shiftedSpsk = Mnemonic.mnemonicToSpsk(mnemonic: shiftedMnemonic)
106+
XCTAssert(shiftedSpsk == "spsk2Nqz6AW1zVwLJ3QgcXhzPNdT3mpRskUKA2UXza5kNRd3NLKrMy", shiftedSpsk ?? "-")
107+
XCTAssert(Mnemonic.validSpsk(shiftedSpsk ?? ""))
108+
109+
guard let normalMnemonic = Mnemonic.shiftedMnemonicToMnemonic(mnemonic: shiftedMnemonic) else {
110+
XCTFail("Couldn't create normal Mnemonic")
111+
return
112+
}
113+
114+
let normalJoinedWords = normalMnemonic.words.joined(separator: " ")
115+
XCTAssert(normalJoinedWords == expectedNormalWords, normalJoinedWords)
116+
117+
let normalSpsk = Mnemonic.mnemonicToSpsk(mnemonic: normalMnemonic)
118+
XCTAssert(normalSpsk == "spsk2Nqz6AW1zVwLJ3QgcXhzPNdT3mpRskUKA2UXza5kNRd3NLKrMy", normalSpsk ?? "-")
119+
120+
let normalSpskBytes = Base58Check.decode(string: normalSpsk ?? "", prefix: Prefix.Keys.Secp256k1.secret)
121+
let normalPrivateKey = PrivateKey(normalSpskBytes ?? [], signingCurve: .secp256k1)
122+
let normalPublicKey = KeyPair.secp256k1PublicKey(fromPrivateKeyBytes: normalPrivateKey.bytes)
123+
XCTAssert(normalPublicKey?.publicKeyHash == expectedTz2Address, normalPublicKey?.publicKeyHash ?? "-")
124+
}
85125
}

0 commit comments

Comments
 (0)