montana/Монтана-iOS/Sources/MontanaApp/Identity/Mnemonic.swift

118 lines
4.7 KiB
Swift
Raw Permalink Normal View History

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