Closer/iphone/Closer/Crypto/RecoveryKeyManager.swift

45 lines
1.9 KiB
Swift

import Foundation
import CryptoKit
/// Recovery-phrase generation and validation.
///
/// Mirrors Android `RecoveryKeyManager.generateRecoveryPhrase()`: a phrase is
/// 10 words drawn uniformly from the bundled 248-word list, lowercased, and
/// separated by a single ASCII space.
///
/// Important correction from the Batch 1 spec: the Android source actually
/// contains **248** words, not 256. The iOS bundle copies that exact list so
/// cross-platform phrase generation stays byte-for-byte compatible.
public enum RecoveryKeyManager {
public static let phraseWordCount = 10
/// Generates a new 10-word recovery phrase.
public static func generatePhrase() throws -> String {
let words = try Wordlist.load()
var rng = SystemRandomNumberGenerator()
let indices = (0..<phraseWordCount).map { _ in Int.random(in: 0..<words.count, using: &rng) }
return indices.map { words[$0] }.joined(separator: " ")
}
/// Normalizes a recovery phrase: trim, lowercase, collapse internal whitespace,
/// join with a single space. Matches Android normalization intent.
public static func normalize(_ phrase: String) -> String {
phrase
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
.components(separatedBy: .whitespacesAndNewlines)
.filter { !$0.isEmpty }
.joined(separator: " ")
}
/// Returns true if the phrase is well-formed: exactly 10 words and every word
/// appears in the bundled wordlist.
public static func isWellFormed(_ phrase: String) throws -> Bool {
let words = try Wordlist.load()
let normalized = normalize(phrase)
let parts = normalized.split(separator: " ", omittingEmptySubsequences: false)
guard parts.count == phraseWordCount else { return false }
return parts.allSatisfy { words.contains(String($0)) }
}
}