From ac345b28595b7ab61177be037fa1455b561d41d2 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 19 Sep 2026 19:20:55 +0100 Subject: [PATCH] Refactor iOS application structure for DES Cracker - Replaced Objective-C AppDelegate with Swift implementation for better performance and modern syntax. - Removed legacy files (AppDelegate.h, AppDelegate.m, main.m, PSCBruteEngine.h, PSCBruteEngine.m, PSCCpuEngine.h, PSCCpuEngine.m, PSCMetalEngine.h, PSCMetalEngine.m, PSCProtocol.h, PSCProtocol.m, PSCSlaveClient.h, PSCSlaveClient.m) to streamline the codebase. - Introduced new Compute engines (CpuEngine and MetalEngine) for enhanced brute force capabilities. - Updated Info.plist to support multiple scenes and added scene delegate for improved lifecycle management. - Modified results.json to reflect updated elapsed time for brute force operations. --- decoded/bruteforce/results.json | 2 +- ios/DESCracker.xcodeproj/project.pbxproj | 184 +++-- .../contents.xcworkspacedata | 7 + .../UserInterfaceState.xcuserstate | Bin 0 -> 17000 bytes ios/DESCracker/AppDelegate.h | 5 - ios/DESCracker/AppDelegate.m | 14 - ios/DESCracker/AppDelegate.swift | 21 + ios/DESCracker/Compute/BruteEngine.swift | 22 + ios/DESCracker/Compute/CpuEngine.swift | 153 ++++ ios/DESCracker/Compute/MetalEngine.swift | 238 ++++++ ios/DESCracker/Compute/PSCBruteEngine.h | 20 - ios/DESCracker/Compute/PSCCpuEngine.h | 7 - ios/DESCracker/Compute/PSCCpuEngine.m | 155 ---- ios/DESCracker/Compute/PSCMetalEngine.h | 9 - ios/DESCracker/Compute/PSCMetalEngine.m | 266 ------- ios/DESCracker/DESCracker-Bridging-Header.h | 1 + ios/DESCracker/Info.plist | 17 + ios/DESCracker/Network/PSCProtocol.h | 57 -- ios/DESCracker/Network/PSCProtocol.m | 146 ---- ios/DESCracker/Network/PSCSlaveClient.h | 25 - ios/DESCracker/Network/PSCSlaveClient.m | 604 ---------------- ios/DESCracker/Network/Protocol.swift | 144 ++++ ios/DESCracker/Network/SlaveClient.swift | 492 +++++++++++++ ios/DESCracker/SceneDelegate.swift | 18 + ios/DESCracker/UI/PSCSlaveViewController.h | 5 - ios/DESCracker/UI/PSCSlaveViewController.m | 619 ---------------- ios/DESCracker/UI/SlaveViewController.swift | 680 ++++++++++++++++++ ios/DESCracker/main.m | 8 - native/metal/des_bruteforce.metal | 45 +- src/net/master.ts | 11 +- src/search/orchestrator.ts | 79 +- src/ui/app.css | 2 +- src/ui/mainWindow.ts | 2 +- 33 files changed, 1978 insertions(+), 2080 deletions(-) create mode 100644 ios/DESCracker.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ios/DESCracker.xcodeproj/project.xcworkspace/xcuserdata/tombutcher.xcuserdatad/UserInterfaceState.xcuserstate delete mode 100644 ios/DESCracker/AppDelegate.h delete mode 100644 ios/DESCracker/AppDelegate.m create mode 100644 ios/DESCracker/AppDelegate.swift create mode 100644 ios/DESCracker/Compute/BruteEngine.swift create mode 100644 ios/DESCracker/Compute/CpuEngine.swift create mode 100644 ios/DESCracker/Compute/MetalEngine.swift delete mode 100644 ios/DESCracker/Compute/PSCBruteEngine.h delete mode 100644 ios/DESCracker/Compute/PSCCpuEngine.h delete mode 100644 ios/DESCracker/Compute/PSCCpuEngine.m delete mode 100644 ios/DESCracker/Compute/PSCMetalEngine.h delete mode 100644 ios/DESCracker/Compute/PSCMetalEngine.m create mode 100644 ios/DESCracker/DESCracker-Bridging-Header.h delete mode 100644 ios/DESCracker/Network/PSCProtocol.h delete mode 100644 ios/DESCracker/Network/PSCProtocol.m delete mode 100644 ios/DESCracker/Network/PSCSlaveClient.h delete mode 100644 ios/DESCracker/Network/PSCSlaveClient.m create mode 100644 ios/DESCracker/Network/Protocol.swift create mode 100644 ios/DESCracker/Network/SlaveClient.swift create mode 100644 ios/DESCracker/SceneDelegate.swift delete mode 100644 ios/DESCracker/UI/PSCSlaveViewController.h delete mode 100644 ios/DESCracker/UI/PSCSlaveViewController.m create mode 100644 ios/DESCracker/UI/SlaveViewController.swift delete mode 100644 ios/DESCracker/main.m diff --git a/decoded/bruteforce/results.json b/decoded/bruteforce/results.json index f7b64b3..23f216d 100644 --- a/decoded/bruteforce/results.json +++ b/decoded/bruteforce/results.json @@ -1,5 +1,5 @@ { - "elapsed_sec": 46.262, + "elapsed_sec": 261.712, "pad_matches": [], "confirmed": [] } diff --git a/ios/DESCracker.xcodeproj/project.pbxproj b/ios/DESCracker.xcodeproj/project.pbxproj index a11e5ae..d22ee71 100644 --- a/ios/DESCracker.xcodeproj/project.pbxproj +++ b/ios/DESCracker.xcodeproj/project.pbxproj @@ -7,43 +7,39 @@ 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 */; }; + A40000000000000000000001 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000001 /* AppDelegate.swift */; }; + A40000000000000000000002 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000002 /* SceneDelegate.swift */; }; + A40000000000000000000003 /* SlaveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000003 /* SlaveViewController.swift */; }; + A40000000000000000000004 /* Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000004 /* Protocol.swift */; }; + A40000000000000000000005 /* SlaveClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000005 /* SlaveClient.swift */; }; + A40000000000000000000006 /* BruteEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000006 /* BruteEngine.swift */; }; + A40000000000000000000007 /* MetalEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000007 /* MetalEngine.swift */; }; + A40000000000000000000008 /* CpuEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000008 /* CpuEngine.swift */; }; + A40000000000000000000009 /* des.c in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000009 /* des.c */; }; + A40000000000000000000010 /* des_bruteforce.metal in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000011 /* des_bruteforce.metal */; }; + A40000000000000000000011 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000013 /* Assets.xcassets */; }; + A40000000000000000000012 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A20000000000000000000014 /* Metal.framework */; }; + A40000000000000000000013 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A20000000000000000000015 /* 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 = ""; }; - A20000000000000000000002 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - A20000000000000000000003 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - A20000000000000000000004 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A20000000000000000000005 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - A20000000000000000000006 /* PSCSlaveViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCSlaveViewController.h; sourceTree = ""; }; - A20000000000000000000007 /* PSCSlaveViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCSlaveViewController.m; sourceTree = ""; }; - A20000000000000000000008 /* PSCProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCProtocol.h; sourceTree = ""; }; - A20000000000000000000009 /* PSCProtocol.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCProtocol.m; sourceTree = ""; }; - A20000000000000000000010 /* PSCSlaveClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCSlaveClient.h; sourceTree = ""; }; - A20000000000000000000011 /* PSCSlaveClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCSlaveClient.m; sourceTree = ""; }; - A20000000000000000000012 /* PSCBruteEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCBruteEngine.h; sourceTree = ""; }; - A20000000000000000000013 /* PSCMetalEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCMetalEngine.h; sourceTree = ""; }; - A20000000000000000000014 /* PSCMetalEngine.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCMetalEngine.m; sourceTree = ""; }; - A20000000000000000000015 /* PSCCpuEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCCpuEngine.h; sourceTree = ""; }; - A20000000000000000000016 /* PSCCpuEngine.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCCpuEngine.m; sourceTree = ""; }; - 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; }; + A20000000000000000000001 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + A20000000000000000000002 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + A20000000000000000000003 /* SlaveViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlaveViewController.swift; sourceTree = ""; }; + A20000000000000000000004 /* Protocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Protocol.swift; sourceTree = ""; }; + A20000000000000000000005 /* SlaveClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlaveClient.swift; sourceTree = ""; }; + A20000000000000000000006 /* BruteEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BruteEngine.swift; sourceTree = ""; }; + A20000000000000000000007 /* MetalEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalEngine.swift; sourceTree = ""; }; + A20000000000000000000008 /* CpuEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CpuEngine.swift; sourceTree = ""; }; + A20000000000000000000009 /* des.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = des.c; path = ../native/common/des.c; sourceTree = SOURCE_ROOT; }; + A20000000000000000000010 /* des.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = des.h; path = ../native/common/des.h; sourceTree = SOURCE_ROOT; }; + A20000000000000000000011 /* des_bruteforce.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; name = des_bruteforce.metal; path = ../native/metal/des_bruteforce.metal; sourceTree = SOURCE_ROOT; }; + A20000000000000000000012 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A20000000000000000000013 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + A20000000000000000000014 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; }; + A20000000000000000000015 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + A20000000000000000000016 /* DESCracker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "DESCracker-Bridging-Header.h"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -51,22 +47,31 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A40000000000000000000011 /* Metal.framework in Frameworks */, - A40000000000000000000012 /* QuartzCore.framework in Frameworks */, + A40000000000000000000012 /* Metal.framework in Frameworks */, + A40000000000000000000013 /* QuartzCore.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + A30000000000000000000000 = { + isa = PBXGroup; + children = ( + A30000000000000000000001 /* DESCracker */, + A30000000000000000000007 /* Frameworks */, + A30000000000000000000006 /* Products */, + ); + sourceTree = ""; + }; A30000000000000000000001 /* DESCracker */ = { isa = PBXGroup; children = ( - A20000000000000000000001 /* main.m */, - A20000000000000000000002 /* AppDelegate.h */, - A20000000000000000000003 /* AppDelegate.m */, - A20000000000000000000004 /* Info.plist */, - A20000000000000000000005 /* Assets.xcassets */, + A20000000000000000000001 /* AppDelegate.swift */, + A20000000000000000000002 /* SceneDelegate.swift */, + A20000000000000000000016 /* DESCracker-Bridging-Header.h */, + A20000000000000000000012 /* Info.plist */, + A20000000000000000000013 /* Assets.xcassets */, A30000000000000000000002 /* UI */, A30000000000000000000003 /* Network */, A30000000000000000000004 /* Compute */, @@ -78,8 +83,7 @@ A30000000000000000000002 /* UI */ = { isa = PBXGroup; children = ( - A20000000000000000000006 /* PSCSlaveViewController.h */, - A20000000000000000000007 /* PSCSlaveViewController.m */, + A20000000000000000000003 /* SlaveViewController.swift */, ); path = UI; sourceTree = ""; @@ -87,10 +91,8 @@ A30000000000000000000003 /* Network */ = { isa = PBXGroup; children = ( - A20000000000000000000008 /* PSCProtocol.h */, - A20000000000000000000009 /* PSCProtocol.m */, - A20000000000000000000010 /* PSCSlaveClient.h */, - A20000000000000000000011 /* PSCSlaveClient.m */, + A20000000000000000000004 /* Protocol.swift */, + A20000000000000000000005 /* SlaveClient.swift */, ); path = Network; sourceTree = ""; @@ -98,11 +100,9 @@ A30000000000000000000004 /* Compute */ = { isa = PBXGroup; children = ( - A20000000000000000000012 /* PSCBruteEngine.h */, - A20000000000000000000013 /* PSCMetalEngine.h */, - A20000000000000000000014 /* PSCMetalEngine.m */, - A20000000000000000000015 /* PSCCpuEngine.h */, - A20000000000000000000016 /* PSCCpuEngine.m */, + A20000000000000000000006 /* BruteEngine.swift */, + A20000000000000000000007 /* MetalEngine.swift */, + A20000000000000000000008 /* CpuEngine.swift */, ); path = Compute; sourceTree = ""; @@ -110,9 +110,9 @@ A30000000000000000000005 /* Native */ = { isa = PBXGroup; children = ( - A20000000000000000000017 /* des.c */, - A20000000000000000000018 /* des.h */, - A20000000000000000000019 /* des_bruteforce.metal */, + A20000000000000000000009 /* des.c */, + A20000000000000000000010 /* des.h */, + A20000000000000000000011 /* des_bruteforce.metal */, ); name = Native; sourceTree = ""; @@ -128,21 +128,12 @@ A30000000000000000000007 /* Frameworks */ = { isa = PBXGroup; children = ( - A20000000000000000000020 /* Metal.framework */, - A20000000000000000000021 /* QuartzCore.framework */, + A20000000000000000000014 /* Metal.framework */, + A20000000000000000000015 /* QuartzCore.framework */, ); name = Frameworks; sourceTree = ""; }; - A30000000000000000000000 /* Root */ = { - isa = PBXGroup; - children = ( - A30000000000000000000001 /* DESCracker */, - A30000000000000000000007 /* Frameworks */, - A30000000000000000000006 /* Products */, - ); - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -174,6 +165,7 @@ TargetAttributes = { A10000000000000000000002 = { CreatedOnToolsVersion = 16.0; + LastSwiftMigration = 1600; }; }; }; @@ -185,7 +177,7 @@ en, Base, ); - mainGroup = A30000000000000000000000 /* Root */; + mainGroup = A30000000000000000000000; productRefGroup = A30000000000000000000006 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -200,7 +192,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - A40000000000000000000010 /* Assets.xcassets in Resources */, + A40000000000000000000011 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -211,15 +203,16 @@ 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 */, + A40000000000000000000001 /* AppDelegate.swift in Sources */, + A40000000000000000000002 /* SceneDelegate.swift in Sources */, + A40000000000000000000003 /* SlaveViewController.swift in Sources */, + A40000000000000000000004 /* Protocol.swift in Sources */, + A40000000000000000000005 /* SlaveClient.swift in Sources */, + A40000000000000000000006 /* BruteEngine.swift in Sources */, + A40000000000000000000007 /* MetalEngine.swift in Sources */, + A40000000000000000000008 /* CpuEngine.swift in Sources */, + A40000000000000000000009 /* des.c in Sources */, + A40000000000000000000010 /* des_bruteforce.metal in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -270,10 +263,14 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_COMPILER_FLAGS = "-fno-unroll-loops"; + MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -315,9 +312,13 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_COMPILER_FLAGS = "-fno-unroll-loops"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; name = Release; @@ -327,16 +328,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B832M34SD9; 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; @@ -353,13 +352,10 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = "DESCracker/DESCracker-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - USER_HEADER_SEARCH_PATHS = ( - "$(SRCROOT)/DESCracker/UI", - "$(SRCROOT)/DESCracker/Network", - "$(SRCROOT)/DESCracker/Compute", - "$(SRCROOT)/../native/common", - ); }; name = Debug; }; @@ -368,16 +364,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B832M34SD9; 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; @@ -394,13 +388,9 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = "DESCracker/DESCracker-Bridging-Header.h"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - USER_HEADER_SEARCH_PATHS = ( - "$(SRCROOT)/DESCracker/UI", - "$(SRCROOT)/DESCracker/Network", - "$(SRCROOT)/DESCracker/Compute", - "$(SRCROOT)/../native/common", - ); }; name = Release; }; diff --git a/ios/DESCracker.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/DESCracker.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/DESCracker.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/DESCracker.xcodeproj/project.xcworkspace/xcuserdata/tombutcher.xcuserdatad/UserInterfaceState.xcuserstate b/ios/DESCracker.xcodeproj/project.xcworkspace/xcuserdata/tombutcher.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..d061ca668915f943053d6dd8e1bd0642ce7d4ca3 GIT binary patch literal 17000 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 literal 0 HcmV?d00001 diff --git a/ios/DESCracker/AppDelegate.h b/ios/DESCracker/AppDelegate.h deleted file mode 100644 index 39df806..0000000 --- a/ios/DESCracker/AppDelegate.h +++ /dev/null @@ -1,5 +0,0 @@ -#import - -@interface AppDelegate : UIResponder -@property (nonatomic, strong) UIWindow *window; -@end diff --git a/ios/DESCracker/AppDelegate.m b/ios/DESCracker/AppDelegate.m deleted file mode 100644 index 2f93bd0..0000000 --- a/ios/DESCracker/AppDelegate.m +++ /dev/null @@ -1,14 +0,0 @@ -#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 diff --git a/ios/DESCracker/AppDelegate.swift b/ios/DESCracker/AppDelegate.swift new file mode 100644 index 0000000..8448aec --- /dev/null +++ b/ios/DESCracker/AppDelegate.swift @@ -0,0 +1,21 @@ +import UIKit + +@main +final class AppDelegate: UIResponder, UIApplicationDelegate { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + true + } + + func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + let config = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + config.delegateClass = SceneDelegate.self + return config + } +} diff --git a/ios/DESCracker/Compute/BruteEngine.swift b/ios/DESCracker/Compute/BruteEngine.swift new file mode 100644 index 0000000..d233af4 --- /dev/null +++ b/ios/DESCracker/Compute/BruteEngine.swift @@ -0,0 +1,22 @@ +import Foundation + +enum EngineError: LocalizedError { + case message(String) + + var errorDescription: String? { + switch self { + case .message(let text): + return text + } + } +} + +protocol BruteEngine: AnyObject { + func runAssign( + _ block: AssignBlock, + onProgress: ((UInt64, UInt64, Double) -> Void)?, + onHit: ((UInt64, UInt64, UInt64) -> Void)?, + completion: ((Int, TimeInterval, Bool, Error?) -> Void)? + ) + func cancel() +} diff --git a/ios/DESCracker/Compute/CpuEngine.swift b/ios/DESCracker/Compute/CpuEngine.swift new file mode 100644 index 0000000..b0a838d --- /dev/null +++ b/ios/DESCracker/Compute/CpuEngine.swift @@ -0,0 +1,153 @@ +import Foundation +import QuartzCore + +final class CpuEngine: BruteEngine { + let coreCount: Int + private let workQueue = DispatchQueue(label: "com.descracker.cpu") + private var cancelled = false + private var generation: UInt64 = 0 + + init() { + let n = ProcessInfo.processInfo.processorCount + coreCount = max(1, n) + } + + func prepare() throws { + if des_selftest() == 0 { + throw EngineError.message("CPU DES self-test failed") + } + } + + func cancel() { + cancelled = true + generation &+= 1 + } + + func runAssign( + _ block: AssignBlock, + onProgress: ((UInt64, UInt64, Double) -> Void)?, + onHit: ((UInt64, UInt64, UInt64) -> Void)?, + completion: ((Int, TimeInterval, Bool, Error?) -> Void)? + ) { + workQueue.async { [weak self] in + guard let self else { return } + self.cancelled = false + self.generation &+= 1 + let generation = self.generation + let t0 = CACurrentMediaTime() + + let keyLen = block.keyLen + let charset = Array(block.charset.utf8) + let padByte = block.padByte + let target = block.target + let start = block.start + let count = block.count + var workers = block.cpuWorkers.map { max(1, $0) } ?? self.coreCount + if workers == 0 { workers = 1 } + + var fills = [UInt64](repeating: 0, count: 8) + var nFills: UInt32 = 0 + for item in block.fills.prefix(8) { + fills[Int(nFills)] = item + nFills += 1 + } + + let done = Locked(UInt64(0)) + let hits = Locked(0) + let cancelledFlag = Locked(false) + let group = DispatchGroup() + let pool = DispatchQueue.global(qos: .userInitiated) + let chunk = count / UInt64(workers) + let rem = count % UInt64(workers) + var cursor = start + + for w in 0.. 0 { + _ = done.add(local) + } + } + } + group.wait() + + let cancelled = cancelledFlag.value || self.cancelled || self.generation != generation + if !cancelled { + var elapsed = CACurrentMediaTime() - t0 + if elapsed < 1e-6 { elapsed = 1e-6 } + onProgress?(count, count, Double(count) / elapsed) + } + completion?(hits.value, CACurrentMediaTime() - t0, cancelled, nil) + } + } +} + +final class Locked { + private let lock = NSLock() + private var storage: Value + + init(_ value: Value) { + storage = value + } + + var value: Value { + get { + lock.lock() + defer { lock.unlock() } + return storage + } + set { + lock.lock() + storage = newValue + lock.unlock() + } + } + + func add(_ amount: UInt64) -> UInt64 where Value == UInt64 { + lock.lock() + storage += amount + let result = storage + lock.unlock() + return result + } +} diff --git a/ios/DESCracker/Compute/MetalEngine.swift b/ios/DESCracker/Compute/MetalEngine.swift new file mode 100644 index 0000000..8fafc6a --- /dev/null +++ b/ios/DESCracker/Compute/MetalEngine.swift @@ -0,0 +1,238 @@ +import Foundation +import Metal +import QuartzCore + +private let maxHits = 256 +private let threadsPerGroup = 256 +private let defaultBatch: UInt32 = 32768 + +private struct Hit { + var index: UInt64 + var key: UInt64 + var plain: UInt64 +} + +private struct Params { + var start: UInt64 + var keyLen: UInt32 + var charsetLen: UInt32 + var padByte: UInt32 + var fillCount: UInt32 + var batchCount: UInt32 + var pad: UInt32 + var target: UInt64 + var fills: (UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64) +} + +final class MetalEngine: BruteEngine { + let gpuName: String + let memoryBytes: UInt64 + var isAvailable: Bool { device != nil } + + private let device: MTLDevice? + private var queue: MTLCommandQueue? + private var library: MTLLibrary? + private var brutePipeline: MTLComputePipelineState? + private let workQueue = DispatchQueue(label: "com.descracker.metal") + private var cancelled = false + private var activeJobId: String? + + init() { + let device = MTLCreateSystemDefaultDevice() + self.device = device + if let device { + gpuName = device.name + memoryBytes = device.recommendedMaxWorkingSetSize + } else { + gpuName = "Metal GPU" + memoryBytes = 0 + } + } + + func cancel() { + cancelled = true + activeJobId = nil + } + + func prepare() throws { + guard let device else { + throw EngineError.message("No Metal GPU") + } + try workQueue.sync { + try self.prepareOnWorkQueue(device: device) + } + } + + private func prepareOnWorkQueue(device: MTLDevice) throws { + if queue != nil { return } + guard let lib = device.makeDefaultLibrary() else { + throw EngineError.message("Failed to load Metal library") + } + guard lib.makeFunction(name: "des_brute") != nil else { + throw EngineError.message("Metal kernel missing (des_brute)") + } + guard let queue = device.makeCommandQueue() else { + throw EngineError.message("Failed to create Metal command queue") + } + library = lib + self.queue = queue + } + + private func ensureBrutePipeline() throws { + if brutePipeline != nil { return } + guard let library, let device else { + throw EngineError.message("Metal engine is not ready") + } + guard let brute = library.makeFunction(name: "des_brute") else { + throw EngineError.message("Metal kernel missing (des_brute)") + } + brutePipeline = try device.makeComputePipelineState(function: brute) + } + + private func currentBatchSize() -> UInt32 { + switch ProcessInfo.processInfo.thermalState { + case .critical: return 4096 + case .serious: return 8192 + default: return defaultBatch + } + } + + func runAssign( + _ block: AssignBlock, + onProgress: ((UInt64, UInt64, Double) -> Void)?, + onHit: ((UInt64, UInt64, UInt64) -> Void)?, + completion: ((Int, TimeInterval, Bool, Error?) -> Void)? + ) { + workQueue.async { [weak self] in + guard let self else { return } + self.cancelled = false + self.activeJobId = block.jobId + let jobId = block.jobId + let t0 = CACurrentMediaTime() + var hits = 0 + var runError: Error? + + guard let device = self.device else { + completion?(0, 0, false, EngineError.message("No Metal GPU")) + return + } + do { + try self.prepareOnWorkQueue(device: device) + try self.ensureBrutePipeline() + } catch { + completion?(0, 0, false, error) + return + } + guard let queue = self.queue, let pipeline = self.brutePipeline else { + completion?(0, 0, false, EngineError.message("Metal engine is not ready")) + return + } + + let charset = Array(block.charset.utf8) + var fills = [UInt64](repeating: 0, count: 8) + var nFills = 0 + for item in block.fills.prefix(8) { + fills[nFills] = item + nFills += 1 + } + + var charsetStorage = charset + if charsetStorage.isEmpty { + charsetStorage = [0] + } + guard let paramsBuf = device.makeBuffer(length: 104, options: .storageModeShared), + let charsetBuf = charsetStorage.withUnsafeBytes({ raw in + device.makeBuffer( + bytes: raw.baseAddress!, + length: max(charset.count, 1), + options: .storageModeShared + ) + }), + let hitsBuf = device.makeBuffer(length: maxHits * MemoryLayout.stride, options: .storageModeShared), + let countBuf = device.makeBuffer(length: 4, options: .storageModeShared) + else { + completion?(0, 0, false, EngineError.message("Failed to allocate Metal buffers")) + return + } + + var done: UInt64 = 0 + let count = block.count + var cancelled = false + while done < count { + autoreleasepool { + if self.cancelled || self.activeJobId != jobId { + cancelled = true + return + } + let batch = self.currentBatchSize() + let remaining = count - done + let n = UInt64(min(UInt64(batch), remaining)) + countBuf.contents().assumingMemoryBound(to: UInt32.self).pointee = 0 + var params = Params( + start: block.start + done, + keyLen: block.keyLen, + charsetLen: UInt32(charset.count), + padByte: block.padByte, + fillCount: UInt32(nFills), + batchCount: UInt32(n), + pad: 0, + target: block.target, + fills: ( + fills[0], fills[1], fills[2], fills[3], + fills[4], fills[5], fills[6], fills[7] + ) + ) + withUnsafeBytes(of: ¶ms) { src in + paramsBuf.contents().copyMemory(from: src.baseAddress!, byteCount: 104) + } + + guard let cmd = queue.makeCommandBuffer(), + let enc = cmd.makeComputeCommandEncoder() + else { + runError = EngineError.message("Failed to encode Metal command") + cancelled = true + return + } + enc.setComputePipelineState(pipeline) + enc.setBuffer(paramsBuf, offset: 0, index: 0) + enc.setBuffer(charsetBuf, offset: 0, index: 1) + enc.setBuffer(hitsBuf, offset: 0, index: 2) + enc.setBuffer(countBuf, offset: 0, index: 3) + var tpg = min(threadsPerGroup, pipeline.maxTotalThreadsPerThreadgroup) + if tpg == 0 { tpg = 1 } + let groups = (Int(n) + tpg - 1) / tpg + enc.dispatchThreadgroups( + MTLSize(width: groups, height: 1, depth: 1), + threadsPerThreadgroup: MTLSize(width: tpg, height: 1, depth: 1) + ) + enc.endEncoding() + cmd.commit() + cmd.waitUntilCompleted() + if let error = cmd.error { + runError = error + cancelled = true + return + } + + var hitn = Int(countBuf.contents().assumingMemoryBound(to: UInt32.self).pointee) + if hitn > maxHits { hitn = maxHits } + let outHits = hitsBuf.contents().assumingMemoryBound(to: Hit.self) + for i in 0.. - -@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 -- (void)runAssign:(PSCAssignBlock *)block - onProgress:(nullable PSCBruteProgressBlock)onProgress - onHit:(nullable PSCBruteHitBlock)onHit - completion:(nullable PSCBruteFinishBlock)completion; -- (void)cancel; -@end - -NS_ASSUME_NONNULL_END - diff --git a/ios/DESCracker/Compute/PSCCpuEngine.h b/ios/DESCracker/Compute/PSCCpuEngine.h deleted file mode 100644 index 48dc8f7..0000000 --- a/ios/DESCracker/Compute/PSCCpuEngine.h +++ /dev/null @@ -1,7 +0,0 @@ -#import -#import "PSCBruteEngine.h" - -@interface PSCCpuEngine : NSObject -@property (nonatomic, assign, readonly) NSUInteger coreCount; -- (BOOL)prepare:(NSError **)error; -@end diff --git a/ios/DESCracker/Compute/PSCCpuEngine.m b/ios/DESCracker/Compute/PSCCpuEngine.m deleted file mode 100644 index 7a1fb6f..0000000 --- a/ios/DESCracker/Compute/PSCCpuEngine.m +++ /dev/null @@ -1,155 +0,0 @@ -#import "PSCCpuEngine.h" -#import "PSCProtocol.h" -#import "des.h" -#import -#import -#import - -@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 diff --git a/ios/DESCracker/Compute/PSCMetalEngine.h b/ios/DESCracker/Compute/PSCMetalEngine.h deleted file mode 100644 index c63ea02..0000000 --- a/ios/DESCracker/Compute/PSCMetalEngine.h +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import "PSCBruteEngine.h" - -@interface PSCMetalEngine : NSObject -@property (nonatomic, readonly) BOOL available; -@property (nonatomic, copy, readonly) NSString *gpuName; -@property (nonatomic, assign, readonly) uint64_t memoryBytes; -- (BOOL)prepare:(NSError **)error; -@end diff --git a/ios/DESCracker/Compute/PSCMetalEngine.m b/ios/DESCracker/Compute/PSCMetalEngine.m deleted file mode 100644 index 31ec959..0000000 --- a/ios/DESCracker/Compute/PSCMetalEngine.m +++ /dev/null @@ -1,266 +0,0 @@ -#import "PSCMetalEngine.h" -#import "PSCProtocol.h" -#import -#import -#import -#import - -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 device; -@property (nonatomic, strong) id queue; -@property (nonatomic, strong) id 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 lib = [self.device newDefaultLibrary]; - if (!lib) { - if (error) { - *error = [NSError errorWithDomain:@"DESCracker" code:2 userInfo:@{ - NSLocalizedDescriptionKey : @"Failed to load Metal library" - }]; - } - return NO; - } - id known = [lib newFunctionWithName:@"des_known_answer"]; - id 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 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 out = [self.device newBufferWithLength:16 options:MTLResourceStorageModeShared]; - id cmd = [self.queue commandBuffer]; - id 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 paramsBuf = [self.device newBufferWithLength:kPSCParamsSize options:MTLResourceStorageModeShared]; - id charsetBuf = [self.device newBufferWithBytes:cs length:MAX(charset_len, 1) - options:MTLResourceStorageModeShared]; - id hitsBuf = [self.device newBufferWithLength:kPSCMaxHits * sizeof(PSCHit) - options:MTLResourceStorageModeShared]; - id 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 cmd = [self.queue commandBuffer]; - id 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 diff --git a/ios/DESCracker/DESCracker-Bridging-Header.h b/ios/DESCracker/DESCracker-Bridging-Header.h new file mode 100644 index 0000000..c4cf166 --- /dev/null +++ b/ios/DESCracker/DESCracker-Bridging-Header.h @@ -0,0 +1 @@ +#include "des.h" diff --git a/ios/DESCracker/Info.plist b/ios/DESCracker/Info.plist index 39a3d4c..ac34da0 100644 --- a/ios/DESCracker/Info.plist +++ b/ios/DESCracker/Info.plist @@ -31,6 +31,23 @@ NSLocalNetworkUsageDescription DES Cracker connects to the desktop master on your local network to receive search work. + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + SceneDelegate + + + + UILaunchScreen UIColorName diff --git a/ios/DESCracker/Network/PSCProtocol.h b/ios/DESCracker/Network/PSCProtocol.h deleted file mode 100644 index e52af1e..0000000 --- a/ios/DESCracker/Network/PSCProtocol.h +++ /dev/null @@ -1,57 +0,0 @@ -#import -#include - -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 *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 - diff --git a/ios/DESCracker/Network/PSCProtocol.m b/ios/DESCracker/Network/PSCProtocol.m deleted file mode 100644 index 429d2b0..0000000 --- a/ios/DESCracker/Network/PSCProtocol.m +++ /dev/null @@ -1,146 +0,0 @@ -#import "PSCProtocol.h" -#import -#import - -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 *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]; -} diff --git a/ios/DESCracker/Network/PSCSlaveClient.h b/ios/DESCracker/Network/PSCSlaveClient.h deleted file mode 100644 index 9db6a8e..0000000 --- a/ios/DESCracker/Network/PSCSlaveClient.h +++ /dev/null @@ -1,25 +0,0 @@ -#import -#import "PSCProtocol.h" - -@class PSCSlaveClient; - -@protocol PSCSlaveClientDelegate -- (void)slaveClient:(PSCSlaveClient *)client didChangeStatus:(NSString *)status connected:(BOOL)connected; -- (void)slaveClient:(PSCSlaveClient *)client didLog:(NSString *)line; -- (void)slaveClient:(PSCSlaveClient *)client didUpdateWorkers:(NSArray *)workers; -- (void)slaveClient:(PSCSlaveClient *)client didHitJob:(NSString *)jobId index:(NSString *)index keyHex:(NSString *)keyHex; -@end - -@interface PSCSlaveClient : NSObject -@property (nonatomic, weak) id 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 *)devices; -- (void)connectToHost:(NSString *)host port:(NSInteger)port; -- (void)disconnect; -@end diff --git a/ios/DESCracker/Network/PSCSlaveClient.m b/ios/DESCracker/Network/PSCSlaveClient.m deleted file mode 100644 index 348f01d..0000000 --- a/ios/DESCracker/Network/PSCSlaveClient.m +++ /dev/null @@ -1,604 +0,0 @@ -#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 () -@property (nonatomic, copy) NSArray *devices; -@property (nonatomic, strong) NSURLSession *session; -@property (nonatomic, strong) NSURLSessionWebSocketTask *task; -@property (nonatomic, strong) NSTimer *pingTimer; -@property (nonatomic, strong) NSMutableDictionary *jobs; -@property (nonatomic, strong) NSMutableDictionary *completedBlocks; -@property (nonatomic, strong) NSMutableDictionary *progressGates; -@property (nonatomic, strong) NSMutableDictionary *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 *)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)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 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 *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 *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 *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 diff --git a/ios/DESCracker/Network/Protocol.swift b/ios/DESCracker/Network/Protocol.swift new file mode 100644 index 0000000..4aee5b7 --- /dev/null +++ b/ios/DESCracker/Network/Protocol.swift @@ -0,0 +1,144 @@ +import Foundation + +enum Wire { + static let protocolVersion = 2 + static let defaultPort = 9876 + static let progressInterval: TimeInterval = 0.5 + static let pingInterval: TimeInterval = 5.0 + + static func formatDec64(_ value: UInt64) -> String { + String(value) + } + + static func formatHex64(_ value: UInt64) -> String { + String(format: "%016llx", value) + } + + static func formatCount(_ value: UInt64) -> String { + let n = Double(value) + if n >= 1e9 { return String(format: "%.1fB", n / 1e9) } + if n >= 1e6 { return String(format: "%.1fM", n / 1e6) } + if n >= 1e3 { return String(format: "%.1fk", n / 1e3) } + return String(format: "%.0f", n) + } + + static func formatRate(_ rate: Double) -> String { + guard rate > 0, rate.isFinite else { return "—" } + return "\(formatCount(UInt64(rate.rounded())))/s" + } + + static func parseDec64(_ value: Any?) -> UInt64 { + if let string = value as? String { + return strtoull(string, nil, 10) + } + if let number = value as? NSNumber { + return number.uint64Value + } + return 0 + } + + static func parseHex64(_ hex: String) -> UInt64 { + guard !hex.isEmpty else { return 0 } + var parsed = hex + if parsed.hasPrefix("0x") || parsed.hasPrefix("0X") { + parsed = String(parsed.dropFirst(2)) + } + return strtoull(parsed, nil, 16) + } + + static func decodeMessage(_ raw: String) -> [String: Any]? { + guard let data = raw.data(using: .utf8), + let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + parsed["type"] is String + else { + return nil + } + return parsed + } + + static func encodeMessage(_ msg: [String: Any]) -> String? { + guard JSONSerialization.isValidJSONObject(msg), + let data = try? JSONSerialization.data(withJSONObject: msg) + else { + return nil + } + return String(data: data, encoding: .utf8) + } +} + +final class ComputeDevice { + var key = "" + var deviceId = 0 + var name = "" + var type = "cpu" + var cores: NSNumber? + var memory: NSNumber? + + func jsonObject() -> [String: Any] { + var json: [String: Any] = [ + "key": key, + "id": deviceId, + "name": name, + "type": type, + ] + if let cores { json["cores"] = cores } + if let memory { json["memory"] = memory } + return json + } +} + +final class AssignBlock { + var jobId = "" + var deviceKey = "" + var start: UInt64 = 0 + var count: UInt64 = 0 + var keyLen: UInt32 = 0 + var charset = "" + var padByte: UInt32 = 0 + var target: UInt64 = 0 + var fills: [UInt64] = [] + var cpuWorkers: Int? + + static func fromJSON(_ json: [String: Any]) -> AssignBlock? { + let block = AssignBlock() + block.jobId = json["jobId"] as? String ?? "" + block.deviceKey = json["deviceKey"] as? String ?? "" + block.start = Wire.parseDec64(json["start"]) + block.count = Wire.parseDec64(json["count"]) + block.keyLen = UInt32(Wire.parseDec64(json["keyLen"])) + block.charset = json["charset"] as? String ?? "" + block.padByte = UInt32(Wire.parseDec64(json["padByte"]) & 0xFF) + if let target = json["target"] as? String { + block.target = Wire.parseHex64(target) + } else { + block.target = Wire.parseDec64(json["target"]) + } + if let fillsVal = json["fills"] as? [Any] { + for item in fillsVal.prefix(8) { + if let hex = item as? String { + block.fills.append(Wire.parseHex64(hex)) + } else { + block.fills.append(Wire.parseDec64(item)) + } + } + } + if json["cpuWorkers"] != nil { + block.cpuWorkers = Int(Wire.parseDec64(json["cpuWorkers"])) + } + if block.jobId.isEmpty || block.count == 0 || block.keyLen == 0 || block.charset.isEmpty { + return nil + } + return block + } +} + +final class WorkerSnapshot { + var deviceKey = "" + var deviceName = "" + var deviceType = "" + var current = "idle" + var pct = 0.0 + var completedBlocks = 0 + var rate = 0.0 + var busy = false +} diff --git a/ios/DESCracker/Network/SlaveClient.swift b/ios/DESCracker/Network/SlaveClient.swift new file mode 100644 index 0000000..5ddadaf --- /dev/null +++ b/ios/DESCracker/Network/SlaveClient.swift @@ -0,0 +1,492 @@ +import Foundation + +protocol SlaveClientDelegate: AnyObject { + func slaveClient(_ client: SlaveClient, didChangeStatus status: String, connected: Bool) + func slaveClient(_ client: SlaveClient, didLog line: String) + func slaveClient(_ client: SlaveClient, didUpdateWorkers workers: [WorkerSnapshot]) + func slaveClient(_ client: SlaveClient, didHitJob jobId: String, index: String, keyHex: String) +} + +final class SlaveClient: NSObject, URLSessionWebSocketDelegate { + weak var delegate: SlaveClientDelegate? + var cpuWorkers: Int + var isConnected: Bool { task?.state == .running } + var metalAvailable: Bool { metal.isAvailable } + var metalName: String { metal.gpuName } + var metalMemory: UInt64 { metal.memoryBytes } + var cpuCores: Int { cpu.coreCount } + + private var devices: [ComputeDevice] = [] + private var session: URLSession! + private var task: URLSessionWebSocketTask? + private var pingTimer: Timer? + private var jobs: [String: DeviceJob] = [:] + private var completedBlocks: [String: Int] = [:] + private var progressGates: [String: ProgressGate] = [:] + private var rateStats: [String: RateStats] = [:] + private let metal = MetalEngine() + private let cpu = CpuEngine() + private let syncQueue = DispatchQueue(label: "com.descracker.slave") + private var connectURL = "" + private var loggedMetalCompile = false + + override init() { + cpuWorkers = max(1, ProcessInfo.processInfo.processorCount) + super.init() + let config = URLSessionConfiguration.default + config.waitsForConnectivity = false + session = URLSession(configuration: config, delegate: self, delegateQueue: nil) + } + + func prepareEngines() throws { + try cpu.prepare() + } + + func prepareEngines(completion: @escaping (Bool, Error?) -> Void) { + DispatchQueue.global(qos: .userInitiated).async { + var captured: Error? + var ok = true + do { + try self.prepareEngines() + } catch { + captured = error + ok = false + } + DispatchQueue.main.async { + completion(ok, captured) + } + } + } + + func setDevices(_ devices: [ComputeDevice]) { + syncQueue.async { + self.devices = devices + for device in self.devices where self.completedBlocks[device.key] == nil { + self.completedBlocks[device.key] = 0 + } + self.emitWorkers() + } + } + + func connect(to host: String, port: Int) { + disconnect() + let urlString = "ws://\(host):\(port)" + connectURL = urlString + notifyStatus("Connecting to \(urlString)…", connected: false) + guard let url = URL(string: urlString) else { + log("Invalid URL \(urlString)") + notifyStatus("Disconnected", connected: false) + return + } + task = session.webSocketTask(with: url) + task?.resume() + } + + func disconnect() { + syncQueue.sync { + clearPing() + killAll() + } + let current = task + task = nil + current?.cancel(with: .normalClosure, reason: nil) + syncQueue.async { self.emitWorkers() } + } + + func urlSession( + _ session: URLSession, + webSocketTask: URLSessionWebSocketTask, + didOpenWithProtocol protocol: String? + ) { + guard webSocketTask === task else { return } + syncQueue.async { + let deviceJSON = self.devices.map { $0.jsonObject() } + let hostname = ProcessInfo.processInfo.hostName + self.send([ + "type": "hello", + "hostname": hostname, + "platform": "ios arm64", + "devices": deviceJSON, + ]) + let exposed = self.devices.isEmpty + ? "no workers" + : self.devices.map(\.name).joined(separator: ", ") + self.notifyStatus("Connected to \(self.connectURL)", connected: true) + self.log("Hello sent (protocol \(Wire.protocolVersion)) — exposing \(exposed)") + self.emitWorkers() + DispatchQueue.main.async { + self.pingTimer?.invalidate() + self.pingTimer = Timer.scheduledTimer(withTimeInterval: Wire.pingInterval, repeats: true) { [weak self] _ in + self?.send(["type": "ping"]) + } + } + self.listen() + } + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + if task !== self.task && self.task != nil { return } + syncQueue.async { + self.clearPing() + self.killAll() + if self.task === task { + self.task = nil + } + if let error, (error as NSError).code != NSURLErrorCancelled { + self.log("WebSocket error: \(error.localizedDescription)") + } + self.notifyStatus("Disconnected", connected: false) + self.emitWorkers() + } + } + + private func listen() { + guard let task else { return } + task.receive { [weak self] result in + guard let self, task === self.task else { return } + switch result { + case .failure: + return + case .success(let message): + if case .string(let text) = message, let msg = Wire.decodeMessage(text) { + self.onServer(msg) + } + self.listen() + } + } + } + + private func onServer(_ msg: [String: Any]) { + let type = msg["type"] as? String + switch type { + case "hello_ack": + log("Master ack v\(msg["protocolVersion"] ?? "?")") + case "assign": + guard let block = AssignBlock.fromJSON(msg) else { + log("Ignored malformed assign") + return + } + runAssign(block) + case "cancel": + syncQueue.async { + self.killAll() + self.log("Cancel received") + self.notifyStatus("Idle (cancelled)", connected: true) + self.emitWorkers() + } + case "shutdown": + DispatchQueue.main.async { self.disconnect() } + default: + break + } + } + + private func engine(for device: ComputeDevice) -> BruteEngine { + device.type == "metal" ? metal : cpu + } + + private func device(forKey key: String) -> ComputeDevice? { + if let match = devices.first(where: { $0.key == key }) { return match } + return devices.first(where: { $0.type == "metal" }) ?? devices.first + } + + private func runAssign(_ block: AssignBlock) { + syncQueue.async { [weak self] in + guard let self else { return } + let matched = self.devices.first(where: { $0.key == block.deviceKey }) + guard let device = matched ?? self.device(forKey: block.deviceKey) else { + self.send(["type": "error", "jobId": block.jobId, "message": "no compute device selected"]) + return + } + if matched == nil { + self.log("No device \(block.deviceKey); using \(device.name)") + } + self.jobs[device.key]?.kill?() + self.clearProgressGate(device.key) + self.log(String( + format: "Assigned %@ len=%u pad=0x%02x start=%llu count=%llu", + device.name, + block.keyLen, + block.padByte, + block.start, + block.count + )) + if device.type == "metal" && !self.loggedMetalCompile { + self.loggedMetalCompile = true + self.log("Compiling Metal kernel for this GPU (first job may pause)…") + } + if device.type == "cpu" && block.cpuWorkers == nil { + block.cpuWorkers = self.cpuWorkers + } + let started = Date() + let hitCount = Locked(0) + let engine = self.engine(for: device) + let deviceKey = device.key + let jobId = block.jobId + let job = DeviceJob(jobId: jobId, count: block.count) + job.kill = { engine.cancel() } + self.jobs[deviceKey] = job + self.beginRate(deviceKey) + self.updateStatus() + self.emitWorkers() + + engine.runAssign( + block, + onProgress: { done, count, rate in + self.syncQueue.async { + if let live = self.jobs[deviceKey], live.jobId == jobId { + live.done = done + live.count = count + live.rate = rate + } + self.sendProgress(jobId: jobId, deviceKey: deviceKey, done: done, count: count, rate: rate) + self.updateStatus() + self.emitWorkers() + } + }, + onHit: { index, key, plain in + self.syncQueue.async { + hitCount.value += 1 + let indexS = Wire.formatDec64(index) + let keyHex = Wire.formatHex64(key) + let plainHex = Wire.formatHex64(plain) + self.send([ + "type": "hit", + "jobId": jobId, + "index": indexS, + "keyHex": keyHex, + "plainHex": plainHex, + ]) + DispatchQueue.main.async { + self.delegate?.slaveClient(self, didHitJob: jobId, index: indexS, keyHex: keyHex) + } + } + }, + completion: { _, elapsed, cancelled, error in + self.syncQueue.async { + let still = self.jobs[deviceKey] + let same = still?.jobId == jobId + if cancelled { + if same, let still { + self.endRate(deviceKey, keys: still.done) + self.jobs.removeValue(forKey: deviceKey) + self.clearProgressGate(deviceKey) + } + self.updateStatus() + self.emitWorkers() + return + } + if let error { + if same, let still { + self.endRate(deviceKey, keys: still.done) + self.jobs.removeValue(forKey: deviceKey) + self.clearProgressGate(deviceKey) + } + self.send([ + "type": "error", + "jobId": jobId, + "message": error.localizedDescription, + ]) + self.log(error.localizedDescription) + self.updateStatus() + self.emitWorkers() + return + } + if same { + self.jobs.removeValue(forKey: deviceKey) + self.endRate(deviceKey, keys: block.count) + self.completedBlocks[deviceKey] = (self.completedBlocks[deviceKey] ?? 0) + 1 + self.clearProgressGate(deviceKey) + let elapsedOut = elapsed > 0 ? elapsed : Date().timeIntervalSince(started) + self.send([ + "type": "block_complete", + "jobId": jobId, + "deviceKey": deviceKey, + "hits": hitCount.value, + "elapsed": elapsedOut, + ]) + } + self.updateStatus() + self.emitWorkers() + } + } + ) + } + } + + private func send(_ msg: [String: Any]) { + guard let json = Wire.encodeMessage(msg), + let task, task.state == .running + else { return } + task.send(.string(json)) { [weak self] error in + if let error { + self?.log("Send failed: \(error.localizedDescription)") + } + } + } + + private func rate(for deviceKey: String) -> RateStats { + if let stats = rateStats[deviceKey] { return stats } + let stats = RateStats() + rateStats[deviceKey] = stats + return stats + } + + private func beginRate(_ deviceKey: String) { + let stats = rate(for: deviceKey) + if stats.busyStarted == nil { + stats.busyStarted = Date() + } + } + + private func endRate(_ deviceKey: String, keys: UInt64) { + let stats = rate(for: deviceKey) + if let started = stats.busyStarted { + stats.activeMs += Date().timeIntervalSince(started) * 1000 + stats.busyStarted = nil + } + if keys > 0 { + stats.keysDone += keys + } + } + + private func killAll() { + for (deviceKey, job) in jobs { + endRate(deviceKey, keys: job.done) + job.kill?() + clearProgressGate(deviceKey) + } + jobs.removeAll() + } + + private func clearPing() { + DispatchQueue.main.async { + self.pingTimer?.invalidate() + self.pingTimer = nil + } + } + + private func progressGate(_ deviceKey: String) -> ProgressGate { + if let gate = progressGates[deviceKey] { return gate } + let gate = ProgressGate() + progressGates[deviceKey] = gate + return gate + } + + private func sendProgress(jobId: String, deviceKey: String, done: UInt64, count: UInt64, rate: Double) { + let gate = progressGate(deviceKey) + gate.payload = [ + "type": "progress", + "jobId": jobId, + "deviceKey": deviceKey, + "done": Wire.formatDec64(done), + "count": Wire.formatDec64(count), + "rate": rate, + ] + let wait = Wire.progressInterval - (Date().timeIntervalSince1970 - gate.lastSent) + if wait <= 0 { + flushProgress(deviceKey) + return + } + if !gate.timerPending { + gate.timerPending = true + let key = deviceKey + syncQueue.asyncAfter(deadline: .now() + wait) { + self.progressGates[key]?.timerPending = false + self.flushProgress(key) + } + } + } + + private func flushProgress(_ deviceKey: String) { + guard let gate = progressGates[deviceKey], let payload = gate.payload else { return } + let live = jobs[deviceKey] + if live == nil || live?.jobId != payload["jobId"] as? String { + gate.payload = nil + return + } + gate.payload = nil + gate.lastSent = Date().timeIntervalSince1970 + send(payload) + } + + private func clearProgressGate(_ deviceKey: String) { + guard let gate = progressGates[deviceKey] else { return } + gate.timerPending = false + gate.payload = nil + } + + private func emitWorkers() { + let rows: [WorkerSnapshot] = devices.map { device in + let live = jobs[device.key] + let done = live?.done ?? 0 + let count = live?.count ?? 0 + let pct = live != nil && count > 0 ? Double(done) * 100.0 / Double(count) : 0 + let row = WorkerSnapshot() + row.deviceKey = device.key + row.deviceName = device.name + row.deviceType = device.type + row.current = live == nil ? "idle" : "\(Wire.formatCount(done)) / \(Wire.formatCount(count))" + row.pct = pct + row.completedBlocks = completedBlocks[device.key] ?? 0 + row.rate = live?.rate ?? 0 + row.busy = live != nil + return row + } + DispatchQueue.main.async { + self.delegate?.slaveClient(self, didUpdateWorkers: rows) + } + } + + private func updateStatus() { + let busy = Array(jobs.values) + let connected = task != nil + if busy.isEmpty { + notifyStatus(connected ? "Idle" : "Disconnected", connected: connected) + return + } + if busy.count == 1, let job = busy.first { + let shortId = job.jobId.count > 8 ? String(job.jobId.prefix(8)) : job.jobId + notifyStatus("Working \(shortId) \(job.done)/\(job.count)", connected: true) + return + } + notifyStatus("\(busy.count) workers assigned", connected: true) + } + + private func notifyStatus(_ status: String, connected: Bool) { + DispatchQueue.main.async { + self.delegate?.slaveClient(self, didChangeStatus: status, connected: connected) + } + } + + private func log(_ line: String) { + DispatchQueue.main.async { + self.delegate?.slaveClient(self, didLog: line) + } + } +} + +private final class DeviceJob { + var jobId: String + var done: UInt64 = 0 + var count: UInt64 + var rate = 0.0 + var kill: (() -> Void)? + + init(jobId: String, count: UInt64) { + self.jobId = jobId + self.count = count + } +} + +private final class RateStats { + var keysDone: UInt64 = 0 + var activeMs: TimeInterval = 0 + var busyStarted: Date? +} + +private final class ProgressGate { + var lastSent: TimeInterval = 0 + var timerPending = false + var payload: [String: Any]? +} diff --git a/ios/DESCracker/SceneDelegate.swift b/ios/DESCracker/SceneDelegate.swift new file mode 100644 index 0000000..c390acb --- /dev/null +++ b/ios/DESCracker/SceneDelegate.swift @@ -0,0 +1,18 @@ +import UIKit + +final class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard let windowScene = scene as? UIWindowScene else { return } + let window = UIWindow(windowScene: windowScene) + window.backgroundColor = Palette.bg + window.rootViewController = SlaveViewController() + window.makeKeyAndVisible() + self.window = window + } +} diff --git a/ios/DESCracker/UI/PSCSlaveViewController.h b/ios/DESCracker/UI/PSCSlaveViewController.h deleted file mode 100644 index 74564f3..0000000 --- a/ios/DESCracker/UI/PSCSlaveViewController.h +++ /dev/null @@ -1,5 +0,0 @@ -#import -#import "PSCSlaveClient.h" - -@interface PSCSlaveViewController : UIViewController -@end diff --git a/ios/DESCracker/UI/PSCSlaveViewController.m b/ios/DESCracker/UI/PSCSlaveViewController.m deleted file mode 100644 index ee8913a..0000000 --- a/ios/DESCracker/UI/PSCSlaveViewController.m +++ /dev/null @@ -1,619 +0,0 @@ -#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 () -@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 *)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 *)selectedDevices { - NSMutableArray *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 *)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 *)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 diff --git a/ios/DESCracker/UI/SlaveViewController.swift b/ios/DESCracker/UI/SlaveViewController.swift new file mode 100644 index 0000000..ec06671 --- /dev/null +++ b/ios/DESCracker/UI/SlaveViewController.swift @@ -0,0 +1,680 @@ +import UIKit + +enum Palette { + static let bg = UIColor(red: 0x10 / 255, green: 0x15 / 255, blue: 0x1c / 255, alpha: 1) + static let card = UIColor(red: 0x17 / 255, green: 0x1e / 255, blue: 0x28 / 255, alpha: 1) + static let border = UIColor(red: 0x2b / 255, green: 0x35 / 255, blue: 0x44 / 255, alpha: 1) + static let text = UIColor(red: 0xe8 / 255, green: 0xee / 255, blue: 0xf4 / 255, alpha: 1) + static let muted = UIColor(red: 0x8b / 255, green: 0x9a / 255, blue: 0xab / 255, alpha: 1) + static let teal = UIColor(red: 0x5e / 255, green: 0xea / 255, blue: 0xd4 / 255, alpha: 1) + static let field = UIColor(red: 0x0c / 255, green: 0x11 / 255, blue: 0x18 / 255, alpha: 1) +} + +private final class WorkCard: UIView { + var nameLabel: UILabel! + var metaLabel: UILabel! + var progress: UIProgressView! + var deviceKey = "" +} + +final class SlaveViewController: UIViewController, SlaveClientDelegate, UITextFieldDelegate, UIGestureRecognizerDelegate { + private let hostKey = "psc.masterHost" + private let portKey = "psc.masterPort" + private let metalKey = "psc.metalOn" + private let cpuKey = "psc.cpuOn" + private let workersKey = "psc.cpuWorkers" + + private let client = SlaveClient() + private var scroll: UIScrollView! + private var stack: UIStackView! + private var statusPill: UILabel! + private var hostField: UITextField! + private var portField: UITextField! + private var connectBtn: UIButton! + private var disconnectBtn: UIButton! + private var metalSwitch: UISwitch! + private var cpuSwitch: UISwitch! + private var metalNameLabel: UILabel! + private var cpuNameLabel: UILabel! + private var workersValue: UILabel! + private var workersStepper: UIStepper! + private var workStack: UIStackView! + private var logView: UITextView! + private var logText = "" + private var metalReady = false + private var enginesPrepared = false + private let logStamp: DateFormatter = { + let fmt = DateFormatter() + fmt.dateFormat = "HH:mm:ss" + return fmt + }() + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = Palette.bg + client.delegate = self + buildUI() + loadDefaults() + applyConnectionEnabled(true) + + let cores = max(1, client.cpuCores) + cpuNameLabel.text = "CPU (\(cores) cores)" + workersStepper.minimumValue = 1 + workersStepper.maximumValue = Double(cores) + if workersStepper.value < 1 || workersStepper.value > Double(cores) { + workersStepper.value = Double(cores) + } + workersValue.text = String(format: "%.0f", workersStepper.value) + client.cpuWorkers = Int(workersStepper.value) + metalNameLabel.text = client.metalName.isEmpty ? "Metal GPU" : client.metalName + metalSwitch.isEnabled = false + rebuildWorkCards([]) + appendLog("Preparing compute engines…") + + client.prepareEngines { [weak self] ok, err in + self?.enginesDidPrepare(ok, error: err) + } + + let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + tap.cancelsTouchesInView = false + tap.delegate = self + view.addGestureRecognizer(tap) + + NotificationCenter.default.addObserver( + self, + selector: #selector(appDidBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardFrameWillChange), + name: UIResponder.keyboardWillChangeFrameNotification, + object: nil + ) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + private func enginesDidPrepare(_ ok: Bool, error: Error?) { + enginesPrepared = ok + metalReady = client.metalAvailable + metalNameLabel.text = metalReady ? client.metalName : "Metal GPU unavailable" + metalSwitch.isEnabled = metalReady + if !metalReady { metalSwitch.isOn = false } + if ok { + appendLog(metalReady ? "CPU self-test passed. Metal GPU available." : "CPU self-test passed. Metal unavailable.") + applyConnectionEnabled(true) + } else { + appendLog("Engine setup: \(error?.localizedDescription ?? "failed")") + applyConnectionEnabled(false) + connectBtn.isEnabled = false + connectBtn.alpha = 0.45 + } + rebuildDevices() + rebuildWorkCards([]) + } + + @objc private func appDidBackground() { + if client.isConnected { + client.disconnect() + applyConnectionEnabled(true) + appendLog("Disconnected (app backgrounded).") + } + } + + private func caption(_ text: String) -> UILabel { + let label = UILabel() + label.text = text.uppercased() + label.textColor = Palette.muted + label.font = .systemFont(ofSize: 11, weight: .bold) + return label + } + + private func card(title: String, arranged views: [UIView]) -> UIView { + let inner = UIStackView() + inner.axis = .vertical + inner.spacing = 10 + let titleLabel = UILabel() + titleLabel.text = title + titleLabel.textColor = Palette.text + titleLabel.font = .systemFont(ofSize: 15, weight: .semibold) + inner.addArrangedSubview(titleLabel) + views.forEach { inner.addArrangedSubview($0) } + let card = UIView() + card.backgroundColor = Palette.card + card.layer.cornerRadius = 12 + card.layer.borderWidth = 1 + card.layer.borderColor = Palette.border.cgColor + inner.translatesAutoresizingMaskIntoConstraints = false + card.addSubview(inner) + NSLayoutConstraint.activate([ + inner.topAnchor.constraint(equalTo: card.topAnchor, constant: 14), + inner.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 14), + inner.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -14), + inner.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14), + ]) + return card + } + + private func field(placeholder: String, keyboard: UIKeyboardType) -> UITextField { + let field = UITextField() + field.placeholder = placeholder + field.textColor = Palette.text + field.keyboardType = keyboard + field.keyboardAppearance = .dark + field.autocapitalizationType = .none + field.autocorrectionType = .no + field.spellCheckingType = .no + field.smartDashesType = .no + field.smartQuotesType = .no + field.smartInsertDeleteType = .no + if #available(iOS 17.0, *) { + field.inlinePredictionType = .no + } + field.delegate = self + field.returnKeyType = .done + field.enablesReturnKeyAutomatically = false + field.font = .monospacedSystemFont(ofSize: 15, weight: .regular) + field.backgroundColor = Palette.field + field.layer.cornerRadius = 8 + field.layer.borderWidth = 1 + field.layer.borderColor = Palette.border.cgColor + field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 1)) + field.leftViewMode = .always + field.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 1)) + field.rightViewMode = .always + field.attributedPlaceholder = NSAttributedString( + string: placeholder, + attributes: [.foregroundColor: Palette.muted] + ) + field.heightAnchor.constraint(equalToConstant: 40).isActive = true + + let bar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 320, height: 44)) + bar.barStyle = .black + bar.isTranslucent = true + let flex = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + let done = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(dismissKeyboard)) + done.tintColor = Palette.teal + bar.items = [flex, done] + bar.sizeToFit() + field.inputAccessoryView = bar + return field + } + + private func button(title: String, primary: Bool, action: Selector) -> UIButton { + let btn = UIButton(type: .system) + btn.setTitle(title, for: .normal) + btn.titleLabel?.font = .systemFont(ofSize: 15, weight: .semibold) + btn.layer.cornerRadius = 8 + btn.heightAnchor.constraint(equalToConstant: 40).isActive = true + if primary { + btn.backgroundColor = Palette.teal + btn.setTitleColor(Palette.bg, for: .normal) + } else { + btn.backgroundColor = Palette.field + btn.layer.borderWidth = 1 + btn.layer.borderColor = Palette.border.cgColor + btn.setTitleColor(Palette.text, for: .normal) + } + btn.addTarget(self, action: action, for: .touchUpInside) + return btn + } + + private func toggleRow(switch toggle: UISwitch, name: UILabel) -> UIView { + let row = UIStackView() + row.axis = .horizontal + row.alignment = .center + row.spacing = 10 + name.textColor = Palette.text + name.font = .systemFont(ofSize: 14, weight: .medium) + name.numberOfLines = 2 + toggle.onTintColor = Palette.teal + row.addArrangedSubview(name) + row.addArrangedSubview(toggle) + name.setContentHuggingPriority(.defaultLow, for: .horizontal) + toggle.setContentHuggingPriority(.required, for: .horizontal) + return row + } + + private func buildUI() { + scroll = UIScrollView() + scroll.translatesAutoresizingMaskIntoConstraints = false + scroll.alwaysBounceVertical = true + scroll.keyboardDismissMode = .interactive + scroll.delaysContentTouches = false + view.addSubview(scroll) + + stack = UIStackView() + stack.axis = .vertical + stack.spacing = 16 + stack.translatesAutoresizingMaskIntoConstraints = false + scroll.addSubview(stack) + + let safe = view.safeAreaLayoutGuide + NSLayoutConstraint.activate([ + scroll.topAnchor.constraint(equalTo: safe.topAnchor), + scroll.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scroll.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scroll.bottomAnchor.constraint(equalTo: view.bottomAnchor), + stack.topAnchor.constraint(equalTo: scroll.contentLayoutGuide.topAnchor, constant: 16), + stack.leadingAnchor.constraint(equalTo: scroll.frameLayoutGuide.leadingAnchor, constant: 16), + stack.trailingAnchor.constraint(equalTo: scroll.frameLayoutGuide.trailingAnchor, constant: -16), + stack.bottomAnchor.constraint(equalTo: scroll.contentLayoutGuide.bottomAnchor, constant: -24), + ]) + + let header = UIStackView() + header.axis = .horizontal + header.alignment = .center + header.spacing = 12 + + let brand = UIStackView() + brand.axis = .vertical + brand.spacing = 2 + let title = UILabel() + title.text = "DES Cracker" + title.textColor = Palette.teal + title.font = .systemFont(ofSize: 22, weight: .bold) + let sub = UILabel() + sub.text = "LAN WORKER" + sub.textColor = Palette.muted + sub.font = .systemFont(ofSize: 10, weight: .bold) + brand.addArrangedSubview(title) + brand.addArrangedSubview(sub) + + statusPill = UILabel() + statusPill.text = "Offline" + statusPill.textAlignment = .center + statusPill.textColor = Palette.muted + statusPill.font = .systemFont(ofSize: 12, weight: .semibold) + statusPill.backgroundColor = Palette.field + statusPill.layer.cornerRadius = 12 + statusPill.layer.masksToBounds = true + statusPill.layer.borderWidth = 1 + statusPill.layer.borderColor = Palette.border.cgColor + statusPill.widthAnchor.constraint(greaterThanOrEqualToConstant: 96).isActive = true + statusPill.heightAnchor.constraint(equalToConstant: 28).isActive = true + header.addArrangedSubview(brand) + header.addArrangedSubview(statusPill) + brand.setContentHuggingPriority(.defaultLow, for: .horizontal) + + hostField = field(placeholder: "192.168.1.10", keyboard: .numbersAndPunctuation) + portField = field(placeholder: "9876", keyboard: .numberPad) + portField.widthAnchor.constraint(equalToConstant: 88).isActive = true + let hostRow = UIStackView() + hostRow.axis = .horizontal + hostRow.spacing = 8 + let hostCol = UIStackView() + hostCol.axis = .vertical + hostCol.spacing = 4 + hostCol.addArrangedSubview(caption("Master host")) + hostCol.addArrangedSubview(hostField) + let portCol = UIStackView() + portCol.axis = .vertical + portCol.spacing = 4 + portCol.addArrangedSubview(caption("Port")) + portCol.addArrangedSubview(portField) + hostRow.addArrangedSubview(hostCol) + hostRow.addArrangedSubview(portCol) + + connectBtn = button(title: "Connect", primary: true, action: #selector(connectTapped)) + disconnectBtn = button(title: "Disconnect", primary: false, action: #selector(disconnectTapped)) + let btnRow = UIStackView() + btnRow.axis = .horizontal + btnRow.spacing = 8 + btnRow.distribution = .fillEqually + btnRow.addArrangedSubview(connectBtn) + btnRow.addArrangedSubview(disconnectBtn) + let connectCard = card(title: "Connection", arranged: [hostRow, btnRow]) + + metalSwitch = UISwitch() + metalSwitch.isOn = true + metalSwitch.addTarget(self, action: #selector(devicesChanged), for: .valueChanged) + metalNameLabel = UILabel() + metalNameLabel.text = "Metal GPU" + let metalRow = toggleRow(switch: metalSwitch, name: metalNameLabel) + + cpuSwitch = UISwitch() + cpuSwitch.isOn = true + cpuSwitch.addTarget(self, action: #selector(devicesChanged), for: .valueChanged) + cpuNameLabel = UILabel() + cpuNameLabel.text = "CPU" + let cpuRow = toggleRow(switch: cpuSwitch, name: cpuNameLabel) + + workersStepper = UIStepper() + workersStepper.minimumValue = 1 + workersStepper.maximumValue = 16 + workersStepper.stepValue = 1 + workersStepper.value = 4 + workersStepper.addTarget(self, action: #selector(workersChanged), for: .valueChanged) + workersValue = UILabel() + workersValue.textColor = Palette.text + workersValue.font = .monospacedDigitSystemFont(ofSize: 14, weight: .medium) + let workerRow = UIStackView() + workerRow.axis = .horizontal + workerRow.alignment = .center + workerRow.spacing = 8 + let workersCaption = UILabel() + workersCaption.text = "CPU workers" + workersCaption.textColor = Palette.muted + workersCaption.font = .systemFont(ofSize: 13, weight: .medium) + workerRow.addArrangedSubview(workersCaption) + workerRow.addArrangedSubview(UIView()) + workerRow.addArrangedSubview(workersValue) + workerRow.addArrangedSubview(workersStepper) + + let hint = UILabel() + hint.text = "Block size is set on the master. This device only exposes workers." + hint.textColor = Palette.muted + hint.font = .systemFont(ofSize: 12) + hint.numberOfLines = 0 + let deviceCard = card(title: "Devices", arranged: [metalRow, cpuRow, workerRow, hint]) + + workStack = UIStackView() + workStack.axis = .vertical + workStack.spacing = 10 + let workCard = card(title: "Assigned work", arranged: [workStack]) + + logView = UITextView(usingTextLayoutManager: false) + logView.isEditable = false + logView.isSelectable = true + logView.isScrollEnabled = true + logView.backgroundColor = Palette.field + logView.textColor = Palette.text + logView.font = .monospacedSystemFont(ofSize: 11, weight: .regular) + logView.layer.cornerRadius = 8 + logView.layer.borderWidth = 1 + logView.layer.borderColor = Palette.border.cgColor + logView.textContainerInset = UIEdgeInsets(top: 8, left: 6, bottom: 8, right: 6) + logView.translatesAutoresizingMaskIntoConstraints = false + let logWrap = UIView() + logWrap.clipsToBounds = true + logWrap.addSubview(logView) + NSLayoutConstraint.activate([ + logWrap.heightAnchor.constraint(equalToConstant: 180), + logView.topAnchor.constraint(equalTo: logWrap.topAnchor), + logView.leadingAnchor.constraint(equalTo: logWrap.leadingAnchor), + logView.trailingAnchor.constraint(equalTo: logWrap.trailingAnchor), + logView.bottomAnchor.constraint(equalTo: logWrap.bottomAnchor), + ]) + let logCard = card(title: "Log", arranged: [logWrap]) + + stack.addArrangedSubview(header) + stack.addArrangedSubview(connectCard) + stack.addArrangedSubview(deviceCard) + stack.addArrangedSubview(workCard) + stack.addArrangedSubview(logCard) + } + + private func loadDefaults() { + let defaults = UserDefaults.standard + let host = defaults.string(forKey: hostKey) ?? "" + hostField.text = host + let port = defaults.integer(forKey: portKey) + portField.text = port > 0 ? String(port) : "9876" + if defaults.object(forKey: metalKey) != nil { + metalSwitch.isOn = defaults.bool(forKey: metalKey) + } + if defaults.object(forKey: cpuKey) != nil { + cpuSwitch.isOn = defaults.bool(forKey: cpuKey) + } + let workers = defaults.integer(forKey: workersKey) + if workers > 0 { + workersStepper.value = Double(workers) + } + } + + private func saveDefaults() { + let defaults = UserDefaults.standard + defaults.set(hostField.text ?? "", forKey: hostKey) + defaults.set(Int(portField.text ?? "") ?? 0, forKey: portKey) + defaults.set(metalSwitch.isOn, forKey: metalKey) + defaults.set(cpuSwitch.isOn, forKey: cpuKey) + defaults.set(Int(workersStepper.value), forKey: workersKey) + } + + private func applyConnectionEnabled(_ canConnect: Bool) { + let connectOK = canConnect && enginesPrepared + connectBtn.isEnabled = connectOK + connectBtn.alpha = connectOK ? 1 : 0.45 + disconnectBtn.isEnabled = !canConnect && enginesPrepared + disconnectBtn.alpha = (!canConnect && enginesPrepared) ? 1 : 0.45 + hostField.isEnabled = canConnect + portField.isEnabled = canConnect + } + + private func selectedDevices() -> [ComputeDevice] { + var devices: [ComputeDevice] = [] + if metalSwitch.isOn && metalReady { + let gpu = ComputeDevice() + gpu.key = "metal:0" + gpu.deviceId = 0 + gpu.name = client.metalName.isEmpty ? "Metal GPU" : client.metalName + gpu.type = "metal" + if client.metalMemory > 0 { + gpu.memory = NSNumber(value: client.metalMemory) + } + devices.append(gpu) + } + if cpuSwitch.isOn { + let cpu = ComputeDevice() + cpu.key = "cpu:0" + cpu.deviceId = 0 + cpu.name = "CPU (\(client.cpuCores) cores)" + cpu.type = "cpu" + cpu.cores = NSNumber(value: client.cpuCores) + devices.append(cpu) + } + return devices + } + + private func rebuildDevices() { + client.cpuWorkers = Int(workersStepper.value) + client.setDevices(selectedDevices()) + } + + private func rebuildWorkCards(_ workersIn: [WorkerSnapshot]) { + workStack.arrangedSubviews.forEach { + workStack.removeArrangedSubview($0) + $0.removeFromSuperview() + } + var workers = workersIn + if workers.isEmpty { + let selected = selectedDevices() + if selected.isEmpty { + let empty = UILabel() + empty.text = "Enable Metal and/or CPU to expose workers." + empty.textColor = Palette.muted + empty.font = .systemFont(ofSize: 13) + empty.numberOfLines = 0 + workStack.addArrangedSubview(empty) + return + } + workers = selected.map { device in + let row = WorkerSnapshot() + row.deviceKey = device.key + row.deviceName = device.name + row.deviceType = device.type + row.current = "idle" + return row + } + } + for row in workers { + let card = WorkCard() + card.deviceKey = row.deviceKey + let name = UILabel() + name.text = row.deviceName + name.textColor = Palette.text + name.font = .systemFont(ofSize: 14, weight: .semibold) + let meta = UILabel() + meta.textColor = Palette.muted + meta.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular) + meta.numberOfLines = 2 + let progress = UIProgressView(progressViewStyle: .default) + progress.progressTintColor = Palette.teal + progress.trackTintColor = Palette.border + card.nameLabel = name + card.metaLabel = meta + card.progress = progress + let col = UIStackView(arrangedSubviews: [name, progress, meta]) + col.axis = .vertical + col.spacing = 6 + col.translatesAutoresizingMaskIntoConstraints = false + card.addSubview(col) + NSLayoutConstraint.activate([ + col.topAnchor.constraint(equalTo: card.topAnchor), + col.leadingAnchor.constraint(equalTo: card.leadingAnchor), + col.trailingAnchor.constraint(equalTo: card.trailingAnchor), + col.bottomAnchor.constraint(equalTo: card.bottomAnchor), + ]) + workStack.addArrangedSubview(card) + apply(row, to: card) + } + } + + private func apply(_ row: WorkerSnapshot, to card: WorkCard) { + card.nameLabel.text = row.deviceName + card.progress.progress = Float(row.pct / 100) + let rate = row.busy ? Wire.formatRate(row.rate) : "—" + card.metaLabel.text = "\(row.current) \(rate) \(row.completedBlocks) blocks" + } + + @objc private func connectTapped() { + view.endEditing(true) + if !enginesPrepared { + appendLog("Compute engines are still starting.") + return + } + let host = hostField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + var port = Int(portField.text ?? "") ?? 0 + if host.isEmpty { + appendLog("Enter the master’s IP address.") + return + } + if port <= 0 || port > 65535 { + port = Wire.defaultPort + portField.text = String(port) + } + if selectedDevices().isEmpty { + appendLog("Enable at least one device before connecting.") + return + } + saveDefaults() + rebuildDevices() + applyConnectionEnabled(false) + client.connect(to: host, port: port) + } + + @objc private func disconnectTapped() { + client.disconnect() + applyConnectionEnabled(true) + } + + @objc private func devicesChanged() { + saveDefaults() + rebuildDevices() + } + + @objc private func workersChanged() { + workersValue.text = String(format: "%.0f", workersStepper.value) + saveDefaults() + rebuildDevices() + } + + private func appendLog(_ line: String) { + let stamp = logStamp.string(from: Date()) + logText += "[\(stamp)] \(line)\n" + if logText.count > 16000 { + let start = logText.index(logText.endIndex, offsetBy: -12000) + if let keep = logText[start...].firstIndex(of: "\n") { + logText = String(logText[logText.index(after: keep)...]) + } + } + logView.text = logText + DispatchQueue.main.async { [weak logView] in + guard let logView, !logView.text.isEmpty else { return } + let loc = max(0, logView.text.count - 1) + logView.scrollRangeToVisible(NSRange(location: loc, length: 1)) + } + } + + private func setStatus(_ status: String, connected: Bool) { + statusPill.text = " \(status) " + if connected { + statusPill.textColor = Palette.bg + statusPill.backgroundColor = Palette.teal + statusPill.layer.borderColor = Palette.teal.cgColor + } else { + statusPill.textColor = Palette.muted + statusPill.backgroundColor = Palette.field + statusPill.layer.borderColor = Palette.border.cgColor + } + if status == "Disconnected" || status == "Offline" { + applyConnectionEnabled(true) + } + } + + @objc private func dismissKeyboard() { + view.endEditing(true) + } + + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + view.window?.makeKeyAndVisible() + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + var view = touch.view + while let current = view { + if current is UIControl || current is UITextField || current is UITextView { + return false + } + view = current.superview + } + return true + } + + @objc private func keyboardFrameWillChange(_ note: Notification) { + guard let screenFrame = note.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } + let inView = view.convert(screenFrame, from: nil) + let overlap = max(0, view.bounds.maxY - inView.origin.y) + var inset = scroll.contentInset + inset.bottom = overlap + scroll.contentInset = inset + scroll.verticalScrollIndicatorInsets = inset + } + + func slaveClient(_ client: SlaveClient, didChangeStatus status: String, connected: Bool) { + setStatus(status, connected: connected) + UIApplication.shared.isIdleTimerDisabled = connected + } + + func slaveClient(_ client: SlaveClient, didLog line: String) { + appendLog(line) + } + + func slaveClient(_ client: SlaveClient, didUpdateWorkers workers: [WorkerSnapshot]) { + if workStack.arrangedSubviews.count != workers.count { + rebuildWorkCards(workers) + return + } + for (i, row) in workers.enumerated() { + guard let card = workStack.arrangedSubviews[i] as? WorkCard, card.deviceKey == row.deviceKey else { + rebuildWorkCards(workers) + return + } + apply(row, to: card) + } + } + + func slaveClient(_ client: SlaveClient, didHitJob jobId: String, index: String, keyHex: String) { + appendLog("HIT index=\(index) key=\(keyHex)") + } +} diff --git a/ios/DESCracker/main.m b/ios/DESCracker/main.m deleted file mode 100644 index e4a9eb6..0000000 --- a/ios/DESCracker/main.m +++ /dev/null @@ -1,8 +0,0 @@ -#import -#import "AppDelegate.h" - -int main(int argc, char *argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/native/metal/des_bruteforce.metal b/native/metal/des_bruteforce.metal index 2f5818c..520de91 100644 --- a/native/metal/des_bruteforce.metal +++ b/native/metal/des_bruteforce.metal @@ -39,20 +39,22 @@ constant uchar PC2_TBL[48] = { }; constant uchar SHIFTS[16] = {1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1}; -constant uchar SBOX[8][64] = { - {14,4,13,1,2,15,11,8,3,10,6,12,5,9,0,7, 0,15,7,4,14,2,13,1,10,6,12,11,9,5,3,8, 4,1,14,8,13,6,2,11,15,12,9,7,3,10,5,0, 15,12,8,2,4,9,1,7,5,11,3,14,10,0,6,13}, - {15,1,8,14,6,11,3,4,9,7,2,13,12,0,5,10, 3,13,4,7,15,2,8,14,12,0,1,10,6,9,11,5, 0,14,7,11,10,4,13,1,5,8,12,6,9,3,2,15, 13,8,10,1,3,15,4,2,11,6,7,12,0,5,14,9}, - {10,0,9,14,6,3,15,5,1,13,12,7,11,4,2,8, 13,7,0,9,3,4,6,10,2,8,5,14,12,11,15,1, 13,6,4,9,8,15,3,0,11,1,2,12,5,10,14,7, 1,10,13,0,6,9,8,7,4,15,14,3,11,5,2,12}, - {7,13,14,3,0,6,9,10,1,2,8,5,11,12,4,15, 13,8,11,5,6,15,0,3,4,7,2,12,1,10,14,9, 10,6,9,0,12,11,7,13,15,1,3,14,5,2,8,4, 3,15,0,6,10,1,13,8,9,4,5,11,12,7,2,14}, - {2,12,4,1,7,10,11,6,8,5,3,15,13,0,14,9, 14,11,2,12,4,7,13,1,5,0,15,10,3,9,8,6, 4,2,1,11,10,13,7,8,15,9,12,5,6,3,0,14, 11,8,12,7,1,14,2,13,6,15,0,9,10,4,5,3}, - {12,1,10,15,9,2,6,8,0,13,3,4,14,7,5,11, 10,15,4,2,7,12,9,5,6,1,13,14,0,11,3,8, 9,14,15,5,2,8,12,3,7,0,4,10,1,13,11,6, 4,3,2,12,9,5,15,10,11,14,1,7,6,0,8,13}, - {4,11,2,14,15,0,8,13,3,12,9,7,5,10,6,1, 13,0,11,7,4,9,1,10,14,3,5,12,2,15,8,6, 1,4,11,13,12,3,7,14,10,15,6,8,0,5,9,2, 6,11,13,8,1,4,10,7,9,5,0,15,14,2,3,12}, - {13,2,8,4,6,15,11,1,10,9,3,14,5,0,12,7, 1,15,13,8,10,3,7,4,12,5,6,11,0,14,9,2, 7,11,4,1,9,12,14,2,0,6,10,13,15,3,5,8, 2,1,14,7,4,10,8,13,15,12,9,0,3,5,6,11} +constant uchar SBOX[512] = { + 14,4,13,1,2,15,11,8,3,10,6,12,5,9,0,7, 0,15,7,4,14,2,13,1,10,6,12,11,9,5,3,8, 4,1,14,8,13,6,2,11,15,12,9,7,3,10,5,0, 15,12,8,2,4,9,1,7,5,11,3,14,10,0,6,13, + 15,1,8,14,6,11,3,4,9,7,2,13,12,0,5,10, 3,13,4,7,15,2,8,14,12,0,1,10,6,9,11,5, 0,14,7,11,10,4,13,1,5,8,12,6,9,3,2,15, 13,8,10,1,3,15,4,2,11,6,7,12,0,5,14,9, + 10,0,9,14,6,3,15,5,1,13,12,7,11,4,2,8, 13,7,0,9,3,4,6,10,2,8,5,14,12,11,15,1, 13,6,4,9,8,15,3,0,11,1,2,12,5,10,14,7, 1,10,13,0,6,9,8,7,4,15,14,3,11,5,2,12, + 7,13,14,3,0,6,9,10,1,2,8,5,11,12,4,15, 13,8,11,5,6,15,0,3,4,7,2,12,1,10,14,9, 10,6,9,0,12,11,7,13,15,1,3,14,5,2,8,4, 3,15,0,6,10,1,13,8,9,4,5,11,12,7,2,14, + 2,12,4,1,7,10,11,6,8,5,3,15,13,0,14,9, 14,11,2,12,4,7,13,1,5,0,15,10,3,9,8,6, 4,2,1,11,10,13,7,8,15,9,12,5,6,3,0,14, 11,8,12,7,1,14,2,13,6,15,0,9,10,4,5,3, + 12,1,10,15,9,2,6,8,0,13,3,4,14,7,5,11, 10,15,4,2,7,12,9,5,6,1,13,14,0,11,3,8, 9,14,15,5,2,8,12,3,7,0,4,10,1,13,11,6, 4,3,2,12,9,5,15,10,11,14,1,7,6,0,8,13, + 4,11,2,14,15,0,8,13,3,12,9,7,5,10,6,1, 13,0,11,7,4,9,1,10,14,3,5,12,2,15,8,6, 1,4,11,13,12,3,7,14,10,15,6,8,0,5,9,2, 6,11,13,8,1,4,10,7,9,5,0,15,14,2,3,12, + 13,2,8,4,6,15,11,1,10,9,3,14,5,0,12,7, 1,15,13,8,10,3,7,4,12,5,6,11,0,14,9,2, 7,11,4,1,9,12,14,2,0,6,10,13,15,3,5,8, 2,1,14,7,4,10,8,13,15,12,9,0,3,5,6,11 }; -inline ulong perm(ulong src, constant uchar *tbl, uint nout, uint srcbits) { +template +inline ulong perm_n(ulong src, constant uchar *tbl, uint srcbits) { ulong outv = 0; - for (uint i = 0; i < nout; i++) { +#pragma clang loop unroll(disable) + for (uint i = 0; i < Nout; i++) { ulong bit = (src >> (srcbits - tbl[i])) & 1UL; outv = (outv << 1) | bit; } @@ -64,33 +66,36 @@ inline uint rotl28(uint v, uint s) { } inline void des_key_schedule(ulong key, thread ulong sk[16]) { - ulong cd = perm(key, PC1_TBL, 56, 64); + ulong cd = perm_n<56>(key, PC1_TBL, 64); uint c = uint(cd >> 28); uint d = uint(cd & 0x0FFFFFFFUL); +#pragma clang loop unroll(disable) for (uint r = 0; r < 16; r++) { c = rotl28(c, SHIFTS[r]); d = rotl28(d, SHIFTS[r]); ulong cd2 = (ulong(c) << 28) | ulong(d); - sk[r] = perm(cd2, PC2_TBL, 48, 56); + sk[r] = perm_n<48>(cd2, PC2_TBL, 56); } } inline uint feistel(uint r, ulong subkey) { - ulong er = perm(ulong(r), E_TBL, 48, 32) ^ subkey; + ulong er = perm_n<48>(ulong(r), E_TBL, 32) ^ subkey; uint s = 0; +#pragma clang loop unroll(disable) for (uint i = 0; i < 8; i++) { uint chunk = uint((er >> (42 - 6 * i)) & 0x3F); uint row = ((chunk & 0x20) >> 4) | (chunk & 1); uint col = (chunk >> 1) & 0xF; - s = (s << 4) | uint(SBOX[i][row * 16 + col]); + s = (s << 4) | uint(SBOX[i * 64 + row * 16 + col]); } - return uint(perm(ulong(s), P_TBL, 32, 32)); + return uint(perm_n<32>(ulong(s), P_TBL, 32)); } inline ulong des_crypt(ulong block, thread ulong sk[16], bool decrypt) { - ulong ip = perm(block, IP_TBL, 64, 64); + ulong ip = perm_n<64>(block, IP_TBL, 64); uint l = uint(ip >> 32); uint r = uint(ip & 0xFFFFFFFFUL); +#pragma clang loop unroll(disable) for (uint i = 0; i < 16; i++) { uint rnd = decrypt ? (15 - i) : i; uint n = l ^ feistel(r, sk[rnd]); @@ -98,7 +103,7 @@ inline ulong des_crypt(ulong block, thread ulong sk[16], bool decrypt) { r = n; } ulong pre = (ulong(r) << 32) | ulong(l); - return perm(pre, FP_TBL, 64, 64); + return perm_n<64>(pre, FP_TBL, 64); } struct Params { @@ -121,16 +126,19 @@ struct Hit { inline ulong make_key(ulong index, constant Params &p, const device uchar *charset) { uchar bytes[8]; +#pragma clang loop unroll(disable) for (uint i = 0; i < 8; i++) { bytes[i] = uchar(p.pad_byte); } ulong n = index; uint clen = p.charset_len; +#pragma clang loop unroll(disable) for (int pos = int(p.key_len) - 1; pos >= 0; pos--) { bytes[pos] = charset[n % clen]; n /= clen; } ulong key = 0; +#pragma clang loop unroll(disable) for (uint i = 0; i < 8; i++) { key = (key << 8) | ulong(bytes[i]); } @@ -138,6 +146,7 @@ inline ulong make_key(ulong index, constant Params &p, const device uchar *chars } inline bool is_fill(ulong pt, constant Params &p) { +#pragma clang loop unroll(disable) for (uint i = 0; i < p.fill_count; i++) { if (pt == p.fills[i]) { return true; diff --git a/src/net/master.ts b/src/net/master.ts index 6a82b10..b27bcc8 100644 --- a/src/net/master.ts +++ b/src/net/master.ts @@ -207,11 +207,8 @@ export class MasterServer { }); } - private workerByJob(info: SlaveInfo, jobId: string, deviceKey?: string): SlaveDeviceState | undefined { - return ( - info.workers.find((w) => w.jobId === jobId) ?? - (deviceKey ? info.workers.find((w) => w.device.key === deviceKey) : undefined) - ); + private workerByJob(info: SlaveInfo, jobId: string): SlaveDeviceState | undefined { + return info.workers.find((w) => w.jobId === jobId); } private onClient(id: string, msg: ClientMessage): void { @@ -244,7 +241,7 @@ export class MasterServer { this.emitSlaves(); break; case "progress": { - const worker = this.workerByJob(info, msg.jobId, msg.deviceKey); + const worker = info.workers.find((w) => w.jobId === msg.jobId); if (!worker) { break; } @@ -259,7 +256,7 @@ export class MasterServer { this.events.onHit?.(info, msg.jobId, msg.index, msg.keyHex, msg.plainHex); break; case "block_complete": { - const worker = this.workerByJob(info, msg.jobId, msg.deviceKey); + const worker = info.workers.find((w) => w.jobId === msg.jobId); if (!worker) { break; } diff --git a/src/search/orchestrator.ts b/src/search/orchestrator.ts index 1037f3c..2e151dc 100644 --- a/src/search/orchestrator.ts +++ b/src/search/orchestrator.ts @@ -140,6 +140,10 @@ class BlockAllocator { this.retries.push({ start: block.start, count: block.count }); } + has(jobId: string): boolean { + return this.inflight.has(jobId); + } + get finished(): boolean { return this.completed === this.space && this.inflight.size === 0 && this.retries.length === 0; } @@ -214,6 +218,7 @@ export class MasterSearch { private overallPrior = 0n; private overallTotal = 1n; private stageCompleted = 0n; + private stagePeak = 0n; private stageTotal = 1n; private stage: StageJob | null = null; private dispatch: (() => void) | null = null; @@ -348,12 +353,7 @@ export class MasterSearch { prev.onProgress?.(slave, worker, jobId, done, count, rate); const tracked = this.roster.get(remoteWorkerId(slave.id, worker.device.key)); if (tracked) { - tracked.jobId = jobId; - tracked.done = done; - tracked.count = count; - tracked.rate = rate; - tracked.busy = true; - beginWork(tracked); + this.applyProgress(tracked, jobId, done, count, rate); } this.emitProgress(); }; @@ -363,6 +363,8 @@ export class MasterSearch { }; this.server.events.onComplete = (slave, worker, jobId) => { prev.onComplete?.(slave, worker, jobId); + this.stageAlloc?.complete(jobId); + this.syncStageCompleted(); const tracked = this.roster.get(remoteWorkerId(slave.id, worker.device.key)); if (tracked) { endWork(tracked, tracked.count); @@ -373,7 +375,6 @@ export class MasterSearch { tracked.count = 0n; tracked.completedBlocks += 1; } - this.stageAlloc?.complete(jobId); this.emitProgress(); if (tracked) { this.feedWorker?.(tracked); @@ -384,6 +385,7 @@ export class MasterSearch { prev.onDropped?.(slave, jobId); if (jobId) { this.stageAlloc?.fail(jobId); + this.syncStageCompleted(); for (const tracked of this.roster.values()) { if (tracked.jobId === jobId) { endWork(tracked, tracked.done); @@ -420,6 +422,7 @@ export class MasterSearch { this.stage = job; this.stageTotal = job.space; this.stageCompleted = 0n; + this.stagePeak = 0n; const alloc = new BlockAllocator(job.space); this.stageAlloc = alloc; this.events.onLog?.( @@ -536,6 +539,7 @@ export class MasterSearch { this.feedWorker = null; this.stageAlloc = null; this.stageCompleted = job.space; + this.stagePeak = job.space; this.emitProgress(); } @@ -574,10 +578,7 @@ export class MasterSearch { onProgress: (done, count, rate) => { const live = this.roster.get(id); if (live) { - live.done = done; - live.count = count; - live.rate = rate; - live.busy = true; + this.applyProgress(live, block.jobId, done, count, rate); } this.emitProgress(device.name); }, @@ -601,10 +602,12 @@ export class MasterSearch { endWork(live, block.count); } } + this.syncStageCompleted(); }) .catch((err: unknown) => { this.events.onLog?.(`${device.name}: ${err instanceof Error ? err.message : String(err)}`); alloc.fail(block.jobId); + this.syncStageCompleted(); keepDevice = false; }) .finally(() => { @@ -623,7 +626,7 @@ export class MasterSearch { if (keepDevice) { localIdle.add(device.key); } - this.stageCompleted = alloc.completed; + this.syncStageCompleted(); this.emitProgress(); if (keepDevice && live && !this.cancel) { this.feedWorker?.(live); @@ -655,13 +658,51 @@ export class MasterSearch { ); } + private syncStageCompleted(): void { + if (this.stageAlloc) { + this.stageCompleted = this.stageAlloc.completed; + } + } + + private applyProgress(worker: TrackedWorker, jobId: string, done: bigint, count: bigint, rate: number): void { + if (worker.jobId !== jobId) { + return; + } + if (this.stageAlloc && !this.stageAlloc.has(jobId)) { + return; + } + worker.done = done < 0n ? 0n : done; + worker.count = count < 0n ? 0n : count; + worker.rate = rate; + worker.busy = true; + beginWork(worker); + } + + private liveDone(worker: TrackedWorker): bigint { + if (!worker.busy || worker.jobId == null) { + return 0n; + } + if (this.stageAlloc && !this.stageAlloc.has(worker.jobId)) { + return 0n; + } + if (worker.done <= 0n) { + return 0n; + } + if (worker.count > 0n && worker.done > worker.count) { + return worker.count; + } + return worker.done; + } + private emitProgress(workerLabel?: string): void { + this.syncStageCompleted(); let inflight = 0n; let rateSum = 0; const rows: WorkerRow[] = []; for (const w of this.roster.values()) { - if (w.busy) { - inflight += w.done; + const liveJob = w.busy && w.jobId != null && (!this.stageAlloc || this.stageAlloc.has(w.jobId)); + if (liveJob) { + inflight += this.liveDone(w); rateSum += w.rate; } const pct = w.busy && w.count > 0n ? Number((w.done * 1000n) / w.count) / 10 : 0; @@ -685,7 +726,15 @@ export class MasterSearch { busy: w.busy, }); } - const stageDone = this.stageCompleted + inflight; + let stageDone = this.stageCompleted + inflight; + if (stageDone > this.stageTotal) { + stageDone = this.stageTotal; + } + if (stageDone < this.stagePeak) { + stageDone = this.stagePeak; + } else { + this.stagePeak = stageDone; + } const overallDone = this.overallPrior + stageDone; const elapsed = Math.max((Date.now() - this.started) / 1000, 1e-6); const rate = rateSum > 0 ? rateSum : Number(overallDone) / elapsed; diff --git a/src/ui/app.css b/src/ui/app.css index 69eb310..410fc19 100644 --- a/src/ui/app.css +++ b/src/ui/app.css @@ -313,7 +313,7 @@ QProgressBar#workerProgress { padding: 0; min-height: 0; text-align: center; - color: #5eead4; + color: #ffffff; font-size: 11px; font-weight: 700; } diff --git a/src/ui/mainWindow.ts b/src/ui/mainWindow.ts index eec74b0..b26a63c 100644 --- a/src/ui/mainWindow.ts +++ b/src/ui/mainWindow.ts @@ -712,7 +712,7 @@ export function createMainWindow(): QMainWindow { setCell(widgets, index, 3, row.current); const value = Math.min(1000, Math.max(0, Math.round(row.pct * 10))); widgets.bar.setValue(value); - widgets.bar.setFormat(row.busy ? `${row.pct.toFixed(1)}%%` : "idle"); + widgets.bar.setFormat(row.busy ? `${row.pct.toFixed(1)}%` : "idle"); setCell(widgets, index, 5, formatNumber(row.completedBlocks)); setCell(widgets, index, 6, formatNumber(row.completedKeys)); setCell(widgets, index, 7, row.rate);