370 lines
11 KiB
Markdown
370 lines
11 KiB
Markdown
|
|
# KeyValidator Component
|
||
|
|
|
||
|
|
The KeyValidator handles all key validation logic including format checking, checksum verification, and level extraction. It implements the comprehensive validation system from the main architecture.
|
||
|
|
|
||
|
|
## Pseudocode
|
||
|
|
|
||
|
|
```swift
|
||
|
|
class KeyValidator {
|
||
|
|
// Key format: VUUW-XXXX-XXXX-XXXX-LN
|
||
|
|
private let keyPattern = "^VUUW-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-L[12]$"
|
||
|
|
private let checksumVerifier = ChecksumVerifier()
|
||
|
|
private let cache = ValidationCache(capacity: 1000)
|
||
|
|
|
||
|
|
struct ValidationResult {
|
||
|
|
let isValid: Bool
|
||
|
|
let level: LicenseLevel?
|
||
|
|
let error: KeyError?
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Main Validation
|
||
|
|
|
||
|
|
func validate(_ key: String) -> ValidationResult {
|
||
|
|
// Step 1: Format validation
|
||
|
|
if !isValidFormat(key) {
|
||
|
|
return ValidationResult(
|
||
|
|
isValid: false,
|
||
|
|
level: nil,
|
||
|
|
error: .invalidFormat
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 2: Extract components
|
||
|
|
let components = key.split(separator: "-")
|
||
|
|
let productID = components[0] // "VUUW"
|
||
|
|
let segment1 = components[1] // 4 chars
|
||
|
|
let segment2 = components[2] // 4 chars
|
||
|
|
let segment3 = components[3] // 4 chars
|
||
|
|
let levelCode = components[4] // "L1" or "L2"
|
||
|
|
|
||
|
|
// Step 3: Validate product ID
|
||
|
|
if productID != "VUUW" {
|
||
|
|
return ValidationResult(
|
||
|
|
isValid: false,
|
||
|
|
level: nil,
|
||
|
|
error: .wrongProduct
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 4: Extract and validate level
|
||
|
|
guard let level = parseLicenseLevel(levelCode) else {
|
||
|
|
return ValidationResult(
|
||
|
|
isValid: false,
|
||
|
|
level: nil,
|
||
|
|
error: .unsupportedLevel
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 5: Verify checksum
|
||
|
|
let baseKey = [productID, segment1, segment2, segment3].joined(separator: "-")
|
||
|
|
let isChecksumValid = verifyChecksum(baseKey, expectedLevel: level)
|
||
|
|
|
||
|
|
if !isChecksumValid {
|
||
|
|
return ValidationResult(
|
||
|
|
isValid: false,
|
||
|
|
level: level,
|
||
|
|
error: .checksumMismatch
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 6: Check for suspicious patterns
|
||
|
|
if hasSuspiciousPattern([segment1, segment2, segment3]) {
|
||
|
|
return ValidationResult(
|
||
|
|
isValid: false,
|
||
|
|
level: level,
|
||
|
|
error: .suspiciousKey
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
return ValidationResult(
|
||
|
|
isValid: true,
|
||
|
|
level: level,
|
||
|
|
error: nil
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Helper Methods
|
||
|
|
|
||
|
|
private func isValidFormat(_ key: String) -> Bool {
|
||
|
|
// Check basic format with regex
|
||
|
|
let regex = try! NSRegularExpression(pattern: keyPattern)
|
||
|
|
let range = NSRange(key.startIndex..., in: key)
|
||
|
|
return regex.firstMatch(in: key, range: range) != nil
|
||
|
|
}
|
||
|
|
|
||
|
|
private func parseLicenseLevel(_ code: String) -> LicenseLevel? {
|
||
|
|
switch code {
|
||
|
|
case "L1": return .level1
|
||
|
|
case "L2": return .level2
|
||
|
|
default: return nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private func verifyChecksum(_ baseKey: String, expectedLevel: LicenseLevel) -> Bool {
|
||
|
|
// Use dedicated checksum verifier
|
||
|
|
let segments = baseKey.split(separator: "-").dropFirst().map(String.init)
|
||
|
|
return checksumVerifier.verify(
|
||
|
|
baseKey: baseKey,
|
||
|
|
segments: segments,
|
||
|
|
level: expectedLevel
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
private func extractEmbeddedChecksum(from key: String) -> String {
|
||
|
|
// Checksum is cleverly embedded in the key segments
|
||
|
|
// This is a simplified representation
|
||
|
|
let segments = key.split(separator: "-").dropFirst()
|
||
|
|
|
||
|
|
var checksum = ""
|
||
|
|
for (index, segment) in segments.enumerated() {
|
||
|
|
// Extract specific character positions based on index
|
||
|
|
let position = (index * 2) % segment.count
|
||
|
|
let char = segment[segment.index(segment.startIndex, offsetBy: position)]
|
||
|
|
checksum.append(char)
|
||
|
|
}
|
||
|
|
|
||
|
|
return checksum
|
||
|
|
}
|
||
|
|
|
||
|
|
private func hasSuspiciousPattern(_ segments: [String]) -> Bool {
|
||
|
|
let suspiciousPatterns = [
|
||
|
|
"AAAA", "1111", "1234", "TEST",
|
||
|
|
"DEMO", "XXXX", "0000", "HACK"
|
||
|
|
]
|
||
|
|
|
||
|
|
for segment in segments {
|
||
|
|
// Check against known bad patterns
|
||
|
|
if suspiciousPatterns.contains(segment) {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if all characters are the same
|
||
|
|
if Set(segment).count == 1 {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for sequential characters
|
||
|
|
if isSequential(segment) {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
private func isSequential(_ str: String) -> Bool {
|
||
|
|
let chars = Array(str)
|
||
|
|
for i in 1..<chars.count {
|
||
|
|
let prevValue = chars[i-1].asciiValue ?? 0
|
||
|
|
let currValue = chars[i].asciiValue ?? 0
|
||
|
|
if currValue != prevValue + 1 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
private func constantTimeCompare(_ a: String, _ b: String) -> Bool {
|
||
|
|
// Prevent timing attacks
|
||
|
|
guard a.count == b.count else { return false }
|
||
|
|
|
||
|
|
var result = 0
|
||
|
|
for (charA, charB) in zip(a, b) {
|
||
|
|
result |= Int(charA.asciiValue ?? 0) ^ Int(charB.asciiValue ?? 0)
|
||
|
|
}
|
||
|
|
|
||
|
|
return result == 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Checksum Calculator
|
||
|
|
|
||
|
|
// MARK: - Checksum Verifier
|
||
|
|
|
||
|
|
struct ChecksumVerifier {
|
||
|
|
private let calculator = ChecksumCalculator()
|
||
|
|
|
||
|
|
func verify(baseKey: String, segments: [String], level: LicenseLevel) -> Bool {
|
||
|
|
// Calculate expected checksum
|
||
|
|
let expectedChecksum = calculator.calculate(baseKey, level: level)
|
||
|
|
|
||
|
|
// Extract embedded checksum from segments
|
||
|
|
let extractedChecksum = extractChecksum(from: segments)
|
||
|
|
|
||
|
|
// Constant-time comparison to prevent timing attacks
|
||
|
|
return constantTimeCompare(expectedChecksum, extractedChecksum)
|
||
|
|
}
|
||
|
|
|
||
|
|
private func extractChecksum(from segments: [String]) -> UInt32 {
|
||
|
|
var checksum: UInt32 = 0
|
||
|
|
|
||
|
|
for (index, segment) in segments.enumerated() {
|
||
|
|
let position = calculateExtractionPosition(index)
|
||
|
|
let char = segment[segment.index(segment.startIndex, offsetBy: position)]
|
||
|
|
|
||
|
|
// Extract bits from character
|
||
|
|
let bits = extractBitsFromChar(char)
|
||
|
|
checksum |= (bits << (index * 8))
|
||
|
|
}
|
||
|
|
|
||
|
|
return checksum
|
||
|
|
}
|
||
|
|
|
||
|
|
private func calculateExtractionPosition(_ index: Int) -> Int {
|
||
|
|
// Use prime numbers for better distribution
|
||
|
|
return (index * 31) % 4
|
||
|
|
}
|
||
|
|
|
||
|
|
private func extractBitsFromChar(_ char: Character) -> UInt32 {
|
||
|
|
// Extract embedded bits from character
|
||
|
|
guard let ascii = char.asciiValue else { return 0 }
|
||
|
|
return UInt32(ascii) & 0xFF
|
||
|
|
}
|
||
|
|
|
||
|
|
private func constantTimeCompare(_ a: UInt32, _ b: UInt32) -> Bool {
|
||
|
|
// Prevent timing attacks by always taking the same time
|
||
|
|
var result: UInt32 = 0
|
||
|
|
|
||
|
|
for i in 0..<4 {
|
||
|
|
let aByte = UInt8((a >> (i * 8)) & 0xFF)
|
||
|
|
let bByte = UInt8((b >> (i * 8)) & 0xFF)
|
||
|
|
result |= UInt32(aByte ^ bByte)
|
||
|
|
}
|
||
|
|
|
||
|
|
return result == 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Checksum Calculator
|
||
|
|
|
||
|
|
struct ChecksumCalculator {
|
||
|
|
private let salt = Data("VoiceUwU2024KeyGen!@#$%".utf8)
|
||
|
|
|
||
|
|
func calculate(_ baseKey: String, level: LicenseLevel) -> UInt32 {
|
||
|
|
// Combine key components
|
||
|
|
let combined = baseKey + level.rawValue
|
||
|
|
let data = Data(combined.utf8)
|
||
|
|
|
||
|
|
// Add salt for security
|
||
|
|
var hasher = SHA256()
|
||
|
|
hasher.update(data)
|
||
|
|
hasher.update(salt)
|
||
|
|
|
||
|
|
// Generate hash
|
||
|
|
let hash = hasher.finalize()
|
||
|
|
|
||
|
|
// Convert to UInt32 checksum
|
||
|
|
let checksum = hash.withUnsafeBytes { bytes in
|
||
|
|
bytes.bindMemory(to: UInt32.self).first ?? 0
|
||
|
|
}
|
||
|
|
|
||
|
|
return checksum
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Validation Cache
|
||
|
|
|
||
|
|
actor ValidationCache {
|
||
|
|
private var cache: LRUCache<String, ValidationResult>
|
||
|
|
private var stats = ValidationStats()
|
||
|
|
|
||
|
|
init(capacity: Int) {
|
||
|
|
self.cache = LRUCache(capacity: capacity)
|
||
|
|
}
|
||
|
|
|
||
|
|
func validate(_ key: String) async -> ValidationResult {
|
||
|
|
// Check cache first
|
||
|
|
if let cached = cache.get(key) {
|
||
|
|
stats.cacheHits += 1
|
||
|
|
return cached
|
||
|
|
}
|
||
|
|
|
||
|
|
stats.cacheMisses += 1
|
||
|
|
|
||
|
|
// Perform validation
|
||
|
|
let result = KeyValidator().validate(key)
|
||
|
|
|
||
|
|
// Cache result
|
||
|
|
cache.set(key, value: result)
|
||
|
|
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
func getCacheStats() -> ValidationStats {
|
||
|
|
return stats
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
struct ValidationStats {
|
||
|
|
var cacheHits: Int = 0
|
||
|
|
var cacheMisses: Int = 0
|
||
|
|
|
||
|
|
var hitRate: Double {
|
||
|
|
let total = cacheHits + cacheMisses
|
||
|
|
return total > 0 ? Double(cacheHits) / Double(total) : 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
struct LRUCache<Key: Hashable, Value> {
|
||
|
|
private let capacity: Int
|
||
|
|
private var cache: [Key: Value] = [:]
|
||
|
|
private var order: [Key] = []
|
||
|
|
|
||
|
|
init(capacity: Int) {
|
||
|
|
self.capacity = capacity
|
||
|
|
}
|
||
|
|
|
||
|
|
mutating func get(_ key: Key) -> Value? {
|
||
|
|
guard let value = cache[key] else { return nil }
|
||
|
|
|
||
|
|
// Move to end (most recently used)
|
||
|
|
order.removeAll { $0 == key }
|
||
|
|
order.append(key)
|
||
|
|
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
|
||
|
|
mutating func set(_ key: Key, value: Value) {
|
||
|
|
// Remove oldest if at capacity
|
||
|
|
if cache.count >= capacity && cache[key] == nil {
|
||
|
|
if let oldest = order.first {
|
||
|
|
cache.removeValue(forKey: oldest)
|
||
|
|
order.removeFirst()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
cache[key] = value
|
||
|
|
order.removeAll { $0 == key }
|
||
|
|
order.append(key)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Validation Rules
|
||
|
|
|
||
|
|
1. **Format Rules**
|
||
|
|
- Must start with "VUUW"
|
||
|
|
- Three segments of 4 alphanumeric characters
|
||
|
|
- End with L1 or L2
|
||
|
|
- Only uppercase letters and numbers
|
||
|
|
- No O, 0, I, 1 to avoid confusion
|
||
|
|
|
||
|
|
2. **Checksum Rules**
|
||
|
|
- Checksum embedded in key segments
|
||
|
|
- Uses product ID + segments + level for calculation
|
||
|
|
- Salt added for security
|
||
|
|
- Constant-time comparison
|
||
|
|
|
||
|
|
3. **Security Rules**
|
||
|
|
- No obvious patterns (AAAA, 1234, etc.)
|
||
|
|
- No all-same characters
|
||
|
|
- No sequential patterns
|
||
|
|
- No test/demo keys in production
|
||
|
|
|
||
|
|
## Error Handling
|
||
|
|
|
||
|
|
- `invalidFormat`: Key doesn't match expected pattern
|
||
|
|
- `wrongProduct`: Not a VoiceUwu key
|
||
|
|
- `unsupportedLevel`: Level code not L1 or L2
|
||
|
|
- `checksumMismatch`: Failed checksum verification
|
||
|
|
- `suspiciousKey`: Contains suspicious patterns
|