118 lines
4.7 KiB
Swift
118 lines
4.7 KiB
Swift
import Foundation
|
||
import CryptoKit
|
||
|
||
/// spec, раздел "Мнемоника и seed → Algorithm M-1 mnemonic_to_master_seed".
|
||
/// Реализует BIP-39-совместимое представление 24 слов = 256 бит entropy + 8 бит checksum.
|
||
enum Mnemonic {
|
||
static let wordCount = 24
|
||
static let entropyBits = 256
|
||
static let checksumBits = 8
|
||
static let kdfIterations: UInt32 = 1_048_576 // 2^20
|
||
static let masterSeedLength = 64
|
||
static let kdfSalt = "mt-seed".data(using: .ascii)!
|
||
|
||
enum MnemonicError: Error, Equatable {
|
||
case wordCount(Int)
|
||
case unknownWord(position: Int)
|
||
case checksumMismatch
|
||
case invalidEntropyLength
|
||
}
|
||
|
||
/// Генерация мнемоники из 32 байт entropy.
|
||
static func encode(entropy: Data) throws -> String {
|
||
guard entropy.count == 32 else { throw MnemonicError.invalidEntropyLength }
|
||
let checksum = Array(SHA256.hash(data: entropy))[0]
|
||
var packed = [UInt8](entropy)
|
||
packed.append(checksum) // 33 байта
|
||
let indices = bitsToIndices(packed)
|
||
return indices.map { Wordlist.canonical[Int($0)] }.joined(separator: " ")
|
||
}
|
||
|
||
/// Парсинг мнемоники + PBKDF2 → 64-байт master_seed.
|
||
static func decodeAndDerive(mnemonic: String) throws -> Data {
|
||
let words = mnemonic.split(whereSeparator: { $0.isWhitespace }).map { String($0) }
|
||
guard words.count == wordCount else { throw MnemonicError.wordCount(words.count) }
|
||
|
||
var indices: [UInt16] = []
|
||
indices.reserveCapacity(wordCount)
|
||
for (i, w) in words.enumerated() {
|
||
guard let idx = Wordlist.index(of: w) else {
|
||
throw MnemonicError.unknownWord(position: i)
|
||
}
|
||
indices.append(idx)
|
||
}
|
||
|
||
let packed = indicesToBits(indices)
|
||
let entropy = Data(packed.prefix(32))
|
||
let providedChecksum = packed[32]
|
||
let computedChecksum = Array(SHA256.hash(data: entropy))[0]
|
||
guard providedChecksum == computedChecksum else { throw MnemonicError.checksumMismatch }
|
||
|
||
return pbkdf2_hmac_sha256(password: entropy, salt: kdfSalt, iterations: kdfIterations, keyLength: masterSeedLength)
|
||
}
|
||
|
||
/// Случайная мнемоника через системный CSPRNG (24 слова).
|
||
static func generate() -> String {
|
||
var entropy = Data(count: 32)
|
||
let result = entropy.withUnsafeMutableBytes { buf in
|
||
SecRandomCopyBytes(kSecRandomDefault, 32, buf.baseAddress!)
|
||
}
|
||
precondition(result == errSecSuccess, "SecRandomCopyBytes failed")
|
||
return try! encode(entropy: entropy)
|
||
}
|
||
|
||
// MARK: - Bit packing (24 × 11 = 264 bits → 33 bytes, MSB-first)
|
||
|
||
private static func bitsToIndices(_ bytes: [UInt8]) -> [UInt16] {
|
||
var indices = [UInt16](repeating: 0, count: wordCount)
|
||
var bitPos = 0
|
||
for i in 0..<wordCount {
|
||
for b in 0..<11 {
|
||
let byteIdx = bitPos / 8
|
||
let bitInByte = 7 - (bitPos % 8)
|
||
let bit = UInt16((bytes[byteIdx] >> bitInByte) & 1)
|
||
indices[i] |= bit << (10 - b)
|
||
bitPos += 1
|
||
}
|
||
}
|
||
return indices
|
||
}
|
||
|
||
private static func indicesToBits(_ indices: [UInt16]) -> [UInt8] {
|
||
var bytes = [UInt8](repeating: 0, count: 33)
|
||
var bitPos = 0
|
||
for i in 0..<wordCount {
|
||
for b in 0..<11 {
|
||
let bit = UInt8((indices[i] >> (10 - b)) & 1)
|
||
let byteIdx = bitPos / 8
|
||
let bitInByte = 7 - (bitPos % 8)
|
||
bytes[byteIdx] |= bit << bitInByte
|
||
bitPos += 1
|
||
}
|
||
}
|
||
return bytes
|
||
}
|
||
|
||
// MARK: - PBKDF2-HMAC-SHA-256 через CommonCrypto bridge
|
||
|
||
private static func pbkdf2_hmac_sha256(password: Data, salt: Data, iterations: UInt32, keyLength: Int) -> Data {
|
||
var derived = Data(count: keyLength)
|
||
let result = derived.withUnsafeMutableBytes { (derivedBytes: UnsafeMutableRawBufferPointer) -> Int32 in
|
||
password.withUnsafeBytes { (passwordBytes: UnsafeRawBufferPointer) -> Int32 in
|
||
salt.withUnsafeBytes { (saltBytes: UnsafeRawBufferPointer) -> Int32 in
|
||
CCKeyDerivationPBKDF(
|
||
CCPBKDFAlgorithm(kCCPBKDF2),
|
||
passwordBytes.baseAddress, password.count,
|
||
saltBytes.baseAddress, salt.count,
|
||
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
|
||
iterations,
|
||
derivedBytes.baseAddress, keyLength
|
||
)
|
||
}
|
||
}
|
||
}
|
||
precondition(result == kCCSuccess, "PBKDF2 failed")
|
||
return derived
|
||
}
|
||
}
|