SPB Git

spb/os-vault Public

Self-custody, multi-chain crypto wallet for macOS. One recovery phrase, six chain families, zero API keys — nothing leaves your Mac.

Swift 96% Shell 3.4% Makefile 0.6%
5.2 KB · 144 lines swift
Raw Blame History
1import CommonCrypto2import Foundation34public class Mnemonic {5    public let phrase: [String]6    let passphrase: String78    public init(strength: Int = 256, wordlist: [String] = Wordlists.english) {9        precondition(strength % 32 == 0, "Invalid entropy")1011        // 1.Random Bytes12        var bytes = [UInt8](repeating: 0, count: strength / 8)13        _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)1415        // 2.Entropy -> Mnemonic16        let entropyBits = String(bytes.flatMap { ("00000000" + String($0, radix: 2)).suffix(8) })17        let checksumBits = Mnemonic.deriveChecksumBits(bytes)18        let bits = entropyBits + checksumBits1920        var phrase = [String]()21        for i in 0 ..< (bits.count / 11) {22            let wi = Int(23                bits[bits.index(bits.startIndex, offsetBy: i * 11) ..< bits24                    .index(bits.startIndex, offsetBy: (i + 1) * 11)],25                radix: 226            )!27            phrase.append(String(wordlist[wi]))28        }2930        self.phrase = phrase31        passphrase = ""32    }3334    public init(phrase: [String], passphrase: String = "") throws {35        if !Mnemonic.isValid(phrase: phrase) {36            throw MnemonicError.invalidMnemonic37        }38        self.phrase = phrase39        self.passphrase = passphrase40    }4142    public init(entropy: [UInt8], wordlist: [String] = Wordlists.english) throws {43        phrase = try Mnemonic.toMnemonic(entropy, wordlist: wordlist)44        passphrase = ""45    }4647    // Entropy -> Mnemonic48    public static func toMnemonic(_ bytes: [UInt8], wordlist: [String] = Wordlists.english) throws -> [String] {49        let entropyBits = String(bytes.flatMap { ("00000000" + String($0, radix: 2)).suffix(8) })50        let checksumBits = Mnemonic.deriveChecksumBits(bytes)51        let bits = entropyBits + checksumBits5253        var phrase = [String]()54        for i in 0 ..< (bits.count / 11) {55            let wi = Int(56                bits[bits.index(bits.startIndex, offsetBy: i * 11) ..< bits57                    .index(bits.startIndex, offsetBy: (i + 1) * 11)],58                radix: 259            )!60            phrase.append(String(wordlist[wi]))61        }62        return phrase63    }6465    // Mnemonic -> Entropy66    public static func toEntropy(_ phrase: [String], wordlist: [String] = Wordlists.english) throws -> [UInt8] {67        let bits = phrase.map { word -> String in68            let index = wordlist.firstIndex(of: word)!69            var str = String(index, radix: 2)70            while str.count < 11 {71                str = "0" + str72            }73            return str74        }.joined(separator: "")7576        let dividerIndex = Int(Double(bits.count / 33).rounded(.down) * 32)77        let entropyBits = String(bits.prefix(dividerIndex))78        let checksumBits = String(bits.suffix(bits.count - dividerIndex))7980        let regex = try! NSRegularExpression(pattern: "[01]{1,8}", options: .caseInsensitive)81        let entropyBytes = regex.matches(82            in: entropyBits,83            options: [],84            range: NSRange(location: 0, length: entropyBits.count)85        ).map {86            UInt8(strtoul(String(entropyBits[Range($0.range, in: entropyBits)!]), nil, 2))87        }88        if checksumBits != Mnemonic.deriveChecksumBits(entropyBytes) {89            throw MnemonicError.invalidMnemonic90        }91        return entropyBytes92    }9394    public static func isValid(phrase: [String], wordlist: [String] = Wordlists.english) -> Bool {95        var bits = ""96        for word in phrase {97            guard let i = wordlist.firstIndex(of: word) else { return false }98            bits += ("00000000000" + String(i, radix: 2)).suffix(11)99        }100101        let dividerIndex = bits.count / 33 * 32102        let entropyBits = String(bits.prefix(dividerIndex))103        let checksumBits = String(bits.suffix(bits.count - dividerIndex))104105        let regex = try! NSRegularExpression(pattern: "[01]{1,8}", options: .caseInsensitive)106        let entropyBytes = regex.matches(107            in: entropyBits,108            options: [],109            range: NSRange(location: 0, length: entropyBits.count)110        ).map {111            UInt8(strtoul(String(entropyBits[Range($0.range, in: entropyBits)!]), nil, 2))112        }113        return checksumBits == deriveChecksumBits(entropyBytes)114    }115116    public static func deriveChecksumBits(_ bytes: [UInt8]) -> String {117        let ENT = bytes.count * 8118        let CS = ENT / 32119120        let hash = Data(bytes).sha256()121        let hashbits = String(hash.flatMap { ("00000000" + String($0, radix: 2)).suffix(8) })122        return String(hashbits.prefix(CS))123    }124125    public var seed: [UInt8] {126        let mnemonic = (phrase.joined(separator: " ") as NSString).decomposedStringWithCompatibilityMapping127        let salt = (("mnemonic" + passphrase) as NSString).decomposedStringWithCompatibilityMapping128        let pbkdf2 = pbkdf2(129            hash: CCPBKDFAlgorithm(kCCPRFHmacAlgSHA512),130            password: mnemonic,131            salt: Data(salt.bytes),132            keyByteCount: 64,133            rounds: 2048134        )!135        return pbkdf2.bytes136    }137}138139extension Mnemonic: Equatable {140    public static func == (lhs: Mnemonic, rhs: Mnemonic) -> Bool {141        lhs.phrase == rhs.phrase && lhs.passphrase == rhs.passphrase142    }143}144