Implement initial iOS application structure for DES Cracker

- Added AppDelegate and main entry point for the application.
- Created Info.plist for application configuration.
- Introduced UI components with PSCSlaveViewController for user interaction.
- Implemented networking and computation engines (CPU and Metal) for DES cracking.
- Established asset catalog for application resources including icons and colors.
- Updated results.json to reflect new elapsed time for brute force operations.
This commit is contained in:
Tom Butcher 2026-09-19 17:02:33 +01:00
parent 8f2d3a2489
commit bb690f3a1f
25 changed files with 2747 additions and 62 deletions

View File

@ -1,5 +1,5 @@
{
"elapsed_sec": 61.97,
"elapsed_sec": 46.262,
"pad_matches": [],
"confirmed": []
}

View File

@ -0,0 +1,431 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
A40000000000000000000001 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000001 /* main.m */; };
A40000000000000000000002 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000003 /* AppDelegate.m */; };
A40000000000000000000003 /* PSCSlaveViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000007 /* PSCSlaveViewController.m */; };
A40000000000000000000004 /* PSCProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000009 /* PSCProtocol.m */; };
A40000000000000000000005 /* PSCSlaveClient.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000011 /* PSCSlaveClient.m */; };
A40000000000000000000006 /* PSCMetalEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000014 /* PSCMetalEngine.m */; };
A40000000000000000000007 /* PSCCpuEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000016 /* PSCCpuEngine.m */; };
A40000000000000000000008 /* des.c in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000017 /* des.c */; };
A40000000000000000000009 /* des_bruteforce.metal in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000019 /* des_bruteforce.metal */; };
A40000000000000000000010 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000005 /* Assets.xcassets */; };
A40000000000000000000011 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A20000000000000000000020 /* Metal.framework */; };
A40000000000000000000012 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A20000000000000000000021 /* QuartzCore.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
A10000000000000000000003 /* DES Cracker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DES Cracker.app"; sourceTree = BUILT_PRODUCTS_DIR; };
A20000000000000000000001 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
A20000000000000000000002 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
A20000000000000000000003 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
A20000000000000000000004 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A20000000000000000000005 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A20000000000000000000006 /* PSCSlaveViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCSlaveViewController.h; sourceTree = "<group>"; };
A20000000000000000000007 /* PSCSlaveViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCSlaveViewController.m; sourceTree = "<group>"; };
A20000000000000000000008 /* PSCProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCProtocol.h; sourceTree = "<group>"; };
A20000000000000000000009 /* PSCProtocol.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCProtocol.m; sourceTree = "<group>"; };
A20000000000000000000010 /* PSCSlaveClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCSlaveClient.h; sourceTree = "<group>"; };
A20000000000000000000011 /* PSCSlaveClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCSlaveClient.m; sourceTree = "<group>"; };
A20000000000000000000012 /* PSCBruteEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCBruteEngine.h; sourceTree = "<group>"; };
A20000000000000000000013 /* PSCMetalEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCMetalEngine.h; sourceTree = "<group>"; };
A20000000000000000000014 /* PSCMetalEngine.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCMetalEngine.m; sourceTree = "<group>"; };
A20000000000000000000015 /* PSCCpuEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCCpuEngine.h; sourceTree = "<group>"; };
A20000000000000000000016 /* PSCCpuEngine.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCCpuEngine.m; sourceTree = "<group>"; };
A20000000000000000000017 /* des.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = des.c; path = ../native/common/des.c; sourceTree = SOURCE_ROOT; };
A20000000000000000000018 /* des.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = des.h; path = ../native/common/des.h; sourceTree = SOURCE_ROOT; };
A20000000000000000000019 /* des_bruteforce.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; name = des_bruteforce.metal; path = ../native/metal/des_bruteforce.metal; sourceTree = SOURCE_ROOT; };
A20000000000000000000020 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
A20000000000000000000021 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
A10000000000000000000005 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A40000000000000000000011 /* Metal.framework in Frameworks */,
A40000000000000000000012 /* QuartzCore.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
A30000000000000000000001 /* DESCracker */ = {
isa = PBXGroup;
children = (
A20000000000000000000001 /* main.m */,
A20000000000000000000002 /* AppDelegate.h */,
A20000000000000000000003 /* AppDelegate.m */,
A20000000000000000000004 /* Info.plist */,
A20000000000000000000005 /* Assets.xcassets */,
A30000000000000000000002 /* UI */,
A30000000000000000000003 /* Network */,
A30000000000000000000004 /* Compute */,
A30000000000000000000005 /* Native */,
);
path = DESCracker;
sourceTree = "<group>";
};
A30000000000000000000002 /* UI */ = {
isa = PBXGroup;
children = (
A20000000000000000000006 /* PSCSlaveViewController.h */,
A20000000000000000000007 /* PSCSlaveViewController.m */,
);
path = UI;
sourceTree = "<group>";
};
A30000000000000000000003 /* Network */ = {
isa = PBXGroup;
children = (
A20000000000000000000008 /* PSCProtocol.h */,
A20000000000000000000009 /* PSCProtocol.m */,
A20000000000000000000010 /* PSCSlaveClient.h */,
A20000000000000000000011 /* PSCSlaveClient.m */,
);
path = Network;
sourceTree = "<group>";
};
A30000000000000000000004 /* Compute */ = {
isa = PBXGroup;
children = (
A20000000000000000000012 /* PSCBruteEngine.h */,
A20000000000000000000013 /* PSCMetalEngine.h */,
A20000000000000000000014 /* PSCMetalEngine.m */,
A20000000000000000000015 /* PSCCpuEngine.h */,
A20000000000000000000016 /* PSCCpuEngine.m */,
);
path = Compute;
sourceTree = "<group>";
};
A30000000000000000000005 /* Native */ = {
isa = PBXGroup;
children = (
A20000000000000000000017 /* des.c */,
A20000000000000000000018 /* des.h */,
A20000000000000000000019 /* des_bruteforce.metal */,
);
name = Native;
sourceTree = "<group>";
};
A30000000000000000000006 /* Products */ = {
isa = PBXGroup;
children = (
A10000000000000000000003 /* DES Cracker.app */,
);
name = Products;
sourceTree = "<group>";
};
A30000000000000000000007 /* Frameworks */ = {
isa = PBXGroup;
children = (
A20000000000000000000020 /* Metal.framework */,
A20000000000000000000021 /* QuartzCore.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
A30000000000000000000000 /* Root */ = {
isa = PBXGroup;
children = (
A30000000000000000000001 /* DESCracker */,
A30000000000000000000007 /* Frameworks */,
A30000000000000000000006 /* Products */,
);
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
A10000000000000000000002 /* DESCracker */ = {
isa = PBXNativeTarget;
buildConfigurationList = A50000000000000000000004 /* Build configuration list for PBXNativeTarget "DESCracker" */;
buildPhases = (
A10000000000000000000004 /* Sources */,
A10000000000000000000005 /* Frameworks */,
A10000000000000000000006 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = DESCracker;
productName = "DES Cracker";
productReference = A10000000000000000000003 /* DES Cracker.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
A10000000000000000000001 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastUpgradeCheck = 1600;
TargetAttributes = {
A10000000000000000000002 = {
CreatedOnToolsVersion = 16.0;
};
};
};
buildConfigurationList = A50000000000000000000001 /* Build configuration list for PBXProject "DESCracker" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = A30000000000000000000000 /* Root */;
productRefGroup = A30000000000000000000006 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
A10000000000000000000002 /* DESCracker */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
A10000000000000000000006 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A40000000000000000000010 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
A10000000000000000000004 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A40000000000000000000001 /* main.m in Sources */,
A40000000000000000000002 /* AppDelegate.m in Sources */,
A40000000000000000000003 /* PSCSlaveViewController.m in Sources */,
A40000000000000000000004 /* PSCProtocol.m in Sources */,
A40000000000000000000005 /* PSCSlaveClient.m in Sources */,
A40000000000000000000006 /* PSCMetalEngine.m in Sources */,
A40000000000000000000007 /* PSCCpuEngine.m in Sources */,
A40000000000000000000008 /* des.c in Sources */,
A40000000000000000000009 /* des_bruteforce.metal in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
A50000000000000000000002 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
};
name = Debug;
};
A50000000000000000000003 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
A50000000000000000000005 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = NO;
GENERATE_INFOPLIST_FILE = NO;
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(SRCROOT)/DESCracker",
"$(SRCROOT)/DESCracker/UI",
"$(SRCROOT)/DESCracker/Network",
"$(SRCROOT)/DESCracker/Compute",
"$(SRCROOT)/../native/common",
);
INFOPLIST_FILE = DESCracker/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "DES Cracker";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.pectusscan.DESCracker;
PRODUCT_NAME = "DES Cracker";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = NO;
TARGETED_DEVICE_FAMILY = "1,2";
USER_HEADER_SEARCH_PATHS = (
"$(SRCROOT)/DESCracker/UI",
"$(SRCROOT)/DESCracker/Network",
"$(SRCROOT)/DESCracker/Compute",
"$(SRCROOT)/../native/common",
);
};
name = Debug;
};
A50000000000000000000006 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = NO;
GENERATE_INFOPLIST_FILE = NO;
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(SRCROOT)/DESCracker",
"$(SRCROOT)/DESCracker/UI",
"$(SRCROOT)/DESCracker/Network",
"$(SRCROOT)/DESCracker/Compute",
"$(SRCROOT)/../native/common",
);
INFOPLIST_FILE = DESCracker/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "DES Cracker";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.pectusscan.DESCracker;
PRODUCT_NAME = "DES Cracker";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = NO;
TARGETED_DEVICE_FAMILY = "1,2";
USER_HEADER_SEARCH_PATHS = (
"$(SRCROOT)/DESCracker/UI",
"$(SRCROOT)/DESCracker/Network",
"$(SRCROOT)/DESCracker/Compute",
"$(SRCROOT)/../native/common",
);
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
A50000000000000000000001 /* Build configuration list for PBXProject "DESCracker" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A50000000000000000000002 /* Debug */,
A50000000000000000000003 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
A50000000000000000000004 /* Build configuration list for PBXNativeTarget "DESCracker" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A50000000000000000000005 /* Debug */,
A50000000000000000000006 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = A10000000000000000000001 /* Project object */;
}

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A10000000000000000000002"
BuildableName = "DES Cracker.app"
BlueprintName = "DESCracker"
ReferencedContainer = "container:DESCracker.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A10000000000000000000002"
BuildableName = "DES Cracker.app"
BlueprintName = "DESCracker"
ReferencedContainer = "container:DESCracker.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A10000000000000000000002"
BuildableName = "DES Cracker.app"
BlueprintName = "DESCracker"
ReferencedContainer = "container:DESCracker.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,5 @@
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (nonatomic, strong) UIWindow *window;
@end

View File

@ -0,0 +1,14 @@
#import "AppDelegate.h"
#import "PSCSlaveViewController.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
self.window.backgroundColor = [UIColor colorWithRed:0x10 / 255.0 green:0x15 / 255.0 blue:0x1c / 255.0 alpha:1];
self.window.rootViewController = [[PSCSlaveViewController alloc] init];
[self.window makeKeyAndVisible];
return YES;
}
@end

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.831",
"green" : "0.918",
"red" : "0.369"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "AppIcon.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.110",
"green" : "0.082",
"red" : "0.063"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
#import <Foundation/Foundation.h>
@class PSCAssignBlock;
NS_ASSUME_NONNULL_BEGIN
typedef void (^PSCBruteProgressBlock)(uint64_t done, uint64_t count, double rate);
typedef void (^PSCBruteHitBlock)(uint64_t index, uint64_t key, uint64_t plain);
typedef void (^PSCBruteFinishBlock)(NSInteger hits, NSTimeInterval elapsed, BOOL cancelled, NSError *_Nullable error);
@protocol PSCBruteEngine <NSObject>
- (void)runAssign:(PSCAssignBlock *)block
onProgress:(nullable PSCBruteProgressBlock)onProgress
onHit:(nullable PSCBruteHitBlock)onHit
completion:(nullable PSCBruteFinishBlock)completion;
- (void)cancel;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,7 @@
#import <Foundation/Foundation.h>
#import "PSCBruteEngine.h"
@interface PSCCpuEngine : NSObject <PSCBruteEngine>
@property (nonatomic, assign, readonly) NSUInteger coreCount;
- (BOOL)prepare:(NSError **)error;
@end

View File

@ -0,0 +1,155 @@
#import "PSCCpuEngine.h"
#import "PSCProtocol.h"
#import "des.h"
#import <stdatomic.h>
#import <string.h>
#import <QuartzCore/QuartzCore.h>
@interface PSCCpuEngine () {
atomic_bool _cancelled;
atomic_ullong _generation;
}
@property (nonatomic, strong) dispatch_queue_t workQueue;
@end
@implementation PSCCpuEngine
- (instancetype)init {
self = [super init];
if (self) {
NSUInteger n = NSProcessInfo.processInfo.processorCount;
_coreCount = n > 0 ? n : 1;
_workQueue = dispatch_queue_create("com.descracker.cpu", DISPATCH_QUEUE_SERIAL);
atomic_init(&_cancelled, false);
atomic_init(&_generation, 0);
}
return self;
}
- (BOOL)prepare:(NSError **)error {
if (!des_selftest()) {
if (error) {
*error = [NSError errorWithDomain:@"DESCracker" code:6 userInfo:@{
NSLocalizedDescriptionKey : @"CPU DES self-test failed"
}];
}
return NO;
}
return YES;
}
- (void)cancel {
atomic_store(&_cancelled, true);
atomic_fetch_add(&_generation, 1);
}
- (void)runAssign:(PSCAssignBlock *)block
onProgress:(PSCBruteProgressBlock)onProgress
onHit:(PSCBruteHitBlock)onHit
completion:(PSCBruteFinishBlock)completion {
dispatch_async(self.workQueue, ^{
atomic_store(&self->_cancelled, false);
uint64_t generation = atomic_fetch_add(&self->_generation, 1) + 1;
NSTimeInterval t0 = CACurrentMediaTime();
uint32_t key_len = block.keyLen;
NSData *charsetData = [block.charset dataUsingEncoding:NSUTF8StringEncoding] ?: [NSData data];
const uint8_t *cs = charsetData.bytes;
uint32_t clen = (uint32_t)charsetData.length;
uint32_t pad_byte = block.padByte;
uint64_t target = block.target;
uint64_t start = block.start;
uint64_t count = block.count;
NSUInteger workers = block.cpuWorkers ? (NSUInteger)MAX(1, block.cpuWorkers.integerValue) : self.coreCount;
if (workers == 0) {
workers = 1;
}
NSMutableData *fillData = [NSMutableData dataWithLength:sizeof(uint64_t) * 8];
uint64_t *fills = fillData.mutableBytes;
uint32_t nfills = 0;
for (NSNumber *item in block.fills) {
if (nfills >= 8) {
break;
}
fills[nfills++] = item.unsignedLongLongValue;
}
__block atomic_ullong done;
atomic_init(&done, 0);
__block NSInteger hits = 0;
__block atomic_bool cancelledFlag;
atomic_init(&cancelledFlag, false);
NSLock *hitLock = [[NSLock alloc] init];
dispatch_queue_t pool = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
dispatch_group_t group = dispatch_group_create();
uint64_t chunk = count / workers;
uint64_t rem = count % workers;
uint64_t cursor = start;
for (NSUInteger w = 0; w < workers; w++) {
uint64_t n = chunk + (w < rem ? 1 : 0);
if (n == 0) {
continue;
}
uint64_t wstart = cursor;
uint64_t wcount = n;
cursor += n;
dispatch_group_async(group, pool, ^{
uint64_t local = 0;
for (uint64_t i = 0; i < wcount; i++) {
if ((local & 0x3FFF) == 0 &&
(atomic_load(&self->_cancelled) || atomic_load(&self->_generation) != generation)) {
atomic_store(&cancelledFlag, true);
break;
}
uint64_t index = wstart + i;
uint64_t key = make_key(index, key_len, clen, pad_byte, cs);
uint64_t sk[16];
des_key_schedule(key, sk);
uint64_t pt = des_crypt(target, sk, 1);
if (is_fill(pt, fills, nfills)) {
[hitLock lock];
hits += 1;
[hitLock unlock];
if (onHit) {
onHit(index, key, pt);
}
}
local++;
if ((local & 0x3FFF) == 0) {
uint64_t soFar = atomic_fetch_add(&done, 0x4000) + 0x4000;
double elapsed = CACurrentMediaTime() - t0;
if (elapsed < 1e-6) {
elapsed = 1e-6;
}
if (onProgress) {
onProgress(soFar > count ? count : soFar, count, soFar / elapsed);
}
local = 0;
}
}
if (local) {
atomic_fetch_add(&done, local);
}
});
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
BOOL cancelled = atomic_load(&cancelledFlag) || atomic_load(&self->_cancelled) ||
atomic_load(&self->_generation) != generation;
if (!cancelled && onProgress) {
double elapsed = CACurrentMediaTime() - t0;
if (elapsed < 1e-6) {
elapsed = 1e-6;
}
onProgress(count, count, count / elapsed);
}
if (completion) {
completion(hits, CACurrentMediaTime() - t0, cancelled, nil);
}
});
}
@end

View File

@ -0,0 +1,9 @@
#import <Foundation/Foundation.h>
#import "PSCBruteEngine.h"
@interface PSCMetalEngine : NSObject <PSCBruteEngine>
@property (nonatomic, readonly) BOOL available;
@property (nonatomic, copy, readonly) NSString *gpuName;
@property (nonatomic, assign, readonly) uint64_t memoryBytes;
- (BOOL)prepare:(NSError **)error;
@end

View File

@ -0,0 +1,266 @@
#import "PSCMetalEngine.h"
#import "PSCProtocol.h"
#import <Metal/Metal.h>
#import <stdatomic.h>
#import <string.h>
#import <QuartzCore/QuartzCore.h>
enum { kPSCMaxHits = 256, kPSCThreadsPerGroup = 256, kPSCParamsSize = 104, kPSCDefaultBatch = 1048576 };
typedef struct {
uint64_t index;
uint64_t key;
uint64_t plain;
} PSCHit;
@interface PSCMetalEngine () {
atomic_bool _cancelled;
}
@property (nonatomic, strong) id<MTLDevice> device;
@property (nonatomic, strong) id<MTLCommandQueue> queue;
@property (nonatomic, strong) id<MTLComputePipelineState> brutePipeline;
@property (nonatomic, strong) dispatch_queue_t workQueue;
@property (nonatomic, copy) NSString *activeJobId;
@end
@implementation PSCMetalEngine
- (instancetype)init {
self = [super init];
if (self) {
_device = MTLCreateSystemDefaultDevice();
_workQueue = dispatch_queue_create("com.descracker.metal", DISPATCH_QUEUE_SERIAL);
atomic_init(&_cancelled, false);
if (_device) {
_gpuName = [_device.name copy] ?: @"Metal GPU";
if ([_device respondsToSelector:@selector(recommendedMaxWorkingSetSize)]) {
_memoryBytes = (uint64_t)_device.recommendedMaxWorkingSetSize;
}
} else {
_gpuName = @"Metal GPU";
}
}
return self;
}
- (BOOL)available {
return self.device != nil && self.brutePipeline != nil;
}
static void PSCWriteParams(void *dst, uint64_t start, uint32_t key_len, uint32_t charset_len, uint32_t pad_byte,
uint32_t fill_count, uint32_t batch_count, uint64_t target, const uint64_t *fills,
int nfills) {
uint8_t *p = dst;
memcpy(p, &start, 8);
p += 8;
memcpy(p, &key_len, 4);
p += 4;
memcpy(p, &charset_len, 4);
p += 4;
memcpy(p, &pad_byte, 4);
p += 4;
memcpy(p, &fill_count, 4);
p += 4;
memcpy(p, &batch_count, 4);
p += 4;
uint32_t z = 0;
memcpy(p, &z, 4);
p += 4;
memcpy(p, &target, 8);
p += 8;
for (int i = 0; i < 8; i++) {
uint64_t f = i < nfills ? fills[i] : 0;
memcpy(p, &f, 8);
p += 8;
}
}
- (uint32_t)currentBatchSize {
NSProcessInfoThermalState thermal = NSProcessInfo.processInfo.thermalState;
if (thermal >= NSProcessInfoThermalStateCritical) {
return 262144;
}
if (thermal >= NSProcessInfoThermalStateSerious) {
return 524288;
}
return kPSCDefaultBatch;
}
- (BOOL)prepare:(NSError **)error {
if (!self.device) {
if (error) {
*error = [NSError errorWithDomain:@"DESCracker" code:1 userInfo:@{
NSLocalizedDescriptionKey : @"No Metal GPU"
}];
}
return NO;
}
NSError *err = nil;
id<MTLLibrary> lib = [self.device newDefaultLibrary];
if (!lib) {
if (error) {
*error = [NSError errorWithDomain:@"DESCracker" code:2 userInfo:@{
NSLocalizedDescriptionKey : @"Failed to load Metal library"
}];
}
return NO;
}
id<MTLFunction> known = [lib newFunctionWithName:@"des_known_answer"];
id<MTLFunction> brute = [lib newFunctionWithName:@"des_brute"];
if (!known || !brute) {
if (error) {
*error = [NSError errorWithDomain:@"DESCracker" code:3 userInfo:@{
NSLocalizedDescriptionKey : @"Metal kernels missing (des_known_answer / des_brute)"
}];
}
return NO;
}
id<MTLComputePipelineState> knownPipe = [self.device newComputePipelineStateWithFunction:known error:&err];
if (!knownPipe) {
if (error) {
*error = err;
}
return NO;
}
self.brutePipeline = [self.device newComputePipelineStateWithFunction:brute error:&err];
if (!self.brutePipeline) {
if (error) {
*error = err;
}
return NO;
}
self.queue = [self.device newCommandQueue];
id<MTLBuffer> out = [self.device newBufferWithLength:16 options:MTLResourceStorageModeShared];
id<MTLCommandBuffer> cmd = [self.queue commandBuffer];
id<MTLComputeCommandEncoder> enc = [cmd computeCommandEncoder];
[enc setComputePipelineState:knownPipe];
[enc setBuffer:out offset:0 atIndex:0];
[enc dispatchThreads:MTLSizeMake(1, 1, 1) threadsPerThreadgroup:MTLSizeMake(1, 1, 1)];
[enc endEncoding];
[cmd commit];
[cmd waitUntilCompleted];
uint64_t *words = out.contents;
uint64_t expected = 0x85E813540F0AB405ULL;
uint64_t plain = 0x0123456789ABCDEFULL;
if (words[0] != expected || words[1] != plain) {
if (error) {
*error = [NSError errorWithDomain:@"DESCracker" code:4 userInfo:@{
NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Metal DES self-test failed ct=%016llx",
(unsigned long long)words[0]]
}];
}
self.brutePipeline = nil;
return NO;
}
return YES;
}
- (void)cancel {
atomic_store(&_cancelled, true);
self.activeJobId = nil;
}
- (void)runAssign:(PSCAssignBlock *)block
onProgress:(PSCBruteProgressBlock)onProgress
onHit:(PSCBruteHitBlock)onHit
completion:(PSCBruteFinishBlock)completion {
dispatch_async(self.workQueue, ^{
atomic_store(&self->_cancelled, false);
self.activeJobId = block.jobId;
NSString *jobId = [block.jobId copy];
NSTimeInterval t0 = CACurrentMediaTime();
NSInteger hits = 0;
NSError *runError = nil;
if (!self.available) {
if (completion) {
completion(0, 0, NO, [NSError errorWithDomain:@"DESCracker" code:5 userInfo:@{
NSLocalizedDescriptionKey : @"Metal engine is not ready"
}]);
}
return;
}
const char *cs = block.charset.UTF8String ?: "";
uint32_t charset_len = (uint32_t)strlen(cs);
uint64_t fills[8] = {0};
int nfills = 0;
for (NSNumber *item in block.fills) {
if (nfills >= 8) {
break;
}
fills[nfills++] = item.unsignedLongLongValue;
}
id<MTLBuffer> paramsBuf = [self.device newBufferWithLength:kPSCParamsSize options:MTLResourceStorageModeShared];
id<MTLBuffer> charsetBuf = [self.device newBufferWithBytes:cs length:MAX(charset_len, 1)
options:MTLResourceStorageModeShared];
id<MTLBuffer> hitsBuf = [self.device newBufferWithLength:kPSCMaxHits * sizeof(PSCHit)
options:MTLResourceStorageModeShared];
id<MTLBuffer> countBuf = [self.device newBufferWithLength:4 options:MTLResourceStorageModeShared];
uint64_t done = 0;
uint64_t count = block.count;
BOOL cancelled = NO;
while (done < count) {
if (atomic_load(&self->_cancelled) || ![self.activeJobId isEqualToString:jobId]) {
cancelled = YES;
break;
}
uint32_t batch = [self currentBatchSize];
uint64_t remaining = count - done;
uint64_t n = batch < remaining ? batch : remaining;
*(uint32_t *)countBuf.contents = 0;
PSCWriteParams(paramsBuf.contents, block.start + done, block.keyLen, charset_len, block.padByte,
(uint32_t)nfills, (uint32_t)n, block.target, fills, nfills);
id<MTLCommandBuffer> cmd = [self.queue commandBuffer];
id<MTLComputeCommandEncoder> enc = [cmd computeCommandEncoder];
[enc setComputePipelineState:self.brutePipeline];
[enc setBuffer:paramsBuf offset:0 atIndex:0];
[enc setBuffer:charsetBuf offset:0 atIndex:1];
[enc setBuffer:hitsBuf offset:0 atIndex:2];
[enc setBuffer:countBuf offset:0 atIndex:3];
NSUInteger groups = (NSUInteger)((n + kPSCThreadsPerGroup - 1) / kPSCThreadsPerGroup);
[enc dispatchThreadgroups:MTLSizeMake(groups, 1, 1)
threadsPerThreadgroup:MTLSizeMake(kPSCThreadsPerGroup, 1, 1)];
[enc endEncoding];
[cmd commit];
[cmd waitUntilCompleted];
if (cmd.error) {
runError = cmd.error;
break;
}
uint32_t hitn = *(uint32_t *)countBuf.contents;
if (hitn > kPSCMaxHits) {
hitn = kPSCMaxHits;
}
PSCHit *outHits = hitsBuf.contents;
for (uint32_t i = 0; i < hitn; i++) {
hits += 1;
if (onHit) {
onHit(outHits[i].index, outHits[i].key, outHits[i].plain);
}
}
done += n;
double elapsed = CACurrentMediaTime() - t0;
if (elapsed < 1e-6) {
elapsed = 1e-6;
}
if (onProgress) {
onProgress(done, count, done / elapsed);
}
}
if ([self.activeJobId isEqualToString:jobId]) {
self.activeJobId = nil;
}
NSTimeInterval elapsed = CACurrentMediaTime() - t0;
if (completion) {
completion(hits, elapsed, cancelled, runError);
}
});
}
@end

57
ios/DESCracker/Info.plist Normal file
View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>DES Cracker</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>DES Cracker</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>DES Cracker connects to the desktop master on your local network to receive search work.</string>
<key>UILaunchScreen</key>
<dict>
<key>UIColorName</key>
<string>LaunchBackground</string>
</dict>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
</dict>
</plist>

View File

@ -0,0 +1,57 @@
#import <Foundation/Foundation.h>
#include <stdint.h>
NS_ASSUME_NONNULL_BEGIN
static const NSInteger kPSCProtocolVersion = 2;
static const NSInteger kPSCDefaultPort = 9876;
static const NSTimeInterval kPSCProgressInterval = 0.5;
static const NSTimeInterval kPSCPingInterval = 5.0;
NSString *PSCFormatDec64(uint64_t value);
NSString *PSCFormatHex64(uint64_t value);
NSString *PSCFormatCount(uint64_t value);
NSString *PSCFormatRate(double rate);
uint64_t PSCParseDec64(id value);
uint64_t PSCParseHex64(NSString *hex);
@interface PSCComputeDevice : NSObject
@property (nonatomic, copy) NSString *key;
@property (nonatomic, assign) NSInteger deviceId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *type;
@property (nonatomic, strong, nullable) NSNumber *cores;
@property (nonatomic, strong, nullable) NSNumber *memory;
- (NSDictionary *)JSONObject;
@end
@interface PSCAssignBlock : NSObject
@property (nonatomic, copy) NSString *jobId;
@property (nonatomic, copy) NSString *deviceKey;
@property (nonatomic, assign) uint64_t start;
@property (nonatomic, assign) uint64_t count;
@property (nonatomic, assign) uint32_t keyLen;
@property (nonatomic, copy) NSString *charset;
@property (nonatomic, assign) uint32_t padByte;
@property (nonatomic, assign) uint64_t target;
@property (nonatomic, copy) NSArray<NSNumber *> *fills;
@property (nonatomic, strong, nullable) NSNumber *cpuWorkers;
+ (nullable instancetype)fromJSON:(NSDictionary *)json;
@end
@interface PSCWorkerSnapshot : NSObject
@property (nonatomic, copy) NSString *deviceKey;
@property (nonatomic, copy) NSString *deviceName;
@property (nonatomic, copy) NSString *deviceType;
@property (nonatomic, copy) NSString *current;
@property (nonatomic, assign) double pct;
@property (nonatomic, assign) NSInteger completedBlocks;
@property (nonatomic, assign) double rate;
@property (nonatomic, assign) BOOL busy;
@end
NSDictionary *_Nullable PSCDecodeMessage(NSString *raw);
NSString *_Nullable PSCEncodeMessage(NSDictionary *msg);
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,146 @@
#import "PSCProtocol.h"
#import <math.h>
#import <stdlib.h>
NSString *PSCFormatDec64(uint64_t value) {
return [NSString stringWithFormat:@"%llu", (unsigned long long)value];
}
NSString *PSCFormatHex64(uint64_t value) {
return [NSString stringWithFormat:@"%016llx", (unsigned long long)value];
}
NSString *PSCFormatCount(uint64_t value) {
double n = (double)value;
if (n >= 1e9) {
return [NSString stringWithFormat:@"%.1fB", n / 1e9];
}
if (n >= 1e6) {
return [NSString stringWithFormat:@"%.1fM", n / 1e6];
}
if (n >= 1e3) {
return [NSString stringWithFormat:@"%.1fk", n / 1e3];
}
return [NSString stringWithFormat:@"%.0f", n];
}
NSString *PSCFormatRate(double rate) {
if (rate <= 0 || !isfinite(rate)) {
return @"—";
}
return [NSString stringWithFormat:@"%@/s", PSCFormatCount((uint64_t)llround(rate))];
}
uint64_t PSCParseDec64(id value) {
if ([value isKindOfClass:[NSString class]]) {
return strtoull([(NSString *)value UTF8String], NULL, 10);
}
if ([value isKindOfClass:[NSNumber class]]) {
return [(NSNumber *)value unsignedLongLongValue];
}
return 0;
}
uint64_t PSCParseHex64(NSString *hex) {
if (hex.length == 0) {
return 0;
}
const char *p = hex.UTF8String;
if (p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) {
p += 2;
}
return strtoull(p, NULL, 16);
}
@implementation PSCComputeDevice
- (NSDictionary *)JSONObject {
NSMutableDictionary *json = [@{
@"key" : self.key ?: @"",
@"id" : @(self.deviceId),
@"name" : self.name ?: @"",
@"type" : self.type ?: @"cpu",
} mutableCopy];
if (self.cores) {
json[@"cores"] = self.cores;
}
if (self.memory) {
json[@"memory"] = self.memory;
}
return json;
}
@end
@implementation PSCAssignBlock
+ (instancetype)fromJSON:(NSDictionary *)json {
if (![json isKindOfClass:[NSDictionary class]]) {
return nil;
}
PSCAssignBlock *block = [[PSCAssignBlock alloc] init];
block.jobId = [json[@"jobId"] isKindOfClass:[NSString class]] ? json[@"jobId"] : @"";
block.deviceKey = [json[@"deviceKey"] isKindOfClass:[NSString class]] ? json[@"deviceKey"] : @"";
block.start = PSCParseDec64(json[@"start"]);
block.count = PSCParseDec64(json[@"count"]);
block.keyLen = (uint32_t)PSCParseDec64(json[@"keyLen"]);
block.charset = [json[@"charset"] isKindOfClass:[NSString class]] ? json[@"charset"] : @"";
block.padByte = (uint32_t)(PSCParseDec64(json[@"padByte"]) & 0xFF);
id target = json[@"target"];
block.target = [target isKindOfClass:[NSString class]] ? PSCParseHex64(target) : PSCParseDec64(target);
NSMutableArray<NSNumber *> *fills = [NSMutableArray array];
id fillsVal = json[@"fills"];
if ([fillsVal isKindOfClass:[NSArray class]]) {
for (id item in (NSArray *)fillsVal) {
if (fills.count >= 8) {
break;
}
uint64_t f = [item isKindOfClass:[NSString class]] ? PSCParseHex64(item) : PSCParseDec64(item);
[fills addObject:@(f)];
}
}
block.fills = fills;
id workers = json[@"cpuWorkers"];
if ([workers isKindOfClass:[NSNumber class]]) {
block.cpuWorkers = workers;
}
if (block.jobId.length == 0 || block.count == 0 || block.keyLen == 0 || block.charset.length == 0) {
return nil;
}
return block;
}
@end
@implementation PSCWorkerSnapshot
@end
NSDictionary *PSCDecodeMessage(NSString *raw) {
if (raw.length == 0) {
return nil;
}
NSData *data = [raw dataUsingEncoding:NSUTF8StringEncoding];
if (!data) {
return nil;
}
id parsed = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (![parsed isKindOfClass:[NSDictionary class]]) {
return nil;
}
NSDictionary *msg = parsed;
if (![msg[@"type"] isKindOfClass:[NSString class]]) {
return nil;
}
return msg;
}
NSString *PSCEncodeMessage(NSDictionary *msg) {
if (!msg) {
return nil;
}
NSData *data = [NSJSONSerialization dataWithJSONObject:msg options:0 error:nil];
if (!data) {
return nil;
}
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}

View File

@ -0,0 +1,25 @@
#import <Foundation/Foundation.h>
#import "PSCProtocol.h"
@class PSCSlaveClient;
@protocol PSCSlaveClientDelegate <NSObject>
- (void)slaveClient:(PSCSlaveClient *)client didChangeStatus:(NSString *)status connected:(BOOL)connected;
- (void)slaveClient:(PSCSlaveClient *)client didLog:(NSString *)line;
- (void)slaveClient:(PSCSlaveClient *)client didUpdateWorkers:(NSArray<PSCWorkerSnapshot *> *)workers;
- (void)slaveClient:(PSCSlaveClient *)client didHitJob:(NSString *)jobId index:(NSString *)index keyHex:(NSString *)keyHex;
@end
@interface PSCSlaveClient : NSObject
@property (nonatomic, weak) id<PSCSlaveClientDelegate> delegate;
@property (nonatomic, readonly, getter=isConnected) BOOL connected;
@property (nonatomic, assign) NSUInteger cpuWorkers;
@property (nonatomic, readonly) BOOL metalAvailable;
@property (nonatomic, copy, readonly) NSString *metalName;
@property (nonatomic, assign, readonly) uint64_t metalMemory;
@property (nonatomic, assign, readonly) NSUInteger cpuCores;
- (BOOL)prepareEngines:(NSError **)error;
- (void)setDevices:(NSArray<PSCComputeDevice *> *)devices;
- (void)connectToHost:(NSString *)host port:(NSInteger)port;
- (void)disconnect;
@end

View File

@ -0,0 +1,604 @@
#import "PSCSlaveClient.h"
#import "PSCCpuEngine.h"
#import "PSCMetalEngine.h"
@interface PSCDeviceJob : NSObject
@property (nonatomic, copy) NSString *jobId;
@property (nonatomic, assign) uint64_t done;
@property (nonatomic, assign) uint64_t count;
@property (nonatomic, assign) double rate;
@property (nonatomic, copy) void (^kill)(void);
@end
@implementation PSCDeviceJob
@end
@interface PSCRateStats : NSObject
@property (nonatomic, assign) uint64_t keysDone;
@property (nonatomic, assign) NSTimeInterval activeMs;
@property (nonatomic, strong) NSDate *busyStarted;
@end
@implementation PSCRateStats
@end
@interface PSCProgressGate : NSObject
@property (nonatomic, assign) NSTimeInterval lastSent;
@property (nonatomic, assign) BOOL timerPending;
@property (nonatomic, copy) NSDictionary *payload;
@end
@implementation PSCProgressGate
@end
@interface PSCSlaveClient () <NSURLSessionWebSocketDelegate>
@property (nonatomic, copy) NSArray<PSCComputeDevice *> *devices;
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSURLSessionWebSocketTask *task;
@property (nonatomic, strong) NSTimer *pingTimer;
@property (nonatomic, strong) NSMutableDictionary<NSString *, PSCDeviceJob *> *jobs;
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *completedBlocks;
@property (nonatomic, strong) NSMutableDictionary<NSString *, PSCProgressGate *> *progressGates;
@property (nonatomic, strong) NSMutableDictionary<NSString *, PSCRateStats *> *rateStats;
@property (nonatomic, strong) PSCMetalEngine *metal;
@property (nonatomic, strong) PSCCpuEngine *cpu;
@property (nonatomic, strong) dispatch_queue_t syncQueue;
@property (nonatomic, copy) NSString *connectURL;
@end
@implementation PSCSlaveClient
- (instancetype)init {
self = [super init];
if (self) {
_devices = @[];
_jobs = [NSMutableDictionary dictionary];
_completedBlocks = [NSMutableDictionary dictionary];
_progressGates = [NSMutableDictionary dictionary];
_rateStats = [NSMutableDictionary dictionary];
_cpuWorkers = MAX(1, NSProcessInfo.processInfo.processorCount);
_metal = [[PSCMetalEngine alloc] init];
_cpu = [[PSCCpuEngine alloc] init];
_syncQueue = dispatch_queue_create("com.descracker.slave", DISPATCH_QUEUE_SERIAL);
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.waitsForConnectivity = NO;
_session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
}
return self;
}
- (BOOL)isConnected {
return self.task != nil && self.task.state == NSURLSessionTaskStateRunning;
}
- (BOOL)metalAvailable {
return self.metal.available;
}
- (NSString *)metalName {
return self.metal.gpuName;
}
- (uint64_t)metalMemory {
return self.metal.memoryBytes;
}
- (NSUInteger)cpuCores {
return self.cpu.coreCount;
}
- (BOOL)prepareEngines:(NSError **)error {
NSError *metalErr = nil;
BOOL metalOK = [self.metal prepare:&metalErr];
NSError *cpuErr = nil;
BOOL cpuOK = [self.cpu prepare:&cpuErr];
if (!metalOK && error) {
*error = metalErr;
}
if (!cpuOK) {
if (error && !metalErr) {
*error = cpuErr;
}
return NO;
}
return metalOK || cpuOK;
}
- (void)setDevices:(NSArray<PSCComputeDevice *> *)devices {
dispatch_async(self.syncQueue, ^{
self.devices = [devices copy] ?: @[];
for (PSCComputeDevice *device in self.devices) {
if (!self.completedBlocks[device.key]) {
self.completedBlocks[device.key] = @0;
}
}
[self emitWorkers];
});
}
- (void)connectToHost:(NSString *)host port:(NSInteger)port {
[self disconnect];
NSString *urlString = [NSString stringWithFormat:@"ws://%@:%ld", host, (long)port];
self.connectURL = urlString;
[self notifyStatus:[NSString stringWithFormat:@"Connecting to %@…", urlString] connected:NO];
NSURL *url = [NSURL URLWithString:urlString];
if (!url) {
[self log:[NSString stringWithFormat:@"Invalid URL %@", urlString]];
[self notifyStatus:@"Disconnected" connected:NO];
return;
}
self.task = [self.session webSocketTaskWithURL:url];
[self.task resume];
}
- (void)disconnect {
dispatch_sync(self.syncQueue, ^{
[self clearPing];
[self killAll];
});
NSURLSessionWebSocketTask *task = self.task;
self.task = nil;
if (task) {
[task cancelWithCloseCode:NSURLSessionWebSocketCloseCodeNormalClosure reason:nil];
}
dispatch_async(self.syncQueue, ^{
[self emitWorkers];
});
}
- (void)URLSession:(NSURLSession *)session
webSocketTask:(NSURLSessionWebSocketTask *)webSocketTask
didOpenWithProtocol:(NSString *)protocol {
(void)session;
(void)protocol;
if (webSocketTask != self.task) {
return;
}
dispatch_async(self.syncQueue, ^{
NSMutableArray *deviceJSON = [NSMutableArray array];
for (PSCComputeDevice *device in self.devices) {
[deviceJSON addObject:[device JSONObject]];
}
NSString *hostname = NSProcessInfo.processInfo.hostName ?: @"iOS";
[self send:@{
@"type" : @"hello",
@"hostname" : hostname,
@"platform" : @"ios arm64",
@"devices" : deviceJSON,
}];
NSString *exposed = self.devices.count == 0 ? @"no workers" : [[self.devices valueForKey:@"name"] componentsJoinedByString:@", "];
[self notifyStatus:[NSString stringWithFormat:@"Connected to %@", self.connectURL] connected:YES];
[self log:[NSString stringWithFormat:@"Hello sent (protocol %ld) — exposing %@", (long)kPSCProtocolVersion, exposed]];
[self emitWorkers];
dispatch_async(dispatch_get_main_queue(), ^{
[self.pingTimer invalidate];
__weak typeof(self) weakSelf = self;
self.pingTimer = [NSTimer scheduledTimerWithTimeInterval:kPSCPingInterval
repeats:YES
block:^(__unused NSTimer *timer) {
[weakSelf send:@{@"type" : @"ping"}];
}];
});
[self listen];
});
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
(void)session;
if (task != self.task && self.task != nil) {
return;
}
dispatch_async(self.syncQueue, ^{
[self clearPing];
[self killAll];
if (self.task == task) {
self.task = nil;
}
if (error && error.code != NSURLErrorCancelled) {
[self log:[NSString stringWithFormat:@"WebSocket error: %@", error.localizedDescription]];
}
[self notifyStatus:@"Disconnected" connected:NO];
[self emitWorkers];
});
}
- (void)listen {
NSURLSessionWebSocketTask *task = self.task;
if (!task) {
return;
}
__weak typeof(self) weakSelf = self;
[task receiveMessageWithCompletionHandler:^(NSURLSessionWebSocketMessage *message, NSError *error) {
PSCSlaveClient *strong = weakSelf;
if (!strong || task != strong.task) {
return;
}
if (error) {
return;
}
if (message.type == NSURLSessionWebSocketMessageTypeString && message.string) {
NSDictionary *msg = PSCDecodeMessage(message.string);
if (msg) {
[strong onServer:msg];
}
}
[strong listen];
}];
}
- (void)onServer:(NSDictionary *)msg {
NSString *type = msg[@"type"];
if ([type isEqualToString:@"hello_ack"]) {
[self log:[NSString stringWithFormat:@"Master ack v%@", msg[@"protocolVersion"] ?: @"?"]];
return;
}
if ([type isEqualToString:@"assign"]) {
PSCAssignBlock *block = [PSCAssignBlock fromJSON:msg];
if (!block) {
[self log:@"Ignored malformed assign"];
return;
}
[self runAssign:block];
return;
}
if ([type isEqualToString:@"cancel"]) {
dispatch_async(self.syncQueue, ^{
[self killAll];
[self log:@"Cancel received"];
[self notifyStatus:@"Idle (cancelled)" connected:YES];
[self emitWorkers];
});
return;
}
if ([type isEqualToString:@"shutdown"]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self disconnect];
});
}
}
- (id<PSCBruteEngine>)engineForDevice:(PSCComputeDevice *)device {
if ([device.type isEqualToString:@"metal"]) {
return self.metal;
}
return self.cpu;
}
- (PSCComputeDevice *)deviceForKey:(NSString *)key {
for (PSCComputeDevice *device in self.devices) {
if ([device.key isEqualToString:key]) {
return device;
}
}
for (PSCComputeDevice *device in self.devices) {
if ([device.type isEqualToString:@"metal"]) {
return device;
}
}
return self.devices.firstObject;
}
- (void)runAssign:(PSCAssignBlock *)block {
dispatch_async(self.syncQueue, ^{
PSCComputeDevice *matched = nil;
for (PSCComputeDevice *device in self.devices) {
if ([device.key isEqualToString:block.deviceKey]) {
matched = device;
break;
}
}
PSCComputeDevice *device = matched ?: [self deviceForKey:block.deviceKey];
if (!device) {
[self send:@{@"type" : @"error", @"jobId" : block.jobId, @"message" : @"no compute device selected"}];
return;
}
if (!matched) {
[self log:[NSString stringWithFormat:@"No device %@; using %@", block.deviceKey, device.name]];
}
PSCDeviceJob *existing = self.jobs[device.key];
if (existing.kill) {
existing.kill();
}
[self clearProgressGate:device.key];
[self log:[NSString stringWithFormat:@"Assigned %@ len=%u pad=0x%02x start=%llu count=%llu", device.name,
block.keyLen, block.padByte, (unsigned long long)block.start,
(unsigned long long)block.count]];
NSDate *started = [NSDate date];
__block NSInteger hitCount = 0;
id<PSCBruteEngine> engine = [self engineForDevice:device];
NSString *deviceKey = device.key;
NSString *jobId = block.jobId;
PSCDeviceJob *job = [[PSCDeviceJob alloc] init];
job.jobId = jobId;
job.done = 0;
job.count = block.count;
job.rate = 0;
__weak typeof(self) weakSelf = self;
job.kill = ^{
[engine cancel];
};
self.jobs[deviceKey] = job;
[self beginRate:deviceKey];
[self updateStatus];
[self emitWorkers];
PSCBruteProgressBlock onProgress = ^(uint64_t done, uint64_t count, double rate) {
dispatch_async(weakSelf.syncQueue, ^{
PSCDeviceJob *live = weakSelf.jobs[deviceKey];
if (live && [live.jobId isEqualToString:jobId]) {
live.done = done;
live.count = count;
live.rate = rate;
}
[weakSelf sendProgressJob:jobId deviceKey:deviceKey done:done count:count rate:rate];
[weakSelf updateStatus];
[weakSelf emitWorkers];
});
};
PSCBruteHitBlock onHit = ^(uint64_t index, uint64_t key, uint64_t plain) {
dispatch_async(weakSelf.syncQueue, ^{
hitCount += 1;
NSString *indexS = PSCFormatDec64(index);
NSString *keyHex = PSCFormatHex64(key);
NSString *plainHex = PSCFormatHex64(plain);
[weakSelf send:@{
@"type" : @"hit",
@"jobId" : jobId,
@"index" : indexS,
@"keyHex" : keyHex,
@"plainHex" : plainHex,
}];
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.delegate slaveClient:weakSelf didHitJob:jobId index:indexS keyHex:keyHex];
});
});
};
[engine runAssign:block
onProgress:onProgress
onHit:onHit
completion:^(NSInteger hits, NSTimeInterval elapsed, BOOL cancelled, NSError *error) {
(void)hits;
dispatch_async(weakSelf.syncQueue, ^{
PSCDeviceJob *still = weakSelf.jobs[deviceKey];
BOOL same = still && [still.jobId isEqualToString:jobId];
if (cancelled) {
if (same) {
[weakSelf endRate:deviceKey keys:still.done];
[weakSelf.jobs removeObjectForKey:deviceKey];
[weakSelf clearProgressGate:deviceKey];
}
[weakSelf updateStatus];
[weakSelf emitWorkers];
return;
}
if (error) {
if (same) {
[weakSelf endRate:deviceKey keys:still.done];
[weakSelf.jobs removeObjectForKey:deviceKey];
[weakSelf clearProgressGate:deviceKey];
}
[weakSelf send:@{
@"type" : @"error",
@"jobId" : jobId,
@"message" : error.localizedDescription ?: @"brute failed",
}];
[weakSelf log:error.localizedDescription];
[weakSelf updateStatus];
[weakSelf emitWorkers];
return;
}
if (same) {
[weakSelf.jobs removeObjectForKey:deviceKey];
[weakSelf endRate:deviceKey keys:block.count];
NSInteger completed = weakSelf.completedBlocks[deviceKey].integerValue + 1;
weakSelf.completedBlocks[deviceKey] = @(completed);
[weakSelf clearProgressGate:deviceKey];
[weakSelf send:@{
@"type" : @"block_complete",
@"jobId" : jobId,
@"deviceKey" : deviceKey,
@"hits" : @(hitCount),
@"elapsed" : @(elapsed > 0 ? elapsed : ([NSDate date].timeIntervalSince1970 - started.timeIntervalSince1970)),
}];
}
[weakSelf updateStatus];
[weakSelf emitWorkers];
});
}];
});
}
- (void)send:(NSDictionary *)msg {
NSString *json = PSCEncodeMessage(msg);
NSURLSessionWebSocketTask *task = self.task;
if (!json || !task || task.state != NSURLSessionTaskStateRunning) {
return;
}
NSURLSessionWebSocketMessage *message = [[NSURLSessionWebSocketMessage alloc] initWithString:json];
[task sendMessage:message completionHandler:^(NSError *error) {
if (error) {
[self log:[NSString stringWithFormat:@"Send failed: %@", error.localizedDescription]];
}
}];
}
- (PSCRateStats *)rateFor:(NSString *)deviceKey {
PSCRateStats *stats = self.rateStats[deviceKey];
if (!stats) {
stats = [[PSCRateStats alloc] init];
self.rateStats[deviceKey] = stats;
}
return stats;
}
- (void)beginRate:(NSString *)deviceKey {
PSCRateStats *stats = [self rateFor:deviceKey];
if (!stats.busyStarted) {
stats.busyStarted = [NSDate date];
}
}
- (void)endRate:(NSString *)deviceKey keys:(uint64_t)keys {
PSCRateStats *stats = [self rateFor:deviceKey];
if (stats.busyStarted) {
stats.activeMs += [[NSDate date] timeIntervalSinceDate:stats.busyStarted] * 1000.0;
stats.busyStarted = nil;
}
if (keys > 0) {
stats.keysDone += keys;
}
}
- (double)averageRate:(NSString *)deviceKey liveDone:(uint64_t)liveDone {
PSCRateStats *stats = [self rateFor:deviceKey];
NSTimeInterval liveMs = stats.busyStarted ? [[NSDate date] timeIntervalSinceDate:stats.busyStarted] * 1000.0 : 0;
double sec = (stats.activeMs + liveMs) / 1000.0;
uint64_t keys = stats.keysDone + liveDone;
if (sec <= 0 || keys == 0) {
return 0;
}
return keys / sec;
}
- (void)killAll {
NSArray<NSString *> *keys = self.jobs.allKeys;
for (NSString *deviceKey in keys) {
PSCDeviceJob *job = self.jobs[deviceKey];
[self endRate:deviceKey keys:job.done];
if (job.kill) {
job.kill();
}
[self clearProgressGate:deviceKey];
}
[self.jobs removeAllObjects];
}
- (void)clearPing {
dispatch_async(dispatch_get_main_queue(), ^{
[self.pingTimer invalidate];
self.pingTimer = nil;
});
}
- (PSCProgressGate *)progressGate:(NSString *)deviceKey {
PSCProgressGate *gate = self.progressGates[deviceKey];
if (!gate) {
gate = [[PSCProgressGate alloc] init];
self.progressGates[deviceKey] = gate;
}
return gate;
}
- (void)sendProgressJob:(NSString *)jobId
deviceKey:(NSString *)deviceKey
done:(uint64_t)done
count:(uint64_t)count
rate:(double)rate {
PSCProgressGate *gate = [self progressGate:deviceKey];
gate.payload = @{
@"type" : @"progress",
@"jobId" : jobId,
@"deviceKey" : deviceKey,
@"done" : PSCFormatDec64(done),
@"count" : PSCFormatDec64(count),
@"rate" : @(rate),
};
NSTimeInterval wait = kPSCProgressInterval - ([NSDate date].timeIntervalSince1970 - gate.lastSent);
if (wait <= 0) {
[self flushProgress:deviceKey];
return;
}
if (!gate.timerPending) {
gate.timerPending = YES;
NSString *key = [deviceKey copy];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(wait * NSEC_PER_SEC)), self.syncQueue, ^{
PSCProgressGate *live = self.progressGates[key];
live.timerPending = NO;
[self flushProgress:key];
});
}
}
- (void)flushProgress:(NSString *)deviceKey {
PSCProgressGate *gate = self.progressGates[deviceKey];
if (!gate.payload) {
return;
}
NSDictionary *payload = gate.payload;
PSCDeviceJob *live = self.jobs[deviceKey];
if (!live || ![live.jobId isEqualToString:payload[@"jobId"]]) {
gate.payload = nil;
return;
}
gate.payload = nil;
gate.lastSent = [NSDate date].timeIntervalSince1970;
[self send:payload];
}
- (void)clearProgressGate:(NSString *)deviceKey {
PSCProgressGate *gate = self.progressGates[deviceKey];
if (!gate) {
return;
}
gate.timerPending = NO;
gate.payload = nil;
}
- (void)emitWorkers {
NSMutableArray<PSCWorkerSnapshot *> *rows = [NSMutableArray array];
for (PSCComputeDevice *device in self.devices) {
PSCDeviceJob *live = self.jobs[device.key];
uint64_t done = live.done;
uint64_t count = live.count;
double pct = live && count > 0 ? (double)done * 100.0 / (double)count : 0;
PSCWorkerSnapshot *row = [[PSCWorkerSnapshot alloc] init];
row.deviceKey = device.key;
row.deviceName = device.name;
row.deviceType = device.type;
row.current = live ? [NSString stringWithFormat:@"%@ / %@", PSCFormatCount(done), PSCFormatCount(count)] : @"idle";
row.pct = pct;
row.completedBlocks = self.completedBlocks[device.key].integerValue;
row.rate = live.rate;
row.busy = live != nil;
[rows addObject:row];
}
NSArray *snapshot = [rows copy];
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate slaveClient:self didUpdateWorkers:snapshot];
});
}
- (void)updateStatus {
NSArray<PSCDeviceJob *> *busy = self.jobs.allValues;
BOOL connected = self.task != nil;
if (busy.count == 0) {
[self notifyStatus:connected ? @"Idle" : @"Disconnected" connected:connected];
return;
}
if (busy.count == 1) {
PSCDeviceJob *job = busy.firstObject;
NSString *shortId = job.jobId.length > 8 ? [job.jobId substringToIndex:8] : job.jobId;
[self notifyStatus:[NSString stringWithFormat:@"Working %@ %llu/%llu", shortId, (unsigned long long)job.done,
(unsigned long long)job.count]
connected:YES];
return;
}
[self notifyStatus:[NSString stringWithFormat:@"%lu workers assigned", (unsigned long)busy.count] connected:YES];
}
- (void)notifyStatus:(NSString *)status connected:(BOOL)connected {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate slaveClient:self didChangeStatus:status connected:connected];
});
}
- (void)log:(NSString *)line {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate slaveClient:self didLog:line];
});
}
@end

View File

@ -0,0 +1,5 @@
#import <UIKit/UIKit.h>
#import "PSCSlaveClient.h"
@interface PSCSlaveViewController : UIViewController
@end

View File

@ -0,0 +1,619 @@
#import "PSCSlaveViewController.h"
static NSString *const kPSCHostKey = @"psc.masterHost";
static NSString *const kPSCPortKey = @"psc.masterPort";
static NSString *const kPSCMetalKey = @"psc.metalOn";
static NSString *const kPSCCpuKey = @"psc.cpuOn";
static NSString *const kPSCWorkersKey = @"psc.cpuWorkers";
static UIColor *PSCColorBG(void) {
return [UIColor colorWithRed:0x10 / 255.0 green:0x15 / 255.0 blue:0x1c / 255.0 alpha:1];
}
static UIColor *PSCColorCard(void) {
return [UIColor colorWithRed:0x17 / 255.0 green:0x1e / 255.0 blue:0x28 / 255.0 alpha:1];
}
static UIColor *PSCColorBorder(void) {
return [UIColor colorWithRed:0x2b / 255.0 green:0x35 / 255.0 blue:0x44 / 255.0 alpha:1];
}
static UIColor *PSCColorText(void) {
return [UIColor colorWithRed:0xe8 / 255.0 green:0xee / 255.0 blue:0xf4 / 255.0 alpha:1];
}
static UIColor *PSCColorMuted(void) {
return [UIColor colorWithRed:0x8b / 255.0 green:0x9a / 255.0 blue:0xab / 255.0 alpha:1];
}
static UIColor *PSCColorTeal(void) {
return [UIColor colorWithRed:0x5e / 255.0 green:0xea / 255.0 blue:0xd4 / 255.0 alpha:1];
}
static UIColor *PSCColorField(void) {
return [UIColor colorWithRed:0x0c / 255.0 green:0x11 / 255.0 blue:0x18 / 255.0 alpha:1];
}
@interface PSCWorkCard : UIView
@property (nonatomic, strong) UILabel *nameLabel;
@property (nonatomic, strong) UILabel *metaLabel;
@property (nonatomic, strong) UIProgressView *progress;
@property (nonatomic, copy) NSString *deviceKey;
@end
@implementation PSCWorkCard
@end
@interface PSCSlaveViewController () <PSCSlaveClientDelegate, UITextFieldDelegate>
@property (nonatomic, strong) PSCSlaveClient *client;
@property (nonatomic, strong) UIScrollView *scroll;
@property (nonatomic, strong) UIStackView *stack;
@property (nonatomic, strong) UILabel *statusPill;
@property (nonatomic, strong) UITextField *hostField;
@property (nonatomic, strong) UITextField *portField;
@property (nonatomic, strong) UIButton *connectBtn;
@property (nonatomic, strong) UIButton *disconnectBtn;
@property (nonatomic, strong) UISwitch *metalSwitch;
@property (nonatomic, strong) UISwitch *cpuSwitch;
@property (nonatomic, strong) UILabel *metalNameLabel;
@property (nonatomic, strong) UILabel *cpuNameLabel;
@property (nonatomic, strong) UILabel *workersValue;
@property (nonatomic, strong) UIStepper *workersStepper;
@property (nonatomic, strong) UIStackView *workStack;
@property (nonatomic, strong) UITextView *logView;
@property (nonatomic, strong) NSMutableString *logText;
@property (nonatomic, assign) BOOL metalReady;
@end
@implementation PSCSlaveViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = PSCColorBG();
self.logText = [NSMutableString string];
self.client = [[PSCSlaveClient alloc] init];
self.client.delegate = self;
[self buildUI];
[self loadDefaults];
[self applyConnectionEnabled:YES];
NSError *err = nil;
BOOL ok = [self.client prepareEngines:&err];
self.metalReady = self.client.metalAvailable;
self.metalNameLabel.text = self.metalReady ? self.client.metalName : @"Metal GPU unavailable";
self.metalSwitch.enabled = self.metalReady;
if (!self.metalReady) {
self.metalSwitch.on = NO;
}
unsigned cores = (unsigned)self.client.cpuCores;
self.cpuNameLabel.text = [NSString stringWithFormat:@"CPU (%u cores)", cores];
self.workersStepper.minimumValue = 1;
self.workersStepper.maximumValue = MAX(1, cores);
if (self.workersStepper.value < 1 || self.workersStepper.value > cores) {
self.workersStepper.value = cores;
}
self.workersValue.text = [NSString stringWithFormat:@"%.0f", self.workersStepper.value];
self.client.cpuWorkers = (NSUInteger)self.workersStepper.value;
if (ok) {
[self appendLog:self.metalReady ? @"Metal and CPU self-tests passed." : @"CPU self-test passed. Metal unavailable."];
} else {
[self appendLog:[NSString stringWithFormat:@"Engine setup: %@", err.localizedDescription ?: @"failed"]];
}
[self rebuildDevices];
[self rebuildWorkCards:@[]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appDidBackground)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)appDidBackground {
if (self.client.isConnected) {
[self.client disconnect];
[self applyConnectionEnabled:YES];
[self appendLog:@"Disconnected (app backgrounded)."];
}
}
- (UILabel *)caption:(NSString *)text {
UILabel *label = [[UILabel alloc] init];
label.text = text.uppercaseString;
label.textColor = PSCColorMuted();
label.font = [UIFont systemFontOfSize:11 weight:UIFontWeightBold];
return label;
}
- (UIView *)cardWithTitle:(NSString *)title arranged:(NSArray<UIView *> *)views {
UIStackView *inner = [[UIStackView alloc] init];
inner.axis = UILayoutConstraintAxisVertical;
inner.spacing = 10;
UILabel *titleLabel = [[UILabel alloc] init];
titleLabel.text = title;
titleLabel.textColor = PSCColorText();
titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightSemibold];
[inner addArrangedSubview:titleLabel];
for (UIView *view in views) {
[inner addArrangedSubview:view];
}
UIView *card = [[UIView alloc] init];
card.backgroundColor = PSCColorCard();
card.layer.cornerRadius = 12;
card.layer.borderWidth = 1;
card.layer.borderColor = PSCColorBorder().CGColor;
inner.translatesAutoresizingMaskIntoConstraints = NO;
[card addSubview:inner];
[NSLayoutConstraint activateConstraints:@[
[inner.topAnchor constraintEqualToAnchor:card.topAnchor constant:14],
[inner.leadingAnchor constraintEqualToAnchor:card.leadingAnchor constant:14],
[inner.trailingAnchor constraintEqualToAnchor:card.trailingAnchor constant:-14],
[inner.bottomAnchor constraintEqualToAnchor:card.bottomAnchor constant:-14],
]];
return card;
}
- (UITextField *)fieldWithPlaceholder:(NSString *)placeholder keyboard:(UIKeyboardType)keyboard {
UITextField *field = [[UITextField alloc] init];
field.placeholder = placeholder;
field.textColor = PSCColorText();
field.keyboardType = keyboard;
field.autocapitalizationType = UITextAutocapitalizationTypeNone;
field.autocorrectionType = UITextAutocorrectionTypeNo;
field.spellCheckingType = UITextSpellCheckingTypeNo;
field.delegate = self;
field.returnKeyType = UIReturnKeyDone;
field.font = [UIFont monospacedSystemFontOfSize:15 weight:UIFontWeightRegular];
field.backgroundColor = PSCColorField();
field.layer.cornerRadius = 8;
field.layer.borderWidth = 1;
field.layer.borderColor = PSCColorBorder().CGColor;
field.leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 1)];
field.leftViewMode = UITextFieldViewModeAlways;
field.rightView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 1)];
field.rightViewMode = UITextFieldViewModeAlways;
field.attributedPlaceholder = [[NSAttributedString alloc]
initWithString:placeholder
attributes:@{NSForegroundColorAttributeName : PSCColorMuted()}];
[field.heightAnchor constraintEqualToConstant:40].active = YES;
return field;
}
- (UIButton *)buttonTitle:(NSString *)title primary:(BOOL)primary action:(SEL)action {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
[btn setTitle:title forState:UIControlStateNormal];
btn.titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightSemibold];
btn.layer.cornerRadius = 8;
[btn.heightAnchor constraintEqualToConstant:40].active = YES;
if (primary) {
btn.backgroundColor = PSCColorTeal();
[btn setTitleColor:PSCColorBG() forState:UIControlStateNormal];
} else {
btn.backgroundColor = PSCColorField();
btn.layer.borderWidth = 1;
btn.layer.borderColor = PSCColorBorder().CGColor;
[btn setTitleColor:PSCColorText() forState:UIControlStateNormal];
}
[btn addTarget:self action:action forControlEvents:UIControlEventTouchUpInside];
return btn;
}
- (UIView *)toggleRowSwitch:(UISwitch *)toggle name:(UILabel *)name {
UIStackView *row = [[UIStackView alloc] init];
row.axis = UILayoutConstraintAxisHorizontal;
row.alignment = UIStackViewAlignmentCenter;
row.spacing = 10;
name.textColor = PSCColorText();
name.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
name.numberOfLines = 2;
toggle.onTintColor = PSCColorTeal();
[row addArrangedSubview:name];
[row addArrangedSubview:toggle];
[name setContentHuggingPriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];
[toggle setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
return row;
}
- (void)buildUI {
self.scroll = [[UIScrollView alloc] init];
self.scroll.translatesAutoresizingMaskIntoConstraints = NO;
self.scroll.alwaysBounceVertical = YES;
self.scroll.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
[self.view addSubview:self.scroll];
self.stack = [[UIStackView alloc] init];
self.stack.axis = UILayoutConstraintAxisVertical;
self.stack.spacing = 16;
self.stack.translatesAutoresizingMaskIntoConstraints = NO;
[self.scroll addSubview:self.stack];
UILayoutGuide *safe = self.view.safeAreaLayoutGuide;
[NSLayoutConstraint activateConstraints:@[
[self.scroll.topAnchor constraintEqualToAnchor:safe.topAnchor],
[self.scroll.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.scroll.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.scroll.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[self.stack.topAnchor constraintEqualToAnchor:self.scroll.contentLayoutGuide.topAnchor constant:16],
[self.stack.leadingAnchor constraintEqualToAnchor:self.scroll.frameLayoutGuide.leadingAnchor constant:16],
[self.stack.trailingAnchor constraintEqualToAnchor:self.scroll.frameLayoutGuide.trailingAnchor constant:-16],
[self.stack.bottomAnchor constraintEqualToAnchor:self.scroll.contentLayoutGuide.bottomAnchor constant:-24],
]];
UIStackView *header = [[UIStackView alloc] init];
header.axis = UILayoutConstraintAxisHorizontal;
header.alignment = UIStackViewAlignmentCenter;
header.spacing = 12;
UIStackView *brand = [[UIStackView alloc] init];
brand.axis = UILayoutConstraintAxisVertical;
brand.spacing = 2;
UILabel *title = [[UILabel alloc] init];
title.text = @"DES Cracker";
title.textColor = PSCColorTeal();
title.font = [UIFont systemFontOfSize:22 weight:UIFontWeightBold];
UILabel *sub = [[UILabel alloc] init];
sub.text = @"LAN WORKER";
sub.textColor = PSCColorMuted();
sub.font = [UIFont systemFontOfSize:10 weight:UIFontWeightBold];
[brand addArrangedSubview:title];
[brand addArrangedSubview:sub];
self.statusPill = [[UILabel alloc] init];
self.statusPill.text = @"Offline";
self.statusPill.textAlignment = NSTextAlignmentCenter;
self.statusPill.textColor = PSCColorMuted();
self.statusPill.font = [UIFont systemFontOfSize:12 weight:UIFontWeightSemibold];
self.statusPill.backgroundColor = PSCColorField();
self.statusPill.layer.cornerRadius = 12;
self.statusPill.layer.masksToBounds = YES;
self.statusPill.layer.borderWidth = 1;
self.statusPill.layer.borderColor = PSCColorBorder().CGColor;
[self.statusPill.widthAnchor constraintGreaterThanOrEqualToConstant:96].active = YES;
[self.statusPill.heightAnchor constraintEqualToConstant:28].active = YES;
[header addArrangedSubview:brand];
[header addArrangedSubview:self.statusPill];
[brand setContentHuggingPriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];
self.hostField = [self fieldWithPlaceholder:@"192.168.1.10" keyboard:UIKeyboardTypeNumbersAndPunctuation];
self.portField = [self fieldWithPlaceholder:@"9876" keyboard:UIKeyboardTypeNumberPad];
[self.portField.widthAnchor constraintEqualToConstant:88].active = YES;
UIStackView *hostRow = [[UIStackView alloc] init];
hostRow.axis = UILayoutConstraintAxisHorizontal;
hostRow.spacing = 8;
UIStackView *hostCol = [[UIStackView alloc] init];
hostCol.axis = UILayoutConstraintAxisVertical;
hostCol.spacing = 4;
[hostCol addArrangedSubview:[self caption:@"Master host"]];
[hostCol addArrangedSubview:self.hostField];
UIStackView *portCol = [[UIStackView alloc] init];
portCol.axis = UILayoutConstraintAxisVertical;
portCol.spacing = 4;
[portCol addArrangedSubview:[self caption:@"Port"]];
[portCol addArrangedSubview:self.portField];
[hostRow addArrangedSubview:hostCol];
[hostRow addArrangedSubview:portCol];
self.connectBtn = [self buttonTitle:@"Connect" primary:YES action:@selector(connectTapped)];
self.disconnectBtn = [self buttonTitle:@"Disconnect" primary:NO action:@selector(disconnectTapped)];
UIStackView *btnRow = [[UIStackView alloc] init];
btnRow.axis = UILayoutConstraintAxisHorizontal;
btnRow.spacing = 8;
btnRow.distribution = UIStackViewDistributionFillEqually;
[btnRow addArrangedSubview:self.connectBtn];
[btnRow addArrangedSubview:self.disconnectBtn];
UIView *connectCard = [self cardWithTitle:@"Connection" arranged:@[ hostRow, btnRow ]];
self.metalSwitch = [[UISwitch alloc] init];
self.metalSwitch.on = YES;
[self.metalSwitch addTarget:self action:@selector(devicesChanged) forControlEvents:UIControlEventValueChanged];
self.metalNameLabel = [[UILabel alloc] init];
self.metalNameLabel.text = @"Metal GPU";
UIView *metalRow = [self toggleRowSwitch:self.metalSwitch name:self.metalNameLabel];
self.cpuSwitch = [[UISwitch alloc] init];
self.cpuSwitch.on = YES;
[self.cpuSwitch addTarget:self action:@selector(devicesChanged) forControlEvents:UIControlEventValueChanged];
self.cpuNameLabel = [[UILabel alloc] init];
self.cpuNameLabel.text = @"CPU";
UIView *cpuRow = [self toggleRowSwitch:self.cpuSwitch name:self.cpuNameLabel];
self.workersStepper = [[UIStepper alloc] init];
self.workersStepper.minimumValue = 1;
self.workersStepper.maximumValue = 16;
self.workersStepper.stepValue = 1;
self.workersStepper.value = 4;
[self.workersStepper addTarget:self action:@selector(workersChanged) forControlEvents:UIControlEventValueChanged];
self.workersValue = [[UILabel alloc] init];
self.workersValue.textColor = PSCColorText();
self.workersValue.font = [UIFont monospacedDigitSystemFontOfSize:14 weight:UIFontWeightMedium];
UIStackView *workerRow = [[UIStackView alloc] init];
workerRow.axis = UILayoutConstraintAxisHorizontal;
workerRow.alignment = UIStackViewAlignmentCenter;
workerRow.spacing = 8;
UILabel *workersCaption = [[UILabel alloc] init];
workersCaption.text = @"CPU workers";
workersCaption.textColor = PSCColorMuted();
workersCaption.font = [UIFont systemFontOfSize:13 weight:UIFontWeightMedium];
[workerRow addArrangedSubview:workersCaption];
[workerRow addArrangedSubview:[[UIView alloc] init]];
[workerRow addArrangedSubview:self.workersValue];
[workerRow addArrangedSubview:self.workersStepper];
UILabel *hint = [[UILabel alloc] init];
hint.text = @"Block size is set on the master. This device only exposes workers.";
hint.textColor = PSCColorMuted();
hint.font = [UIFont systemFontOfSize:12];
hint.numberOfLines = 0;
UIView *deviceCard = [self cardWithTitle:@"Devices" arranged:@[ metalRow, cpuRow, workerRow, hint ]];
self.workStack = [[UIStackView alloc] init];
self.workStack.axis = UILayoutConstraintAxisVertical;
self.workStack.spacing = 10;
UIView *workCard = [self cardWithTitle:@"Assigned work" arranged:@[ self.workStack ]];
self.logView = [[UITextView alloc] init];
self.logView.editable = NO;
self.logView.backgroundColor = PSCColorField();
self.logView.textColor = PSCColorText();
self.logView.font = [UIFont monospacedSystemFontOfSize:11 weight:UIFontWeightRegular];
self.logView.layer.cornerRadius = 8;
self.logView.layer.borderWidth = 1;
self.logView.layer.borderColor = PSCColorBorder().CGColor;
self.logView.textContainerInset = UIEdgeInsetsMake(8, 6, 8, 6);
[self.logView.heightAnchor constraintEqualToConstant:180].active = YES;
UIView *logCard = [self cardWithTitle:@"Log" arranged:@[ self.logView ]];
[self.stack addArrangedSubview:header];
[self.stack addArrangedSubview:connectCard];
[self.stack addArrangedSubview:deviceCard];
[self.stack addArrangedSubview:workCard];
[self.stack addArrangedSubview:logCard];
}
- (void)loadDefaults {
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
NSString *host = [defaults stringForKey:kPSCHostKey];
self.hostField.text = host.length ? host : @"";
NSInteger port = [defaults integerForKey:kPSCPortKey];
self.portField.text = port > 0 ? [NSString stringWithFormat:@"%ld", (long)port] : @"9876";
if ([defaults objectForKey:kPSCMetalKey] != nil) {
self.metalSwitch.on = [defaults boolForKey:kPSCMetalKey];
}
if ([defaults objectForKey:kPSCCpuKey] != nil) {
self.cpuSwitch.on = [defaults boolForKey:kPSCCpuKey];
}
NSInteger workers = [defaults integerForKey:kPSCWorkersKey];
if (workers > 0) {
self.workersStepper.value = workers;
}
}
- (void)saveDefaults {
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
[defaults setObject:self.hostField.text ?: @"" forKey:kPSCHostKey];
[defaults setInteger:self.portField.text.integerValue forKey:kPSCPortKey];
[defaults setBool:self.metalSwitch.on forKey:kPSCMetalKey];
[defaults setBool:self.cpuSwitch.on forKey:kPSCCpuKey];
[defaults setInteger:(NSInteger)self.workersStepper.value forKey:kPSCWorkersKey];
}
- (void)applyConnectionEnabled:(BOOL)canConnect {
self.connectBtn.enabled = canConnect;
self.connectBtn.alpha = canConnect ? 1 : 0.45;
self.disconnectBtn.enabled = !canConnect;
self.disconnectBtn.alpha = canConnect ? 0.45 : 1;
self.hostField.enabled = canConnect;
self.portField.enabled = canConnect;
}
- (NSArray<PSCComputeDevice *> *)selectedDevices {
NSMutableArray<PSCComputeDevice *> *devices = [NSMutableArray array];
if (self.metalSwitch.on && self.metalReady) {
PSCComputeDevice *gpu = [[PSCComputeDevice alloc] init];
gpu.key = @"metal:0";
gpu.deviceId = 0;
gpu.name = self.client.metalName ?: @"Metal GPU";
gpu.type = @"metal";
if (self.client.metalMemory > 0) {
gpu.memory = @(self.client.metalMemory);
}
[devices addObject:gpu];
}
if (self.cpuSwitch.on) {
PSCComputeDevice *cpu = [[PSCComputeDevice alloc] init];
cpu.key = @"cpu:0";
cpu.deviceId = 0;
cpu.name = [NSString stringWithFormat:@"CPU (%lu cores)", (unsigned long)self.client.cpuCores];
cpu.type = @"cpu";
cpu.cores = @(self.client.cpuCores);
[devices addObject:cpu];
}
return devices;
}
- (void)rebuildDevices {
self.client.cpuWorkers = (NSUInteger)self.workersStepper.value;
[self.client setDevices:[self selectedDevices]];
}
- (void)rebuildWorkCards:(NSArray<PSCWorkerSnapshot *> *)workers {
for (UIView *view in self.workStack.arrangedSubviews) {
[self.workStack removeArrangedSubview:view];
[view removeFromSuperview];
}
if (workers.count == 0) {
NSArray *selected = [self selectedDevices];
if (selected.count == 0) {
UILabel *empty = [[UILabel alloc] init];
empty.text = @"Enable Metal and/or CPU to expose workers.";
empty.textColor = PSCColorMuted();
empty.font = [UIFont systemFontOfSize:13];
empty.numberOfLines = 0;
[self.workStack addArrangedSubview:empty];
return;
}
NSMutableArray *idle = [NSMutableArray array];
for (PSCComputeDevice *device in selected) {
PSCWorkerSnapshot *row = [[PSCWorkerSnapshot alloc] init];
row.deviceKey = device.key;
row.deviceName = device.name;
row.deviceType = device.type;
row.current = @"idle";
[idle addObject:row];
}
workers = idle;
}
for (PSCWorkerSnapshot *row in workers) {
PSCWorkCard *card = [[PSCWorkCard alloc] init];
card.deviceKey = row.deviceKey;
UILabel *name = [[UILabel alloc] init];
name.text = row.deviceName;
name.textColor = PSCColorText();
name.font = [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold];
UILabel *meta = [[UILabel alloc] init];
meta.textColor = PSCColorMuted();
meta.font = [UIFont monospacedDigitSystemFontOfSize:12 weight:UIFontWeightRegular];
meta.numberOfLines = 2;
UIProgressView *progress = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
progress.progressTintColor = PSCColorTeal();
progress.trackTintColor = PSCColorBorder();
card.nameLabel = name;
card.metaLabel = meta;
card.progress = progress;
UIStackView *col = [[UIStackView alloc] initWithArrangedSubviews:@[ name, progress, meta ]];
col.axis = UILayoutConstraintAxisVertical;
col.spacing = 6;
col.translatesAutoresizingMaskIntoConstraints = NO;
[card addSubview:col];
[NSLayoutConstraint activateConstraints:@[
[col.topAnchor constraintEqualToAnchor:card.topAnchor],
[col.leadingAnchor constraintEqualToAnchor:card.leadingAnchor],
[col.trailingAnchor constraintEqualToAnchor:card.trailingAnchor],
[col.bottomAnchor constraintEqualToAnchor:card.bottomAnchor],
]];
[self.workStack addArrangedSubview:card];
[self applySnapshot:row toCard:card];
}
}
- (void)applySnapshot:(PSCWorkerSnapshot *)row toCard:(PSCWorkCard *)card {
card.nameLabel.text = row.deviceName;
card.progress.progress = (float)(row.pct / 100.0);
NSString *rate = row.busy ? PSCFormatRate(row.rate) : @"—";
card.metaLabel.text = [NSString stringWithFormat:@"%@ %@ %ld blocks", row.current, rate, (long)row.completedBlocks];
}
- (void)connectTapped {
[self.view endEditing:YES];
NSString *host = [self.hostField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSInteger port = self.portField.text.integerValue;
if (host.length == 0) {
[self appendLog:@"Enter the master’s IP address."];
return;
}
if (port <= 0 || port > 65535) {
port = kPSCDefaultPort;
self.portField.text = [NSString stringWithFormat:@"%ld", (long)port];
}
if ([self selectedDevices].count == 0) {
[self appendLog:@"Enable at least one device before connecting."];
return;
}
[self saveDefaults];
[self rebuildDevices];
[self applyConnectionEnabled:NO];
[self.client connectToHost:host port:port];
}
- (void)disconnectTapped {
[self.client disconnect];
[self applyConnectionEnabled:YES];
}
- (void)devicesChanged {
[self saveDefaults];
[self rebuildDevices];
}
- (void)workersChanged {
self.workersValue.text = [NSString stringWithFormat:@"%.0f", self.workersStepper.value];
[self saveDefaults];
[self rebuildDevices];
}
- (void)appendLog:(NSString *)line {
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"HH:mm:ss";
NSString *stamp = [fmt stringFromDate:[NSDate date]];
[self.logText appendFormat:@"[%@] %@\n", stamp, line];
self.logView.text = self.logText;
if (self.logView.text.length > 0) {
NSRange bottom = NSMakeRange(self.logView.text.length - 1, 1);
[self.logView scrollRangeToVisible:bottom];
}
}
- (void)setStatus:(NSString *)status connected:(BOOL)connected {
self.statusPill.text = [NSString stringWithFormat:@" %@ ", status];
if (connected) {
self.statusPill.textColor = PSCColorBG();
self.statusPill.backgroundColor = PSCColorTeal();
self.statusPill.layer.borderColor = PSCColorTeal().CGColor;
} else {
self.statusPill.textColor = PSCColorMuted();
self.statusPill.backgroundColor = PSCColorField();
self.statusPill.layer.borderColor = PSCColorBorder().CGColor;
}
if ([status isEqualToString:@"Disconnected"] || [status isEqualToString:@"Offline"]) {
[self applyConnectionEnabled:YES];
}
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder];
return YES;
}
#pragma mark - PSCSlaveClientDelegate
- (void)slaveClient:(PSCSlaveClient *)client didChangeStatus:(NSString *)status connected:(BOOL)connected {
(void)client;
[self setStatus:status connected:connected];
[UIApplication sharedApplication].idleTimerDisabled = connected;
}
- (void)slaveClient:(PSCSlaveClient *)client didLog:(NSString *)line {
(void)client;
[self appendLog:line];
}
- (void)slaveClient:(PSCSlaveClient *)client didUpdateWorkers:(NSArray<PSCWorkerSnapshot *> *)workers {
(void)client;
if (self.workStack.arrangedSubviews.count != workers.count) {
[self rebuildWorkCards:workers];
return;
}
for (NSUInteger i = 0; i < workers.count; i++) {
UIView *view = self.workStack.arrangedSubviews[i];
if (![view isKindOfClass:[PSCWorkCard class]]) {
[self rebuildWorkCards:workers];
return;
}
PSCWorkCard *card = (PSCWorkCard *)view;
PSCWorkerSnapshot *row = workers[i];
if (![card.deviceKey isEqualToString:row.deviceKey]) {
[self rebuildWorkCards:workers];
return;
}
[self applySnapshot:row toCard:card];
}
}
- (void)slaveClient:(PSCSlaveClient *)client didHitJob:(NSString *)jobId index:(NSString *)index keyHex:(NSString *)keyHex {
(void)client;
(void)jobId;
[self appendLog:[NSString stringWithFormat:@"HIT index=%@ key=%@", index, keyHex]];
}
@end

8
ios/DESCracker/main.m Normal file
View File

@ -0,0 +1,8 @@
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

View File

@ -4,9 +4,19 @@ import type { ComputeDevice } from "../search/nativeRunner";
import { preferredDevice, runBrute } from "../search/nativeRunner";
import { localWorkerId, type WorkerRow } from "../search/orchestrator";
import { formatNumber, formatRate } from "../search/planner";
import { decodeMsg, encodeMsg, type AssignBlock, type ServerMessage } from "./protocol";
import { decodeMsg, encodeMsg, type AssignBlock, type ClientMessage, type ServerMessage } from "./protocol";
import { WebSocket } from "ws";
const WS_PROGRESS_INTERVAL_MS = 500;
type ProgressPayload = Extract<ClientMessage, { type: "progress" }>;
interface ProgressGate {
lastSent: number;
timer: NodeJS.Timeout | null;
payload: ProgressPayload | null;
}
export interface SlaveEvents {
onLog?: (line: string) => void;
onStatus?: (text: string) => void;
@ -28,6 +38,7 @@ export class SlaveClient {
private ping: NodeJS.Timeout | null = null;
private jobs = new Map<string, DeviceJob>();
private completedBlocks = new Map<string, number>();
private progressGates = new Map<string, ProgressGate>();
private rateStats = new Map<string, { keysDone: bigint; activeMs: number; busyStarted: number | null }>();
private devices: ComputeDevice[] = [];
private cpuWorkers = Math.max(1, os.cpus().length);
@ -157,6 +168,7 @@ export class SlaveClient {
for (const [deviceKey, job] of this.jobs) {
this.endRate(deviceKey, job.done);
job.kill();
this.clearProgressGate(deviceKey);
}
this.jobs.clear();
}
@ -174,6 +186,66 @@ export class SlaveClient {
}
}
private progressGate(deviceKey: string): ProgressGate {
let gate = this.progressGates.get(deviceKey);
if (!gate) {
gate = { lastSent: 0, timer: null, payload: null };
this.progressGates.set(deviceKey, gate);
}
return gate;
}
private sendProgress(jobId: string, deviceKey: string, done: bigint, count: bigint, rate: number): void {
const gate = this.progressGate(deviceKey);
gate.payload = {
type: "progress",
jobId,
deviceKey,
done: done.toString(),
count: count.toString(),
rate,
};
const wait = WS_PROGRESS_INTERVAL_MS - (Date.now() - gate.lastSent);
if (wait <= 0) {
this.flushProgress(deviceKey);
return;
}
if (!gate.timer) {
gate.timer = setTimeout(() => {
gate.timer = null;
this.flushProgress(deviceKey);
}, wait);
}
}
private flushProgress(deviceKey: string): void {
const gate = this.progressGates.get(deviceKey);
if (!gate?.payload) {
return;
}
const payload = gate.payload;
const live = this.jobs.get(deviceKey);
if (!live || live.jobId !== payload.jobId) {
gate.payload = null;
return;
}
gate.payload = null;
gate.lastSent = Date.now();
this.send(payload);
}
private clearProgressGate(deviceKey: string): void {
const gate = this.progressGates.get(deviceKey);
if (!gate) {
return;
}
if (gate.timer) {
clearTimeout(gate.timer);
gate.timer = null;
}
gate.payload = null;
}
private emitWorkers(): void {
const host = os.hostname();
const rows: WorkerRow[] = this.devices.map((device) => {
@ -250,6 +322,7 @@ export class SlaveClient {
this.events.onLog?.(`No device ${block.deviceKey}; using ${device.name}`);
}
this.jobs.get(device.key)?.kill();
this.clearProgressGate(device.key);
this.events.onLog?.(
`Assigned ${device.name} len=${block.keyLen} pad=0x${block.padByte.toString(16).padStart(2, "0")} ` +
`start=${block.start} count=${block.count}`,
@ -276,14 +349,7 @@ export class SlaveClient {
live.count = count;
live.rate = rate;
}
this.send({
type: "progress",
jobId: block.jobId,
deviceKey: device.key,
done: done.toString(),
count: count.toString(),
rate,
});
this.sendProgress(block.jobId, device.key, done, count, rate);
this.events.onProgress?.(block.jobId, done, count, rate);
this.updateStatus();
this.emitWorkers();
@ -319,6 +385,7 @@ export class SlaveClient {
this.jobs.delete(device.key);
this.endRate(device.key, BigInt(block.count));
this.completedBlocks.set(device.key, (this.completedBlocks.get(device.key) ?? 0) + 1);
this.clearProgressGate(device.key);
this.send({
type: "block_complete",
jobId: block.jobId,
@ -335,6 +402,7 @@ export class SlaveClient {
const live = this.jobs.get(device.key);
this.endRate(device.key, live?.done ?? 0n);
this.jobs.delete(device.key);
this.clearProgressGate(device.key);
}
this.send({ type: "error", jobId: block.jobId, message });
this.events.onLog?.(message);

View File

@ -72,30 +72,53 @@ export interface WorkerRow {
busy: boolean;
}
interface KeyRange {
start: bigint;
count: bigint;
}
interface ReservedBlock extends KeyRange {
workerId: string;
}
class BlockAllocator {
private next = 0n;
private inflight = new Map<string, { start: bigint; count: bigint }>();
private retries: Array<{ start: bigint; count: bigint }> = [];
private inflight = new Map<string, ReservedBlock>();
private retries: KeyRange[] = [];
completed = 0n;
constructor(readonly space: bigint) {}
take(blockSize: bigint): { jobId: string; start: bigint; count: bigint } | null {
private leftover(): bigint {
return this.next >= this.space ? 0n : this.space - this.next;
}
/** Next unallocated slice, reserved for `workerId` until complete/fail. */
take(
workerId: string,
blockSize: bigint,
otherIdle = 0,
): { jobId: string; start: bigint; count: bigint } | null {
const queued = this.retries.shift();
if (queued) {
const jobId = randomUUID();
this.inflight.set(jobId, queued);
this.inflight.set(jobId, { ...queued, workerId });
return { jobId, ...queued };
}
if (this.next >= this.space) {
const leftover = this.leftover();
if (leftover <= 0n) {
return null;
}
const size = blockSize < 1n ? 1n : blockSize;
const wanted = blockSize < 1n ? 1n : blockSize;
let count = wanted < leftover ? wanted : leftover;
if (count === leftover && otherIdle > 0 && leftover > 1n) {
const share = leftover / BigInt(otherIdle + 1);
count = share > 0n ? share : 1n;
}
const start = this.next;
const count = size < this.space - start ? size : this.space - start;
this.next += count;
const jobId = randomUUID();
this.inflight.set(jobId, { start, count });
this.inflight.set(jobId, { start, count, workerId });
return { jobId, start, count };
}
@ -114,7 +137,7 @@ class BlockAllocator {
return;
}
this.inflight.delete(jobId);
this.retries.push(block);
this.retries.push({ start: block.start, count: block.count });
}
get finished(): boolean {
@ -194,6 +217,7 @@ export class MasterSearch {
private stageTotal = 1n;
private stage: StageJob | null = null;
private dispatch: (() => void) | null = null;
private feedWorker: ((worker: TrackedWorker) => boolean) | null = null;
private stageAlloc: BlockAllocator | null = null;
constructor(
@ -252,6 +276,7 @@ export class MasterSearch {
}
this.unsub = [];
this.dispatch = null;
this.feedWorker = null;
}
const elapsed = (Date.now() - this.started) / 1000;
@ -350,6 +375,9 @@ export class MasterSearch {
}
this.stageAlloc?.complete(jobId);
this.emitProgress();
if (tracked) {
this.feedWorker?.(tracked);
}
this.dispatch?.();
};
this.server.events.onDropped = (slave, jobId) => {
@ -410,39 +438,41 @@ export class MasterSearch {
}
};
const dispatch = () => {
if (this.cancel) {
checkDone();
return;
}
const idleWorkers = (): TrackedWorker[] => {
const idle: TrackedWorker[] = [];
for (const device of this.params.devices) {
if (!localIdle.has(device.key)) {
continue;
}
const worker = this.roster.get(localWorkerId(device.key));
if (!worker) {
continue;
if (worker && !worker.busy) {
idle.push(worker);
}
const block = alloc.take(this.sizeFor(worker));
if (!block) {
break;
}
localIdle.delete(device.key);
this.startLocal(device, job, block, data, firstCt, alloc, localIdle, dispatch, checkDone);
}
if (this.server) {
for (const remote of this.server.idleWorkers()) {
const worker = this.roster.get(remoteWorkerId(remote.slaveId, remote.device.key));
if (!worker) {
continue;
if (worker && !worker.busy) {
idle.push(worker);
}
const block = alloc.take(this.sizeFor(worker));
}
}
return idle;
};
const feedWorker = (worker: TrackedWorker): boolean => {
if (this.cancel || worker.busy) {
return false;
}
const others = idleWorkers().filter((w) => w.id !== worker.id).length;
const block = alloc.take(worker.id, this.sizeFor(worker), others);
if (!block) {
break;
return false;
}
const ok = this.server.assign(remote.slaveId, remote.device.key, {
if (worker.slaveId && this.server) {
const ok = this.server.assign(worker.slaveId, worker.deviceKey, {
jobId: block.jobId,
deviceKey: remote.device.key,
deviceKey: worker.deviceKey,
start: block.start.toString(),
count: block.count.toString(),
keyLen: job.length,
@ -450,19 +480,37 @@ export class MasterSearch {
padByte: job.pad,
target: PAD_CT.toString("hex"),
fills: FILL_HEX,
cpuWorkers: remote.device.type === "cpu" ? this.params.cpuWorkersFor(worker.id) : undefined,
cpuWorkers: worker.deviceType === "cpu" ? this.params.cpuWorkersFor(worker.id) : undefined,
});
if (!ok) {
alloc.fail(block.jobId);
} else {
return false;
}
worker.jobId = block.jobId;
worker.busy = true;
worker.done = 0n;
worker.count = block.count;
worker.rate = 0;
beginWork(worker);
return true;
}
const device = this.params.devices.find((d) => d.key === worker.deviceKey);
if (!device || !localIdle.has(device.key)) {
alloc.fail(block.jobId);
return false;
}
localIdle.delete(device.key);
this.startLocal(device, job, block, data, firstCt, alloc, localIdle, checkDone);
return true;
};
const dispatch = () => {
if (this.cancel) {
checkDone();
return;
}
for (const worker of idleWorkers()) {
feedWorker(worker);
}
this.emitProgress();
const hasRemote = !!this.server && this.server.listWorkers().length > 0;
@ -480,10 +528,12 @@ export class MasterSearch {
checkDone();
};
this.feedWorker = feedWorker;
this.dispatch = dispatch;
dispatch();
await finished;
this.dispatch = null;
this.feedWorker = null;
this.stageAlloc = null;
this.stageCompleted = job.space;
this.emitProgress();
@ -497,7 +547,6 @@ export class MasterSearch {
firstCt: Buffer,
alloc: BlockAllocator,
localIdle: Set<string>,
dispatch: () => void,
checkDone: () => void,
): void {
const id = localWorkerId(device.key);
@ -576,7 +625,10 @@ export class MasterSearch {
}
this.stageCompleted = alloc.completed;
this.emitProgress();
dispatch();
if (keepDevice && live && !this.cancel) {
this.feedWorker?.(live);
}
this.dispatch?.();
checkDone();
});
}