Skip to content

Commit

Permalink
add methods necessary to create shifted Mnemonics, and reverse
Browse files Browse the repository at this point in the history
  • Loading branch information
simonmcl committed May 14, 2024
1 parent 9c07785 commit 05b8b40
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 0 deletions.
6 changes: 6 additions & 0 deletions Sources/KukaiCryptoSwift/Mnemonic/EntropyGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ extension Int: EntropyGenerator {
public static var strongest: Int { 256 }
}

extension Data: EntropyGenerator {
public func entropy() -> Result<Data, Error> {
return Result.success(self)
}
}

extension EntropyGenerator where Self: StringProtocol {
/**
* Interprets `self` as a string of pre-computed _entropy_, at least if its
Expand Down
122 changes: 122 additions & 0 deletions Sources/KukaiCryptoSwift/Mnemonic/Mnemonic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ public enum MnemonicError: Swift.Error {
case seedDerivationFailed
case seedPhraseInvalid(String)
case error(Swift.Error)
case invalidWordCount
case invalidWordToShift
case invalidMnemonic
}

/**
Expand Down Expand Up @@ -181,4 +184,123 @@ public struct Mnemonic: Equatable, Codable {

return Mnemonic.isValidChecksum(phrase: words, wordlist: vocabulary)
}

/**
Modifed from: https://github.com/pengpengliu/BIP39/blob/master/Sources/BIP39/Mnemonic.swift
Convert the current Mnemonic back into entropy
*/
public func toEntropy(ignoreChecksum: Bool, wordlist: WordList = WordList.english) throws -> [UInt8] {
let wordListWords = wordlist.words

let bits = try words.map { (word) -> String in
guard let index = wordListWords.firstIndex(of: word) else {
throw MnemonicError.invalidMnemonic
}

var str = String(index, radix:2)
while str.count < 11 {
str = "0" + str
}
return str
}.joined(separator: "")

let dividerIndex = Int(Double(bits.count / 33).rounded(.down) * 32)
let entropyBits = String(bits.prefix(dividerIndex))
let checksumBits = String(bits.suffix(bits.count - dividerIndex))

let regex = try! NSRegularExpression(pattern: "[01]{1,8}", options: .caseInsensitive)
let entropyBytes = regex.matches(in: entropyBits, options: [], range: NSRange(location: 0, length: entropyBits.count)).map {
UInt8(strtoul(String(entropyBits[Range($0.range, in: entropyBits)!]), nil, 2))
}

if !ignoreChecksum && (checksumBits != Mnemonic.deriveChecksumBits(entropyBytes)) {
throw MnemonicError.invalidMnemonic
}

return entropyBytes
}

/**
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
*/
public static func shiftedMnemonic(fromSpskPrivateKey pk: PrivateKey) -> Mnemonic? {
guard let entropy = Base58Check.decode(string: pk.base58CheckRepresentation, prefix: Prefix.Keys.Secp256k1.secret) else {
return nil
}

let data = Data(entropy)
guard let mnemonic = try? Mnemonic(entropy: data) else {
return nil
}

return try? shiftChecksum(mnemonic: mnemonic)
}

/**
Shift the checksum of of a `Mnemonic` so that it won't be accepted by tradtional improts
*/
public static func shiftChecksum(mnemonic: Mnemonic, wordList: WordList = WordList.english) throws -> Mnemonic {
var mutableMnemonic = mnemonic
guard mutableMnemonic.words.count == 24,
let lastWord = mutableMnemonic.words.last,
let shiftedWord = try? Mnemonic.getShiftedWord(word: lastWord, wordList: wordList) else {
throw MnemonicError.invalidWordCount
}

var isValidMnemonic = (mutableMnemonic.isValid() ? 1 : 0)
mutableMnemonic.phrase = mutableMnemonic.phrase.replacingOccurrences(of: lastWord, with: shiftedWord)
isValidMnemonic += (mutableMnemonic.isValid() ? 1 : 0)

if isValidMnemonic != 1 {
throw MnemonicError.invalidMnemonic
} else {
return mutableMnemonic
}
}

/**
Return a shifted word to replace the last word in a mnemonic
*/
public static func getShiftedWord(word: String, wordList: WordList = WordList.english) throws -> String {
let words = wordList.words
guard let wordIndex = words.firstIndex(of: word) else {
throw MnemonicError.invalidWordToShift
}

let checksumByte = wordIndex % 256
let newIndex = wordIndex - checksumByte + ((checksumByte + 128) % 256)

if words.count > newIndex {
return words[newIndex]
} else {
throw MnemonicError.invalidWordToShift
}
}

/**
Convert a mnemonic to a Base58 encoded private key string. Helpful when determining if a shifted mnemonic is valid
*/
public static func mnemonicToSpsk(mnemonic: Mnemonic, wordList: WordList = WordList.english) -> String? {
guard let bytes = try? mnemonic.toEntropy(ignoreChecksum: true, wordlist: wordList) else {
return nil
}

return Base58Check.encode(message: bytes, prefix: Prefix.Keys.Secp256k1.secret)
}

/**
Convert a shifted Mnemoinc back to normal
*/
public static func shiftedMnemonicToMnemonic(mnemonic: Mnemonic) -> Mnemonic? {
return try? shiftChecksum(mnemonic: mnemonic)
}

/**
Check if a supplied Spsk string is valid
*/
public static func validSpsk(_ sk: String) -> Bool {
let canDecode = Base58Check.decode(string: sk, prefix: Prefix.Keys.Secp256k1.secret)

return sk.count == 54 && canDecode != nil
}
}
40 changes: 40 additions & 0 deletions Tests/KukaiCryptoSwiftTests/MnemonicTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,44 @@ final class MnemonicTests: XCTestCase {
let mnemonic8 = try Mnemonic(seedPhrase: "Kit trigger pledge excess payment sentence dutch mandate start sense seed venture")
XCTAssert(mnemonic8.isValid() == false)
}

func testShifting() throws {
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]
let privateKey = PrivateKey(privateKeyBytes, signingCurve: .secp256k1)

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"
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"
let expectedTz2Address = "tz2HpbGQcmU3UyusJ78Sbqeg9fYteamSMDGo"


// Test shift
guard let shiftedMnemonic = Mnemonic.shiftedMnemonic(fromSpskPrivateKey: privateKey) else {
XCTFail("Couldn't create shifted Mnemonic")
return
}

let joinedWords = shiftedMnemonic.words.joined(separator: " ")
XCTAssert(joinedWords == expectedShiftedWords, joinedWords)

// Test unshift
let shiftedSpsk = Mnemonic.mnemonicToSpsk(mnemonic: shiftedMnemonic)
XCTAssert(shiftedSpsk == "spsk2Nqz6AW1zVwLJ3QgcXhzPNdT3mpRskUKA2UXza5kNRd3NLKrMy", shiftedSpsk ?? "-")
XCTAssert(Mnemonic.validSpsk(shiftedSpsk ?? ""))

guard let normalMnemonic = Mnemonic.shiftedMnemonicToMnemonic(mnemonic: shiftedMnemonic) else {
XCTFail("Couldn't create normal Mnemonic")
return
}

let normalJoinedWords = normalMnemonic.words.joined(separator: " ")
XCTAssert(normalJoinedWords == expectedNormalWords, normalJoinedWords)

let normalSpsk = Mnemonic.mnemonicToSpsk(mnemonic: normalMnemonic)
XCTAssert(normalSpsk == "spsk2Nqz6AW1zVwLJ3QgcXhzPNdT3mpRskUKA2UXza5kNRd3NLKrMy", normalSpsk ?? "-")

let normalSpskBytes = Base58Check.decode(string: normalSpsk ?? "", prefix: Prefix.Keys.Secp256k1.secret)
let normalPrivateKey = PrivateKey(normalSpskBytes ?? [], signingCurve: .secp256k1)
let normalPublicKey = KeyPair.secp256k1PublicKey(fromPrivateKeyBytes: normalPrivateKey.bytes)
XCTAssert(normalPublicKey?.publicKeyHash == expectedTz2Address, normalPublicKey?.publicKeyHash ?? "-")
}
}

0 comments on commit 05b8b40

Please sign in to comment.