import Foundation import CryptoKit import CommonCrypto enum LicenseFileCryptoError: Error { case invalidFormat case keyDerivationFailed case decryptionFailed } enum LicenseFileCrypto { private static let magic = "XUQM-LICENSE-V1" private static let passphrase = "xuqm-license-file-v1.2026.internal" private static let keyByteCount = 32 private static let pbkdf2Iterations: UInt32 = 120_000 // Format: MAGIC.base64UrlSalt.base64UrlIV.base64UrlCiphertext // Base64: URL-safe, no padding, no wrap // Algorithm: AES/GCM/NoPadding, PBKDF2WithHmacSHA256, 256-bit key, 128-bit tag static func decrypt(_ content: String) throws -> String { let parts = content.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: ".") guard parts.count == 4, parts[0] == magic else { throw LicenseFileCryptoError.invalidFormat } guard let salt = base64UrlDecode(parts[1]), let iv = base64UrlDecode(parts[2]), let ciphertext = base64UrlDecode(parts[3]) else { throw LicenseFileCryptoError.invalidFormat } let key = try deriveKey(salt: salt) // CryptoKit SealedBox(combined:) expects nonce(12) || ciphertext || tag(16) // Android JCE appends the 16-byte GCM tag at end of ciphertext — matches this format exactly let combined = iv + ciphertext let sealedBox = try AES.GCM.SealedBox(combined: combined) let decrypted = try AES.GCM.open(sealedBox, using: key) guard let plaintext = String(data: decrypted, encoding: .utf8) else { throw LicenseFileCryptoError.decryptionFailed } return plaintext } private static func deriveKey(salt: Data) throws -> SymmetricKey { let passphraseBytes = Array(passphrase.utf8) let saltBytes = Array(salt) var derivedKey = [UInt8](repeating: 0, count: keyByteCount) let status = CCKeyDerivationPBKDF( CCPBKDFAlgorithm(kCCPBKDF2), passphrase, passphraseBytes.count, saltBytes, saltBytes.count, CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256), pbkdf2Iterations, &derivedKey, keyByteCount ) guard status == kCCSuccess else { throw LicenseFileCryptoError.keyDerivationFailed } return SymmetricKey(data: Data(derivedKey)) } private static func base64UrlDecode(_ value: String) -> Data? { var s = value.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") let rem = s.count % 4 if rem > 0 { s += String(repeating: "=", count: 4 - rem) } return Data(base64Encoded: s) } }