diff --git a/PlayTools.xcodeproj/project.pbxproj b/PlayTools.xcodeproj/project.pbxproj index b3c4e774..42b5194e 100644 --- a/PlayTools.xcodeproj/project.pbxproj +++ b/PlayTools.xcodeproj/project.pbxproj @@ -82,6 +82,7 @@ B1271729288284BE0025112B /* DiscordActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1271728288284BE0025112B /* DiscordActivity.swift */; }; B1E8CF8A28BBE2AB004340D3 /* Keymapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E8CF8928BBE2AB004340D3 /* Keymapping.swift */; }; B6D774FF2ACFC3D900C0D9D8 /* SwordRPC in Frameworks */ = {isa = PBXBuildFile; productRef = B6D774FE2ACFC3D900C0D9D8 /* SwordRPC */; }; + EEB248592B81D074000C230A /* PlayedAppleDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB248582B81D074000C230A /* PlayedAppleDB.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -177,6 +178,7 @@ B127172428817C040025112B /* DiscordIPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscordIPC.swift; sourceTree = ""; }; B1271728288284BE0025112B /* DiscordActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscordActivity.swift; sourceTree = ""; }; B1E8CF8928BBE2AB004340D3 /* Keymapping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keymapping.swift; sourceTree = ""; }; + EEB248582B81D074000C230A /* PlayedAppleDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayedAppleDB.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -524,6 +526,7 @@ isa = PBXGroup; children = ( ABCECEE529750BA600746595 /* PlayedApple.swift */, + EEB248582B81D074000C230A /* PlayedAppleDB.swift */, AB7DA47429B85BFB0034ACB2 /* PlayShadow.m */, AB7DA47629B8A78B0034ACB2 /* PlayShadow.h */, ); @@ -678,7 +681,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [[ -z \"$FASTLANE\" ]]; then\n export PATH=\"$PATH:/opt/homebrew/bin\"\n if which swiftlint > /dev/null; then\n swiftlint\n else\n echo \"error: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\n exit -1\n fi\nfi\n"; + shellScript = "if [[ -z \"$FASTLANE\" ]]; then\n export PATH=\"$PATH:/opt/homebrew/bin:/opt/local/bin\"\n if which swiftlint > /dev/null; then\n swiftlint\n else\n echo \"error: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\n exit -1\n fi\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -702,6 +705,7 @@ AA7197A1287A481500623C15 /* CircleMenuLoader.swift in Sources */, 6E76639B28D0FAE700DE4AF9 /* Plugin.swift in Sources */, B1271729288284BE0025112B /* DiscordActivity.swift in Sources */, + EEB248592B81D074000C230A /* PlayedAppleDB.swift in Sources */, 954389CC2B39F03D00B063BB /* EditorView.swift in Sources */, 954389C22B38922400B063BB /* MouseArea.swift in Sources */, AA7197AB287A481500623C15 /* Element.swift in Sources */, diff --git a/PlayTools/MysticRunes/PlayedApple.swift b/PlayTools/MysticRunes/PlayedApple.swift index 551adb7f..f47e38a3 100644 --- a/PlayTools/MysticRunes/PlayedApple.swift +++ b/PlayTools/MysticRunes/PlayedApple.swift @@ -14,70 +14,7 @@ import Security public class PlayKeychain: NSObject { static let shared = PlayKeychain() - - private static func getKeychainDirectory() -> URL? { - let bundleID = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String ?? "" - let keychainFolder = URL(fileURLWithPath: "/Users/\(NSUserName())/Library/Containers/io.playcover.PlayCover") - .appendingPathComponent("PlayChain") - .appendingPathComponent(bundleID) - - // Create the keychain folder if it doesn't exist - if !FileManager.default.fileExists(atPath: keychainFolder.path) { - do { - try FileManager.default.createDirectory(at: keychainFolder, - withIntermediateDirectories: true, - attributes: nil) - } catch { - debugLogger("Failed to create keychain folder") - } - } - - return keychainFolder - } - - private static func getKeychainPath(_ attributes: NSDictionary) -> URL { - let keychainFolder = getKeychainDirectory() - // if attributes["r_Ref"] as? Int == 1 { - // attributes.setValue("keys", forKey: "class") - // What the hell Apple - // } - // Generate a key path based on the key attributes - let tagName = (attributes[kSecAttrApplicationTag as String] as? Data) - .map({return String(data: $0, encoding: .utf8)!}) - let accountName = attributes[kSecAttrAccount as String] as? String - let serviceName = attributes[kSecAttrService as String] as? String - let classType = attributes[kSecClass as String] as? String - let keychainName = [ - tagName, - accountName, - serviceName, - classType] - .compactMap({ return $0 }) - .joined(separator: "-") - return keychainFolder! - .appendingPathComponent("\(keychainName).plist") - } - - private static func findSimilarKeys(_ attributes: NSDictionary) -> URL? { - // Things we can fuzz: accountName - - let keychainFolder = getKeychainDirectory() - let serviceName = attributes[kSecAttrService as String] as? String ?? "" - let classType = attributes[kSecClass as String] as? String ?? "" - - let everyKeys = try? FileManager.default.contentsOfDirectory(at: keychainFolder!, - includingPropertiesForKeys: nil, - options: .skipsHiddenFiles) - let searchRegex = try? NSRegularExpression(pattern: "\(serviceName)-.*-\(classType).plist", - options: .caseInsensitive) - - for key in everyKeys! where searchRegex!.matches(in: key.path, - options: [], - range: NSRange(location: 0, length: key.path.count)).count > 0 { - return keychainFolder!.appendingPathComponent(key.lastPathComponent) - } - return nil - } + private static let db = PlayKeychainDB.shared @objc public static func debugLogger(_ logContent: String) { if PlaySettings.shared.settingsData.playChainDebugging { @@ -88,24 +25,28 @@ public class PlayKeychain: NSObject { // Store the entire dictionary as a plist // SecItemAdd(CFDictionaryRef attributes, CFTypeRef *result) @objc static public func add(_ attributes: NSDictionary, result: UnsafeMutablePointer?>?) -> OSStatus { - let keychainPath = getKeychainPath(attributes) - // Check if the keychain file already exists - // if FileManager.default.fileExists(atPath: keychainPath.path) { - // debugLogger("Keychain file already exists") - // return errSecDuplicateItem - // } - // Write the dictionary to the keychain file - do { - try attributes.write(to: keychainPath) - debugLogger("Wrote keychain file to \(keychainPath)") - } catch { + guard let keychainDict = db.insert(attributes) else { debugLogger("Failed to write keychain file") return errSecIO } + debugLogger("Wrote keychain item to db") // Place v_Data in the result guard let vData = attributes["v_Data"] as? CFTypeRef else { return errSecSuccess } + + if attributes["r_Attributes"] as? Int == 1 { + // Create a dummy dictionary and return it + let dummyDict = keychainDict + if attributes["r_Data"] as? Int != 1 { + dummyDict.removeObject(forKey: kSecValueData) + dummyDict.removeObject(forKey: kSecValueRef) + dummyDict.removeObject(forKey: kSecValuePersistentRef) + } + result?.pointee = Unmanaged.passRetained(dummyDict) + return errSecSuccess + } + if attributes["class"] as? String == "keys" { // kSecAttrKeyType is stored as `type` in the dictionary // kSecAttrKeyClass is stored as `kcls` in the dictionary @@ -124,31 +65,22 @@ public class PlayKeychain: NSObject { // SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate) @objc static public func update(_ query: NSDictionary, attributesToUpdate: NSDictionary) -> OSStatus { - // Get the path to the keychain file - let keychainPath = getKeychainPath(query) - // Read the dictionary from the keychain file - let keychainDict = NSDictionary(contentsOf: keychainPath) - debugLogger("Read keychain file from \(keychainPath)") - // Check if the file exist - if keychainDict == nil { - debugLogger("Keychain file not found at \(keychainPath)") + guard let keychainDict = db.query(query)?.first else { + debugLogger("Keychain item not found in db") return errSecItemNotFound } + debugLogger("Select keychain item from db") // Reconstruct the dictionary (subscripting won't work as assignment is not allowed) let newKeychainDict = NSMutableDictionary() - for (key, value) in keychainDict! { + for (key, value) in keychainDict { newKeychainDict.setValue(value, forKey: key as! String) // swiftlint:disable:this force_cast } // Update the dictionary for (key, value) in attributesToUpdate { newKeychainDict.setValue(value, forKey: key as! String) // swiftlint:disable:this force_cast } - // Write the dictionary to the keychain file - do { - try newKeychainDict.write(to: keychainPath) - debugLogger("Wrote keychain file to \(keychainPath)") - } catch { - debugLogger("Failed to write keychain file") + guard db.update(newKeychainDict) else { + debugLogger("Failed to update keychain item to db") return errSecIO } @@ -157,91 +89,74 @@ public class PlayKeychain: NSObject { // SecItemDelete(CFDictionaryRef query) @objc static public func delete(_ query: NSDictionary) -> OSStatus { - // Get the path to the keychain file - let keychainPath = getKeychainPath(query) - // Check if the keychain file doesn't exist - if !FileManager.default.fileExists(atPath: keychainPath.path) { + guard db.query(query)?.first != nil else { + debugLogger("Failed to find keychain item") return errSecItemNotFound } - // Delete the keychain file - do { - try FileManager.default.removeItem(at: keychainPath) - debugLogger("Deleted keychain file at \(keychainPath)") - } catch { - debugLogger("Failed to delete keychain file") + guard db.delete(query) else { + debugLogger("Failed to delete keychain item") return errSecIO } + debugLogger("Deleted keychain item in db") return errSecSuccess } // SecItemCopyMatching(CFDictionaryRef query, CFTypeRef *result) @objc static public func copyMatching(_ query: NSDictionary, result: UnsafeMutablePointer?>?) -> OSStatus { - // Get the path to the keychain file - var keychainPath = getKeychainPath(query) - // If the keychain file doesn't exist, attempt to find a similar key - if !FileManager.default.fileExists(atPath: keychainPath.path) { + guard let keychainDicts = db.query(query), + let keychainDict = keychainDicts.first else { + debugLogger("Keychain item not found in db") return errSecItemNotFound - // if let similarKey = findSimilarKeys(query) { - // NSLog("Found similar key at \(similarKey)") - // keychainPath = similarKey - // } else { - // debugLogger("Keychain file not found at \(keychainPath)") - // return errSecItemNotFound - // } } - // Read the dictionary from the keychain file - let keychainDict = NSDictionary(contentsOf: keychainPath) + if query[kSecMatchLimit as String] as? String == kSecMatchLimitAll as String { + result?.pointee = Unmanaged.passRetained(keychainDicts.map({ + $0.removeObject(forKey: kSecValueData) + $0.removeObject(forKey: kSecValueRef) + $0.removeObject(forKey: kSecValuePersistentRef) + return $0 + }) as CFTypeRef) + return errSecSuccess + } // Check the `r_Attributes` key. If it is set to 1 in the query let classType = query[kSecClass as String] as? String ?? "" - // If the keychain file doesn't exist, return errSecItemNotFound - if keychainDict == nil { - debugLogger("Keychain file not found at \(keychainPath)") - return errSecItemNotFound - } - if query["r_Attributes"] as? Int == 1 { - // if the keychainDict is nil, we need to return errSecItemNotFound - if keychainDict == nil { - debugLogger("Keychain file not found at \(keychainPath)") - return errSecItemNotFound - } - // Create a dummy dictionary and return it - let dummyDict = NSMutableDictionary() - dummyDict.setValue(classType, forKey: "class") - dummyDict.setValue(keychainDict![kSecAttrAccount as String], forKey: "acct") - dummyDict.setValue(keychainDict![kSecAttrService as String], forKey: "svce") - dummyDict.setValue(keychainDict![kSecAttrGeneric as String], forKey: "gena") + let dummyDict = keychainDict + if query["r_Data"] as? Int != 1 { + dummyDict.removeObject(forKey: kSecValueData) + dummyDict.removeObject(forKey: kSecValueRef) + dummyDict.removeObject(forKey: kSecValuePersistentRef) + } result?.pointee = Unmanaged.passRetained(dummyDict) return errSecSuccess } - // Check for r_Ref + // Check for r_Ref if query["r_Ref"] as? Int == 1 { // Return the data on v_PersistentRef or v_Data if they exist var key: CFTypeRef? - if let vData = keychainDict!["v_Data"] { + if let vData = keychainDict[kSecValueData] { NSLog("found v_Data") - debugLogger("Read keychain file from \(keychainPath)") + debugLogger("Read keychain item from db") key = vData as CFTypeRef } - if let vPersistentRef = keychainDict!["v_PersistentRef"] { + if let vPersistentRef = keychainDict[kSecValuePersistentRef] { NSLog("found persistent ref") - debugLogger("Read keychain file from \(keychainPath)") + debugLogger("Read keychain item from db") key = vPersistentRef as CFTypeRef } if key == nil { - debugLogger("Keychain file not found at \(keychainPath)") + debugLogger("Keychain item not found in db") return errSecItemNotFound } - + let dummyKeyAttrs = [ - kSecAttrKeyType: keychainDict?["type"] ?? kSecAttrKeyTypeRSA, - kSecAttrKeyClass: keychainDict!["kcls"] ?? kSecAttrKeyClassPublic + kSecAttrKeyType: keychainDict[kSecAttrKeyType] ?? kSecAttrKeyTypeRSA, + kSecAttrKeyClass: keychainDict[kSecAttrKeyClass] ?? kSecAttrKeyClassPublic ] as CFDictionary let secKey = SecKeyCreateWithData(key as! CFData, dummyKeyAttrs, nil) // swiftlint:disable:this force_cast @@ -250,16 +165,16 @@ public class PlayKeychain: NSObject { } // Return v_Data if it exists - if let vData = keychainDict!["v_Data"] { - debugLogger("Read keychain file from \(keychainPath)") + if let vData = keychainDict[kSecValueData] { + debugLogger("Read keychain file from db") // Check the class type, if it is a key we need to return the data // as SecKeyRef, otherwise we can return it as a CFTypeRef if classType == "keys" { // kSecAttrKeyType is stored as `type` in the dictionary // kSecAttrKeyClass is stored as `kcls` in the dictionary let keyAttributes = [ - kSecAttrKeyType: keychainDict!["type"] as! CFString, // swiftlint:disable:this force_cast - kSecAttrKeyClass: keychainDict!["kcls"] as! CFString // swiftlint:disable:this force_cast + kSecAttrKeyType: keychainDict[kSecAttrKeyType] as! CFString, // swiftlint:disable:this force_cast + kSecAttrKeyClass: keychainDict[kSecAttrKeyClass] as! CFString // swiftlint:disable:this force_cast ] let keyData = vData as! Data // swiftlint:disable:this force_cast let key = SecKeyCreateWithData(keyData as CFData, keyAttributes as CFDictionary, nil) diff --git a/PlayTools/MysticRunes/PlayedAppleDB.swift b/PlayTools/MysticRunes/PlayedAppleDB.swift new file mode 100644 index 00000000..700b67c2 --- /dev/null +++ b/PlayTools/MysticRunes/PlayedAppleDB.swift @@ -0,0 +1,417 @@ +// +// PlayedAppleBackend.swift +// PlayTools +// +// Created by Ryu-ga on 2/20/24. +// + +import Foundation +import Security +import SQLite3 + +class PlayKeychainDB: NSObject { + public static let shared = PlayKeychainDB() + + private var dbLock: DispatchSemaphore = .init(value: 1) + private var dbVersion: Int = 1 + + func query(_ attributes: NSDictionary) -> [NSMutableDictionary]? { + guard let table_name = attributes[kSecClass] as? String, + let primaryColumns = PlayedAppleDBConstants.primaries[table_name as CFString] else { + return nil + } + + let select_where = primaryColumns.compactMap({ + guard let attr = attributes[$0] else { return nil } // use only requested ones + if CFGetTypeID(attr as CFTypeRef) == CFDataGetTypeID(), + let string = (attr as? Data).map({ String(data: $0, encoding: .utf8) }) { + return "\($0) LIKE '\(string!)'" // non null-termination in db + } + return "\($0) = '\(attr)'" + }).joined(separator: " AND ") + guard select_where.count > 0 else { return nil } + let select_limit = attributes[kSecMatchLimit] as? String == kSecMatchLimitOne as String ? 1 : Int.max + + let select_query = "SELECT * FROM \(table_name) WHERE \(select_where) LIMIT \(select_limit)" + var stmt: OpaquePointer? + + var dictArr: [NSMutableDictionary] = [] + guard usingDB({ sqlite3DB in + defer { sqlite3_finalize(stmt) } + guard sqlite3_prepare(sqlite3DB, select_query, -1, &stmt, nil) == SQLITE_OK, + let stmt = stmt else { + PlayKeychain.debugLogger("Failed query \(select_query)") + PlayKeychain.debugLogger("Failed to query to db table: \(String(cString: sqlite3_errmsg(sqlite3DB)))") + return false + } + + while sqlite3_step(stmt) == SQLITE_ROW && dictArr.count < select_limit { + let newDict: NSMutableDictionary = [:] + let columns = sqlite3_column_count(stmt) + newDict[kSecClass] = table_name + newDict[kSecAttrSynchronizable] = attributes[kSecAttrSynchronizable] + for index in 0.. NSMutableDictionary? { + guard let table_name = attributes[kSecClass] as? String, + let primaryColumns = PlayedAppleDBConstants.primaries[table_name as CFString], + let secondaryColumns = PlayedAppleDBConstants.attributes[table_name as CFString] else { + return nil + } + + var columns_query = primaryColumns.map({ "\($0)" }) + columns_query.append(contentsOf: secondaryColumns.compactMap({ + attributes[$0] != nil ? "\($0)" : nil + })) + columns_query.append(contentsOf: PlayedAppleDBConstants.values.compactMap({ + attributes[$0] != nil ? "\($0)" : nil + })) + + let insert_values = columns_query.map({ attributes[$0] as CFTypeRef }) + + let insert_columns = columns_query.joined(separator: ", ") + let insert_placeholders = Array(repeating: "?", count: insert_values.count).joined(separator: ", ") + + let insert_query = "INSERT INTO \(table_name) (\(insert_columns)) VALUES (\(insert_placeholders))" + var stmt: OpaquePointer? + + let newDict: NSMutableDictionary = [:] + newDict[kSecClassKey] = table_name + for column in columns_query { + newDict[column] = attributes[column] + } + + guard usingDB({ sqlite3DB in + defer { sqlite3_finalize(stmt) } + guard sqlite3_prepare(sqlite3DB, insert_query, -1, &stmt, nil) == SQLITE_OK, + let stmt = stmt else { + let errorMessage = String(cString: sqlite3_errmsg(sqlite3DB)) + PlayKeychain.debugLogger("Failed query \(insert_query)") + PlayKeychain.debugLogger("Failed to make query: \(errorMessage)") + return false + } + + for (index, value) in insert_values.enumerated() + where !encodeData(stmt: stmt, index: Int32(index + 1), value: value) { + let erorrMessage = String(cString: sqlite3_errmsg(sqlite3DB)) + PlayKeychain.debugLogger("Failed to insert into db: \(erorrMessage)") + return false + } + + return sqlite3_step(stmt) == SQLITE_DONE + }) else { return nil } + + return newDict + } + + func update(_ attributes: NSDictionary) -> Bool { + guard let table_name = attributes[kSecClass] as? String, + let primaryColumns = PlayedAppleDBConstants.primaries[table_name as CFString], + let secondaryColumns = PlayedAppleDBConstants.attributes[table_name as CFString] else { + return false + } + + var columns_query = primaryColumns.map({ "\($0)" }) + columns_query.append(contentsOf: secondaryColumns.compactMap({ + attributes[$0] != nil ? "\($0)" : nil + })) + columns_query.append(contentsOf: PlayedAppleDBConstants.values.compactMap({ + attributes[$0] != nil ? "\($0)" : nil + })) + + let update_values = columns_query.map({ attributes[$0] as CFTypeRef }) + + let update_columns = columns_query.map({ return "\($0) = ?" }).joined(separator: ", ") + let update_where = primaryColumns.compactMap({ + guard let attr = attributes[$0] else { return nil } + if CFGetTypeID(attr as CFTypeRef) == CFDataGetTypeID(), + let string = (attr as? Data).map({ return String(data: $0, encoding: .utf8) }) { + return "\($0) LIKE '\(string!)'" // \0 does not exists in db due to casting + } + return "\($0) = '\(attr)'" + }).joined(separator: " AND ") + + let update_query = "UPDATE \(table_name) SET \(update_columns) WHERE \(update_where)" + var stmt: OpaquePointer? + + guard usingDB({ sqlite3DB in + defer { sqlite3_finalize(stmt) } + guard sqlite3_prepare(sqlite3DB, update_query, -1, &stmt, nil) == SQLITE_OK, + let stmt = stmt else { + let errorMessage = String(cString: sqlite3_errmsg(sqlite3DB)) + PlayKeychain.debugLogger("Failed query \(update_query)") + PlayKeychain.debugLogger("Failed to make query: \(errorMessage)") + return false + } + + for (index, value) in update_values.enumerated() + where !encodeData(stmt: stmt, index: Int32(index + 1), value: value) { + let errorMessage = String(cString: sqlite3_errmsg(sqlite3DB)) + PlayKeychain.debugLogger("Failed to update into db: \(errorMessage)") + return false + } + + return sqlite3_step(stmt) == SQLITE_DONE + }) else { return false } + + return true + } + + func delete(_ attributes: NSDictionary) -> Bool { + guard let table_name = attributes[kSecClass] as? String, + let primaryColumns = PlayedAppleDBConstants.primaries[table_name as CFString] else { + return false + } + + let delete_where = primaryColumns.compactMap({ + guard let attr = attributes[$0] else { return nil } // use only requested ones + if CFGetTypeID(attr as CFTypeRef) == CFDataGetTypeID(), + let string = (attr as? Data).map({ return String(data: $0, encoding: .utf8) }) { + return "\($0) LIKE '\(string!)'" // non null-termination in db + } + return "\($0) = '\(attr)'" + }).joined(separator: " AND ") + + let delete_query = "DELETE FROM \(table_name) where \(delete_where)" + var stmt: OpaquePointer? + + guard usingDB({ sqlite3DB in + defer { sqlite3_finalize(stmt) } + guard sqlite3_prepare(sqlite3DB, delete_query, -1, &stmt, nil) == SQLITE_OK, + let stmt = stmt else { + let errorMessage = String(cString: sqlite3_errmsg(sqlite3DB)) + PlayKeychain.debugLogger("Failed query \(delete_query)") + PlayKeychain.debugLogger("Failed to delte items from db table: \(errorMessage)") + return false + } + + return sqlite3_step(stmt) == SQLITE_OK + }) else { return false } + + return true + } + + private func structDB(_ sqlite3DB: OpaquePointer) -> Bool { + for (key, value) in PlayedAppleDBConstants.primaries { + var columns: [CFString] = value + columns.append(contentsOf: PlayedAppleDBConstants.attributes[key]!) + columns.append(contentsOf: PlayedAppleDBConstants.values) + let columnsSetting = columns.map({ return "\($0) TEXT" }).joined(separator: ", ") + let primaryKeysSetting = "PRIMARY KEY (\(value.map({ return "\($0)" }).joined(separator: ", ")))" + let create_table_query = "CREATE TABLE IF NOT EXISTS \(key) (\(columnsSetting), \(primaryKeysSetting));" + guard sqlite3_exec(sqlite3DB, create_table_query, nil, nil, nil) == SQLITE_OK else { + let errorMessage = String(cString: sqlite3_errmsg(sqlite3DB)) + PlayKeychain.debugLogger("Failed query \(create_table_query)") + PlayKeychain.debugLogger("Failed to create db table: \(errorMessage)") + return false + } + } + + sqlite3_exec(sqlite3DB, "PRAGMA user_version = \(dbVersion)", nil, nil, nil) + + return true + } + + private func usingDB(_ callback: (OpaquePointer) -> Bool) -> Bool { + dbLock.wait() + defer { dbLock.signal() } + + guard let sqlite3DB = connectToDB() else { return false } + let result = callback(sqlite3DB) + guard disconnectFromDB(sqlite3DB) else { return false } + return result + } + + private func connectToDB() -> OpaquePointer? { + var sqlite3DB: OpaquePointer? + let bundleID = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String ?? "Shared" + let keychainDB = URL(fileURLWithPath: "/Users/\(NSUserName())/Library/Containers/io.playcover.PlayCover") + .appendingPathComponent("PlayChain") + .appendingPathComponent("\(bundleID).db") + + let alreadyCreated = FileManager.default.fileExists(atPath: keychainDB.path) + + guard sqlite3_open(keychainDB.path, &sqlite3DB) == SQLITE_OK, + let sqlite3DB = sqlite3DB else { + PlayKeychain.debugLogger("Failed to connect to DB") + return nil + } + + if !alreadyCreated || !structDB(sqlite3DB) { + _ = disconnectFromDB(sqlite3DB) + return nil + } + + return sqlite3DB + } + + private func disconnectFromDB(_ sqlite3DB: OpaquePointer?) -> Bool { + guard sqlite3_close(sqlite3DB) == SQLITE_OK else { + PlayKeychain.debugLogger("Failed to disconnect from DB") + return false + } + + return true + } + + private func encodeData(stmt: OpaquePointer, index: Int32, value: CFTypeRef) -> Bool { + let sqlite_transient = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + var result = SQLITE_FAIL + switch CFGetTypeID(value) { + case CFStringGetTypeID(): + let string = value as! String // swiftlint:disable:this force_cast + result = sqlite3_bind_text(stmt, index, string, -1, sqlite_transient) + case CFDataGetTypeID(): + let data = value as! CFData // swiftlint:disable:this force_cast + let ptr = CFDataGetBytePtr(data) + let size = CFDataGetLength(data) + result = sqlite3_bind_blob(stmt, index, ptr, Int32(size), sqlite_transient) + case CFNullGetTypeID(): + result = sqlite3_bind_null(stmt, index) + default: + PlayKeychain.debugLogger("Cannot encode this data type: \(CFGetTypeID(value))") + result = sqlite3_bind_null(stmt, index) + } + + return result == SQLITE_OK + } + + private func decodeData(stmt: OpaquePointer, index: Int32) -> CFTypeRef? { + switch sqlite3_column_type(stmt, index) { + case SQLITE_TEXT: + guard let ptr = sqlite3_column_text(stmt, index) else { return nil } + return CFStringCreateWithCString(nil, ptr, kCFStringEncodingASCII) + case SQLITE_BLOB: + guard let ptr = sqlite3_column_blob(stmt, index) else { return nil } + let size = sqlite3_column_bytes(stmt, index) + return CFDataCreate(nil, ptr, CFIndex(size)) + case SQLITE_NULL: + return nil + default: + PlayKeychain.debugLogger("Cannot decode this data \(sqlite3_column_type(stmt, index))") + return nil + } + } +} + +struct PlayedAppleDBConstants { + // https://developer.apple.com/documentation/security/keychain_services/keychain_items/item_class_keys_and_values + // Synchronizable does not matter. + static let primaries = [ + kSecClassGenericPassword: [ + kSecAttrAccessGroup, + kSecAttrAccount, + kSecAttrService + // kSecAttrSynchronizable + ], + kSecClassInternetPassword: [ + kSecAttrAccessGroup, + kSecAttrAccount, + kSecAttrAuthenticationType, + kSecAttrPath, + kSecAttrPort, + kSecAttrProtocol, + kSecAttrSecurityDomain, + kSecAttrServer + // kSecAttrSynchronizable + ], + kSecClassCertificate: [ + kSecAttrAccessGroup, + kSecAttrCertificateType, + kSecAttrIssuer, + kSecAttrSerialNumber + // kSecAttrSynchronizable + ], + kSecClassKey: [ + kSecAttrAccessGroup, + kSecAttrApplicationLabel, + kSecAttrApplicationTag, + kSecAttrEffectiveKeySize, + kSecAttrKeyClass, + kSecAttrKeySizeInBits, + kSecAttrKeyType + // kSecAttrSynchronizable + ], + kSecClassIdentity: [ + kSecAttrAccessGroup, + kSecAttrCertificateType, + kSecAttrIssuer, + kSecAttrSerialNumber + // kSecAttrSynchronizable + ] + ] + static let attributes = [ + kSecClassGenericPassword: [ + kSecAttrAccessControl, + kSecAttrAccessible, + kSecAttrCreationDate, + kSecAttrModificationDate, + kSecAttrDescription, + kSecAttrComment, + kSecAttrCreator, + kSecAttrType, + kSecAttrLabel, + kSecAttrIsInvisible, + kSecAttrIsNegative, + kSecAttrGeneric + ], + kSecClassInternetPassword: [ + kSecAttrAccessControl, + kSecAttrAccessible, + kSecAttrCreationDate, + kSecAttrModificationDate, + kSecAttrDescription, + kSecAttrComment, + kSecAttrCreator, + kSecAttrType, + kSecAttrLabel, + kSecAttrIsInvisible, + kSecAttrIsNegative, + kSecAttrGeneric + ], + kSecClassCertificate: [ + kSecAttrCertificateEncoding, + kSecAttrLabel, + kSecAttrSubject, + kSecAttrSubjectKeyID, + kSecAttrPublicKeyHash + ], + kSecClassKey: [ + kSecAttrAccessible, + kSecAttrLabel, + kSecAttrIsPermanent, + kSecAttrCanEncrypt, + kSecAttrCanDecrypt, + kSecAttrCanDerive, + kSecAttrCanSign, + kSecAttrCanVerify, + kSecAttrCanWrap, + kSecAttrCanUnwrap + ], + kSecClassIdentity: [ + kSecAttrCertificateEncoding, + kSecAttrLabel, + kSecAttrSubject, + kSecAttrSubjectKeyID, + kSecAttrPublicKeyHash + ] + ] + static let values = [ + kSecValueData, + kSecValueRef, + kSecValuePersistentRef + ] +}