Files
CmdBar/src/LicenseManager.swift
2024-12-27 15:30:07 -08:00

130 lines
4.5 KiB
Swift

//
// LicenseManager.swift
// CmdBar
//
// Created by Igor Kolokolnikov on 8/26/23.
//
import Cocoa
import CryptoKit
fileprivate struct LicenseModel: Codable {
let valid: Bool
let error: String
let email: String
let key: String
}
// Converts string to a hash.
fileprivate func hashString(_ string: String) -> String {
return SHA256.hash(data: Data(string.utf8)).compactMap { String(format: "%02x", $0) }.joined()
}
// Converts passphrase to a SymmetricKey.
fileprivate func passToKey(from passphrase: String) -> SymmetricKey {
let passphraseData = Data(passphrase.utf8)
let keyData = SHA256.hash(data: passphraseData).withUnsafeBytes { buffer in
return Data(buffer)
}
return SymmetricKey(data: keyData)
}
final class LicenseManager {
static let standard = LicenseManager()
private let passkey = passToKey(from: "B2B67FB0E22B814CE5A6E7DA729A068BF21C9490919EA0B42FCEB322F9C9BA82")
private let defaults = UserDefaults.standard
private var timer: DispatchSourceTimer?
private var quitAfterMinutes = 20.0 * 60.0 // quit after 20 minutes
func isRegistered() -> Bool {
guard let email = defaults.object(forKey: "Email") as? String,
let key = defaults.object(forKey: "Key") as? String,
let licenseData = defaults.object(forKey: "License") as? Data,
let unsealedBox = try? AES.GCM.SealedBox(combined: licenseData),
let licenseHashData = try? AES.GCM.open(unsealedBox, using: passkey),
!email.isEmpty,
!key.isEmpty
else { return false }
let currentHashData = Data(SHA256.hash(data: Data(hashString("\(email)\(key)\(NSUserName())").utf8))
.compactMap { String(format: "%02x", $0) }.joined().utf8)
return licenseHashData == currentHashData
}
func activateLicense(email: String, key: String, completionHandler: @escaping (_ data: Resulting<Bool, String>) -> Void) {
guard let url = URL(string: EndpointConstants.validateURL)?.appending(component: key)
else { completionHandler(.failure); return }
URLSession.shared.dataTask(with: url) { [weak self] (data, response, error) in
guard
let data = data,
error == nil,
let response = response as? HTTPURLResponse,
(200...299).contains(response.statusCode)
else { completionHandler(.failure); return }
guard let jsonResponse = try? JSONDecoder().decode(LicenseModel.self, from: data)
else { completionHandler(.failure); return }
if jsonResponse.email == email && jsonResponse.key == key {
self?.saveLicense(with: email, and: key)
self?.stopTimer()
completionHandler(.success)
} else {
completionHandler(.failure)
}
}.resume()
}
func deactivateLicense() {
clearLicense()
startTimer()
}
func getLicense() -> (email: String, key: String) {
guard
let email = defaults.object(forKey: "Email") as? String,
let key = defaults.object(forKey: "Key") as? String
else { return ("", "") }
return (email, key)
}
private func saveLicense(with email: String, and key: String) {
let defaults = UserDefaults.standard
let licenseHashData = Data(SHA256.hash(data: Data(hashString("\(email)\(key)\(NSUserName())").utf8))
.compactMap { String(format: "%02x", $0) }.joined().utf8)
guard let sealedBoxData = try? AES.GCM.seal(licenseHashData, using: passkey).combined else { return }
defaults.set(email, forKey: "Email")
defaults.set(key, forKey: "Key")
defaults.set(sealedBoxData, forKey: "License")
}
func clearLicense() {
defaults.set(nil, forKey: "Email")
defaults.set(nil, forKey: "Key")
defaults.set(nil, forKey: "License")
}
// MARK: - Timer
func startTimer() {
timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
timer?.schedule(
deadline: .now() + quitAfterMinutes,
repeating: DispatchTimeInterval.never
)
timer?.setEventHandler {
DispatchQueue.main.async { NSApplication.shared.terminate(self) }
}
timer?.resume()
}
func stopTimer() {
timer?.cancel()
timer = nil
}
}