347 lines
13 KiB
Swift
347 lines
13 KiB
Swift
//
|
|
// DeviceUUID.swift
|
|
//
|
|
//
|
|
// Created Nut.Tech on 02.08.2022.
|
|
//
|
|
|
|
#if os(iOS)
|
|
import UIKit
|
|
#elseif os(macOS)
|
|
import AppKit
|
|
#endif
|
|
import KeyChainAccess
|
|
|
|
/// Class allow retrive UUID with different life cycle.
|
|
final public class DeviceUUID {
|
|
|
|
private enum Constants {
|
|
|
|
enum Keys {
|
|
static let installationUUIDKey = "installationUUIDKey"
|
|
static let deviceUUIDKey = "deviceUUIDKey"
|
|
static let devicesUUIDsKey = "devicesUUIDsKey"
|
|
static let devicesUUIDsToggleKey = "devicesUUIDsToggleKey"
|
|
static let devicesUUIDs = "devicesUUIDs"
|
|
}
|
|
|
|
enum Locals {
|
|
static let DevicesUUIDsDidChangeNotification = NSNotification.Name(rawValue: "DevicesUUIDsDidChangeNotification")
|
|
}
|
|
}
|
|
|
|
/// Shared instance for class
|
|
static public var shared = DeviceUUID()
|
|
|
|
|
|
/// Changes each time the app gets launched (persistent to session).
|
|
lazy private(set) public var session: String = {
|
|
self.generateUuid()
|
|
}()
|
|
|
|
/// Changes each time the app gets installed (persistent to installation).
|
|
lazy private(set) public var installation: String = {
|
|
self.getValue(forKey: Constants.Keys.installationUUIDKey,
|
|
defaultValue: nil,
|
|
keychain: false,
|
|
synchronizable: false)
|
|
}()
|
|
|
|
/// Changes each time all the apps of the same vendor are uninstalled (this works exactly as identifierForVendor).
|
|
lazy private(set) public var vendor: String? = {
|
|
#if os(iOS)
|
|
UIDevice.current
|
|
.identifierForVendor?
|
|
.uuidString
|
|
.lowercased()
|
|
.replacingOccurrences(of: "-", with: "")
|
|
#elseif os(macOS)
|
|
return nil
|
|
#endif
|
|
}()
|
|
|
|
/// Changes only on system reset, this is the best replacement to the good old udid (persistent to device)
|
|
lazy private(set) public var device: String = {
|
|
self.getValue(forKey: Constants.Keys.deviceUUIDKey,
|
|
defaultValue: nil,
|
|
synchronizable: false)
|
|
}()
|
|
|
|
/// List of all uuidForDevice of the same user.
|
|
/// In this way it's possible manage guest accounts across multiple devices easily
|
|
lazy private(set) public var devices: [String] = {
|
|
let devicesString = self.getValue(forKey: Constants.Keys.devicesUUIDsKey,
|
|
defaultValue: self.device)
|
|
return devicesString.components(separatedBy: "|")
|
|
}()
|
|
|
|
/// Changes each time (no persistent), but allows to keep in memory more temporary uuids.
|
|
public func uuid(forKey key: String) -> String {
|
|
guard let uuid = self.uuids[key] else {
|
|
let value = self.generateUuid()
|
|
self.uuids[key] = value
|
|
return value
|
|
}
|
|
return uuid
|
|
}
|
|
|
|
/// Changes each time (no persistent)
|
|
public func generateUuid() -> String {
|
|
let uuidRef = CFUUIDCreate(nil)
|
|
let uuidStringRef = CFUUIDCreateString(nil, uuidRef)
|
|
|
|
return ((uuidStringRef as? String) ?? "")
|
|
.lowercased()
|
|
.replacingOccurrences(of: "-", with: "")
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private var uuids = [String: String]()
|
|
private var isCloudAvailable = false
|
|
|
|
// MARK: - Init
|
|
|
|
init() {
|
|
self.initCloudUUIDsDevices()
|
|
}
|
|
|
|
// MARK: - Internal
|
|
|
|
func getValue(forKey key: String,
|
|
defaultValue: String? = nil,
|
|
userDefaults: Bool = true,
|
|
keychain: Bool = true,
|
|
service: String? = nil,
|
|
accessGroup: String? = nil,
|
|
synchronizable: Bool = true) -> String {
|
|
|
|
if let newValue = Self.getValue(forKey: key,
|
|
userDefaults:
|
|
userDefaults,
|
|
keychain: keychain,
|
|
service: service,
|
|
accessGroup: accessGroup) {
|
|
return newValue
|
|
} else {
|
|
let value = defaultValue ?? self.generateUuid()
|
|
Self.setValue(value,
|
|
forKey: key,
|
|
userDefaults: userDefaults,
|
|
keychain: keychain,
|
|
service: service,
|
|
accessGroup: accessGroup,
|
|
synchronizable: synchronizable)
|
|
return value
|
|
}
|
|
}
|
|
|
|
public func uuidForDeviceMigratingValue(forKey key: String,
|
|
service: String? = nil,
|
|
accessGroup: String? = nil,
|
|
commitMigration: Bool) -> String? {
|
|
|
|
if let uuidToMigrate = Self.getValue(forKey: key,
|
|
service: service,
|
|
accessGroup: accessGroup) {
|
|
return self.uuid(forDeviceMigratingValue: uuidToMigrate, commitMigration: commitMigration)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func updateUUIDDevice(value: String) {
|
|
self.device = value
|
|
Self.setValue(value,
|
|
forKey: Constants.Keys.deviceUUIDKey,
|
|
synchronizable: false)
|
|
}
|
|
|
|
func uuid(forDeviceMigratingValue value: String, commitMigration: Bool) -> String? {
|
|
|
|
if self.isValidUUID(value) {
|
|
|
|
let oldValue = self.device
|
|
let newValue = value
|
|
|
|
guard oldValue != newValue else { return oldValue }
|
|
|
|
if commitMigration {
|
|
self.updateUUIDDevice(value: newValue)
|
|
|
|
let deviceSet = NSMutableOrderedSet(array: self.devices)
|
|
deviceSet.add(newValue)
|
|
deviceSet.remove(oldValue)
|
|
|
|
if let uuidsArray = deviceSet.array as? [String] {
|
|
self.updateUUIDsDevices(with: uuidsArray)
|
|
}
|
|
self.syncCloudUUIDsDevices()
|
|
|
|
return self.device
|
|
} else {
|
|
return oldValue
|
|
}
|
|
} else {
|
|
|
|
let exception = NSException(name: NSExceptionName(rawValue: "Invalid uuid to migrate"),
|
|
reason: "uuid value should be a string of 32 or 36 characters.")
|
|
exception.raise()
|
|
return nil
|
|
}
|
|
|
|
}
|
|
|
|
private func initCloudUUIDsDevices() {
|
|
|
|
self.isCloudAvailable = false
|
|
guard FileManager.default.ubiquityIdentityToken != nil else { return }
|
|
self.isCloudAvailable = true
|
|
|
|
NotificationCenter.default.addObserver(self,
|
|
selector: #selector(self.changesCloudUUIDsDevicesNotification),
|
|
name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
|
|
object: nil)
|
|
|
|
self.syncCloudUUIDsDevices()
|
|
}
|
|
|
|
private func syncCloudUUIDsDevices() {
|
|
if self.isCloudAvailable {
|
|
let iCloud = NSUbiquitousKeyValueStore.default
|
|
//if keychain contains more device identifiers than icloud, maybe that icloud has been empty, so re-write these identifiers to iCloud
|
|
for uuidOfUserDevice in self.devices {
|
|
let uuidOfUserDeviceAsKey = "\(Constants.Keys.deviceUUIDKey)_\(uuidOfUserDevice)"
|
|
|
|
if iCloud.string(forKey: uuidOfUserDeviceAsKey) != uuidOfUserDevice {
|
|
iCloud.set(uuidOfUserDevice, forKey: uuidOfUserDeviceAsKey)
|
|
}
|
|
}
|
|
|
|
//toggle a boolean value to force notification on other devices, useful for debug
|
|
let uuidsOfUserDevicesToggler = !iCloud.bool(forKey: Constants.Keys.devicesUUIDsToggleKey)
|
|
iCloud.set(uuidsOfUserDevicesToggler, forKey: Constants.Keys.devicesUUIDsToggleKey)
|
|
iCloud.synchronize()
|
|
}
|
|
}
|
|
|
|
private func updateUUIDsDevices(with value: [String]) {
|
|
self.devices = value
|
|
Self.setValue(value.joined(separator: "|"),
|
|
forKey: Constants.Keys.devicesUUIDsKey)
|
|
}
|
|
|
|
private func isValidUUID(_ value: String) -> Bool {
|
|
let pattern = "^[0-9a-f]{32}|[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$"
|
|
guard let regExp = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else {
|
|
return false
|
|
}
|
|
|
|
let uuidValueRange = NSRange(location: 0, length: value.count)
|
|
let matchRange = regExp.rangeOfFirstMatch(in: value, options: [], range: uuidValueRange)
|
|
var matchValue: String?
|
|
|
|
if !NSEqualRanges(matchRange, NSRange(location: NSNotFound, length: 0)) {
|
|
matchValue = (value as NSString).substring(with: matchRange)
|
|
return matchValue == value ? true : false
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
@objc
|
|
private func changesCloudUUIDsDevicesNotification(_ notification: Notification?) {
|
|
|
|
if self.isCloudAvailable {
|
|
let uuidsSet = NSMutableOrderedSet(array: self.devices)
|
|
let uuidsCount = uuidsSet.count
|
|
|
|
let iCloud = NSUbiquitousKeyValueStore.default
|
|
let iCloudDict = iCloud.dictionaryRepresentation as NSDictionary
|
|
|
|
iCloudDict.enumerateKeysAndObjects { key, obj, stop in
|
|
let uuidKey = key as? NSString
|
|
if uuidKey?.range(of: Constants.Keys.deviceUUIDKey).location == 0 {
|
|
if let uuidValue = obj as? String {
|
|
if uuidKey?.range(of: uuidValue).location != NSNotFound,
|
|
self.isValidUUID(uuidValue) {
|
|
uuidsSet.add(uuidValue)
|
|
} else {
|
|
print("invalid uuid")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if uuidsSet.count > uuidsCount,
|
|
let uuidsArray = uuidsSet.array as? [String] {
|
|
self.updateUUIDsDevices(with: uuidsArray)
|
|
let userInfo = [Constants.Keys.devicesUUIDs: self.devices]
|
|
NotificationCenter.default.post(name: Constants.Locals.DevicesUUIDsDidChangeNotification,
|
|
object: self,
|
|
userInfo: userInfo)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Static
|
|
|
|
static private func getValue(forKey key: String,
|
|
userDefaults: Bool = true,
|
|
keychain: Bool = true,
|
|
service: String? = nil,
|
|
accessGroup: String? = nil) -> String? {
|
|
|
|
let keychainStore = Self.makeKeychain(service: service, accessGroup: accessGroup)
|
|
var value = try? keychainStore.getString(key)
|
|
|
|
if userDefaults,
|
|
!value.isExist {
|
|
value = UserDefaults.standard.string(forKey: key)
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
@discardableResult
|
|
static private func setValue(_ value: String?,
|
|
forKey key: String,
|
|
userDefaults: Bool = true,
|
|
keychain: Bool = true,
|
|
service: String? = nil,
|
|
accessGroup: String? = nil,
|
|
synchronizable: Bool = true) -> Error? {
|
|
|
|
if let value = value,
|
|
userDefaults {
|
|
UserDefaults.standard.set(value, forKey: key)
|
|
UserDefaults.standard.synchronize()
|
|
}
|
|
|
|
if let value = value, keychain {
|
|
let keychainStore = Self.makeKeychain(service: service, accessGroup: accessGroup).synchronizable(synchronizable)
|
|
do {
|
|
try keychainStore.set(value, key: key)
|
|
} catch {
|
|
return error
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
static private func makeKeychain(service: String? = nil, accessGroup: String? = nil) -> Keychain {
|
|
|
|
if let service = service {
|
|
if let accessGroup = accessGroup {
|
|
return Keychain(service: service, accessGroup: accessGroup)
|
|
} else {
|
|
return Keychain(service: service)
|
|
}
|
|
} else if let accessGroup = accessGroup {
|
|
return Keychain(accessGroup: accessGroup)
|
|
}
|
|
|
|
return Keychain()
|
|
}
|
|
}
|