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