Skip to content

Commit

Permalink
PlayChain keychain emulation support (PlayCover#65)
Browse files Browse the repository at this point in the history
* feat: Apple Keychain Emulation (beta)

* feat: Make PlayChain optional

* fix: Store Keychains in PlayCover directories

* fix: remove more logging when debug isn't enabled

* fix: trailing space after keychainFolder

* fix: cleanup logging functions

* fix: use debugLogger from PlayKeychain instead of redefining it

* fix: correctly return NotFound for non supported types

* fix: Nicer keychainFolder

* Revert "fix: Nicer keychainFolder"

This reverts commit 992e860 due to it
storing the keychain inside the app container instead of PlayCover's

* fix: team IDs and logging

* fix: indentations

* fix: tighter checks for Update operations

* fix: remove Package.resolved
  • Loading branch information
ohaiibuzzle authored and IsaacMarovitz committed May 8, 2023
1 parent 0a264d6 commit 10e48d5
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 9 deletions.
20 changes: 16 additions & 4 deletions PlayTools.xcodeproj/project.pbxproj
Expand Up @@ -7,14 +7,14 @@
objects = {

/* Begin PBXBuildFile section */
2847AE48298EFC0F00B0F983 /* PlayScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2847AE47298EFC0F00B0F983 /* PlayScreen.swift */; };
6E76639B28D0FAE700DE4AF9 /* Plugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E76639A28D0FAE700DE4AF9 /* Plugin.swift */; };
6E76639C28D0FAE700DE4AF9 /* Plugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E76639A28D0FAE700DE4AF9 /* Plugin.swift */; };
6E7663A128D0FB5300DE4AF9 /* AKPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7663A028D0FB5300DE4AF9 /* AKPlugin.swift */; };
6E7663A528D0FEBE00DE4AF9 /* AKPluginLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7663A428D0FEBE00DE4AF9 /* AKPluginLoader.swift */; };
6E84A14528D0F94E00BF7495 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA818CBA287ABFD5000BEE9D /* UIKit.framework */; };
6E84A15028D0F97500BF7495 /* AKInterface.bundle in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6E84A14C28D0F96D00BF7495 /* AKInterface.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
AA71970B287A44D200623C15 /* PlayLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = AA719702287A44D200623C15 /* PlayLoader.m */; };
AA71970C287A44D200623C15 /* PlayScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA719703287A44D200623C15 /* PlayScreen.swift */; };
AA71970D287A44D200623C15 /* PlaySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA719704287A44D200623C15 /* PlaySettings.swift */; };
AA71970E287A44D200623C15 /* PlayLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = AA719705287A44D200623C15 /* PlayLoader.h */; };
AA71970F287A44D200623C15 /* PlayCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA719706287A44D200623C15 /* PlayCover.swift */; };
Expand Down Expand Up @@ -45,6 +45,7 @@
AA719870287A81A000623C15 /* UIEvent+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = AA719842287A81A000623C15 /* UIEvent+Private.h */; };
AA719872287A81A000623C15 /* UITouch+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = AA719844287A81A000623C15 /* UITouch+Private.h */; };
AA818CB9287ABFB1000BEE9D /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA818CB8287ABFB1000BEE9D /* IOKit.framework */; };
ABCECEE629750BA600746595 /* PlayedApple.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCECEE529750BA600746595 /* PlayedApple.swift */; };
B127172228817AB90025112B /* SwordRPC in Frameworks */ = {isa = PBXBuildFile; productRef = B127172128817AB90025112B /* SwordRPC */; };
B127172528817C040025112B /* DiscordIPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B127172428817C040025112B /* DiscordIPC.swift */; };
B1271729288284BE0025112B /* DiscordActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1271728288284BE0025112B /* DiscordActivity.swift */; };
Expand All @@ -66,14 +67,14 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
2847AE47298EFC0F00B0F983 /* PlayScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayScreen.swift; sourceTree = "<group>"; };
6E76639628D0FA6200DE4AF9 /* AKInterface-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AKInterface-Bridging-Header.h"; sourceTree = "<group>"; };
6E76639A28D0FAE700DE4AF9 /* Plugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Plugin.swift; sourceTree = "<group>"; };
6E7663A028D0FB5300DE4AF9 /* AKPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AKPlugin.swift; sourceTree = "<group>"; };
6E7663A428D0FEBE00DE4AF9 /* AKPluginLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AKPluginLoader.swift; sourceTree = "<group>"; };
6E84A14C28D0F96D00BF7495 /* AKInterface.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AKInterface.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
AA7196D8287A447700623C15 /* PlayTools.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PlayTools.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AA719702287A44D200623C15 /* PlayLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlayLoader.m; sourceTree = "<group>"; };
AA719703287A44D200623C15 /* PlayScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayScreen.swift; sourceTree = "<group>"; };
AA719704287A44D200623C15 /* PlaySettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaySettings.swift; sourceTree = "<group>"; };
AA719705287A44D200623C15 /* PlayLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlayLoader.h; sourceTree = "<group>"; };
AA719706287A44D200623C15 /* PlayCover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayCover.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -107,6 +108,7 @@
AA719844287A81A000623C15 /* UITouch+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UITouch+Private.h"; sourceTree = "<group>"; };
AA818CB8287ABFB1000BEE9D /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/System/Library/Frameworks/IOKit.framework; sourceTree = DEVELOPER_DIR; };
AA818CBA287ABFD5000BEE9D /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/System/iOSSupport/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; };
ABCECEE529750BA600746595 /* PlayedApple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayedApple.swift; sourceTree = "<group>"; };
B127172428817C040025112B /* DiscordIPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscordIPC.swift; sourceTree = "<group>"; };
B1271728288284BE0025112B /* DiscordActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscordActivity.swift; sourceTree = "<group>"; };
B1E8CF8928BBE2AB004340D3 /* Keymapping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keymapping.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -165,18 +167,19 @@
AA7196DA287A447700623C15 /* PlayTools */ = {
isa = PBXGroup;
children = (
ABCECEE729750BB100746595 /* MysticRunes */,
B127172328817AC70025112B /* DiscordActivity */,
AA719721287A480C00623C15 /* Controls */,
AA719799287A481500623C15 /* Keymap */,
AA71978C287A481500623C15 /* Menu */,
AA719791287A481500623C15 /* Utils */,
AA71970A287A44D200623C15 /* Info.plist */,
AA719706287A44D200623C15 /* PlayCover.swift */,
AA719703287A44D200623C15 /* PlayScreen.swift */,
AA719704287A44D200623C15 /* PlaySettings.swift */,
AA719705287A44D200623C15 /* PlayLoader.h */,
AA719702287A44D200623C15 /* PlayLoader.m */,
AA719708287A44D200623C15 /* PlayTools.h */,
2847AE47298EFC0F00B0F983 /* PlayScreen.swift */,
);
path = PlayTools;
sourceTree = "<group>";
Expand Down Expand Up @@ -262,6 +265,14 @@
name = Frameworks;
sourceTree = "<group>";
};
ABCECEE729750BB100746595 /* MysticRunes */ = {
isa = PBXGroup;
children = (
ABCECEE529750BA600746595 /* PlayedApple.swift */,
);
path = MysticRunes;
sourceTree = "<group>";
};
B127172328817AC70025112B /* DiscordActivity */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -434,7 +445,6 @@
AA71970D287A44D200623C15 /* PlaySettings.swift in Sources */,
AA719759287A480D00623C15 /* PlayAction.swift in Sources */,
6E7663A528D0FEBE00DE4AF9 /* AKPluginLoader.swift in Sources */,
2847AE48298EFC0F00B0F983 /* PlayScreen.swift in Sources */,
AA7197A2287A481500623C15 /* CircleMenu.swift in Sources */,
AA71978B287A480D00623C15 /* MenuController.swift in Sources */,
AA7197AA287A481500623C15 /* Toast.swift in Sources */,
Expand All @@ -447,6 +457,8 @@
AA7197A3287A481500623C15 /* CircleMenuButton.swift in Sources */,
AA7197A9287A481500623C15 /* PlayInfo.swift in Sources */,
AA71986A287A81A000623C15 /* PTFakeMetaTouch.m in Sources */,
AA71970C287A44D200623C15 /* PlayScreen.swift in Sources */,
ABCECEE629750BA600746595 /* PlayedApple.swift in Sources */,
AA71986C287A81A000623C15 /* NSObject+Swizzle.m in Sources */,
AA71970F287A44D200623C15 /* PlayCover.swift in Sources */,
B127172528817C040025112B /* DiscordIPC.swift in Sources */,
Expand Down
167 changes: 167 additions & 0 deletions PlayTools/MysticRunes/PlayedApple.swift
@@ -0,0 +1,167 @@
//
// PlayWeRuinedIt.swift
// PlayTools
//
// Created by Venti on 16/01/2023.
//

import Foundation
import Security

// Implementation for PlayKeychain
// World's Most Advanced Keychain Replacement Solution:tm:
// This is a joke, don't take it seriously

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 keychainPath(_ attributes: NSDictionary) -> URL {
let keychainFolder = getKeychainDirectory()
// Generate a key path based on the key attributes
let accountName = attributes[kSecAttrAccount as String] as? String ?? ""
let serviceName = attributes[kSecAttrService as String] as? String ?? ""
let classType = attributes[kSecClass as String] as? String ?? ""
return keychainFolder!
.appendingPathComponent("\(serviceName)-\(accountName)-\(classType).plist")
}

@objc public static func debugLogger(_ logContent: String) {
if PlaySettings.shared.settingsData.playChainDebugging {
NSLog("PC-DEBUG: \(logContent)")
}
}
// Emulates SecItemAdd, SecItemUpdate, SecItemDelete and SecItemCopyMatching
// Store the entire dictionary as a plist
// SecItemAdd(CFDictionaryRef attributes, CFTypeRef *result)
@objc static public func add(_ attributes: NSDictionary, result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus {
let keychainPath = keychainPath(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 {
debugLogger("Failed to write keychain file")
return errSecIO
}
// Place v_data in the result
if let v_data = attributes["v_data"] {
result?.pointee = v_data as CFTypeRef
}
return errSecSuccess
}

// SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate)
@objc static public func update(_ query: NSDictionary, attributesToUpdate: NSDictionary) -> OSStatus {
// Get the path to the keychain file
let keychainPath = keychainPath(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)")
return errSecItemNotFound
}
// Reconstruct the dictionary (subscripting won't work as assignment is not allowed)
let newKeychainDict = NSMutableDictionary()
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")
return errSecIO
}

return errSecSuccess
}

// SecItemDelete(CFDictionaryRef query)
@objc static public func delete(_ query: NSDictionary) -> OSStatus {
// Get the path to the keychain file
let keychainPath = keychainPath(query)
// Delete the keychain file
do {
try FileManager.default.removeItem(at: keychainPath)
debugLogger("Deleted keychain file at \(keychainPath)")
} catch {
debugLogger("Failed to delete keychain file")
return errSecIO
}
return errSecSuccess
}

// SecItemCopyMatching(CFDictionaryRef query, CFTypeRef *result)
@objc static public func copyMatching(_ query: NSDictionary, result: UnsafeMutablePointer<CFTypeRef?>?)
-> OSStatus {
// Get the path to the keychain file
let keychainPath = keychainPath(query)
// Read the dictionary from the keychain file
let keychainDict = NSDictionary(contentsOf: keychainPath)
// Check the `r_Attributes` key. If it is set to 1 in the query
// DROP, NOT IMPLEMENTED
let classType = query[kSecClass as String] as? String ?? ""
if query["r_Attributes"] as? Int == 1 {
return errSecItemNotFound
}
// If the keychain file doesn't exist, return errSecItemNotFound
if keychainDict == nil {
debugLogger("Keychain file not found at \(keychainPath)")
return errSecItemNotFound
}
// Return v_Data if it exists
if let vData = keychainDict!["v_Data"] {
debugLogger("Read keychain file from \(keychainPath)")
// 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
]
let keyData = vData as! Data // swiftlint:disable:this force_cast
let key = SecKeyCreateWithData(keyData as CFData, keyAttributes as CFDictionary, nil)
result?.pointee = key
return errSecSuccess
}
result?.pointee = vData as CFTypeRef
return errSecSuccess
}

return errSecItemNotFound
}
}
64 changes: 64 additions & 0 deletions PlayTools/PlayLoader.m
Expand Up @@ -92,6 +92,70 @@ static int pt_sysctlbyname(const char *name, void *oldp, size_t *oldlenp, void *
DYLD_INTERPOSE(pt_sysctlbyname, sysctlbyname)
DYLD_INTERPOSE(pt_sysctl, sysctl)

// Interpose Apple Keychain functions (SecItemCopyMatching, SecItemAdd, SecItemUpdate, SecItemDelete)
// This allows us to intercept keychain requests and return our own data

// Use the implementations from PlayKeychain
static OSStatus pt_SecItemCopyMatching(CFDictionaryRef query, CFTypeRef *result) {
OSStatus retval;
if ([[PlaySettings shared] playChain]) {
retval = [PlayKeychain copyMatching:(__bridge NSDictionary * _Nonnull)(query) result:result];
} else {
retval = SecItemCopyMatching(query, result);
}
if (result != NULL) {
[PlayKeychain debugLogger:[NSString stringWithFormat:@"SecItemCopyMatching: %@", query]];
[PlayKeychain debugLogger:[NSString stringWithFormat:@"SecItemCopyMatching result: %@", *result]];
}
return retval;
}

static OSStatus pt_SecItemAdd(CFDictionaryRef attributes, CFTypeRef *result) {
OSStatus retval;
if ([[PlaySettings shared] playChain]) {
retval = [PlayKeychain add:(__bridge NSDictionary * _Nonnull)(attributes) result:result];
} else {
retval = SecItemAdd(attributes, result);
}
if (result != NULL) {
[PlayKeychain debugLogger: [NSString stringWithFormat:@"SecItemAdd: %@", attributes]];
[PlayKeychain debugLogger: [NSString stringWithFormat:@"SecItemAdd result: %@", *result]];
}
return retval;
}

static OSStatus pt_SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate) {
OSStatus retval;
if ([[PlaySettings shared] playChain]) {
retval = [PlayKeychain update:(__bridge NSDictionary * _Nonnull)(query) attributesToUpdate:(__bridge NSDictionary * _Nonnull)(attributesToUpdate)];
} else {
retval = SecItemUpdate(query, attributesToUpdate);
}
if (attributesToUpdate != NULL) {
[PlayKeychain debugLogger: [NSString stringWithFormat:@"SecItemUpdate: %@", query]];
[PlayKeychain debugLogger: [NSString stringWithFormat:@"SecItemUpdate attributesToUpdate: %@", attributesToUpdate]];
}
return retval;

}

static OSStatus pt_SecItemDelete(CFDictionaryRef query) {
OSStatus retval;
if ([[PlaySettings shared] playChain]) {
retval = [PlayKeychain delete:(__bridge NSDictionary * _Nonnull)(query)];
} else {
retval = SecItemDelete(query);
}
[PlayKeychain debugLogger: [NSString stringWithFormat:@"SecItemDelete: %@", query]];
return retval;
}

DYLD_INTERPOSE(pt_SecItemCopyMatching, SecItemCopyMatching)
DYLD_INTERPOSE(pt_SecItemAdd, SecItemAdd)
DYLD_INTERPOSE(pt_SecItemUpdate, SecItemUpdate)
DYLD_INTERPOSE(pt_SecItemDelete, SecItemDelete)


@implementation PlayLoader

static void __attribute__((constructor)) initialize(void) {
Expand Down
13 changes: 8 additions & 5 deletions PlayTools/PlaySettings.swift
Expand Up @@ -33,11 +33,9 @@ let settings = PlaySettings.shared

lazy var sensitivity = settingsData.sensitivity / 100

@objc lazy var windowSizeHeight = CGFloat(settingsData.windowHeight)
lazy var windowSizeHeight = CGFloat(settingsData.windowHeight)

@objc lazy var windowSizeWidth = CGFloat(settingsData.windowWidth)

@objc lazy var inverseScreenValues = settingsData.inverseScreenValues
lazy var windowSizeWidth = CGFloat(settingsData.windowWidth)

@objc lazy var adaptiveDisplay = settingsData.resolution == 0 ? false : true

Expand All @@ -61,6 +59,10 @@ let settings = PlaySettings.shared
return "J320xAP"
}
}()

@objc lazy var playChain = settingsData.playChain

@objc lazy var playChainDebugging = settingsData.playChainDebugging
}

struct AppSettingsData: Codable {
Expand All @@ -78,5 +80,6 @@ struct AppSettingsData: Codable {
var bypass = false
var discordActivity = DiscordActivity()
var version = "2.0.0"
var inverseScreenValues = false
var playChain = false
var playChainDebugging = false
}

0 comments on commit 10e48d5

Please sign in to comment.