Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PlayChain keychain emulation support #65

Merged
merged 14 commits into from Jan 21, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions PlayTools.xcodeproj/project.pbxproj
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 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,6 +167,7 @@
AA7196DA287A447700623C15 /* PlayTools */ = {
isa = PBXGroup;
children = (
ABCECEE729750BB100746595 /* MysticRunes */,
B127172328817AC70025112B /* DiscordActivity */,
AA719721287A480C00623C15 /* Controls */,
AA719799287A481500623C15 /* Keymap */,
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 @@ -447,6 +458,7 @@
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
@@ -0,0 +1,32 @@
{
ohaiibuzzle marked this conversation as resolved.
Show resolved Hide resolved
"pins" : [
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "919eb1d83e02121cdb434c7bfc1f0c66ef17febe",
"version" : "1.0.2"
}
},
{
"identity" : "swift-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio",
"state" : {
"revision" : "b4e0a274f7f34210e97e2f2c50ab02a10b549250",
"version" : "2.41.1"
}
},
{
"identity" : "swordrpc",
"kind" : "remoteSourceControl",
"location" : "https://github.com/khoralee/swordRPC",
"state" : {
"branch" : "main",
"revision" : "68e86ed7ffcf6e39bb5f52e692cab3c96f9d9f7c"
}
}
],
"version" : 2
}
247 changes: 247 additions & 0 deletions PlayTools/MysticRunes/PlayedApple.swift
@@ -0,0 +1,247 @@
//
// 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? {
// // Get the keychain folder
// let keychainFolder = FileManager.default.urls(for: .documentDirectory,
// in: .userDomainMask)
// .first?.appendingPathComponent("Keychain")
//
let bundleID = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String ?? ""
let keychainFolder = URL(fileURLWithPath: "/Users/\(NSUserName())/Library/Containers/io.playcover.PlayCover")
JoseMoreville marked this conversation as resolved.
Show resolved Hide resolved
.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 {
if PlaySettings.shared.settingsData.playChainDebugging {
NSLog("BUZZINC: Failed to create keychain folder")
}
ohaiibuzzle marked this conversation as resolved.
Show resolved Hide resolved
}
}

return keychainFolder
}

// 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 {
// Get the keychain folder
let keychainFolder = getKeychainDirectory()

// Get the account name (optional, may not exist, non-fatal)
// In cases where the account name doesn't exist, ignore it
let accountName = attributes[kSecAttrAccount as String] as? String ?? ""

// Get the service name (optional, may not exist, non-fatal)
// In cases where the service name doesn't exist, ignore it
let serviceName = attributes[kSecAttrService as String] as? String ?? ""

// Get the class
let classType = attributes[kSecClass as String] as? String ?? ""

// Get the path to the keychain file
let keychainPath = keychainFolder!
.appendingPathComponent("\(serviceName)-\(accountName)-\(classType).plist")

// Check if the keychain file already exists
if FileManager.default.fileExists(atPath: keychainPath.path) {
if PlaySettings.shared.settingsData.playChainDebugging {
NSLog("BUZZINC: Keychain file already exists")
}
return errSecDuplicateItem
}

// Write the dictionary to the keychain file
do {
try attributes.write(to: keychainPath)
if PlaySettings.shared.settingsData.playChainDebugging {
NSLog("BUZZINC: Wrote keychain file to \(keychainPath)")
}
} catch {
if PlaySettings.shared.settingsData.playChainDebugging {
NSLog("BUZZINC: 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 keychain folder
let keychainFolder = getKeychainDirectory()

// Get the account name (optional, may not exist, non-fatal)
// In cases where the account name doesn't exist, ignore it
let accountName = query[kSecAttrAccount as String] as? String ?? ""

// Get the service name (optional, may not exist, non-fatal)
// In cases where the service name doesn't exist, ignore it
let serviceName = query[kSecAttrService as String] as? String ?? ""

// Get the class
let classType = query[kSecClass as String] as? String ?? ""

// Get the path to the keychain file
let keychainPath = keychainFolder!
.appendingPathComponent("\(serviceName)-\(accountName)-\(classType).plist")

// Read the dictionary from the keychain file
let keychainDict = NSDictionary(contentsOf: keychainPath)
if PlaySettings.shared.settingsData.playChainDebugging {
NSLog("BUZZINC: Read keychain file from \(keychainPath)")
}

// 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
ohaiibuzzle marked this conversation as resolved.
Show resolved Hide resolved
}

// 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)
if PlaySettings.shared.settingsData.playChainDebugging {
NSLog("BUZZINC: Wrote keychain file to \(keychainPath)")
}
} catch {
if PlaySettings.shared.settingsData.playChainDebugging {
NSLog("BUZZINC: Failed to write keychain file")
}
return errSecIO
}

return errSecSuccess
}

// SecItemDelete(CFDictionaryRef query)
@objc static public func delete(_ query: NSDictionary) -> OSStatus {
// Get the keychain folder
let keychainFolder = getKeychainDirectory()

// Get the account name (optional, may not exist, non-fatal)
// In cases where the account name doesn't exist, ignore it
let accountName = query[kSecAttrAccount as String] as? String ?? ""

// Get the service name (optional, may not exist, non-fatal)
// In cases where the service name doesn't exist, ignore it
let serviceName = query[kSecAttrService as String] as? String ?? ""

// Get the class
let classType = query[kSecClass as String] as? String ?? ""

// Get the path to the keychain file
let keychainPath = keychainFolder!
.appendingPathComponent("\(serviceName)-\(accountName)-\(classType).plist")

// Delete the keychain file
do {
try FileManager.default.removeItem(at: keychainPath)
if PlaySettings.shared.settingsData.playChainDebugging {
NSLog("BUZZINC: Deleted keychain file at \(keychainPath)")
}
} catch {
if PlaySettings.shared.settingsData.playChainDebugging {
NSLog("BUZZINC: 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 keychain folder
let keychainFolder = getKeychainDirectory()

// Get the account name (optional, may not exist, non-fatal)
// In cases where the account name doesn't exist, ignore it
let accountName = query[kSecAttrAccount as String] as? String ?? ""

// Get the service name (optional, may not exist, non-fatal)
// In cases where the service name doesn't exist, ignore it
let serviceName = query[kSecAttrService as String] as? String ?? ""

// Get the class
let classType = query[kSecClass as String] as? String ?? ""

// Get the path to the keychain file
let keychainPath = keychainFolder!
.appendingPathComponent("\(serviceName)-\(accountName)-\(classType).plist")

// 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
if query["r_Attributes"] as? Int == 1 {
return errSecItemNotFound
}

// If the keychain file doesn't exist, return errSecItemNotFound
if keychainDict == nil {
if PlaySettings.shared.settingsData.playChainDebugging {
NSLog("BUZZINC: Keychain file not found at \(keychainPath)")
}
return errSecItemNotFound
}

// Return v_Data if it exists
if let vData = keychainDict!["v_Data"] {
if PlaySettings.shared.settingsData.playChainDebugging {
NSLog("BUZZINC: 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 errSecSuccess
}
}
60 changes: 60 additions & 0 deletions PlayTools/PlayLoader.m
Expand Up @@ -91,6 +91,66 @@ 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) {
if ([[PlaySettings shared] playChain]) {
OSStatus retval = [PlayKeychain copyMatching:(__bridge NSDictionary * _Nonnull)(query) result:result];
if (result != NULL && [[PlaySettings shared] playChainDebugging]) {
NSLog(@"SecItemCopyMatching: %@", query);
NSLog(@"SecItemCopyMatching result: %@", *result);
}
return retval;
} else {
return SecItemCopyMatching(query, result);
}
}

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

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

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

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