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%
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