montana/macOS/MontanaPresence/Crypto/MontanaSeed.swift

198 lines
6.6 KiB
Swift
Raw Normal View History

//
// MontanaSeed.swift
// Montana Protocol macOS
//
// BIP-39 Mnemonic (24 words) Deterministic ML-DSA-65 Keypair
// Standard: BIP-39 + PBKDF2-SHA512 + SHA256-CTR DRBG
//
// Flow: entropy(256 bit) 24 words PBKDF2 64-byte seed ML-DSA-65 keypair
//
import Foundation
public struct MontanaSeed {
// MARK: - Constants
/// Entropy size for 24-word mnemonic: 256 bits = 32 bytes
static let entropySize = 32
/// PBKDF2 iterations (BIP-39 standard)
static let pbkdf2Iterations: UInt32 = 2048
/// Derived seed size: 512 bits = 64 bytes
static let seedSize = 64
// MARK: - Generate Mnemonic
/// Generate a new 24-word BIP-39 mnemonic from secure random entropy
/// - Returns: Array of 24 words
public static func generateMnemonic() -> [String]? {
// 1. Generate 256 bits of secure random entropy
var entropy = [UInt8](repeating: 0, count: entropySize)
let status = SecRandomCopyBytes(kSecRandomDefault, entropySize, &entropy)
guard status == errSecSuccess else { return nil }
return mnemonicFromEntropy(entropy)
}
/// Convert entropy bytes to mnemonic words
/// - Parameter entropy: 32 bytes of entropy
/// - Returns: 24 mnemonic words
static func mnemonicFromEntropy(_ entropy: [UInt8]) -> [String]? {
guard entropy.count == entropySize else { return nil }
// 2. Compute checksum: SHA-256(entropy), take first 8 bits
var hash = [UInt8](repeating: 0, count: 32)
entropy.withUnsafeBufferPointer { ptr in
CC_SHA256(ptr.baseAddress, CC_LONG(entropy.count), &hash)
}
let checksumByte = hash[0] // first 8 bits for 256-bit entropy
// 3. Combine entropy + checksum = 264 bits
var bits = [Bool]()
bits.reserveCapacity(264)
for byte in entropy {
for j in (0..<8).reversed() {
bits.append((byte >> j) & 1 == 1)
}
}
for j in (0..<8).reversed() {
bits.append((checksumByte >> j) & 1 == 1)
}
// 4. Split into 24 groups of 11 bits word indices
let wordlist = BIP39Wordlist.english
guard wordlist.count == 2048 else { return nil }
var words = [String]()
words.reserveCapacity(24)
for i in 0..<24 {
var index = 0
for j in 0..<11 {
if bits[i * 11 + j] {
index |= (1 << (10 - j))
}
}
guard index < 2048 else { return nil }
words.append(wordlist[index])
}
return words
}
// MARK: - Validate Mnemonic
/// Validate a BIP-39 mnemonic (24 words, checksum verification)
/// - Parameter words: Array of mnemonic words
/// - Returns: true if valid
public static func validateMnemonic(_ words: [String]) -> Bool {
guard words.count == 24 else { return false }
let wordlist = BIP39Wordlist.english
guard wordlist.count == 2048 else { return false }
// Convert words to bit array
var bits = [Bool]()
bits.reserveCapacity(264)
for word in words {
guard let index = wordlist.firstIndex(of: word.lowercased()) else {
return false
}
for j in (0..<11).reversed() {
bits.append((index >> j) & 1 == 1)
}
}
guard bits.count == 264 else { return false }
// Extract entropy (first 256 bits) and checksum (last 8 bits)
var entropy = [UInt8](repeating: 0, count: 32)
for i in 0..<32 {
var byte: UInt8 = 0
for j in 0..<8 {
if bits[i * 8 + j] {
byte |= UInt8(1 << (7 - j))
}
}
entropy[i] = byte
}
var checksumByte: UInt8 = 0
for j in 0..<8 {
if bits[256 + j] {
checksumByte |= UInt8(1 << (7 - j))
}
}
// Verify checksum: SHA-256(entropy)[0] == checksumByte
var hash = [UInt8](repeating: 0, count: 32)
entropy.withUnsafeBufferPointer { ptr in
CC_SHA256(ptr.baseAddress, CC_LONG(entropy.count), &hash)
}
return hash[0] == checksumByte
}
// MARK: - Mnemonic Seed (PBKDF2-SHA512)
/// Derive 64-byte seed from mnemonic using PBKDF2-SHA512 (BIP-39 standard)
/// - Parameter words: 24 mnemonic words
/// - Returns: 64-byte seed
public static func mnemonicToSeed(_ words: [String]) -> Data? {
guard validateMnemonic(words) else { return nil }
let mnemonic = words.joined(separator: " ")
let salt = "mnemonic" // BIP-39 standard salt (no passphrase)
guard let mnemonicData = mnemonic.data(using: .utf8),
let saltData = salt.data(using: .utf8) else { return nil }
var derivedKey = [UInt8](repeating: 0, count: seedSize)
let status = mnemonicData.withUnsafeBytes { mnemonicPtr in
saltData.withUnsafeBytes { saltPtr in
CCKeyDerivationPBKDF(
CCPBKDFAlgorithm(kCCPBKDF2),
mnemonicPtr.baseAddress?.assumingMemoryBound(to: Int8.self),
mnemonicData.count,
saltPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),
saltData.count,
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA512),
pbkdf2Iterations,
&derivedKey,
seedSize
)
}
}
guard status == kCCSuccess else { return nil }
return Data(derivedKey)
}
// MARK: - Seed ML-DSA-65 Keypair (Deterministic)
/// Derive ML-DSA-65 keypair deterministically from 64-byte seed
/// Uses SHA256-CTR as deterministic PRNG injected into liboqs
/// - Parameter seed: 64-byte seed from PBKDF2
/// - Returns: (privateKey, publicKey) tuple
public static func deriveKeypair(from seed: Data) -> (privateKey: Data, publicKey: Data)? {
guard seed.count == seedSize else { return nil }
return MLDSA65.generateKeypairFromSeed(seed: seed)
}
// MARK: - Full Flow: Mnemonic Keypair
/// Generate keypair from mnemonic words
/// - Parameter words: 24 mnemonic words
/// - Returns: (privateKey, publicKey) tuple
public static func keypairFromMnemonic(_ words: [String]) -> (privateKey: Data, publicKey: Data)? {
guard let seed = mnemonicToSeed(words) else { return nil }
defer {
var mutableSeed = seed
MLDSA65.zeroMemory(&mutableSeed)
}
return deriveKeypair(from: seed)
}
}