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

118 lines
4.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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