commit 8f2d3a2489951f102aa54bce4f311156811588a9 Author: Tom Butcher Date: Sat Sep 19 16:10:35 2026 +0100 Add initial project structure for DES Key Cracker - Created .gitignore to exclude build artifacts and dependencies. - Added package.json and package-lock.json for project dependencies and scripts. - Included pnpm workspace configuration for managing packages. - Implemented TypeScript configuration in tsconfig.json. - Added README.md with project description and usage instructions. - Introduced native code for DES encryption and decryption in C/C++. - Created initial decoded data structure for handling scan results. - Established basic file structure for decoded outputs and native builds. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e8a0ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +node_modules/ +dist/ +bin/ +build/ +deploy/ +tmp-jam-pack-nodegui/ +.DS_Store +*.o +*.obj +*.pdb +*.ilk +.cache/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea9cb0f --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# DES Key Cracker + +Native desktop app (Node.js + [NodeGui](https://docs.nodegui.org/docs/api/manual/synopsis/)) that recovers the 8-byte DES key for a TechMed / Pectus `.med` scan, using CPU threads plus GPU acceleration: + +- **macOS:** Apple Metal (`des_brute` kernel) +- **Windows:** NVIDIA CUDA when the CUDA Toolkit is installed +- **Both:** multi-core CPU fallback + +Search is short printable keys over a charset, padded to 8 bytes. + +Machines can cooperate on a LAN. The **master** listens on a WebSocket port and hands each **slave worker** (each exposed CPU or GPU) a block described only by generation parameters (`start`, `count`, `deviceKey`, `keyLen`, `charset`, `padByte`) — not a list of keys. Each worker has its own block size; CPU workers default to 1,048,576 keys and GPU workers to 16,777,216 so the GPU keeps more of the keyspace. + +## Requirements + +- Node.js 18+ (the app runs under **qode**, which NodeGui installs) +- CMake and a C++ toolchain (needed to install `@nodegui/nodegui`) +- **macOS:** Xcode command-line tools (clang + Metal) +- **Windows CPU:** clang++, g++, or MSVC (`cl`) +- **Windows CUDA:** [CUDA Toolkit](https://developer.nvidia.com/cuda-toolkit) (`nvcc`) and an NVIDIA GPU. Without `nvcc` the app still runs on CPU. + +## Build + +```bash +npm install +npm run build:native +npm run build +npm start +``` + +`build:native` compiles whatever this machine supports into `bin/`: + +| Binary | Platform | +|---|---| +| `med_cpu` | macOS and Windows | +| `med_gpu` | macOS Metal | +| `med_cuda` | Windows (or `FORCE_CUDA=1` if `nvcc` is on the PATH) | + +## Using the app + +1. Set **Role** in the toolbar to Master or Slave. +2. Under **Devices**, tick local cards to use or expose. On the master, set **Block size** on each local and remote card; **Workers** (CPU thread count) appears only on CPU cards. +3. **Master:** choose the `.med` file, charset, min/max length, and pads. Optionally **Start service** so other computers can join. Click **Start search**. The master searches on its selected local devices and assigns a block of that device’s size to every exposed slave device. +4. **Slave:** tick the devices to expose, set CPU threads on CPU cards if needed, enter the master’s IP and port, and **Connect**. The slave does not set block size — the master does. Each exposed device stays listed on **Workers** with current-block progress and a completed-block count. The slave does not need the `.med` file. + +Hits are written under `decoded/bruteforce/` (`decrypted_*.bin`, `hit_*.json`, `results.json`). + +Do not expose the WebSocket port to the public internet; it has no authentication. + +## Native runner CLI + +All three runners share the same commands: + +``` +med_* devices +med_* selftest [--device N] +med_* brute --device N --key-len L --charset S --pad XX \ + --target HEX --fills HEX,... --start I --count N --batch B +``` + +Metal also needs `--shader native/metal/des_bruteforce.metal`. CPU accepts `--workers N`. diff --git a/decoded/README.md b/decoded/README.md new file mode 100644 index 0000000..9c73f45 --- /dev/null +++ b/decoded/README.md @@ -0,0 +1,67 @@ +# Decoded Pectus Clinic torso scan + +Source: `Pectus Clinic - Butcher, Thomas - Torso - Original 20181023, 165650.med` + +## What this file is + +This is a **TechMed 3D** proprietary `.med` scan (same family as `.iMed` from 3DsizeMe / My3DsizeMe). + +The filename and date match **Pectus Scan**, TechMed’s white-label iPad app for Pectus Services (App Store id 1435160086, first shipped 8 October 2018). This capture is from **23 October 2018, 16:56:50** — an original torso scan of **Thomas Butcher**, branded under **Pectus Clinic**. + +Capture hardware is an **Occipital Structure Sensor** on an iPad (structured-light surface scan, no X-ray/CT dose). Pectus Services then measured these files in their **T-Scan** tool to plan a T-Joe brace. + +Official open exports from MSoft / MSoft+ are STL, OBJ, PLY, VRML, ASCII, and AOP. This `.med` is the locked working copy, not those exports. + +## Patient / scan metadata (from the filename) + +| Field | Value | +| --- | --- | +| Practice / app brand | Pectus Clinic | +| Patient | Butcher, Thomas | +| Body part | Torso | +| Scan type | Original | +| Timestamp | 2018-10-23 16:56:50 | + +TechMed names files: + +`{Practice} - {Last}, {First} - {BodyPart} - {ScanType} {YYYYMMDD}, {HHMMSS}.med` + +## Binary layout + +Total size: **756,064 bytes**. + +| Offset | Length | Contents | +| ---: | ---: | --- | +| 0 | 565,512 | Encrypted payload (mesh and other records) | +| 565,512 | 8 | Encrypted zero / fill block `7c1cddebb73720e0` | +| 565,520 | 128 | Encrypted trailer (16 blocks; two identical) | +| 565,648 | 190,352 | Encrypted zero padding (23,794 × the same 8-byte block) | +| 756,000 | 64 | Raw zeros, not encrypted | + +The repeating tail is the giveaway: an **8-byte block cipher in ECB mode** encrypting a large preallocated zero buffer. AES is ruled out (16-byte blocks would not produce two identical 8-byte halves). XOR with that block does not reveal STL/OBJ/PLY/XML/JPEG/PNG. + +A size that fits the buffer: + +- **23,563 × 24-byte vertices** (position + normal) used +- **31,500 × 24-byte slots** allocated (= 756,000 bytes) + +That is a plausible Structure Sensor torso cloud/mesh. Photos and landmark/measurement fields that 3DsizeMe normally stores would sit in the same encrypted payload. + +## What could not be recovered from the file alone + +The mesh coordinates, faces, colour, photos, and any in-file measurements are behind a vendor key that is not stored in the file. Common TechMed / Pectus / MSoft passwords and derived DES/3DES/Blowfish/CAST/TEA keys were tried against the known `E(0) = 7c1cddebb73720e0` pair; none matched. + +To get an open 3D file you need one of: + +1. **MSoft** (Qwadra / TechMed) — File → Save As → STL / OBJ / PLY. Pay-per-scan tokens exist if you do not have a licence. +2. **Pectus Scan / 3DsizeMe / MSoft+** on the original iPad workflow, if it can re-open this `.med` and export. +3. **Pectus Services T-Scan** or the clinic that took the scan — ask them to re-export STL/OBJ. + +Contacts: support@techmed3d.com, info@pectusservices.com. + +## Files written here + +- `metadata.json` — identity, software, hashes +- `format_layout.json` — region map and cipher notes +- `header_64b.hex.txt` — first 64 ciphertext bytes +- `trailer_128b.hex.txt` — 128-byte trailer dump diff --git a/decoded/bruteforce/results.json b/decoded/bruteforce/results.json new file mode 100644 index 0000000..246ae8c --- /dev/null +++ b/decoded/bruteforce/results.json @@ -0,0 +1,5 @@ +{ + "elapsed_sec": 61.97, + "pad_matches": [], + "confirmed": [] +} diff --git a/decoded/format_layout.json b/decoded/format_layout.json new file mode 100644 index 0000000..26d7514 --- /dev/null +++ b/decoded/format_layout.json @@ -0,0 +1,84 @@ +{ + "cipher": { + "mode": "ECB", + "block_size_bytes": 8, + "evidence": [ + "A single 8-byte ciphertext (7c1cddebb73720e0) repeats 23,794 times in the tail \u2014 classic ECB encryption of a constant (almost certainly zeros).", + "The 16-byte view of that pattern is X||X, which rules out AES (16-byte blocks) unless E_k(0) accidentally split into two identical halves (probability ~2^-64).", + "CBC/CTR/OFB would not emit a long run of one identical block for zero plaintext.", + "Repeating-XOR with that 8-byte value does not produce a recognizable header, mesh, XML, or image magic, so this is a real 8-byte block cipher (DES, 3DES, Blowfish, CAST, RC2, TEA, \u2026), not XOR." + ], + "encrypted_zero_ciphertext": "7c1cddebb73720e0", + "first_8_bytes_ciphertext": "64bf90c5ed6aa99c", + "tried_and_failed": "Common TechMed / Pectus / MSoft / 3DsizeMe passwords with DES, 3DES, Blowfish, CAST, RC2, TEA, XTEA, and MD5/SHA-derived keys. No key recovered from the file alone." + }, + "size_hypotheses": [ + { + "allocation_bytes": 756000, + "record_bytes": 24, + "allocated_records": 31500, + "used_bytes": 565512, + "used_records": 23563, + "interpretation": "24-byte vertices (xyz + normal) \u2014 23,563 used of 31,500 reserved. Fits a Structure Sensor torso surface well." + }, + { + "allocation_bytes": 756000, + "record_bytes": 36, + "allocated_records": 21000, + "used_bytes": 565512, + "used_records": null, + "interpretation": "36-byte vertices (xyz + normal + rgb) \u2014 565512 is not an integer number of 36-byte records, so the used region would need a header or mixed record types." + }, + { + "allocation_bytes": 756000, + "record_bytes": 12, + "used_records": 47126, + "interpretation": "xyz-only point cloud, 47,126 points." + } + ], + "regions": [ + { + "name": "encrypted_payload", + "offset": 0, + "end": 565512, + "length": 565512, + "description": "High-entropy 8-byte ECB ciphertext. Almost every 8-byte block is unique (70686/70689). This is the 3D surface (and likely photos / landmarks / patient fields)." + }, + { + "name": "encrypted_zero_block", + "offset": 565512, + "end": 565520, + "length": 8, + "ciphertext_hex": "7c1cddebb73720e0", + "description": "Ciphertext of an all-zero (or constant-fill) 8-byte plaintext. Acts as a known ECB pair." + }, + { + "name": "encrypted_trailer", + "offset": 565520, + "end": 565648, + "length": 128, + "description": "16 additional 8-byte ciphertext blocks. Two consecutive blocks at +88/+96 are identical (32232bfa237396b2), so two identical plaintext records sit here. Likely counts, bbox, checksum, or metadata." + }, + { + "name": "encrypted_zero_padding", + "offset": 565648, + "end": 756000, + "length": 190352, + "repeating_block_hex": "7c1cddebb73720e0", + "repeating_block_count": 23794, + "description": "Preallocated buffer filled with zeros, then encrypted in 8-byte ECB. Reveals block size and that leftover vertex/face slots were unused." + }, + { + "name": "unencrypted_tail_zeros", + "offset": 756000, + "end": 756064, + "length": 64, + "description": "Raw 64 zero bytes, not passed through the cipher." + } + ], + "entropy": { + "payload_chi_square_vs_uniform": 300.59, + "payload_chi_square_expected": 255, + "note": "Byte histogram is nearly flat (counts 2034\u20132342). Consistent with encryption, not raw floats or plaintext mesh." + } +} diff --git a/decoded/header_64b.hex.txt b/decoded/header_64b.hex.txt new file mode 100644 index 0000000..7d5f0b3 --- /dev/null +++ b/decoded/header_64b.hex.txt @@ -0,0 +1,6 @@ +First 64 bytes of ciphertext (no recognizable magic): + +00000000 64 bf 90 c5 ed 6a a9 9c 54 9c 7f 84 75 79 68 c1 d....j..T...uyh. +00000010 83 0f 39 e4 11 dc ff d1 02 d6 e5 15 d1 49 61 6b ..9..........Iak +00000020 2a a9 01 97 26 72 c6 05 ee 65 62 20 27 27 87 8e *...&r...eb ''.. +00000030 8c 41 a0 6a c8 57 85 73 04 43 95 79 5b d9 4c 1d .A.j.W.s.C.y[.L. diff --git a/decoded/metadata.json b/decoded/metadata.json new file mode 100644 index 0000000..b64825b --- /dev/null +++ b/decoded/metadata.json @@ -0,0 +1,39 @@ +{ + "source_file": "Pectus Clinic - Butcher, Thomas - Torso - Original 20181023, 165650.med", + "source_size_bytes": 756064, + "md5": "bcfa924bb48ad80fb9ffccf13a1fff16", + "sha256": "3fb7fa74cf613a7897b8425f969280f1953cee049551da4de9a7766915452957", + "decoded_at": "2026-09-19T13:13:41.969688+00:00", + "identity": { + "practice_or_app_brand": "Pectus Clinic", + "patient_last_name": "Butcher", + "patient_first_name": "Thomas", + "body_part": "Torso", + "scan_type": "Original", + "captured_local": "2018-10-23T16:56:50", + "captured_date": "2018-10-23", + "captured_time": "16:56:50", + "notes": "Filename follows TechMed 3D / 3DsizeMe / My3DsizeMe naming: '{Practice} - {Last}, {First} - {BodyPart} - {ScanType} {YYYYMMDD}, {HHMMSS}.med'" + }, + "software": { + "likely_capture_app": "Pectus Scan (TechMed 3D white-label of 3DsizeMe / My3DsizeMe)", + "app_store_id": 1435160086, + "app_first_release": "2018-10-08", + "scan_date_relative_to_app_launch": "15 days after Pectus Scan 1.0.0", + "vendor": "TechMed 3D Inc. (now Qwadra Qu\u00e9bec)", + "sibling_formats": [ + ".iMed (raw capture from 3DsizeMe)", + ".med (MSoft processed / re-openable proprietary mesh)" + ], + "downstream_analysis_software": "Pectus Services T-Scan (measures deformity for T-Joe brace fabrication)", + "official_export_formats": [ + "STL", + "OBJ", + "PLY", + "VRML", + "ASCII", + "AOP" + ], + "hardware": "Occipital Structure Sensor on iPad (white-light / structured-light surface scan, no ionizing radiation)" + } +} diff --git a/decoded/trailer_128b.hex.txt b/decoded/trailer_128b.hex.txt new file mode 100644 index 0000000..60f53dc --- /dev/null +++ b/decoded/trailer_128b.hex.txt @@ -0,0 +1,29 @@ +Encrypted trailer at offset 565520 (128 bytes, 16 DES-sized blocks). +Blocks 11 and 12 are identical: 32232bfa237396b2 + +0008a110 73 8f d0 c0 94 48 c4 f4 de 13 4f c0 19 7c b2 cf s....H....O..|.. +0008a120 ec 7f db 18 16 15 2e 6f fd 30 f4 95 a8 55 b8 d5 .......o.0...U.. +0008a130 65 f0 11 03 32 98 b3 97 28 c4 c1 bc 01 c1 bd d1 e...2...(....... +0008a140 e5 20 73 85 5f cf 3f a6 27 9d 0e 93 93 05 f3 1b . s._.?.'....... +0008a150 70 1f cd 61 a0 f1 67 86 70 79 c0 c3 b8 ca b1 cf p..a..g.py...... +0008a160 e5 80 fe fd 10 a8 39 0e 32 23 2b fa 23 73 96 b2 ......9.2#+.#s.. +0008a170 32 23 2b fa 23 73 96 b2 63 1e 74 24 b8 d7 ab 24 2#+.#s..c.t$...$ +0008a180 e7 bf 55 83 f1 4c 3f fc 48 fc a2 cb 80 fd 81 83 ..U..L?.H....... + +8-byte blocks: + [00] 738fd0c09448c4f4 + [01] de134fc0197cb2cf + [02] ec7fdb1816152e6f + [03] fd30f495a855b8d5 + [04] 65f011033298b397 + [05] 28c4c1bc01c1bdd1 + [06] e52073855fcf3fa6 + [07] 279d0e939305f31b + [08] 701fcd61a0f16786 + [09] 7079c0c3b8cab1cf + [10] e580fefd10a8390e + [11] 32232bfa237396b2 + [12] 32232bfa237396b2 + [13] 631e7424b8d7ab24 + [14] e7bf5583f14c3ffc + [15] 48fca2cb80fd8183 diff --git a/native/common/des.c b/native/common/des.c new file mode 100644 index 0000000..1cb3617 --- /dev/null +++ b/native/common/des.c @@ -0,0 +1,148 @@ +#include "des.h" + +/* DES bit numbering: bit 1 is the MSB of a 64-bit word. Tables match the Metal kernel. */ + +static const uint8_t IP_TBL[64] = { + 58,50,42,34,26,18,10, 2, 60,52,44,36,28,20,12, 4, + 62,54,46,38,30,22,14, 6, 64,56,48,40,32,24,16, 8, + 57,49,41,33,25,17, 9, 1, 59,51,43,35,27,19,11, 3, + 61,53,45,37,29,21,13, 5, 63,55,47,39,31,23,15, 7 +}; +static const uint8_t FP_TBL[64] = { + 40, 8,48,16,56,24,64,32, 39, 7,47,15,55,23,63,31, + 38, 6,46,14,54,22,62,30, 37, 5,45,13,53,21,61,29, + 36, 4,44,12,52,20,60,28, 35, 3,43,11,51,19,59,27, + 34, 2,42,10,50,18,58,26, 33, 1,41, 9,49,17,57,25 +}; +static const uint8_t E_TBL[48] = { + 32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9, + 8, 9,10,11,12,13, 12,13,14,15,16,17, + 16,17,18,19,20,21, 20,21,22,23,24,25, + 24,25,26,27,28,29, 28,29,30,31,32, 1 +}; +static const uint8_t P_TBL[32] = { + 16, 7,20,21,29,12,28,17, 1,15,23,26, 5,18,31,10, + 2, 8,24,14,32,27, 3, 9, 19,13,30, 6,22,11, 4,25 +}; +static const uint8_t PC1_TBL[56] = { + 57,49,41,33,25,17, 9, 1,58,50,42,34,26,18, + 10, 2,59,51,43,35,27, 19,11, 3,60,52,44,36, + 63,55,47,39,31,23,15, 7,62,54,46,38,30,22, + 14, 6,61,53,45,37,29, 21,13, 5,28,20,12, 4 +}; +static const uint8_t PC2_TBL[48] = { + 14,17,11,24, 1, 5, 3,28,15, 6,21,10, + 23,19,12, 4,26, 8, 16, 7,27,20,13, 2, + 41,52,31,37,47,55, 30,40,51,45,33,48, + 44,49,39,56,34,53, 46,42,50,36,29,32 +}; +static const uint8_t SHIFTS[16] = {1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1}; + +static const uint8_t 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} +}; + +static uint64_t perm(uint64_t src, const uint8_t *tbl, uint32_t nout, uint32_t srcbits) { + uint64_t outv = 0; + uint32_t i; + for (i = 0; i < nout; i++) { + uint64_t bit = (src >> (srcbits - tbl[i])) & 1ULL; + outv = (outv << 1) | bit; + } + return outv; +} + +static uint32_t rotl28(uint32_t v, uint32_t s) { + return ((v << s) | (v >> (28 - s))) & 0x0FFFFFFFu; +} + +static uint32_t feistel(uint32_t r, uint64_t subkey) { + uint64_t er = perm((uint64_t)r, E_TBL, 48, 32) ^ subkey; + uint32_t s = 0; + uint32_t i; + for (i = 0; i < 8; i++) { + uint32_t chunk = (uint32_t)((er >> (42 - 6 * i)) & 0x3F); + uint32_t row = ((chunk & 0x20) >> 4) | (chunk & 1); + uint32_t col = (chunk >> 1) & 0xF; + s = (s << 4) | (uint32_t)SBOX[i][row * 16 + col]; + } + return (uint32_t)perm((uint64_t)s, P_TBL, 32, 32); +} + +void des_key_schedule(uint64_t key, uint64_t sk[16]) { + uint64_t cd = perm(key, PC1_TBL, 56, 64); + uint32_t c = (uint32_t)(cd >> 28); + uint32_t d = (uint32_t)(cd & 0x0FFFFFFFULL); + uint32_t r; + for (r = 0; r < 16; r++) { + c = rotl28(c, SHIFTS[r]); + d = rotl28(d, SHIFTS[r]); + uint64_t cd2 = ((uint64_t)c << 28) | (uint64_t)d; + sk[r] = perm(cd2, PC2_TBL, 48, 56); + } +} + +uint64_t des_crypt(uint64_t block, const uint64_t sk[16], int decrypt) { + uint64_t ip = perm(block, IP_TBL, 64, 64); + uint32_t l = (uint32_t)(ip >> 32); + uint32_t r = (uint32_t)(ip & 0xFFFFFFFFULL); + uint32_t i; + for (i = 0; i < 16; i++) { + uint32_t rnd = decrypt ? (15 - i) : i; + uint32_t n = l ^ feistel(r, sk[rnd]); + l = r; + r = n; + } + uint64_t pre = ((uint64_t)r << 32) | (uint64_t)l; + return perm(pre, FP_TBL, 64, 64); +} + +uint64_t make_key(uint64_t index, uint32_t key_len, uint32_t charset_len, uint32_t pad_byte, + const uint8_t *charset) { + uint8_t bytes[8]; + uint32_t i; + int pos; + uint64_t n = index; + uint64_t key = 0; + for (i = 0; i < 8; i++) { + bytes[i] = (uint8_t)pad_byte; + } + for (pos = (int)key_len - 1; pos >= 0; pos--) { + bytes[pos] = charset[n % charset_len]; + n /= charset_len; + } + for (i = 0; i < 8; i++) { + key = (key << 8) | (uint64_t)bytes[i]; + } + return key; +} + +int is_fill(uint64_t pt, const uint64_t *fills, uint32_t fill_count) { + uint32_t i; + for (i = 0; i < fill_count; i++) { + if (pt == fills[i]) { + return 1; + } + } + return 0; +} + +int des_selftest(void) { + uint64_t key = 0x133457799BBCDFF1ULL; + uint64_t pt = 0x0123456789ABCDEFULL; + uint64_t expected = 0x85E813540F0AB405ULL; + uint64_t sk[16]; + uint64_t ct; + uint64_t roundtrip; + des_key_schedule(key, sk); + ct = des_crypt(pt, sk, 0); + roundtrip = des_crypt(ct, sk, 1); + return ct == expected && roundtrip == pt; +} diff --git a/native/common/des.h b/native/common/des.h new file mode 100644 index 0000000..9e1b7d7 --- /dev/null +++ b/native/common/des.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void des_key_schedule(uint64_t key, uint64_t sk[16]); +uint64_t des_crypt(uint64_t block, const uint64_t sk[16], int decrypt); +uint64_t make_key(uint64_t index, uint32_t key_len, uint32_t charset_len, uint32_t pad_byte, + const uint8_t *charset); +int is_fill(uint64_t pt, const uint64_t *fills, uint32_t fill_count); +int des_selftest(void); + +#ifdef __cplusplus +} +#endif diff --git a/native/cpu/med_cpu.cpp b/native/cpu/med_cpu.cpp new file mode 100644 index 0000000..ec35ac3 --- /dev/null +++ b/native/cpu/med_cpu.cpp @@ -0,0 +1,197 @@ +#include "../common/des.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +static void fail(const char *msg) { + std::fprintf(stderr, "%s\n", msg); + std::exit(2); +} + +static uint64_t parse_hex_u64(const char *hex) { + const char *p = hex; + if (p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) { + p += 2; + } + return std::strtoull(p, nullptr, 16); +} + +static std::map parse_opts(int argc, char **argv) { + std::map opts; + for (int i = 2; i + 1 < argc; i += 2) { + if (std::strncmp(argv[i], "--", 2) != 0) { + i -= 1; + continue; + } + opts[argv[i] + 2] = argv[i + 1]; + } + return opts; +} + +static unsigned cpu_count() { + unsigned n = std::thread::hardware_concurrency(); + return n ? n : 1; +} + +static void run_devices() { + unsigned n = cpu_count(); + std::printf("[{\"id\":0,\"name\":\"CPU (%u cores)\",\"type\":\"cpu\",\"cores\":%u}]\n", n, n); +} + +static void run_selftest() { + if (!des_selftest()) { + fail("DES self-test failed"); + } + std::fprintf(stderr, "cpu_selftest ok cores=%u\n", cpu_count()); +} + +static void sleep_ms(int ms) { +#ifdef _WIN32 + Sleep((DWORD)ms); +#else + usleep((useconds_t)ms * 1000); +#endif +} + +static void run_brute(const std::map &opts) { + uint32_t key_len = (uint32_t)std::strtoul(opts.count("key-len") ? opts.at("key-len").c_str() : "0", nullptr, 10); + std::string charset = opts.count("charset") ? opts.at("charset") : ""; + uint32_t pad_byte = (uint32_t)(parse_hex_u64(opts.count("pad") ? opts.at("pad").c_str() : "00") & 0xFF); + uint64_t target = parse_hex_u64(opts.count("target") ? opts.at("target").c_str() : "0"); + uint64_t start = std::strtoull(opts.count("start") ? opts.at("start").c_str() : "0", nullptr, 10); + uint64_t count = std::strtoull(opts.count("count") ? opts.at("count").c_str() : "0", nullptr, 10); + unsigned workers = (unsigned)std::strtoul(opts.count("workers") ? opts.at("workers").c_str() : "0", nullptr, 10); + if (key_len == 0 || charset.empty() || count == 0) { + fail("brute requires --key-len --charset --count"); + } + if (workers == 0) { + workers = cpu_count(); + } + + uint64_t fills[8] = {0}; + uint32_t nfills = 0; + std::string fills_str = opts.count("fills") ? opts.at("fills") : "0000000000000000"; + std::string token; + for (size_t i = 0, begin = 0; i <= fills_str.size(); i++) { + if (i == fills_str.size() || fills_str[i] == ',') { + token = fills_str.substr(begin, i - begin); + if (!token.empty() && nfills < 8) { + fills[nfills++] = parse_hex_u64(token.c_str()); + } + begin = i + 1; + } + } + + const uint8_t *cs = reinterpret_cast(charset.data()); + uint32_t clen = (uint32_t)charset.size(); + std::atomic done{0}; + std::mutex out_mu; + + auto worker_fn = [&](uint64_t wstart, uint64_t wcount) { + uint64_t local = 0; + for (uint64_t i = 0; i < wcount; i++) { + 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)) { + std::lock_guard lock(out_mu); + std::printf("{\"index\":%llu,\"key_hex\":\"%016llx\",\"plain_hex\":\"%016llx\"}\n", + (unsigned long long)index, (unsigned long long)key, (unsigned long long)pt); + std::fflush(stdout); + } + local++; + if ((local & 0x3FFF) == 0) { + done.fetch_add(0x4000, std::memory_order_relaxed); + local = 0; + } + } + if (local) { + done.fetch_add(local, std::memory_order_relaxed); + } + }; + + std::vector threads; + uint64_t chunk = count / workers; + uint64_t rem = count % workers; + uint64_t cursor = start; + auto t0 = std::chrono::steady_clock::now(); + std::atomic finished{false}; + + std::thread reporter([&]() { + while (!finished.load(std::memory_order_relaxed)) { + uint64_t d = done.load(std::memory_order_relaxed); + if (d > count) { + d = count; + } + auto now = std::chrono::steady_clock::now(); + double elapsed = std::chrono::duration(now - t0).count(); + if (elapsed < 1e-6) { + elapsed = 1e-6; + } + std::fprintf(stderr, "gpu %llu/%llu %.0f keys/s\n", (unsigned long long)d, + (unsigned long long)count, d / elapsed); + sleep_ms(200); + } + }); + + for (unsigned w = 0; w < workers; w++) { + uint64_t n = chunk + (w < rem ? 1 : 0); + if (n == 0) { + continue; + } + threads.emplace_back(worker_fn, cursor, n); + cursor += n; + } + for (auto &t : threads) { + t.join(); + } + finished.store(true, std::memory_order_relaxed); + reporter.join(); + + auto now = std::chrono::steady_clock::now(); + double elapsed = std::chrono::duration(now - t0).count(); + if (elapsed < 1e-6) { + elapsed = 1e-6; + } + std::fprintf(stderr, "gpu %llu/%llu %.0f keys/s\n", (unsigned long long)count, + (unsigned long long)count, count / elapsed); +} + +int main(int argc, char **argv) { + if (argc < 2) { + fail("usage: med_cpu devices | selftest | brute [options]"); + } + if (std::strcmp(argv[1], "devices") == 0) { + run_devices(); + return 0; + } + auto opts = parse_opts(argc, argv); + if (std::strcmp(argv[1], "selftest") == 0) { + run_selftest(); + return 0; + } + if (std::strcmp(argv[1], "brute") == 0) { + run_selftest(); + run_brute(opts); + return 0; + } + fail("unknown command"); + return 2; +} diff --git a/native/cuda/med_cuda.cu b/native/cuda/med_cuda.cu new file mode 100644 index 0000000..ca7e9ac --- /dev/null +++ b/native/cuda/med_cuda.cu @@ -0,0 +1,381 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum { kMaxHits = 256, kThreadsPerGroup = 256 }; + +#define CUDA_CHECK(expr) \ + do { \ + cudaError_t _err = (expr); \ + if (_err != cudaSuccess) { \ + std::fprintf(stderr, "CUDA error %s:%d: %s\n", __FILE__, __LINE__, \ + cudaGetErrorString(_err)); \ + std::exit(2); \ + } \ + } while (0) + +struct Params { + uint64_t start; + uint32_t key_len; + uint32_t charset_len; + uint32_t pad_byte; + uint32_t fill_count; + uint32_t batch_count; + uint32_t _pad; + uint64_t target; + uint64_t fills[8]; +}; + +struct Hit { + uint64_t index; + uint64_t key; + uint64_t plain; +}; + +static void fail(const char *msg) { + std::fprintf(stderr, "%s\n", msg); + std::exit(2); +} + +static uint64_t parse_hex_u64(const char *hex) { + const char *p = hex; + if (p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) p += 2; + return std::strtoull(p, nullptr, 16); +} + +static std::map parse_opts(int argc, char **argv) { + std::map opts; + for (int i = 2; i + 1 < argc; i += 2) { + if (std::strncmp(argv[i], "--", 2) != 0) { + i -= 1; + continue; + } + opts[argv[i] + 2] = argv[i + 1]; + } + return opts; +} + +static void json_escape(const char *src, char *dst, size_t dstsz) { + size_t o = 0; + for (size_t i = 0; src[i] && o + 2 < dstsz; i++) { + unsigned char c = (unsigned char)src[i]; + if (c == '"' || c == '\\') { + dst[o++] = '\\'; + dst[o++] = (char)c; + } else if (c < 32) { + dst[o++] = '?'; + } else { + dst[o++] = (char)c; + } + } + dst[o] = 0; +} + +__device__ __constant__ unsigned char IP_TBL[64] = { + 58,50,42,34,26,18,10, 2, 60,52,44,36,28,20,12, 4, + 62,54,46,38,30,22,14, 6, 64,56,48,40,32,24,16, 8, + 57,49,41,33,25,17, 9, 1, 59,51,43,35,27,19,11, 3, + 61,53,45,37,29,21,13, 5, 63,55,47,39,31,23,15, 7 +}; +__device__ __constant__ unsigned char FP_TBL[64] = { + 40, 8,48,16,56,24,64,32, 39, 7,47,15,55,23,63,31, + 38, 6,46,14,54,22,62,30, 37, 5,45,13,53,21,61,29, + 36, 4,44,12,52,20,60,28, 35, 3,43,11,51,19,59,27, + 34, 2,42,10,50,18,58,26, 33, 1,41, 9,49,17,57,25 +}; +__device__ __constant__ unsigned char E_TBL[48] = { + 32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9, + 8, 9,10,11,12,13, 12,13,14,15,16,17, + 16,17,18,19,20,21, 20,21,22,23,24,25, + 24,25,26,27,28,29, 28,29,30,31,32, 1 +}; +__device__ __constant__ unsigned char P_TBL[32] = { + 16, 7,20,21,29,12,28,17, 1,15,23,26, 5,18,31,10, + 2, 8,24,14,32,27, 3, 9, 19,13,30, 6,22,11, 4,25 +}; +__device__ __constant__ unsigned char PC1_TBL[56] = { + 57,49,41,33,25,17, 9, 1,58,50,42,34,26,18, + 10, 2,59,51,43,35,27, 19,11, 3,60,52,44,36, + 63,55,47,39,31,23,15, 7,62,54,46,38,30,22, + 14, 6,61,53,45,37,29, 21,13, 5,28,20,12, 4 +}; +__device__ __constant__ unsigned char PC2_TBL[48] = { + 14,17,11,24, 1, 5, 3,28,15, 6,21,10, + 23,19,12, 4,26, 8, 16, 7,27,20,13, 2, + 41,52,31,37,47,55, 30,40,51,45,33,48, + 44,49,39,56,34,53, 46,42,50,36,29,32 +}; +__device__ __constant__ unsigned char SHIFTS[16] = {1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1}; +__device__ __constant__ unsigned char 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} +}; + +__device__ uint64_t perm(uint64_t src, const unsigned char *tbl, unsigned nout, unsigned srcbits) { + uint64_t outv = 0; + for (unsigned i = 0; i < nout; i++) { + uint64_t bit = (src >> (srcbits - tbl[i])) & 1ULL; + outv = (outv << 1) | bit; + } + return outv; +} + +__device__ unsigned rotl28(unsigned v, unsigned s) { + return ((v << s) | (v >> (28 - s))) & 0x0FFFFFFFu; +} + +__device__ void des_key_schedule(uint64_t key, uint64_t sk[16]) { + uint64_t cd = perm(key, PC1_TBL, 56, 64); + unsigned c = (unsigned)(cd >> 28); + unsigned d = (unsigned)(cd & 0x0FFFFFFFULL); + for (unsigned r = 0; r < 16; r++) { + c = rotl28(c, SHIFTS[r]); + d = rotl28(d, SHIFTS[r]); + uint64_t cd2 = ((uint64_t)c << 28) | (uint64_t)d; + sk[r] = perm(cd2, PC2_TBL, 48, 56); + } +} + +__device__ unsigned feistel(unsigned r, uint64_t subkey) { + uint64_t er = perm((uint64_t)r, E_TBL, 48, 32) ^ subkey; + unsigned s = 0; + for (unsigned i = 0; i < 8; i++) { + unsigned chunk = (unsigned)((er >> (42 - 6 * i)) & 0x3F); + unsigned row = ((chunk & 0x20) >> 4) | (chunk & 1); + unsigned col = (chunk >> 1) & 0xF; + s = (s << 4) | (unsigned)SBOX[i][row * 16 + col]; + } + return (unsigned)perm((uint64_t)s, P_TBL, 32, 32); +} + +__device__ uint64_t des_crypt(uint64_t block, const uint64_t sk[16], int decrypt) { + uint64_t ip = perm(block, IP_TBL, 64, 64); + unsigned l = (unsigned)(ip >> 32); + unsigned r = (unsigned)(ip & 0xFFFFFFFFULL); + for (unsigned i = 0; i < 16; i++) { + unsigned rnd = decrypt ? (15 - i) : i; + unsigned n = l ^ feistel(r, sk[rnd]); + l = r; + r = n; + } + uint64_t pre = ((uint64_t)r << 32) | (uint64_t)l; + return perm(pre, FP_TBL, 64, 64); +} + +__device__ uint64_t make_key(uint64_t index, const Params &p, const unsigned char *charset) { + unsigned char bytes[8]; + for (int i = 0; i < 8; i++) { + bytes[i] = (unsigned char)p.pad_byte; + } + uint64_t n = index; + unsigned clen = p.charset_len; + for (int pos = (int)p.key_len - 1; pos >= 0; pos--) { + bytes[pos] = charset[n % clen]; + n /= clen; + } + uint64_t key = 0; + for (int i = 0; i < 8; i++) { + key = (key << 8) | (uint64_t)bytes[i]; + } + return key; +} + +__device__ int is_fill(uint64_t pt, const Params &p) { + for (unsigned i = 0; i < p.fill_count; i++) { + if (pt == p.fills[i]) { + return 1; + } + } + return 0; +} + +__global__ void des_known_answer(uint64_t *out) { + if (threadIdx.x != 0 || blockIdx.x != 0) return; + uint64_t key = 0x133457799BBCDFF1ULL; + uint64_t pt = 0x0123456789ABCDEFULL; + uint64_t sk[16]; + des_key_schedule(key, sk); + out[0] = des_crypt(pt, sk, 0); + out[1] = des_crypt(out[0], sk, 1); +} + +__global__ void des_brute(Params params, const unsigned char *charset, Hit *hits, unsigned *hit_count) { + unsigned gid = blockIdx.x * blockDim.x + threadIdx.x; + if (gid >= params.batch_count) return; + uint64_t index = params.start + (uint64_t)gid; + uint64_t key = make_key(index, params, charset); + uint64_t sk[16]; + des_key_schedule(key, sk); + uint64_t pt = des_crypt(params.target, sk, 1); + if (!is_fill(pt, params)) return; + unsigned slot = atomicAdd(hit_count, 1u); + if (slot < 256) { + hits[slot].index = index; + hits[slot].key = key; + hits[slot].plain = pt; + } +} + +static int pick_device(const std::map &opts) { + int count = 0; + CUDA_CHECK(cudaGetDeviceCount(&count)); + if (count <= 0) fail("no CUDA GPU"); + int idx = opts.count("device") ? std::atoi(opts.at("device").c_str()) : 0; + if (idx < 0 || idx >= count) idx = 0; + CUDA_CHECK(cudaSetDevice(idx)); + return idx; +} + +static void run_devices() { + int count = 0; + cudaError_t err = cudaGetDeviceCount(&count); + if (err != cudaSuccess) { + std::printf("[]\n"); + return; + } + std::printf("["); + for (int i = 0; i < count; i++) { + cudaDeviceProp prop; + CUDA_CHECK(cudaGetDeviceProperties(&prop, i)); + char name[512]; + json_escape(prop.name, name, sizeof(name)); + std::printf("%s{\"id\":%d,\"name\":\"%s\",\"type\":\"cuda\",\"memory\":%llu}", + i ? "," : "", i, name, (unsigned long long)prop.totalGlobalMem); + } + std::printf("]\n"); +} + +static void run_selftest() { + uint64_t *out = nullptr; + CUDA_CHECK(cudaMalloc(&out, 16)); + des_known_answer<<<1, 1>>>(out); + CUDA_CHECK(cudaDeviceSynchronize()); + uint64_t words[2]; + CUDA_CHECK(cudaMemcpy(words, out, 16, cudaMemcpyDeviceToHost)); + CUDA_CHECK(cudaFree(out)); + uint64_t expected = 0x85E813540F0AB405ULL; + uint64_t plain = 0x0123456789ABCDEFULL; + if (words[0] != expected || words[1] != plain) { + std::fprintf(stderr, "DES self-test failed ct=%016llx expected=%016llx roundtrip=%016llx\n", + (unsigned long long)words[0], (unsigned long long)expected, + (unsigned long long)words[1]); + std::exit(2); + } + cudaDeviceProp prop; + int dev = 0; + CUDA_CHECK(cudaGetDevice(&dev)); + CUDA_CHECK(cudaGetDeviceProperties(&prop, dev)); + std::fprintf(stderr, "cuda_selftest ok gpu=%s\n", prop.name); +} + +static void run_brute(const std::map &opts) { + uint32_t key_len = (uint32_t)std::strtoul(opts.count("key-len") ? opts.at("key-len").c_str() : "0", nullptr, 10); + std::string charset = opts.count("charset") ? opts.at("charset") : ""; + uint32_t pad_byte = (uint32_t)(parse_hex_u64(opts.count("pad") ? opts.at("pad").c_str() : "00") & 0xFF); + uint64_t target = parse_hex_u64(opts.count("target") ? opts.at("target").c_str() : "0"); + uint64_t start = std::strtoull(opts.count("start") ? opts.at("start").c_str() : "0", nullptr, 10); + uint64_t count = std::strtoull(opts.count("count") ? opts.at("count").c_str() : "0", nullptr, 10); + uint64_t batch = std::strtoull(opts.count("batch") ? opts.at("batch").c_str() : "16777216", nullptr, 10); + if (key_len == 0 || charset.empty() || count == 0) { + fail("brute requires --key-len --charset --count"); + } + + Params params{}; + params.key_len = key_len; + params.charset_len = (uint32_t)charset.size(); + params.pad_byte = pad_byte; + params.target = target; + std::string fills_str = opts.count("fills") ? opts.at("fills") : "0000000000000000"; + uint32_t nfills = 0; + std::string token; + for (size_t i = 0, begin = 0; i <= fills_str.size(); i++) { + if (i == fills_str.size() || fills_str[i] == ',') { + token = fills_str.substr(begin, i - begin); + if (!token.empty() && nfills < 8) { + params.fills[nfills++] = parse_hex_u64(token.c_str()); + } + begin = i + 1; + } + } + params.fill_count = nfills; + + unsigned char *d_charset = nullptr; + Hit *d_hits = nullptr; + unsigned *d_hitn = nullptr; + CUDA_CHECK(cudaMalloc(&d_charset, charset.size())); + CUDA_CHECK(cudaMemcpy(d_charset, charset.data(), charset.size(), cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMalloc(&d_hits, kMaxHits * sizeof(Hit))); + CUDA_CHECK(cudaMalloc(&d_hitn, sizeof(unsigned))); + + uint64_t done = 0; + auto t0 = std::chrono::steady_clock::now(); + std::vector host_hits(kMaxHits); + while (done < count) { + uint64_t n = batch < (count - done) ? batch : (count - done); + params.start = start + done; + params.batch_count = (uint32_t)n; + unsigned zero = 0; + CUDA_CHECK(cudaMemcpy(d_hitn, &zero, sizeof(zero), cudaMemcpyHostToDevice)); + unsigned groups = (unsigned)((n + kThreadsPerGroup - 1) / kThreadsPerGroup); + des_brute<<>>(params, d_charset, d_hits, d_hitn); + CUDA_CHECK(cudaDeviceSynchronize()); + unsigned hitn = 0; + CUDA_CHECK(cudaMemcpy(&hitn, d_hitn, sizeof(hitn), cudaMemcpyDeviceToHost)); + if (hitn > kMaxHits) hitn = kMaxHits; + if (hitn) { + CUDA_CHECK(cudaMemcpy(host_hits.data(), d_hits, hitn * sizeof(Hit), cudaMemcpyDeviceToHost)); + for (unsigned i = 0; i < hitn; i++) { + std::printf("{\"index\":%llu,\"key_hex\":\"%016llx\",\"plain_hex\":\"%016llx\"}\n", + (unsigned long long)host_hits[i].index, + (unsigned long long)host_hits[i].key, + (unsigned long long)host_hits[i].plain); + std::fflush(stdout); + } + } + done += n; + auto now = std::chrono::steady_clock::now(); + double elapsed = std::chrono::duration(now - t0).count(); + if (elapsed < 1e-6) elapsed = 1e-6; + std::fprintf(stderr, "gpu %llu/%llu %.0f keys/s\n", (unsigned long long)done, + (unsigned long long)count, done / elapsed); + } + + CUDA_CHECK(cudaFree(d_charset)); + CUDA_CHECK(cudaFree(d_hits)); + CUDA_CHECK(cudaFree(d_hitn)); +} + +int main(int argc, char **argv) { + if (argc < 2) { + fail("usage: med_cuda devices | selftest [--device N] | brute [--device N] [options]"); + } + if (std::strcmp(argv[1], "devices") == 0) { + run_devices(); + return 0; + } + auto opts = parse_opts(argc, argv); + pick_device(opts); + if (std::strcmp(argv[1], "selftest") == 0) { + run_selftest(); + return 0; + } + if (std::strcmp(argv[1], "brute") == 0) { + run_selftest(); + run_brute(opts); + return 0; + } + fail("unknown command"); + return 2; +} diff --git a/native/metal/des_bruteforce.metal b/native/metal/des_bruteforce.metal new file mode 100644 index 0000000..2f5818c --- /dev/null +++ b/native/metal/des_bruteforce.metal @@ -0,0 +1,178 @@ +#include +using namespace metal; + +// DES bit numbering: bit 1 is the MSB of a 64-bit word. + +constant uchar IP_TBL[64] = { + 58,50,42,34,26,18,10, 2, 60,52,44,36,28,20,12, 4, + 62,54,46,38,30,22,14, 6, 64,56,48,40,32,24,16, 8, + 57,49,41,33,25,17, 9, 1, 59,51,43,35,27,19,11, 3, + 61,53,45,37,29,21,13, 5, 63,55,47,39,31,23,15, 7 +}; +constant uchar FP_TBL[64] = { + 40, 8,48,16,56,24,64,32, 39, 7,47,15,55,23,63,31, + 38, 6,46,14,54,22,62,30, 37, 5,45,13,53,21,61,29, + 36, 4,44,12,52,20,60,28, 35, 3,43,11,51,19,59,27, + 34, 2,42,10,50,18,58,26, 33, 1,41, 9,49,17,57,25 +}; +constant uchar E_TBL[48] = { + 32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9, + 8, 9,10,11,12,13, 12,13,14,15,16,17, + 16,17,18,19,20,21, 20,21,22,23,24,25, + 24,25,26,27,28,29, 28,29,30,31,32, 1 +}; +constant uchar P_TBL[32] = { + 16, 7,20,21,29,12,28,17, 1,15,23,26, 5,18,31,10, + 2, 8,24,14,32,27, 3, 9, 19,13,30, 6,22,11, 4,25 +}; +constant uchar PC1_TBL[56] = { + 57,49,41,33,25,17, 9, 1,58,50,42,34,26,18, + 10, 2,59,51,43,35,27, 19,11, 3,60,52,44,36, + 63,55,47,39,31,23,15, 7,62,54,46,38,30,22, + 14, 6,61,53,45,37,29, 21,13, 5,28,20,12, 4 +}; +constant uchar PC2_TBL[48] = { + 14,17,11,24, 1, 5, 3,28,15, 6,21,10, + 23,19,12, 4,26, 8, 16, 7,27,20,13, 2, + 41,52,31,37,47,55, 30,40,51,45,33,48, + 44,49,39,56,34,53, 46,42,50,36,29,32 +}; +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} +}; + +inline ulong perm(ulong src, constant uchar *tbl, uint nout, uint srcbits) { + ulong outv = 0; + for (uint i = 0; i < nout; i++) { + ulong bit = (src >> (srcbits - tbl[i])) & 1UL; + outv = (outv << 1) | bit; + } + return outv; +} + +inline uint rotl28(uint v, uint s) { + return ((v << s) | (v >> (28 - s))) & 0x0FFFFFFFu; +} + +inline void des_key_schedule(ulong key, thread ulong sk[16]) { + ulong cd = perm(key, PC1_TBL, 56, 64); + uint c = uint(cd >> 28); + uint d = uint(cd & 0x0FFFFFFFUL); + 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); + } +} + +inline uint feistel(uint r, ulong subkey) { + ulong er = perm(ulong(r), E_TBL, 48, 32) ^ subkey; + uint s = 0; + 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]); + } + return uint(perm(ulong(s), P_TBL, 32, 32)); +} + +inline ulong des_crypt(ulong block, thread ulong sk[16], bool decrypt) { + ulong ip = perm(block, IP_TBL, 64, 64); + uint l = uint(ip >> 32); + uint r = uint(ip & 0xFFFFFFFFUL); + for (uint i = 0; i < 16; i++) { + uint rnd = decrypt ? (15 - i) : i; + uint n = l ^ feistel(r, sk[rnd]); + l = r; + r = n; + } + ulong pre = (ulong(r) << 32) | ulong(l); + return perm(pre, FP_TBL, 64, 64); +} + +struct Params { + ulong start; + uint key_len; + uint charset_len; + uint pad_byte; + uint fill_count; + uint batch_count; + uint _pad; + ulong target; + ulong fills[8]; +}; + +struct Hit { + ulong index; + ulong key; + ulong plain; +}; + +inline ulong make_key(ulong index, constant Params &p, const device uchar *charset) { + uchar bytes[8]; + for (uint i = 0; i < 8; i++) { + bytes[i] = uchar(p.pad_byte); + } + ulong n = index; + uint clen = p.charset_len; + for (int pos = int(p.key_len) - 1; pos >= 0; pos--) { + bytes[pos] = charset[n % clen]; + n /= clen; + } + ulong key = 0; + for (uint i = 0; i < 8; i++) { + key = (key << 8) | ulong(bytes[i]); + } + return key; +} + +inline bool is_fill(ulong pt, constant Params &p) { + for (uint i = 0; i < p.fill_count; i++) { + if (pt == p.fills[i]) { + return true; + } + } + return false; +} + +kernel void des_known_answer(device ulong *out [[buffer(0)]], + uint gid [[thread_position_in_grid]]) { + if (gid != 0) return; + ulong key = 0x133457799BBCDFF1UL; + ulong pt = 0x0123456789ABCDEFUL; + ulong sk[16]; + des_key_schedule(key, sk); + out[0] = des_crypt(pt, sk, false); + out[1] = des_crypt(out[0], sk, true); +} + +kernel void des_brute(constant Params ¶ms [[buffer(0)]], + const device uchar *charset [[buffer(1)]], + device Hit *hits [[buffer(2)]], + device atomic_uint *hit_count [[buffer(3)]], + uint gid [[thread_position_in_grid]]) { + if (gid >= params.batch_count) return; + ulong index = params.start + ulong(gid); + ulong key = make_key(index, params, charset); + ulong sk[16]; + des_key_schedule(key, sk); + ulong pt = des_crypt(params.target, sk, true); + if (!is_fill(pt, params)) return; + uint slot = atomic_fetch_add_explicit(hit_count, 1u, memory_order_relaxed); + if (slot < 256) { + hits[slot].index = index; + hits[slot].key = key; + hits[slot].plain = pt; + } +} diff --git a/native/metal/med_gpu.m b/native/metal/med_gpu.m new file mode 100644 index 0000000..8c6eae8 --- /dev/null +++ b/native/metal/med_gpu.m @@ -0,0 +1,255 @@ +#import +#import +#import +#import +#import + +enum { kMaxHits = 256, kThreadsPerGroup = 256, kParamsSize = 104 }; + +typedef struct { + uint64_t index; + uint64_t key; + uint64_t plain; +} Hit; + +static void fail(const char *msg) { + fprintf(stderr, "%s\n", msg); + exit(2); +} + +static uint64_t parse_hex_u64(const char *hex) { + const char *p = hex; + if (p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) p += 2; + return strtoull(p, NULL, 16); +} + +static NSDictionary *parse_opts(int argc, const char **argv) { + NSMutableDictionary *opts = [NSMutableDictionary dictionary]; + for (int i = 2; i + 1 < argc; i += 2) { + if (strncmp(argv[i], "--", 2) != 0) { + i -= 1; + continue; + } + opts[@(argv[i] + 2)] = @(argv[i + 1]); + } + return opts; +} + +static void json_escape(const char *src, char *dst, size_t dstsz) { + size_t o = 0; + for (size_t i = 0; src[i] && o + 2 < dstsz; i++) { + unsigned char c = (unsigned char)src[i]; + if (c == '"' || c == '\\') { + dst[o++] = '\\'; + dst[o++] = (char)c; + } else if (c < 32) { + dst[o++] = '?'; + } else { + dst[o++] = (char)c; + } + } + dst[o] = 0; +} + +static void write_params(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; + } +} + +static NSArray> *all_devices(void) { + NSArray> *devs = MTLCopyAllDevices(); + if (devs && devs.count > 0) { + return devs; + } + id def = MTLCreateSystemDefaultDevice(); + if (!def) { + return @[]; + } + return @[def]; +} + +static id pick_device(NSDictionary *opts) { + NSArray> *devs = all_devices(); + if (devs.count == 0) { + fail("no Metal GPU"); + } + int idx = opts[@"device"] ? opts[@"device"].intValue : 0; + if (idx < 0 || idx >= (int)devs.count) { + idx = 0; + } + return devs[(NSUInteger)idx]; +} + +static void run_devices(void) { + NSArray> *devs = all_devices(); + printf("["); + for (NSUInteger i = 0; i < devs.count; i++) { + char name[512]; + json_escape(devs[i].name.UTF8String ?: "Metal GPU", name, sizeof(name)); + uint64_t mem = 0; + if ([devs[i] respondsToSelector:@selector(recommendedMaxWorkingSetSize)]) { + mem = (uint64_t)devs[i].recommendedMaxWorkingSetSize; + } + printf("%s{\"id\":%lu,\"name\":\"%s\",\"type\":\"metal\",\"memory\":%llu}", + i ? "," : "", (unsigned long)i, name, (unsigned long long)mem); + } + printf("]\n"); +} + +static id compile_library(id device, NSString *path) { + NSError *err = nil; + NSString *src = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&err]; + if (!src) { + fprintf(stderr, "cannot read shader: %s\n", err.localizedDescription.UTF8String); + exit(2); + } + MTLCompileOptions *copts = [[MTLCompileOptions alloc] init]; + id lib = [device newLibraryWithSource:src options:copts error:&err]; + if (!lib) { + fprintf(stderr, "Metal compile failed:\n%s\n", err.localizedDescription.UTF8String); + exit(2); + } + return lib; +} + +static void run_selftest(id device, id lib) { + NSError *err = nil; + id fn = [lib newFunctionWithName:@"des_known_answer"]; + id pipe = [device newComputePipelineStateWithFunction:fn error:&err]; + if (!pipe) fail("failed to build DES known-answer pipeline"); + id queue = [device newCommandQueue]; + id out = [device newBufferWithLength:16 options:MTLResourceStorageModeShared]; + id cmd = [queue commandBuffer]; + id enc = [cmd computeCommandEncoder]; + [enc setComputePipelineState:pipe]; + [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) { + fprintf(stderr, "DES self-test failed ct=%016llx expected=%016llx roundtrip=%016llx\n", + words[0], expected, words[1]); + exit(2); + } + fprintf(stderr, "metal_selftest ok gpu=%s\n", device.name.UTF8String); +} + +static void run_brute(id device, id lib, NSDictionary *opts) { + NSError *err = nil; + id fn = [lib newFunctionWithName:@"des_brute"]; + id pipe = [device newComputePipelineStateWithFunction:fn error:&err]; + if (!pipe) fail("failed to build DES brute pipeline"); + id queue = [device newCommandQueue]; + + NSString *keyLenStr = opts[@"key-len"] ?: @"0"; + NSString *charsetStr = opts[@"charset"] ?: @""; + NSString *padStr = opts[@"pad"] ?: @"00"; + NSString *targetStr = opts[@"target"] ?: @"0"; + NSString *startStr = opts[@"start"] ?: @"0"; + NSString *countStr = opts[@"count"] ?: @"0"; + NSString *batchStr = opts[@"batch"] ?: @"16777216"; + uint32_t key_len = (uint32_t)keyLenStr.intValue; + const char *cs = charsetStr.UTF8String; + uint32_t charset_len = (uint32_t)strlen(cs); + uint32_t pad_byte = (uint32_t)(parse_hex_u64(padStr.UTF8String) & 0xFF); + uint64_t target = parse_hex_u64(targetStr.UTF8String); + uint64_t start = strtoull(startStr.UTF8String, NULL, 10); + uint64_t count = strtoull(countStr.UTF8String, NULL, 10); + uint64_t batch = strtoull(batchStr.UTF8String, NULL, 10); + if (key_len == 0 || charset_len == 0 || count == 0) fail("brute requires --key-len --charset --count"); + + uint64_t fills[8] = {0}; + int nfills = 0; + NSString *fillsStr = opts[@"fills"] ?: @"0000000000000000"; + NSArray *parts = [fillsStr componentsSeparatedByString:@","]; + for (NSString *part in parts) { + if (nfills >= 8) break; + fills[nfills++] = parse_hex_u64(part.UTF8String); + } + + id paramsBuf = [device newBufferWithLength:kParamsSize options:MTLResourceStorageModeShared]; + id charsetBuf = [device newBufferWithBytes:cs length:charset_len options:MTLResourceStorageModeShared]; + id hitsBuf = [device newBufferWithLength:kMaxHits * sizeof(Hit) options:MTLResourceStorageModeShared]; + id countBuf = [device newBufferWithLength:4 options:MTLResourceStorageModeShared]; + + uint64_t done = 0; + CFAbsoluteTime t0 = CFAbsoluteTimeGetCurrent(); + while (done < count) { + uint64_t n = batch < (count - done) ? batch : (count - done); + *(uint32_t *)countBuf.contents = 0; + write_params(paramsBuf.contents, start + done, key_len, charset_len, pad_byte, + (uint32_t)nfills, (uint32_t)n, target, fills, nfills); + + id cmd = [queue commandBuffer]; + id enc = [cmd computeCommandEncoder]; + [enc setComputePipelineState:pipe]; + [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 + kThreadsPerGroup - 1) / kThreadsPerGroup); + [enc dispatchThreadgroups:MTLSizeMake(groups, 1, 1) + threadsPerThreadgroup:MTLSizeMake(kThreadsPerGroup, 1, 1)]; + [enc endEncoding]; + [cmd commit]; + [cmd waitUntilCompleted]; + + uint32_t hitn = *(uint32_t *)countBuf.contents; + if (hitn > kMaxHits) hitn = kMaxHits; + Hit *hits = hitsBuf.contents; + for (uint32_t i = 0; i < hitn; i++) { + printf("{\"index\":%llu,\"key_hex\":\"%016llx\",\"plain_hex\":\"%016llx\"}\n", + hits[i].index, hits[i].key, hits[i].plain); + fflush(stdout); + } + done += n; + double elapsed = CFAbsoluteTimeGetCurrent() - t0; + if (elapsed < 1e-6) elapsed = 1e-6; + fprintf(stderr, "gpu %llu/%llu %.0f keys/s\n", done, count, done / elapsed); + } +} + +int main(int argc, const char **argv) { + if (argc < 2) { + fail("usage: med_gpu devices | selftest --shader PATH | brute --shader PATH [options]"); + } + @autoreleasepool { + if (strcmp(argv[1], "devices") == 0) { + run_devices(); + return 0; + } + NSDictionary *opts = parse_opts(argc, argv); + NSString *shader = opts[@"shader"]; + if (!shader) fail("missing --shader"); + id device = pick_device(opts); + id lib = compile_library(device, shader); + if (strcmp(argv[1], "selftest") == 0) { + run_selftest(device, lib); + } else if (strcmp(argv[1], "brute") == 0) { + run_selftest(device, lib); + run_brute(device, lib, opts); + } else { + fail("unknown command"); + } + } + return 0; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c239ecc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1368 @@ +{ + "name": "pectus-scan-converter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pectus-scan-converter", + "version": "1.0.0", + "dependencies": { + "@nodegui/nodegui": "^0.74.2", + "lucide-static": "^1.47.0", + "ws": "^8.18.3" + }, + "devDependencies": { + "@types/node": "^24.5.2", + "@types/ws": "^8.18.1", + "typescript": "^5.9.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodegui/artifact-installer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nodegui/artifact-installer/-/artifact-installer-1.1.0.tgz", + "integrity": "sha512-x/rIewhjnLhf2b3lAy5ZIPSTS39LRpwMTKeEg6sn4RwvWwyNmreGzpvGi6Gkbl6FUpftn70nfOBOpaeO3hO8PQ==", + "license": "ISC", + "dependencies": { + "7zip-min": "^1.1.1", + "env-paths": "^2.2.0", + "make-dir": "^3.0.0", + "node-fetch": "^2.6.0", + "progress": "^2.0.3" + } + }, + "node_modules/@nodegui/nodegui": { + "version": "0.74.2", + "resolved": "https://registry.npmjs.org/@nodegui/nodegui/-/nodegui-0.74.2.tgz", + "integrity": "sha512-YrKMXF+RZOCRo0z5UWtmVfWW9xLdQC82jewjyD4Jdb4Wbcez7YP+k4I6nL+bH0fflvvDOTFJmuvvxUM765xaqg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nodegui/artifact-installer": "^1.1.0", + "@nodegui/qode": "24.12.0-rc19", + "@paralleldrive/cuid2": "^3.3.0", + "cmake-js": "^7.4.0", + "cross-env": "^7.0.3", + "manage-path": "^2.0.0", + "memoize-one": "^5.2.1", + "mkdirp": "^3.0.1", + "node-addon-api": "^8.5.0", + "postcss": "^7.0.17", + "postcss-nodegui-autoprefixer": "0.0.7", + "tar": "^6.0.1" + }, + "bin": { + "qode": "scripts/qode.js" + }, + "engines": { + "node": ">=18.x.x" + }, + "funding": { + "url": "https://github.com/sponsors/a7ul" + } + }, + "node_modules/@nodegui/qode": { + "version": "24.12.0-rc19", + "resolved": "https://registry.npmjs.org/@nodegui/qode/-/qode-24.12.0-rc19.tgz", + "integrity": "sha512-18goj/U6XHHPO+NlyXOf0PuWYnNIAwsqfz7xFl7z+hTyDWKDXX9Ok+7G8uhMo5mhQG3fwPzWa9A8RYs/ni/w9A==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.1", + "progress": "^2.0.3", + "tar": "^6.1.6" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-3.3.0.tgz", + "integrity": "sha512-OqiFvSOF0dBSesELYY2CAMa4YINvlLpvKOz/rv6NeZEqiyttlHgv98Juwv4Ch+GrEV7IZ8jfI2VcEoYUjXXCjw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.0.1", + "bignumber.js": "^9.3.1", + "error-causes": "^3.0.2" + }, + "bin": { + "cuid2": "bin/cuid2.js" + } + }, + "node_modules/@types/node": { + "version": "24.13.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.6.tgz", + "integrity": "sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/7zip-bin": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz", + "integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==", + "license": "MIT" + }, + "node_modules/7zip-min": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/7zip-min/-/7zip-min-1.4.5.tgz", + "integrity": "sha512-S+FzNwJFKF5LgQYs+hPQo+qeffdi+259Ak63rWEfkHP9arsU8gbe5K+4HscuWN1ih1lP1gTjDNPddbU0qhPtHQ==", + "license": "MIT", + "dependencies": { + "7zip-bin": "5.1.1" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cmake-js": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/cmake-js/-/cmake-js-7.4.0.tgz", + "integrity": "sha512-Lw0JxEHrmk+qNj1n9W9d4IvkDdYTBn7l2BW6XmtLj7WPpIo2shvxUy+YokfjMxAAOELNonQwX3stkPhM5xSC2Q==", + "license": "MIT", + "dependencies": { + "axios": "^1.6.5", + "debug": "^4", + "fs-extra": "^11.2.0", + "memory-stream": "^1.0.0", + "node-api-headers": "^1.1.0", + "npmlog": "^6.0.2", + "rc": "^1.2.7", + "semver": "^7.5.4", + "tar": "^6.2.0", + "url-join": "^4.0.1", + "which": "^2.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "cmake-js": "bin/cmake-js" + }, + "engines": { + "node": ">= 14.15.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-causes": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/error-causes/-/error-causes-3.0.2.tgz", + "integrity": "sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lucide-static": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-1.47.0.tgz", + "integrity": "sha512-yWIrkdXc688Feq5VjOktsKmV5Ikc7y5Nu3rrdtbr8nWjkJWk8QlnZfVtIak22Af+fNhZ7k4cTJpZo1zmj7X5sA==", + "license": "ISC" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/manage-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/manage-path/-/manage-path-2.0.0.tgz", + "integrity": "sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, + "node_modules/memory-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/memory-stream/-/memory-stream-1.0.0.tgz", + "integrity": "sha512-Wm13VcsPIMdG96dzILfij09PvuS3APtcKNh7M28FsCA/w6+1mjR7hhPmfFNoilX9xU7wTdhsH5lJAm6XNzdtww==", + "license": "MIT", + "dependencies": { + "readable-stream": "^3.4.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz", + "integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-api-headers": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.9.0.tgz", + "integrity": "sha512-2oNILP4jXwRB4ywnYKjVk1YyJ96n2D4EOVJO6S3oYZ5PtbJrw3Yt9TpAuX3nBLMuzn74rnfGQrv13pS9vC+YiA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "license": "MIT", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-nodegui-autoprefixer": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/postcss-nodegui-autoprefixer/-/postcss-nodegui-autoprefixer-0.0.7.tgz", + "integrity": "sha512-cPNAIz9siY+ssc3ecdPaeflfdc2HRS6tgQGu59YKLiiaZT3iMw578l1LTFfmEnJtv1RvNEQUNn/YjHyySXla2Q==", + "license": "MIT", + "dependencies": { + "postcss": "^7.0.17" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c8e3677 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "des-key-cracker", + "version": "1.0.0", + "description": "DES Key Cracker — recover 8-byte DES keys for TechMed / Pectus .med scans with Metal, CUDA, and LAN workers", + "main": "dist/index.js", + "private": true, + "scripts": { + "build": "tsc && node -e \"require('fs').copyFileSync('src/ui/app.css','dist/ui/app.css')\"", + "build:native": "node scripts/build-native.js", + "start": "qode dist/index.js", + "prestart": "npm run build" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@nodegui/nodegui": "^0.74.2", + "lucide-static": "^1.47.0", + "ws": "^8.18.3" + }, + "devDependencies": { + "@types/node": "^24.5.2", + "@types/ws": "^8.18.1", + "typescript": "^5.9.2" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..1d97446 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,961 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@nodegui/nodegui': + specifier: ^0.74.2 + version: 0.74.2 + lucide-static: + specifier: ^1.47.0 + version: 1.47.0 + ws: + specifier: ^8.18.3 + version: 8.21.3 + devDependencies: + '@types/node': + specifier: ^24.5.2 + version: 24.13.5 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + typescript: + specifier: ^5.9.2 + version: 5.9.3 + +packages: + + 7zip-bin@5.1.1: + resolution: {integrity: sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==} + + 7zip-min@1.4.5: + resolution: {integrity: sha512-S+FzNwJFKF5LgQYs+hPQo+qeffdi+259Ak63rWEfkHP9arsU8gbe5K+4HscuWN1ih1lP1gTjDNPddbU0qhPtHQ==} + + '@noble/hashes@2.4.0': + resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==} + engines: {node: '>= 20.19.0'} + + '@nodegui/artifact-installer@1.1.0': + resolution: {integrity: sha512-x/rIewhjnLhf2b3lAy5ZIPSTS39LRpwMTKeEg6sn4RwvWwyNmreGzpvGi6Gkbl6FUpftn70nfOBOpaeO3hO8PQ==} + + '@nodegui/nodegui@0.74.2': + resolution: {integrity: sha512-YrKMXF+RZOCRo0z5UWtmVfWW9xLdQC82jewjyD4Jdb4Wbcez7YP+k4I6nL+bH0fflvvDOTFJmuvvxUM765xaqg==} + engines: {node: '>=18.x.x'} + hasBin: true + + '@nodegui/qode@24.12.0-rc19': + resolution: {integrity: sha512-18goj/U6XHHPO+NlyXOf0PuWYnNIAwsqfz7xFl7z+hTyDWKDXX9Ok+7G8uhMo5mhQG3fwPzWa9A8RYs/ni/w9A==} + + '@paralleldrive/cuid2@3.3.0': + resolution: {integrity: sha512-OqiFvSOF0dBSesELYY2CAMa4YINvlLpvKOz/rv6NeZEqiyttlHgv98Juwv4Ch+GrEV7IZ8jfI2VcEoYUjXXCjw==} + hasBin: true + + '@types/node@24.13.5': + resolution: {integrity: sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + + are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.20.0: + resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + cmake-js@7.4.0: + resolution: {integrity: sha512-Lw0JxEHrmk+qNj1n9W9d4IvkDdYTBn7l2BW6XmtLj7WPpIo2shvxUy+YokfjMxAAOELNonQwX3stkPhM5xSC2Q==} + engines: {node: '>= 14.15.0'} + hasBin: true + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-causes@3.0.2: + resolution: {integrity: sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + fs-extra@11.4.0: + resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} + engines: {node: '>=14.14'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + lucide-static@1.47.0: + resolution: {integrity: sha512-yWIrkdXc688Feq5VjOktsKmV5Ikc7y5Nu3rrdtbr8nWjkJWk8QlnZfVtIak22Af+fNhZ7k4cTJpZo1zmj7X5sA==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + manage-path@2.0.0: + resolution: {integrity: sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + + memory-stream@1.0.0: + resolution: {integrity: sha512-Wm13VcsPIMdG96dzILfij09PvuS3APtcKNh7M28FsCA/w6+1mjR7hhPmfFNoilX9xU7wTdhsH5lJAm6XNzdtww==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + node-addon-api@8.9.2: + resolution: {integrity: sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==} + engines: {node: ^18 || ^20 || >= 21} + + node-api-headers@1.9.0: + resolution: {integrity: sha512-2oNILP4jXwRB4ywnYKjVk1YyJ96n2D4EOVJO6S3oYZ5PtbJrw3Yt9TpAuX3nBLMuzn74rnfGQrv13pS9vC+YiA==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@0.2.1: + resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} + + postcss-nodegui-autoprefixer@0.0.7: + resolution: {integrity: sha512-cPNAIz9siY+ssc3ecdPaeflfdc2HRS6tgQGu59YKLiiaZT3iMw578l1LTFfmEnJtv1RvNEQUNn/YjHyySXla2Q==} + + postcss@7.0.39: + resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==} + engines: {node: '>=6.0.0'} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + url-join@4.0.1: + resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + +snapshots: + + 7zip-bin@5.1.1: {} + + 7zip-min@1.4.5: + dependencies: + 7zip-bin: 5.1.1 + + '@noble/hashes@2.4.0': {} + + '@nodegui/artifact-installer@1.1.0': + dependencies: + 7zip-min: 1.4.5 + env-paths: 2.2.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + progress: 2.0.3 + transitivePeerDependencies: + - encoding + + '@nodegui/nodegui@0.74.2': + dependencies: + '@nodegui/artifact-installer': 1.1.0 + '@nodegui/qode': 24.12.0-rc19 + '@paralleldrive/cuid2': 3.3.0 + cmake-js: 7.4.0 + cross-env: 7.0.3 + manage-path: 2.0.0 + memoize-one: 5.2.1 + mkdirp: 3.0.1 + node-addon-api: 8.9.2 + postcss: 7.0.39 + postcss-nodegui-autoprefixer: 0.0.7 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@nodegui/qode@24.12.0-rc19': + dependencies: + env-paths: 2.2.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + progress: 2.0.3 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + + '@paralleldrive/cuid2@3.3.0': + dependencies: + '@noble/hashes': 2.4.0 + bignumber.js: 9.3.1 + error-causes: 3.0.2 + + '@types/node@24.13.5': + dependencies: + undici-types: 7.18.2 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.13.5 + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + aproba@2.1.0: {} + + are-we-there-yet@3.0.1: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + + asynckit@0.4.0: {} + + axios@1.20.0(debug@4.4.3): + dependencies: + follow-redirects: 1.16.0(debug@4.4.3) + form-data: 4.0.6 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + + bignumber.js@9.3.1: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + chownr@2.0.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cmake-js@7.4.0: + dependencies: + axios: 1.20.0(debug@4.4.3) + debug: 4.4.3 + fs-extra: 11.4.0 + memory-stream: 1.0.0 + node-api-headers: 1.9.0 + npmlog: 6.0.2 + rc: 1.2.8 + semver: 7.8.5 + tar: 6.2.1 + url-join: 4.0.1 + which: 2.0.2 + yargs: 17.7.3 + transitivePeerDependencies: + - supports-color + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-support@1.1.3: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + console-control-strings@1.1.0: {} + + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-extend@0.6.0: {} + + delayed-stream@1.0.0: {} + + delegates@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + emoji-regex@8.0.0: {} + + env-paths@2.2.1: {} + + error-causes@3.0.2: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + escalade@3.2.0: {} + + follow-redirects@1.16.0(debug@4.4.3): + optionalDependencies: + debug: 4.4.3 + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + fs-extra@11.4.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + function-bind@1.1.2: {} + + gauge@4.0.4: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-unicode@2.0.1: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + inherits@2.0.4: {} + + ini@1.3.8: {} + + is-fullwidth-code-point@3.0.0: {} + + isexe@2.0.0: {} + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + lucide-static@1.47.0: {} + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + manage-path@2.0.0: {} + + math-intrinsics@1.1.0: {} + + memoize-one@5.2.1: {} + + memory-stream@1.0.0: + dependencies: + readable-stream: 3.6.2 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimist@1.2.8: {} + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp@1.0.4: {} + + mkdirp@3.0.1: {} + + ms@2.1.3: {} + + node-addon-api@8.9.2: {} + + node-api-headers@1.9.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + npmlog@6.0.2: + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + + path-key@3.1.1: {} + + picocolors@0.2.1: {} + + postcss-nodegui-autoprefixer@0.0.7: + dependencies: + postcss: 7.0.39 + + postcss@7.0.39: + dependencies: + picocolors: 0.2.1 + source-map: 0.6.1 + + progress@2.0.3: {} + + proxy-from-env@2.1.0: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + require-directory@2.1.1: {} + + safe-buffer@5.2.1: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + set-blocking@2.0.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@3.0.7: {} + + source-map@0.6.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@2.0.1: {} + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + tr46@0.0.3: {} + + typescript@5.9.3: {} + + undici-types@7.18.2: {} + + universalify@2.0.1: {} + + url-join@4.0.1: {} + + util-deprecate@1.0.2: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + ws@8.21.3: {} + + y18n@5.0.8: {} + + yallist@4.0.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..b7a72ed --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + '@nodegui/nodegui': true + '@nodegui/qode': true diff --git a/scripts/build-native.js b/scripts/build-native.js new file mode 100644 index 0000000..1e548a8 --- /dev/null +++ b/scripts/build-native.js @@ -0,0 +1,113 @@ +#!/usr/bin/env node +"use strict"; + +const { spawnSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); +const os = require("os"); + +const root = path.resolve(__dirname, ".."); +const bin = path.join(root, "bin"); +const isWin = process.platform === "win32"; +const isMac = process.platform === "darwin"; + +fs.mkdirSync(bin, { recursive: true }); + +function run(cmd, args, opts = {}) { + console.log(`$ ${cmd} ${args.join(" ")}`); + const result = spawnSync(cmd, args, { stdio: "inherit", ...opts }); + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + process.exit(result.status || 1); + } +} + +function which(cmd) { + const probe = isWin ? "where" : "which"; + const result = spawnSync(probe, [cmd], { encoding: "utf8" }); + return result.status === 0; +} + +function compileCpu() { + const out = path.join(bin, isWin ? "med_cpu.exe" : "med_cpu"); + const cpp = path.join(root, "native", "cpu", "med_cpu.cpp"); + const csrc = path.join(root, "native", "common", "des.c"); + const obj = path.join(bin, isWin ? "des.obj" : "des.o"); + const compilers = isWin + ? [ + ["clang++", () => { + run("clang", ["-O2", "-c", csrc, "-o", obj]); + run("clang++", ["-O2", "-std=c++17", "-pthread", cpp, obj, "-o", out]); + }], + ["g++", () => { + run("gcc", ["-O2", "-c", csrc, "-o", obj]); + run("g++", ["-O2", "-std=c++17", "-pthread", cpp, obj, "-o", out]); + }], + ["cl", () => { + run("cl", ["/nologo", "/O2", "/c", `/Fo${obj}`, csrc]); + run("cl", ["/nologo", "/O2", "/EHsc", "/std:c++17", `/Fe:${out}`, cpp, obj]); + }], + ] + : [ + ["c++", () => { + run("cc", ["-O2", "-c", csrc, "-o", obj]); + run("c++", ["-O2", "-std=c++17", "-pthread", cpp, obj, "-o", out]); + }], + ]; + + let lastErr = null; + for (const [cmd, fn] of compilers) { + if (cmd !== "c++" && !which(cmd)) { + continue; + } + try { + fn(); + console.log(`[native] CPU runner -> ${out}`); + return; + } catch (err) { + lastErr = err; + } + } + throw lastErr || new Error("no C++ compiler found for med_cpu"); +} + +function compileMetal() { + if (!isMac) { + console.log("[native] skip Metal (not macOS)"); + return; + } + const out = path.join(bin, "med_gpu"); + run("clang", [ + "-O2", + "-fobjc-arc", + "-framework", + "Metal", + "-framework", + "Foundation", + path.join(root, "native", "metal", "med_gpu.m"), + "-o", + out, + ]); + console.log(`[native] Metal runner -> ${out}`); +} + +function compileCuda() { + if (!isWin && process.env.FORCE_CUDA !== "1") { + console.log("[native] skip CUDA (Windows-only unless FORCE_CUDA=1)"); + return; + } + if (!which("nvcc")) { + console.warn("[native] nvcc not found; CUDA backend skipped (CPU still works)"); + return; + } + const out = path.join(bin, isWin ? "med_cuda.exe" : "med_cuda"); + run("nvcc", ["-O2", "-std=c++17", "-o", out, path.join(root, "native", "cuda", "med_cuda.cu")]); + console.log(`[native] CUDA runner -> ${out}`); +} + +compileCpu(); +compileMetal(); +compileCuda(); +console.log(`[native] done (${os.platform()} ${os.arch()})`); diff --git a/src/crypto/constants.ts b/src/crypto/constants.ts new file mode 100644 index 0000000..deccb2b --- /dev/null +++ b/src/crypto/constants.ts @@ -0,0 +1,57 @@ +export const PAD_CT = Buffer.from("7c1cddebb73720e0", "hex"); + +export const DEFAULT_CHARSET = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#_-"; + +export const DEFAULT_PADS = "00,20"; +/** GPU workers take large blocks so they keep the majority of the keyspace. */ +export const DEFAULT_GPU_BLOCK_SIZE = 16_777_216; +/** CPU workers take smaller blocks so faster GPU workers are fed more often. */ +export const DEFAULT_CPU_BLOCK_SIZE = 1_048_576; +export const DEFAULT_BLOCK_SIZE = DEFAULT_GPU_BLOCK_SIZE; +export const DEFAULT_GPU_BATCH = 16_777_216; +export const DEFAULT_PORT = 9876; +export const PROTOCOL_VERSION = 2; + +export function defaultBlockSize(deviceType: string): number { + return deviceType === "cpu" ? DEFAULT_CPU_BLOCK_SIZE : DEFAULT_GPU_BLOCK_SIZE; +} + +export const FILLS: Buffer[] = [ + Buffer.alloc(8, 0x00), + Buffer.alloc(8, 0xff), + Buffer.alloc(8, 0xcd), + Buffer.alloc(8, 0xab), + Buffer.alloc(8, 0xcc), + Buffer.alloc(8, 0x20), + Buffer.alloc(8, 0xaa), + Buffer.alloc(8, 0x55), +]; + +export const FILL_HEX = FILLS.map((f) => f.toString("hex")); + +export const HEADER_MAGICS: Buffer[] = [ + Buffer.from("iMed"), + Buffer.from("IMED"), + Buffer.from("IMed"), + Buffer.from("MED\x00"), + Buffer.from("MESH"), + Buffer.from("mesh"), + Buffer.from("ply\n"), + Buffer.from("solid"), + Buffer.from("> BigInt(srcbits - tbl[i])) & 1n; + outv = (outv << 1n) | bit; + } + return outv; +} + +function rotl28(v: number, s: number): number { + return ((v << s) | (v >>> (28 - s))) & 0x0fffffff; +} + +function feistel(r: number, subkey: bigint): number { + const er = perm(BigInt(r >>> 0), E_TBL, 48, 32) ^ subkey; + let s = 0; + for (let i = 0; i < 8; i++) { + const chunk = Number((er >> BigInt(42 - 6 * i)) & 0x3fn); + const row = ((chunk & 0x20) >> 4) | (chunk & 1); + const col = (chunk >> 1) & 0xf; + s = (s << 4) | SBOX[i][row * 16 + col]; + } + return Number(perm(BigInt(s >>> 0), P_TBL, 32, 32)); +} + +function keySchedule(key: bigint): bigint[] { + const cd = perm(key, PC1_TBL, 56, 64); + let c = Number(cd >> 28n); + let d = Number(cd & 0x0fffffffn); + const sk: bigint[] = []; + for (let r = 0; r < 16; r++) { + c = rotl28(c, SHIFTS[r]); + d = rotl28(d, SHIFTS[r]); + const cd2 = (BigInt(c) << 28n) | BigInt(d); + sk.push(perm(cd2, PC2_TBL, 48, 56)); + } + return sk; +} + +function crypt(block: bigint, sk: bigint[], decrypt: boolean): bigint { + const ip = perm(block, IP_TBL, 64, 64); + let l = Number(ip >> 32n) >>> 0; + let r = Number(ip & 0xffffffffn) >>> 0; + for (let i = 0; i < 16; i++) { + const rnd = decrypt ? 15 - i : i; + const n = (l ^ feistel(r, sk[rnd])) >>> 0; + l = r; + r = n; + } + const pre = (BigInt(r) << 32n) | BigInt(l); + return perm(pre, FP_TBL, 64, 64); +} + +export function bufferToU64BE(buf: Buffer): bigint { + return buf.readBigUInt64BE(0); +} + +export function u64ToBufferBE(value: bigint): Buffer { + const buf = Buffer.alloc(8); + buf.writeBigUInt64BE(value); + return buf; +} + +export function desKeySchedule(key: Buffer): bigint[] { + const padded = Buffer.alloc(8); + key.copy(padded, 0, 0, Math.min(8, key.length)); + return keySchedule(bufferToU64BE(padded)); +} + +export function desCryptBlock(key: Buffer, block: Buffer, decrypt: boolean): Buffer { + const sk = desKeySchedule(key); + const pt = crypt(bufferToU64BE(block), sk, decrypt); + return u64ToBufferBE(pt); +} + +export function desDecryptBlock(key: Buffer, block: Buffer): Buffer { + return desCryptBlock(key, block, true); +} + +export function desDecryptFile(key: Buffer, data: Buffer): Buffer { + const out = Buffer.from(data); + const usable = data.length - (data.length % 8); + const sk = desKeySchedule(key); + for (let i = 0; i < usable; i += 8) { + const pt = crypt(data.readBigUInt64BE(i), sk, true); + out.writeBigUInt64BE(pt, i); + } + return out; +} + +export function hexKeyToBuffer(hex: string): Buffer { + const clean = hex.replace(/^0x/i, ""); + return Buffer.from(clean.padStart(16, "0"), "hex"); +} diff --git a/src/crypto/hits.ts b/src/crypto/hits.ts new file mode 100644 index 0000000..0be993e --- /dev/null +++ b/src/crypto/hits.ts @@ -0,0 +1,99 @@ +import fs from "fs"; +import path from "path"; +import { HEADER_MAGICS } from "./constants"; +import { desDecryptBlock, desDecryptFile, hexKeyToBuffer } from "./des"; + +export interface HitRecord { + cipher: "des"; + password: string; + derivation: string; + key_hex: string; + pad_fill: string; + first_pt_hex: string | null; + backend: string; + payload_ok?: boolean; + payload_reason?: string; + first64_ascii?: string; + decrypt_error?: string; +} + +export function payloadLooksReal(first64: Buffer, more?: Buffer): { ok: boolean; reason: string } { + for (const magic of HEADER_MAGICS) { + if (first64.subarray(0, magic.length).equals(magic) || first64.includes(magic)) { + return { ok: true, reason: "magic" }; + } + } + let printable = 0; + for (const b of first64) { + if ((b >= 32 && b < 127) || b === 9 || b === 10 || b === 13) { + printable++; + } + } + if (printable >= 48) { + return { ok: true, reason: "printable_header" }; + } + const blob = Buffer.concat([first64, more ?? Buffer.alloc(0)]); + if (blob.includes(Buffer.from("Butcher")) || blob.includes(Buffer.from("Thomas")) || + blob.includes(Buffer.from("Pectus")) || blob.includes(Buffer.from("Torso"))) { + return { ok: true, reason: "patient_string" }; + } + let inRange = 0; + for (let i = 0; i + 4 <= first64.length; i += 4) { + const f = first64.readFloatLE(i); + if (Number.isFinite(f) && Math.abs(f) < 5000) { + inRange++; + } + } + if (inRange >= 12) { + return { ok: true, reason: "float32_scan_range" }; + } + const count = first64.readUInt32LE(0); + if (count >= 1000 && count <= 200_000) { + return { ok: true, reason: `vertex_count_${count}` }; + } + return { ok: false, reason: "unstructured" }; +} + +export function asciiPreview(buf: Buffer): string { + return [...buf].map((b) => (b >= 32 && b < 127 ? String.fromCharCode(b) : ".")).join(""); +} + +export function saveHit(hit: HitRecord, data: Buffer, outDir: string): string | null { + fs.mkdirSync(outDir, { recursive: true }); + const key = hexKeyToBuffer(hit.key_hex); + try { + const plain = desDecryptFile(key, data); + const first64 = plain.subarray(0, 64); + const more = plain.subarray(0, 4096); + const check = payloadLooksReal(first64, more); + hit.payload_ok = check.ok; + hit.payload_reason = check.reason; + hit.first64_ascii = asciiPreview(first64); + const stamp = `${hit.cipher}_${hit.derivation}_${hit.key_hex.slice(0, 16)}`; + const binPath = path.join(outDir, `decrypted_${stamp}.bin`); + fs.writeFileSync(binPath, plain); + fs.writeFileSync(path.join(outDir, `hit_${stamp}.json`), JSON.stringify(hit, null, 2) + "\n"); + return binPath; + } catch (err) { + hit.decrypt_error = err instanceof Error ? err.message : String(err); + return null; + } +} + +export function firstPlainHex(keyHex: string, firstCt: Buffer): string | null { + try { + const pt = desDecryptBlock(hexKeyToBuffer(keyHex), firstCt); + return pt.toString("hex"); + } catch { + return null; + } +} + +export function writeResults(outDir: string, elapsedSec: number, hits: HitRecord[]): void { + fs.mkdirSync(outDir, { recursive: true }); + const confirmed = hits.filter((h) => h.payload_ok); + fs.writeFileSync( + path.join(outDir, "results.json"), + JSON.stringify({ elapsed_sec: Math.round(elapsedSec * 1000) / 1000, pad_matches: hits, confirmed }, null, 2) + "\n", + ); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..9e4c0d9 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,10 @@ +import { QMainWindow } from "@nodegui/nodegui"; +import { createMainWindow } from "./ui/mainWindow"; + +function main(): void { + const win: QMainWindow = createMainWindow(); + win.show(); + (global as typeof globalThis & { win?: QMainWindow }).win = win; +} + +main(); diff --git a/src/net/lan.ts b/src/net/lan.ts new file mode 100644 index 0000000..5068e1e --- /dev/null +++ b/src/net/lan.ts @@ -0,0 +1,14 @@ +import os from "os"; + +export function localIPv4(): string[] { + const out = ["0.0.0.0"]; + for (const addrs of Object.values(os.networkInterfaces())) { + for (const addr of addrs ?? []) { + const family = String(addr.family) === "IPv4" || String(addr.family) === "4"; + if (family && !addr.internal) { + out.push(addr.address); + } + } + } + return out; +} diff --git a/src/net/master.ts b/src/net/master.ts new file mode 100644 index 0000000..6a82b10 --- /dev/null +++ b/src/net/master.ts @@ -0,0 +1,296 @@ +import { randomUUID } from "crypto"; +import http from "http"; +import type { IncomingMessage } from "http"; +import os from "os"; +import { WebSocket, WebSocketServer } from "ws"; +import { PROTOCOL_VERSION } from "../crypto/constants"; +import type { ComputeDevice } from "../search/nativeRunner"; +import { decodeMsg, encodeMsg, type AssignBlock, type ClientMessage } from "./protocol"; + +export interface SlaveDeviceState { + device: ComputeDevice; + idle: boolean; + jobId: string | null; + progressDone: bigint; + progressCount: bigint; + rate: number; +} + +export interface SlaveInfo { + id: string; + hostname: string; + ip: string; + platform: string; + devices: ComputeDevice[]; + workers: SlaveDeviceState[]; +} + +export interface RemoteWorker { + slaveId: string; + hostname: string; + ip: string; + device: ComputeDevice; + idle: boolean; + jobId: string | null; + progressDone: bigint; + progressCount: bigint; + rate: number; +} + +export interface MasterEvents { + onLog?: (line: string) => void; + onSlaves?: (slaves: SlaveInfo[]) => void; + onProgress?: ( + slave: SlaveInfo, + worker: SlaveDeviceState, + jobId: string, + done: bigint, + count: bigint, + rate: number, + ) => void; + onHit?: (slave: SlaveInfo, jobId: string, index: string, keyHex: string, plainHex: string) => void; + onComplete?: (slave: SlaveInfo, worker: SlaveDeviceState, jobId: string) => void; + onDropped?: (slave: SlaveInfo, jobId: string | null) => void; +} + +interface Slot { + ws: WebSocket; + info: SlaveInfo; +} + +export class MasterServer { + private wss: WebSocketServer | null = null; + private httpServer: http.Server | null = null; + private slots = new Map(); + readonly events: MasterEvents; + + constructor(events: MasterEvents = {}) { + this.events = events; + } + + get listening(): boolean { + return this.wss != null; + } + + list(): SlaveInfo[] { + return [...this.slots.values()].map((s) => s.info); + } + + listWorkers(): RemoteWorker[] { + const rows: RemoteWorker[] = []; + for (const slot of this.slots.values()) { + for (const worker of slot.info.workers) { + rows.push({ + slaveId: slot.info.id, + hostname: slot.info.hostname, + ip: slot.info.ip, + device: worker.device, + idle: worker.idle, + jobId: worker.jobId, + progressDone: worker.progressDone, + progressCount: worker.progressCount, + rate: worker.rate, + }); + } + } + return rows; + } + + idleWorkers(): RemoteWorker[] { + return this.listWorkers().filter((w) => w.idle); + } + + async listen(host: string, port: number): Promise { + await this.close(); + const httpServer = http.createServer(); + const wss = new WebSocketServer({ server: httpServer }); + this.httpServer = httpServer; + this.wss = wss; + wss.on("connection", (ws, req) => this.onConnection(ws, req)); + await new Promise((resolve, reject) => { + httpServer.once("error", reject); + httpServer.listen(port, host, () => resolve()); + }); + this.events.onLog?.(`Master listening on ws://${host === "0.0.0.0" ? os.hostname() : host}:${port}`); + } + + async close(): Promise { + for (const slot of this.slots.values()) { + try { + slot.ws.send(encodeMsg({ type: "shutdown" })); + slot.ws.close(); + } catch { + /* ignore */ + } + } + this.slots.clear(); + this.emitSlaves(); + const wss = this.wss; + const httpServer = this.httpServer; + this.wss = null; + this.httpServer = null; + if (wss) { + await new Promise((resolve) => wss.close(() => resolve())); + } + if (httpServer) { + await new Promise((resolve) => httpServer.close(() => resolve())); + } + } + + assign(slaveId: string, deviceKey: string, block: Omit): boolean { + const slot = this.slots.get(slaveId); + if (!slot) { + return false; + } + const worker = slot.info.workers.find((w) => w.device.key === deviceKey); + if (!worker || !worker.idle) { + return false; + } + worker.idle = false; + worker.jobId = block.jobId; + worker.progressDone = 0n; + worker.progressCount = BigInt(block.count); + worker.rate = 0; + slot.ws.send(encodeMsg({ type: "assign", ...block, deviceKey })); + this.emitSlaves(); + return true; + } + + cancelAll(): void { + for (const slot of this.slots.values()) { + slot.ws.send(encodeMsg({ type: "cancel" })); + for (const worker of slot.info.workers) { + worker.idle = true; + worker.jobId = null; + worker.progressDone = 0n; + worker.rate = 0; + } + } + this.emitSlaves(); + } + + private onConnection(ws: WebSocket, req: IncomingMessage): void { + const id = randomUUID(); + const ip = req.socket.remoteAddress?.replace(/^::ffff:/, "") ?? "unknown"; + const info: SlaveInfo = { + id, + hostname: ip, + ip, + platform: "unknown", + devices: [], + workers: [], + }; + this.slots.set(id, { ws, info }); + ws.on("message", (data) => { + const msg = decodeMsg(data.toString()); + if (msg) { + this.onClient(id, msg); + } + }); + ws.on("close", () => { + const slot = this.slots.get(id); + this.slots.delete(id); + if (slot) { + const jobs = slot.info.workers.map((w) => w.jobId).filter((jobId): jobId is string => jobId != null); + if (jobs.length === 0) { + this.events.onDropped?.(slot.info, null); + } else { + for (const jobId of jobs) { + this.events.onDropped?.(slot.info, jobId); + } + } + } + this.emitSlaves(); + }); + ws.on("error", () => { + ws.close(); + }); + } + + 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 onClient(id: string, msg: ClientMessage): void { + const slot = this.slots.get(id); + if (!slot) { + return; + } + const { info } = slot; + switch (msg.type) { + case "hello": + info.hostname = msg.hostname; + info.platform = msg.platform; + info.devices = msg.devices; + info.workers = msg.devices.map((device) => ({ + device, + idle: true, + jobId: null, + progressDone: 0n, + progressCount: 0n, + rate: 0, + })); + slot.ws.send(encodeMsg({ type: "hello_ack", protocolVersion: PROTOCOL_VERSION })); + this.events.onLog?.( + `Slave connected: ${info.hostname} (${info.ip}) — ${ + info.workers.length === 0 + ? "no workers exposed" + : info.workers.map((w) => w.device.name).join(", ") + }`, + ); + this.emitSlaves(); + break; + case "progress": { + const worker = this.workerByJob(info, msg.jobId, msg.deviceKey); + if (!worker) { + break; + } + worker.progressDone = BigInt(msg.done); + worker.progressCount = BigInt(msg.count); + worker.rate = msg.rate; + this.events.onProgress?.(info, worker, msg.jobId, worker.progressDone, worker.progressCount, msg.rate); + this.emitSlaves(); + break; + } + case "hit": + 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); + if (!worker) { + break; + } + worker.idle = true; + worker.jobId = null; + worker.progressDone = worker.progressCount; + worker.rate = 0; + this.events.onComplete?.(info, worker, msg.jobId); + this.emitSlaves(); + break; + } + case "error": { + this.events.onLog?.(`Slave ${info.hostname}: ${msg.message}`); + const worker = msg.jobId ? this.workerByJob(info, msg.jobId) : undefined; + if (worker) { + worker.idle = true; + worker.jobId = null; + worker.rate = 0; + } + this.events.onDropped?.(info, msg.jobId ?? worker?.jobId ?? null); + this.emitSlaves(); + break; + } + case "ping": + break; + default: + break; + } + } + + private emitSlaves(): void { + this.events.onSlaves?.(this.list()); + } +} diff --git a/src/net/protocol.ts b/src/net/protocol.ts new file mode 100644 index 0000000..5d33b9b --- /dev/null +++ b/src/net/protocol.ts @@ -0,0 +1,48 @@ +import { PROTOCOL_VERSION } from "../crypto/constants"; +import type { ComputeDevice } from "../search/nativeRunner"; + +export { PROTOCOL_VERSION }; + +export interface AssignBlock { + type: "assign"; + jobId: string; + deviceKey: string; + start: string; + count: string; + keyLen: number; + charset: string; + padByte: number; + target: string; + fills: string[]; + cpuWorkers?: number; +} + +export type ServerMessage = + | { type: "hello_ack"; protocolVersion: number } + | AssignBlock + | { type: "cancel"; jobId?: string } + | { type: "shutdown" }; + +export type ClientMessage = + | { type: "hello"; hostname: string; platform: string; devices: ComputeDevice[] } + | { type: "progress"; jobId: string; deviceKey: string; done: string; count: string; rate: number } + | { type: "hit"; jobId: string; index: string; keyHex: string; plainHex: string } + | { type: "block_complete"; jobId: string; deviceKey: string; hits: number; elapsed: number } + | { type: "error"; message: string; jobId?: string } + | { type: "ping" }; + +export function encodeMsg(msg: ServerMessage | ClientMessage): string { + return JSON.stringify(msg); +} + +export function decodeMsg(raw: string): T | null { + try { + const parsed = JSON.parse(raw) as T; + if (!parsed || typeof parsed !== "object" || !("type" in parsed)) { + return null; + } + return parsed; + } catch { + return null; + } +} diff --git a/src/net/slave.ts b/src/net/slave.ts new file mode 100644 index 0000000..a1a7eba --- /dev/null +++ b/src/net/slave.ts @@ -0,0 +1,345 @@ +import os from "os"; +import { PROTOCOL_VERSION } from "../crypto/constants"; +import type { ComputeDevice } from "../search/nativeRunner"; +import { preferredDevice, runBrute } from "../search/nativeRunner"; +import { localWorkerId, type WorkerRow } from "../search/orchestrator"; +import { formatNumber, formatRate } from "../search/planner"; +import { decodeMsg, encodeMsg, type AssignBlock, type ServerMessage } from "./protocol"; +import { WebSocket } from "ws"; + +export interface SlaveEvents { + onLog?: (line: string) => void; + onStatus?: (text: string) => void; + onProgress?: (jobId: string, done: bigint, count: bigint, rate: number) => void; + onHit?: (jobId: string, index: string, keyHex: string, plainHex: string) => void; + onWorkers?: (rows: WorkerRow[]) => void; +} + +interface DeviceJob { + jobId: string; + kill: () => void; + done: bigint; + count: bigint; + rate: number; +} + +export class SlaveClient { + private ws: WebSocket | null = null; + private ping: NodeJS.Timeout | null = null; + private jobs = new Map(); + private completedBlocks = new Map(); + private rateStats = new Map(); + private devices: ComputeDevice[] = []; + private cpuWorkers = Math.max(1, os.cpus().length); + readonly events: SlaveEvents; + + constructor(events: SlaveEvents = {}) { + this.events = events; + } + + get connected(): boolean { + return this.ws != null && this.ws.readyState === WebSocket.OPEN; + } + + setDevices(devices: ComputeDevice[], cpuWorkers: number): void { + this.devices = devices; + this.cpuWorkers = cpuWorkers; + for (const device of devices) { + if (!this.completedBlocks.has(device.key)) { + this.completedBlocks.set(device.key, 0); + } + } + this.emitWorkers(); + } + + connect(host: string, port: number): void { + this.disconnect(); + const url = `ws://${host}:${port}`; + this.events.onStatus?.(`Connecting to ${url}…`); + const ws = new WebSocket(url); + this.ws = ws; + ws.on("open", () => { + ws.send( + encodeMsg({ + type: "hello", + hostname: os.hostname(), + platform: `${os.platform()} ${os.arch()}`, + devices: this.devices, + }), + ); + this.events.onStatus?.(`Connected to ${url}`); + this.events.onLog?.( + `Hello sent (protocol ${PROTOCOL_VERSION}) — exposing ${ + this.devices.length === 0 ? "no workers" : this.devices.map((d) => d.name).join(", ") + }`, + ); + this.emitWorkers(); + this.ping = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(encodeMsg({ type: "ping" })); + } + }, 5000); + }); + ws.on("message", (data) => { + const msg = decodeMsg(data.toString()); + if (msg) { + void this.onServer(msg); + } + }); + ws.on("close", () => { + this.clearPing(); + this.killAll(); + if (this.ws === ws) { + this.ws = null; + } + this.events.onStatus?.("Disconnected"); + this.emitWorkers(); + }); + ws.on("error", (err) => { + this.events.onLog?.(`WebSocket error: ${err.message}`); + }); + } + + disconnect(): void { + this.clearPing(); + this.killAll(); + const ws = this.ws; + this.ws = null; + if (ws) { + try { + ws.close(); + } catch { + /* ignore */ + } + } + this.emitWorkers(); + } + + private rateFor(deviceKey: string): { keysDone: bigint; activeMs: number; busyStarted: number | null } { + let stats = this.rateStats.get(deviceKey); + if (!stats) { + stats = { keysDone: 0n, activeMs: 0, busyStarted: null }; + this.rateStats.set(deviceKey, stats); + } + return stats; + } + + private beginRate(deviceKey: string): void { + const stats = this.rateFor(deviceKey); + if (stats.busyStarted == null) { + stats.busyStarted = Date.now(); + } + } + + private endRate(deviceKey: string, keys: bigint): void { + const stats = this.rateFor(deviceKey); + if (stats.busyStarted != null) { + stats.activeMs += Date.now() - stats.busyStarted; + stats.busyStarted = null; + } + if (keys > 0n) { + stats.keysDone += keys; + } + } + + private averageRate(deviceKey: string, liveDone: bigint): number { + const stats = this.rateFor(deviceKey); + const liveMs = stats.busyStarted != null ? Date.now() - stats.busyStarted : 0; + const sec = (stats.activeMs + liveMs) / 1000; + const keys = stats.keysDone + liveDone; + if (sec <= 0 || keys <= 0n) { + return 0; + } + return Number(keys) / sec; + } + + private killAll(): void { + for (const [deviceKey, job] of this.jobs) { + this.endRate(deviceKey, job.done); + job.kill(); + } + this.jobs.clear(); + } + + private clearPing(): void { + if (this.ping) { + clearInterval(this.ping); + this.ping = null; + } + } + + private send(msg: Parameters[0]): void { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(encodeMsg(msg)); + } + } + + private emitWorkers(): void { + const host = os.hostname(); + const rows: WorkerRow[] = this.devices.map((device) => { + const live = this.jobs.get(device.key); + const done = live?.done ?? 0n; + const count = live?.count ?? 0n; + const pct = live && count > 0n ? Number((done * 1000n) / count) / 10 : 0; + const avg = this.averageRate(device.key, live?.done ?? 0n); + return { + id: localWorkerId(device.key), + host, + ip: "local", + device: device.name, + deviceKey: device.key, + deviceType: device.type, + blockSize: live ? Number(live.count > 0n && live.count <= 2147483647n ? live.count : 0n) : 0, + current: live ? `${formatNumber(done)} / ${formatNumber(count)}` : "idle", + pct, + completedBlocks: this.completedBlocks.get(device.key) ?? 0, + completedKeys: this.rateFor(device.key).keysDone, + rate: live && live.rate > 0 ? formatRate(live.rate) : "—", + rateNum: live?.rate ?? 0, + avgRate: avg > 0 ? formatRate(avg) : "—", + avgRateNum: avg, + busy: !!live, + }; + }); + this.events.onWorkers?.(rows); + } + + private updateStatus(): void { + const busy = [...this.jobs.values()]; + if (busy.length === 0) { + this.events.onStatus?.(this.connected ? "Idle" : "Disconnected"); + return; + } + this.events.onStatus?.( + busy.length === 1 + ? `Working ${busy[0].jobId.slice(0, 8)} ${busy[0].done.toString()}/${busy[0].count.toString()}` + : `${busy.length} workers assigned`, + ); + } + + private async onServer(msg: ServerMessage): Promise { + switch (msg.type) { + case "hello_ack": + this.events.onLog?.(`Master ack v${msg.protocolVersion}`); + break; + case "assign": + void this.runAssign(msg); + break; + case "cancel": + this.killAll(); + this.events.onLog?.("Cancel received"); + this.events.onStatus?.("Idle (cancelled)"); + this.emitWorkers(); + break; + case "shutdown": + this.disconnect(); + break; + default: + break; + } + } + + private async runAssign(block: AssignBlock): Promise { + const matched = this.devices.find((d) => d.key === block.deviceKey); + const device = matched ?? preferredDevice(this.devices); + if (!device) { + this.send({ type: "error", jobId: block.jobId, message: "no compute device selected" }); + return; + } + if (!matched) { + this.events.onLog?.(`No device ${block.deviceKey}; using ${device.name}`); + } + this.jobs.get(device.key)?.kill(); + this.events.onLog?.( + `Assigned ${device.name} len=${block.keyLen} pad=0x${block.padByte.toString(16).padStart(2, "0")} ` + + `start=${block.start} count=${block.count}`, + ); + const started = Date.now(); + let hitCount = 0; + const handle = runBrute( + { + device, + keyLen: block.keyLen, + charset: block.charset, + padByte: block.padByte, + start: BigInt(block.start), + count: BigInt(block.count), + workers: block.cpuWorkers ?? this.cpuWorkers, + targetHex: block.target, + fills: block.fills, + }, + { + onProgress: (done, count, rate) => { + const live = this.jobs.get(device.key); + if (live && live.jobId === block.jobId) { + live.done = done; + live.count = count; + live.rate = rate; + } + this.send({ + type: "progress", + jobId: block.jobId, + deviceKey: device.key, + done: done.toString(), + count: count.toString(), + rate, + }); + this.events.onProgress?.(block.jobId, done, count, rate); + this.updateStatus(); + this.emitWorkers(); + }, + onHit: (hit) => { + hitCount += 1; + this.send({ + type: "hit", + jobId: block.jobId, + index: hit.index, + keyHex: hit.keyHex, + plainHex: hit.plainHex, + }); + this.events.onHit?.(block.jobId, hit.index, hit.keyHex, hit.plainHex); + }, + onLog: (line) => this.events.onLog?.(line), + }, + ); + this.jobs.set(device.key, { + jobId: block.jobId, + kill: handle.kill, + done: 0n, + count: BigInt(block.count), + rate: 0, + }); + this.beginRate(device.key); + this.updateStatus(); + this.emitWorkers(); + try { + await handle.promise; + const still = this.jobs.get(device.key); + if (still?.jobId === block.jobId) { + this.jobs.delete(device.key); + this.endRate(device.key, BigInt(block.count)); + this.completedBlocks.set(device.key, (this.completedBlocks.get(device.key) ?? 0) + 1); + this.send({ + type: "block_complete", + jobId: block.jobId, + deviceKey: device.key, + hits: hitCount, + elapsed: (Date.now() - started) / 1000, + }); + } + this.updateStatus(); + this.emitWorkers(); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + if (this.jobs.get(device.key)?.jobId === block.jobId) { + const live = this.jobs.get(device.key); + this.endRate(device.key, live?.done ?? 0n); + this.jobs.delete(device.key); + } + this.send({ type: "error", jobId: block.jobId, message }); + this.events.onLog?.(message); + this.updateStatus(); + this.emitWorkers(); + } + } +} diff --git a/src/paths.ts b/src/paths.ts new file mode 100644 index 0000000..13a4068 --- /dev/null +++ b/src/paths.ts @@ -0,0 +1,54 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; + +export function projectRoot(): string { + const fromDir = path.resolve(__dirname, ".."); + if (fs.existsSync(path.join(fromDir, "package.json"))) { + return fromDir; + } + return process.cwd(); +} + +export function binDir(): string { + return path.join(projectRoot(), "bin"); +} + +export function exeName(base: string): string { + return process.platform === "win32" ? `${base}.exe` : base; +} + +export function runnerPath(kind: "cpu" | "metal" | "cuda"): string | null { + const names: Record = { + cpu: "med_cpu", + metal: "med_gpu", + cuda: "med_cuda", + }; + const p = path.join(binDir(), exeName(names[kind])); + return fs.existsSync(p) ? p : null; +} + +export function metalShaderPath(): string { + return path.join(projectRoot(), "native", "metal", "des_bruteforce.metal"); +} + +export function defaultMedPath(): string { + const root = projectRoot(); + try { + const match = fs.readdirSync(root).find((name) => name.toLowerCase().endsWith(".med")); + if (match) { + return path.join(root, match); + } + } catch { + /* ignore */ + } + return ""; +} + +export function defaultOutDir(): string { + return path.join(projectRoot(), "decoded", "bruteforce"); +} + +export function cpuCoreCount(): number { + return Math.max(1, os.cpus().length); +} diff --git a/src/search/nativeRunner.ts b/src/search/nativeRunner.ts new file mode 100644 index 0000000..02e8003 --- /dev/null +++ b/src/search/nativeRunner.ts @@ -0,0 +1,248 @@ +import { spawn, ChildProcess } from "child_process"; +import os from "os"; +import { FILL_HEX, PAD_CT } from "../crypto/constants"; +import { metalShaderPath, runnerPath } from "../paths"; +import { parseGpuProgress } from "./planner"; + +export type DeviceType = "cpu" | "metal" | "cuda"; + +export interface ComputeDevice { + key: string; + id: number; + name: string; + type: DeviceType; + cores?: number; + memory?: number; +} + +export interface NativeHit { + index: string; + keyHex: string; + plainHex: string; +} + +export interface BruteOptions { + device: ComputeDevice; + keyLen: number; + charset: string; + padByte: number; + start: bigint; + count: bigint; + batch?: number; + workers?: number; + targetHex?: string; + fills?: string[]; +} + +export interface BruteHandlers { + onProgress?: (done: bigint, total: bigint, rate: number) => void; + onHit?: (hit: NativeHit) => void; + onLog?: (line: string) => void; +} + +function pumpLines(stream: NodeJS.ReadableStream, onLine: (line: string) => void): Promise { + return new Promise((resolve, reject) => { + let buf = ""; + stream.setEncoding("utf8"); + stream.on("data", (chunk: string) => { + buf += chunk; + let nl = buf.indexOf("\n"); + while (nl >= 0) { + const line = buf.slice(0, nl); + buf = buf.slice(nl + 1); + if (line.trim()) { + onLine(line.replace(/\r$/, "")); + } + nl = buf.indexOf("\n"); + } + }); + stream.on("end", () => { + if (buf.trim()) { + onLine(buf.replace(/\r$/, "")); + } + resolve(); + }); + stream.on("error", reject); + }); +} + +function spawnRunner(kind: DeviceType, args: string[]): ChildProcess { + const bin = runnerPath(kind); + if (!bin) { + throw new Error(`${kind} runner not built. Run npm run build:native.`); + } + return spawn(bin, args, { + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }); +} + +async function readCommandJson(kind: DeviceType, args: string[]): Promise { + const proc = spawnRunner(kind, args); + let stdout = ""; + let stderr = ""; + proc.stdout?.on("data", (c: Buffer) => { + stdout += c.toString(); + }); + proc.stderr?.on("data", (c: Buffer) => { + stderr += c.toString(); + }); + const code: number = await new Promise((resolve, reject) => { + proc.on("error", reject); + proc.on("close", resolve); + }); + if (code !== 0) { + throw new Error(stderr.trim() || `${kind} ${args[0]} failed (${code})`); + } + return stdout.trim(); +} + +function parseDeviceList(kind: DeviceType, raw: string): ComputeDevice[] { + if (!raw) { + return []; + } + const parsed = JSON.parse(raw) as Array<{ id: number; name: string; type?: string; cores?: number; memory?: number }>; + return parsed.map((d) => ({ + key: `${kind}:${d.id}`, + id: d.id, + name: d.name, + type: kind, + cores: d.cores, + memory: d.memory, + })); +} + +export async function listComputeDevices(): Promise { + const devices: ComputeDevice[] = []; + const cpuBin = runnerPath("cpu"); + if (cpuBin) { + try { + devices.push(...parseDeviceList("cpu", await readCommandJson("cpu", ["devices"]))); + } catch { + devices.push({ + key: "cpu:0", + id: 0, + name: `CPU (${os.cpus().length} cores)`, + type: "cpu", + cores: os.cpus().length, + }); + } + } else { + devices.push({ + key: "cpu:0", + id: 0, + name: `CPU (${os.cpus().length} cores) — build native runner`, + type: "cpu", + cores: os.cpus().length, + }); + } + + if (process.platform === "darwin" && runnerPath("metal")) { + try { + devices.push(...parseDeviceList("metal", await readCommandJson("metal", ["devices"]))); + } catch { + /* no Metal */ + } + } + if (runnerPath("cuda")) { + try { + devices.push(...parseDeviceList("cuda", await readCommandJson("cuda", ["devices"]))); + } catch { + /* no CUDA */ + } + } + return devices; +} + +export function preferredDevice(devices: ComputeDevice[]): ComputeDevice { + return devices.find((d) => d.type === "cuda") || devices.find((d) => d.type === "metal") || devices[0]; +} + +export function runBrute(opts: BruteOptions, handlers: BruteHandlers = {}): { promise: Promise; kill: () => void } { + const args = [ + "brute", + "--device", + String(opts.device.id), + "--key-len", + String(opts.keyLen), + "--charset", + opts.charset, + "--pad", + opts.padByte.toString(16).padStart(2, "0"), + "--target", + opts.targetHex ?? PAD_CT.toString("hex"), + "--fills", + (opts.fills ?? FILL_HEX).join(","), + "--start", + opts.start.toString(), + "--count", + opts.count.toString(), + "--batch", + String(opts.batch ?? (opts.device.type === "cpu" ? 200_000 : 16_777_216)), + ]; + if (opts.device.type === "metal") { + args.push("--shader", metalShaderPath()); + } + if (opts.device.type === "cpu" && opts.workers) { + args.push("--workers", String(opts.workers)); + } + + const proc = spawnRunner(opts.device.type, args); + const hits: NativeHit[] = []; + let killed = false; + + const promise = (async () => { + const outP = proc.stdout + ? pumpLines(proc.stdout, (line) => { + if (!line.startsWith("{")) { + return; + } + try { + const rec = JSON.parse(line) as { index: number | string; key_hex: string; plain_hex: string }; + const hit: NativeHit = { + index: String(rec.index), + keyHex: rec.key_hex, + plainHex: rec.plain_hex, + }; + hits.push(hit); + handlers.onHit?.(hit); + } catch { + handlers.onLog?.(line); + } + }) + : Promise.resolve(); + const errP = proc.stderr + ? pumpLines(proc.stderr, (line) => { + const parsed = parseGpuProgress(line); + if (parsed) { + handlers.onProgress?.(parsed.done, parsed.total, parsed.rate); + } else if (!line.includes("_selftest")) { + handlers.onLog?.(line); + } + }) + : Promise.resolve(); + const [code] = await Promise.all([ + new Promise((resolve, reject) => { + proc.on("error", reject); + proc.on("close", (c) => resolve(c ?? 1)); + }), + outP, + errP, + ]); + if (killed) { + return hits; + } + if (code !== 0) { + throw new Error(`${opts.device.type} brute failed (exit ${code})`); + } + return hits; + })(); + + return { + promise, + kill: () => { + killed = true; + proc.kill("SIGTERM"); + }, + }; +} diff --git a/src/search/orchestrator.ts b/src/search/orchestrator.ts new file mode 100644 index 0000000..241c0bb --- /dev/null +++ b/src/search/orchestrator.ts @@ -0,0 +1,667 @@ +import { randomUUID } from "crypto"; +import fs from "fs"; +import { FILL_HEX, PAD_CT } from "../crypto/constants"; +import { firstPlainHex, saveHit, writeResults, type HitRecord } from "../crypto/hits"; +import { hexKeyToBuffer } from "../crypto/des"; +import type { MasterServer } from "../net/master"; +import { formatEta, formatNumber, formatRate, passwordFromKey, planShortJobs, type StageJob } from "./planner"; +import { runBrute, type ComputeDevice, type DeviceType } from "./nativeRunner"; + +export function localWorkerId(deviceKey: string): string { + return `local:${deviceKey}`; +} + +export function remoteWorkerId(slaveId: string, deviceKey: string): string { + return `slave:${slaveId}:${deviceKey}`; +} + +export interface SearchParams { + filePath: string; + minLen: number; + maxLen: number; + charset: string; + pads: number[]; + gpuBatch: number; + devices: ComputeDevice[]; + outDir: string; + blockSizeFor: (workerId: string, deviceType: DeviceType) => bigint; + cpuWorkersFor: (workerId: string) => number; +} + +export interface ProgressSnapshot { + length: number; + pad: number; + cipher: string; + stageDone: bigint; + stageTotal: bigint; + stagePct: number; + overallDone: bigint; + overallTotal: bigint; + overallPct: number; + rate: number; + elapsed: number; + etaSec: number | null; + hits: number; + workerLabel?: string; +} + +export interface SearchEvents { + onLog?: (line: string) => void; + onProgress?: (snap: ProgressSnapshot) => void; + onHit?: (hit: HitRecord, path: string | null) => void; + onWorkers?: (rows: WorkerRow[]) => void; + onDone?: (hits: HitRecord[], cancelled: boolean) => void; +} + +export interface WorkerRow { + id: string; + host: string; + ip: string; + device: string; + deviceKey: string; + deviceType: DeviceType; + blockSize: number; + current: string; + pct: number; + completedBlocks: number; + completedKeys: bigint; + rate: string; + rateNum: number; + avgRate: string; + avgRateNum: number; + busy: boolean; +} + +class BlockAllocator { + private next = 0n; + private inflight = new Map(); + private retries: Array<{ start: bigint; count: bigint }> = []; + completed = 0n; + + constructor(readonly space: bigint) {} + + take(blockSize: bigint): { jobId: string; start: bigint; count: bigint } | null { + const queued = this.retries.shift(); + if (queued) { + const jobId = randomUUID(); + this.inflight.set(jobId, queued); + return { jobId, ...queued }; + } + if (this.next >= this.space) { + return null; + } + const size = blockSize < 1n ? 1n : blockSize; + const start = this.next; + const count = size < this.space - start ? size : this.space - start; + this.next += count; + const jobId = randomUUID(); + this.inflight.set(jobId, { start, count }); + return { jobId, start, count }; + } + + complete(jobId: string): void { + const block = this.inflight.get(jobId); + if (!block) { + return; + } + this.inflight.delete(jobId); + this.completed += block.count; + } + + fail(jobId: string): void { + const block = this.inflight.get(jobId); + if (!block) { + return; + } + this.inflight.delete(jobId); + this.retries.push(block); + } + + get finished(): boolean { + return this.completed === this.space && this.inflight.size === 0 && this.retries.length === 0; + } +} + +interface TrackedWorker { + id: string; + host: string; + ip: string; + device: string; + deviceKey: string; + deviceType: DeviceType; + slaveId?: string; + jobId: string | null; + done: bigint; + count: bigint; + rate: number; + completedBlocks: number; + busy: boolean; + keysDone: bigint; + activeMs: number; + busyStarted: number | null; +} + +function beginWork(worker: TrackedWorker): void { + if (worker.busyStarted == null) { + worker.busyStarted = Date.now(); + } +} + +function endWork(worker: TrackedWorker, keys: bigint): void { + if (worker.busyStarted != null) { + worker.activeMs += Date.now() - worker.busyStarted; + worker.busyStarted = null; + } + if (keys > 0n) { + worker.keysDone += keys; + } +} + +function averageRate(worker: TrackedWorker): number { + const liveMs = worker.busyStarted != null ? Date.now() - worker.busyStarted : 0; + const sec = (worker.activeMs + liveMs) / 1000; + const keys = worker.keysDone + (worker.busy ? worker.done : 0n); + if (sec <= 0 || keys <= 0n) { + return 0; + } + return Number(keys) / sec; +} + +function displayRate(rate: number): string { + return rate > 0 ? formatRate(rate) : "—"; +} + +function clampBlockSize(size: bigint): number { + if (size <= 0n) { + return 0; + } + if (size > 2147483647n) { + return 2147483647; + } + return Number(size); +} + +export class MasterSearch { + private cancel = false; + private kills = new Set<() => void>(); + private unsub: Array<() => void> = []; + private started = 0; + private hits: HitRecord[] = []; + private roster = new Map(); + private overallPrior = 0n; + private overallTotal = 1n; + private stageCompleted = 0n; + private stageTotal = 1n; + private stage: StageJob | null = null; + private dispatch: (() => void) | null = null; + private stageAlloc: BlockAllocator | null = null; + + constructor( + private readonly server: MasterServer | null, + private readonly params: SearchParams, + private readonly events: SearchEvents, + ) {} + + stop(): void { + this.cancel = true; + for (const kill of this.kills) { + kill(); + } + this.kills.clear(); + this.server?.cancelAll(); + } + + async run(): Promise { + this.cancel = false; + this.hits = []; + this.started = Date.now(); + if (this.params.devices.length === 0 && !(this.server && this.server.listWorkers().length > 0)) { + throw new Error("Select at least one local device, or connect a slave that exposes workers."); + } + const data = fs.readFileSync(this.params.filePath); + const firstCt = data.subarray(0, 8); + const jobs = planShortJobs( + this.params.minLen, + this.params.maxLen, + this.params.charset.length, + this.params.pads, + ); + this.overallTotal = jobs.reduce((s, j) => s + j.space, 0n) || 1n; + this.overallPrior = 0n; + this.roster.clear(); + this.rebuildRoster(); + this.events.onLog?.( + `${jobs.length} stages lengths ${this.params.minLen}-${this.params.maxLen} ` + + `charset ${this.params.charset.length} pads ${this.params.pads.map((p) => p.toString(16).padStart(2, "0")).join(",")} ` + + `total keys ${this.overallTotal.toString()}`, + ); + + this.bindServer(data, firstCt); + + try { + for (const job of jobs) { + if (this.cancel) { + break; + } + await this.runStage(job, data, firstCt); + this.overallPrior += job.space; + } + } finally { + for (const off of this.unsub) { + off(); + } + this.unsub = []; + this.dispatch = null; + } + + const elapsed = (Date.now() - this.started) / 1000; + writeResults(this.params.outDir, elapsed, this.hits); + this.events.onDone?.(this.hits, this.cancel); + return this.hits; + } + + private sizeFor(worker: TrackedWorker): bigint { + return this.params.blockSizeFor(worker.id, worker.deviceType); + } + + private rebuildRoster(): void { + const next = new Map(); + for (const device of this.params.devices) { + const id = localWorkerId(device.key); + const prev = this.roster.get(id); + next.set(id, { + id, + host: "local", + ip: "local", + device: device.name, + deviceKey: device.key, + deviceType: device.type, + jobId: prev?.jobId ?? null, + done: prev?.done ?? 0n, + count: prev?.count ?? 0n, + rate: prev?.rate ?? 0, + completedBlocks: prev?.completedBlocks ?? 0, + busy: prev?.busy ?? false, + keysDone: prev?.keysDone ?? 0n, + activeMs: prev?.activeMs ?? 0, + busyStarted: prev?.busyStarted ?? null, + }); + } + if (this.server) { + for (const remote of this.server.listWorkers()) { + const id = remoteWorkerId(remote.slaveId, remote.device.key); + const prev = this.roster.get(id); + next.set(id, { + id, + host: remote.hostname, + ip: remote.ip, + device: remote.device.name, + deviceKey: remote.device.key, + deviceType: remote.device.type, + slaveId: remote.slaveId, + jobId: prev?.jobId ?? remote.jobId, + done: prev?.done ?? remote.progressDone, + count: prev?.count ?? remote.progressCount, + rate: prev?.rate ?? remote.rate, + completedBlocks: prev?.completedBlocks ?? 0, + busy: prev?.busy ?? !remote.idle, + keysDone: prev?.keysDone ?? 0n, + activeMs: prev?.activeMs ?? 0, + busyStarted: prev?.busyStarted ?? null, + }); + } + } + this.roster = next; + } + + private bindServer(data: Buffer, firstCt: Buffer): void { + if (!this.server) { + return; + } + const prev = { ...this.server.events }; + this.server.events.onProgress = (slave, worker, jobId, done, count, rate) => { + 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.emitProgress(); + }; + this.server.events.onHit = (slave, jobId, index, keyHex, plainHex) => { + prev.onHit?.(slave, jobId, index, keyHex, plainHex); + this.recordHit(keyHex, plainHex, `slave_${slave.hostname}`, data, firstCt); + }; + this.server.events.onComplete = (slave, worker, jobId) => { + prev.onComplete?.(slave, worker, jobId); + const tracked = this.roster.get(remoteWorkerId(slave.id, worker.device.key)); + if (tracked) { + endWork(tracked, tracked.count); + tracked.jobId = null; + tracked.busy = false; + tracked.rate = 0; + tracked.done = 0n; + tracked.count = 0n; + tracked.completedBlocks += 1; + } + this.stageAlloc?.complete(jobId); + this.emitProgress(); + this.dispatch?.(); + }; + this.server.events.onDropped = (slave, jobId) => { + prev.onDropped?.(slave, jobId); + if (jobId) { + this.stageAlloc?.fail(jobId); + for (const tracked of this.roster.values()) { + if (tracked.jobId === jobId) { + endWork(tracked, tracked.done); + tracked.jobId = null; + tracked.busy = false; + tracked.rate = 0; + } + } + } + this.rebuildRoster(); + this.emitProgress(); + this.dispatch?.(); + }; + this.server.events.onSlaves = (_slaves) => { + prev.onSlaves?.(_slaves); + const before = [...this.roster.keys()].sort().join("|"); + this.rebuildRoster(); + const after = [...this.roster.keys()].sort().join("|"); + if (before !== after) { + this.emitProgress(); + this.dispatch?.(); + } + }; + this.unsub.push(() => { + this.server!.events.onProgress = prev.onProgress; + this.server!.events.onHit = prev.onHit; + this.server!.events.onComplete = prev.onComplete; + this.server!.events.onDropped = prev.onDropped; + this.server!.events.onSlaves = prev.onSlaves; + }); + } + + private async runStage(job: StageJob, data: Buffer, firstCt: Buffer): Promise { + this.stage = job; + this.stageTotal = job.space; + this.stageCompleted = 0n; + const alloc = new BlockAllocator(job.space); + this.stageAlloc = alloc; + this.events.onLog?.( + `Stage DES len=${job.length} pad=0x${job.pad.toString(16).padStart(2, "0")} ${job.space.toString()} keys`, + ); + + const localIdle = new Set(this.params.devices.map((d) => d.key)); + let resolve!: () => void; + const finished = new Promise((r) => { + resolve = r; + }); + + const checkDone = () => { + if (this.cancel || alloc.finished) { + resolve(); + } + }; + + const dispatch = () => { + if (this.cancel) { + checkDone(); + return; + } + for (const device of this.params.devices) { + if (!localIdle.has(device.key)) { + continue; + } + const worker = this.roster.get(localWorkerId(device.key)); + if (!worker) { + continue; + } + const block = alloc.take(this.sizeFor(worker)); + if (!block) { + break; + } + localIdle.delete(device.key); + this.startLocal(device, job, block, data, firstCt, alloc, localIdle, dispatch, checkDone); + } + if (this.server) { + for (const remote of this.server.idleWorkers()) { + const worker = this.roster.get(remoteWorkerId(remote.slaveId, remote.device.key)); + if (!worker) { + continue; + } + const block = alloc.take(this.sizeFor(worker)); + if (!block) { + break; + } + const ok = this.server.assign(remote.slaveId, remote.device.key, { + jobId: block.jobId, + deviceKey: remote.device.key, + start: block.start.toString(), + count: block.count.toString(), + keyLen: job.length, + charset: this.params.charset, + padByte: job.pad, + target: PAD_CT.toString("hex"), + fills: FILL_HEX, + cpuWorkers: remote.device.type === "cpu" ? this.params.cpuWorkersFor(worker.id) : undefined, + }); + if (!ok) { + alloc.fail(block.jobId); + } else { + worker.jobId = block.jobId; + worker.busy = true; + worker.done = 0n; + worker.count = block.count; + worker.rate = 0; + beginWork(worker); + } + } + } + this.emitProgress(); + const hasRemote = !!this.server && this.server.listWorkers().length > 0; + if ( + !this.cancel && + !alloc.finished && + localIdle.size === 0 && + this.kills.size === 0 && + ![...this.roster.values()].some((w) => w.busy) && + !hasRemote + ) { + this.events.onLog?.("No workers remaining; stopping."); + this.cancel = true; + } + checkDone(); + }; + + this.dispatch = dispatch; + dispatch(); + await finished; + this.dispatch = null; + this.stageAlloc = null; + this.stageCompleted = job.space; + this.emitProgress(); + } + + private startLocal( + device: ComputeDevice, + job: StageJob, + block: { jobId: string; start: bigint; count: bigint }, + data: Buffer, + firstCt: Buffer, + alloc: BlockAllocator, + localIdle: Set, + dispatch: () => void, + checkDone: () => void, + ): void { + const id = localWorkerId(device.key); + const tracked = this.roster.get(id); + if (tracked) { + tracked.jobId = block.jobId; + tracked.busy = true; + tracked.done = 0n; + tracked.count = block.count; + tracked.rate = 0; + beginWork(tracked); + } + const handle = runBrute( + { + device, + keyLen: job.length, + charset: this.params.charset, + padByte: job.pad, + start: block.start, + count: block.count, + batch: device.type === "cpu" ? 200_000 : this.params.gpuBatch, + workers: this.params.cpuWorkersFor(id), + }, + { + 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.emitProgress(device.name); + }, + onHit: (hit) => { + this.recordHit(hit.keyHex, hit.plainHex, device.type, data, firstCt); + }, + onLog: (line) => this.events.onLog?.(line), + }, + ); + this.kills.add(handle.kill); + let keepDevice = true; + void handle.promise + .then(() => { + if (this.cancel) { + alloc.fail(block.jobId); + } else { + alloc.complete(block.jobId); + const live = this.roster.get(id); + if (live) { + live.completedBlocks += 1; + endWork(live, block.count); + } + } + }) + .catch((err: unknown) => { + this.events.onLog?.(`${device.name}: ${err instanceof Error ? err.message : String(err)}`); + alloc.fail(block.jobId); + keepDevice = false; + }) + .finally(() => { + this.kills.delete(handle.kill); + const live = this.roster.get(id); + if (live) { + if (live.busyStarted != null) { + endWork(live, this.cancel ? live.done : 0n); + } + live.jobId = null; + live.busy = false; + live.rate = 0; + live.done = 0n; + live.count = 0n; + } + if (keepDevice) { + localIdle.add(device.key); + } + this.stageCompleted = alloc.completed; + this.emitProgress(); + dispatch(); + checkDone(); + }); + } + + private recordHit(keyHex: string, plainHex: string, backend: string, data: Buffer, firstCt: Buffer): void { + const job = this.stage; + const key = hexKeyToBuffer(keyHex); + const length = job?.length ?? 8; + const pad = job?.pad ?? 0; + const hit: HitRecord = { + cipher: "des", + password: passwordFromKey(key, length), + derivation: `${backend}_len${length}_pad${pad.toString(16).padStart(2, "0")}`, + key_hex: key.toString("hex"), + pad_fill: plainHex, + first_pt_hex: firstPlainHex(keyHex, firstCt), + backend, + }; + this.hits.push(hit); + const saved = saveHit(hit, data, this.params.outDir); + this.events.onHit?.(hit, saved); + this.events.onLog?.( + `HIT '${hit.password}' ${hit.key_hex} ${hit.payload_ok ? "CONFIRMED" : "pad-only"} -> ${saved ?? "(not saved)"}`, + ); + } + + private emitProgress(workerLabel?: string): void { + let inflight = 0n; + let rateSum = 0; + const rows: WorkerRow[] = []; + for (const w of this.roster.values()) { + if (w.busy) { + inflight += w.done; + rateSum += w.rate; + } + const pct = w.busy && w.count > 0n ? Number((w.done * 1000n) / w.count) / 10 : 0; + const avg = averageRate(w); + rows.push({ + id: w.id, + host: w.host, + ip: w.ip, + device: w.device, + deviceKey: w.deviceKey, + deviceType: w.deviceType, + blockSize: clampBlockSize(this.sizeFor(w)), + current: w.busy ? `${formatNumber(w.done)} / ${formatNumber(w.count)}` : "idle", + pct, + completedBlocks: w.completedBlocks, + completedKeys: w.keysDone, + rate: w.busy ? displayRate(w.rate) : "—", + rateNum: w.busy ? w.rate : 0, + avgRate: displayRate(avg), + avgRateNum: avg, + busy: w.busy, + }); + } + const stageDone = this.stageCompleted + inflight; + const overallDone = this.overallPrior + stageDone; + const elapsed = Math.max((Date.now() - this.started) / 1000, 1e-6); + const rate = rateSum > 0 ? rateSum : Number(overallDone) / elapsed; + const remain = this.overallTotal > overallDone ? this.overallTotal - overallDone : 0n; + const snap: ProgressSnapshot = { + length: this.stage?.length ?? 0, + pad: this.stage?.pad ?? 0, + cipher: "des", + stageDone, + stageTotal: this.stageTotal, + stagePct: this.stageTotal > 0n ? Number((stageDone * 1000n) / this.stageTotal) / 10 : 0, + overallDone, + overallTotal: this.overallTotal, + overallPct: Number((overallDone * 1000n) / this.overallTotal) / 10, + rate, + elapsed, + etaSec: rate > 0 ? Number(remain) / rate : null, + hits: this.hits.length, + workerLabel, + }; + this.events.onProgress?.(snap); + this.events.onWorkers?.(rows); + } +} + +export function describeProgress(snap: ProgressSnapshot): string { + return ( + `${formatRate(snap.rate)} elapsed ${formatEta(snap.elapsed)} ` + + `ETA ${formatEta(snap.etaSec)} hits ${snap.hits}` + ); +} diff --git a/src/search/planner.ts b/src/search/planner.ts new file mode 100644 index 0000000..398a371 --- /dev/null +++ b/src/search/planner.ts @@ -0,0 +1,96 @@ +export interface StageJob { + length: number; + pad: number; + cipher: "des"; + space: bigint; +} + +export function planShortJobs(minLen: number, maxLen: number, charsetN: number, pads: number[]): StageJob[] { + const jobs: StageJob[] = []; + for (let length = minLen; length <= maxLen; length++) { + const space = BigInt(charsetN) ** BigInt(length); + for (const pad of pads) { + jobs.push({ length, pad, cipher: "des", space }); + } + } + return jobs; +} + +export function parsePads(text: string): number[] { + return text + .split(",") + .map((p) => p.trim()) + .filter(Boolean) + .map((p) => parseInt(p, 16)) + .filter((n) => Number.isFinite(n) && n >= 0 && n <= 255); +} + +export function formatNumber(value: number | bigint): string { + const n = typeof value === "bigint" ? Number(value) : value; + if (!Number.isFinite(n)) { + return "0"; + } + const sign = n < 0 ? "-" : ""; + const abs = Math.abs(n); + if (abs >= 1_000_000_000) { + return `${sign}${(abs / 1_000_000_000).toFixed(1)}B`; + } + if (abs >= 1_000_000) { + return `${sign}${(abs / 1_000_000).toFixed(1)}M`; + } + if (abs >= 1_000) { + return `${sign}${(abs / 1_000).toFixed(1)}k`; + } + return `${sign}${Math.round(abs)}`; +} + +export function formatRate(rate: number): string { + return `${formatNumber(rate)}/s`; +} + +export function formatEta(seconds: number | null | undefined): string { + if (seconds == null || seconds < 0 || seconds > 1e8) { + return "--"; + } + const total = Math.floor(seconds); + const hours = Math.floor(total / 3600); + const minutes = Math.floor((total % 3600) / 60); + const secs = total % 60; + if (hours) { + return `${hours}h${String(minutes).padStart(2, "0")}m`; + } + if (minutes) { + return `${minutes}m${String(secs).padStart(2, "0")}s`; + } + return `${secs}s`; +} + +export function keyBytesFromIndex(index: bigint, length: number, charset: string, padByte: number): Buffer { + const n = charset.length; + const raw = Buffer.alloc(length); + let i = index; + for (let pos = length - 1; pos >= 0; pos--) { + raw[pos] = charset.charCodeAt(Number(i % BigInt(n))); + i /= BigInt(n); + } + return Buffer.concat([raw, Buffer.alloc(Math.max(0, 8 - length), padByte)]).subarray(0, 8); +} + +export function passwordFromKey(key: Buffer, length: number): string { + return key.subarray(0, length).toString("ascii"); +} + +export function parseGpuProgress(line: string): { done: bigint; total: bigint; rate: number } | null { + const parts = line.trim().split(/\s+/); + if (parts.length < 3 || parts[0] !== "gpu" || !parts[1].includes("/")) { + return null; + } + const [doneS, totalS] = parts[1].split("/", 2); + const done = BigInt(doneS); + const total = BigInt(totalS); + const rate = Number(parts[2]); + if (!Number.isFinite(rate)) { + return null; + } + return { done, total, rate }; +} diff --git a/src/ui/app.css b/src/ui/app.css new file mode 100644 index 0000000..69eb310 --- /dev/null +++ b/src/ui/app.css @@ -0,0 +1,539 @@ +/* DES Key Cracker — Qt Style Sheet + Spacing scale: 4 / 8 / 12 / 16 / 20 / 24 */ + +QMainWindow { + background-color: #10151c; + color: #e8eef4; + font-family: "SF Pro Text", "Segoe UI", "Helvetica Neue", sans-serif; + font-size: 13px; +} + +QWidget { + color: #e8eef4; + font-family: "SF Pro Text", "Segoe UI", "Helvetica Neue", sans-serif; + font-size: 13px; + background-color: transparent; +} + +QLabel, +QCheckBox, +QRadioButton { + background-color: transparent; + border: none; +} + +#toolbar { + background-color: #171e28; + border-bottom: 1px solid #2b3544; + min-height: 88px; +} + +#body { + background-color: #10151c; +} + +#footer { + background-color: #171e28; + border-top: 1px solid #2b3544; +} + +#brand { + color: #5eead4; + font-size: 18px; + font-weight: 700; + letter-spacing: 0.2px; + padding: 0; + margin: 0; +} + +#brandSub { + color: #7d8b9c; + font-size: 10px; + font-weight: 700; + letter-spacing: 1.6px; + padding: 2px 0 0 0; +} + +#svgIcon { + background-color: transparent; +} + +#vSep { + color: #2b3544; + background-color: #2b3544; + border: none; + max-width: 1px; + min-width: 1px; + margin: 8px 4px; +} + +#fieldCaption { + color: #8b9aab; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.6px; + padding: 0 0 2px 2px; +} + +#muted { + color: #8b9aab; + font-size: 12px; + padding: 0; +} + +QLabel#sectionTitle { + color: #f8fafc; + font-size: 18px; + font-weight: 700; + padding: 0; +} + +#hint { + color: #c5d0dc; + font-size: 14px; + line-height: 1.45; +} + +#statusPill, +#statusPillLive { + border-radius: 12px; + padding: 8px 14px; + font-size: 12px; + font-weight: 700; + min-width: 92px; +} + +#statusPill { + background-color: #243041; + color: #9eb0c3; + border: 1px solid #3d4a5c; +} + +#statusPillLive { + background-color: #134e4a; + color: #5eead4; + border: 1px solid #14b8a6; +} + +QLabel#stats { + color: #cbd5e1; + font-size: 12px; + padding: 8px 2px 0 2px; +} + +#page { + background-color: #10151c; +} + +QPushButton { + background-color: #243041; + color: #e8eef4; + border: 1px solid #3d4a5c; + border-radius: 12px; + padding: 3px 12px 3px 12px; + min-height: 26px; + min-width: 72px; +} + +QPushButton:hover { + background-color: #2d3c50; +} + +QPushButton:pressed { + background-color: #1c2633; +} + +QPushButton:disabled { + color: #66788c; + background-color: #1a222c; + border-color: #2b3544; +} + +QPushButton#primaryBtn { + background-color: #0f766e; + border: 1px solid #2dd4bf; + color: #f0fdfa; + font-weight: 600; +} + +QPushButton#primaryBtn:hover { + background-color: #0d9488; +} + +QPushButton#dangerBtn { + background-color: #7f1d1d; + border: 1px solid #f87171; + color: #fff1f2; + font-weight: 600; +} + +QPushButton#dangerBtn:hover { + background-color: #991b1b; +} + +QPushButton#ghostBtn { + background-color: transparent; + border: 1px solid #3d4a5c; +} + +QPushButton#ghostBtn:hover { + background-color: #243041; +} + +QLineEdit, +QSpinBox, +QComboBox { + background-color: #0c1117; + color: #e8eef4; + border: 1px solid #3d4a5c; + border-radius: 12px; + padding: 3px 12px 3px 10px; + min-height: 26px; + selection-background-color: #0f766e; +} + +QPlainTextEdit { + background-color: #0c1117; + color: #d5deea; + border: 1px solid #2b3544; + border-radius: 12px; + padding: 8px; + font-family: "SF Mono", Menlo, Consolas, monospace; + font-size: 12px; + selection-background-color: #0f766e; +} + +QLineEdit:focus, +QSpinBox:focus, +QComboBox:focus, +QPlainTextEdit:focus { + border: 1px solid #2dd4bf; +} + +QComboBox::drop-down { + border: none; + width: 22px; +} + +QComboBox:disabled { + color: #66788c; + background-color: #1a222c; + border-color: #2b3544; +} + +QComboBox QAbstractItemView { + background-color: #171e28; + color: #e8eef4; + selection-background-color: #0f766e; + selection-color: #f0fdfa; +} + +QComboBox QAbstractItemView::item { + padding: 6px 10px; +} + +QSpinBox::up-button, +QSpinBox::down-button { + width: 0; + height: 0; + border: none; +} + +QTabWidget::pane { + border: none; + border-top: 1px solid #2b3544; + background-color: #10151c; + border-radius: 0; + top: -1px; + padding: 0; + margin: 0; +} + +QTabBar::tab { + background-color: #171e28; + color: #8b9aab; + padding: 10px 18px; + margin-right: 4px; + min-width: 88px; + border-top: 1px solid #2b3544; + border-right: 1px solid #2b3544; + border-bottom: none; + border-left: 1px solid #2b3544; + border-top-left-radius: 12px; + border-top-right-radius: 12px; + font-weight: 600; +} + +QTabBar::tab:selected { + background-color: #10151c; + color: #5eead4; + margin-bottom: -1px; +} + +QTabBar::tab:hover:!selected { + color: #e8eef4; + background-color: #1c2530; +} + +#progressRow { + margin: 0; +} + +#progressTrack, +QLabel#progressPct { + border: 1px solid #2b3544; + min-height: 26px; + max-height: 26px; + margin: 0; + padding: 0; +} + +#progressTrack { + background-color: #0c1117; + border-right: none; + border-top-left-radius: 12px; + border-top-right-radius: 0; + border-bottom-left-radius: 12px; + border-bottom-right-radius: 0; +} + +#progressTrack::chunk { + background-color: #14b8a6; + border-top-left-radius: 11px; + border-top-right-radius: 0; + border-bottom-left-radius: 11px; + border-bottom-right-radius: 0; +} + +QProgressBar#workerProgress { + background-color: #0c1117; + border: none; + border-radius: 0; + margin: 0; + padding: 0; + min-height: 0; + text-align: center; + color: #5eead4; + font-size: 11px; + font-weight: 700; +} + +QProgressBar#workerProgress::chunk { + background-color: #14b8a6; + border-radius: 0; + margin: 0; +} + +QScrollArea#deviceScroll { + border: none; + background-color: transparent; +} + +QWidget#deviceList { + background-color: transparent; +} + +QFrame#deviceCard { + background-color: #171e28; + border: 1px solid #3d4a5c; + border-radius: 12px; +} + +QFrame#deviceCard QCheckBox { + min-width: 16px; + max-width: 20px; + padding: 0; + spacing: 0; +} + +QLabel#deviceHost { + color: #f8fafc; + font-size: 14px; + font-weight: 700; +} + +QLabel#deviceKind { + color: #5eead4; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.8px; +} + +QLabel#deviceName { + color: #e8eef4; + font-size: 13px; +} + +QLabel#deviceMeta { + color: #8b9aab; + font-size: 12px; +} + +QLabel#progressPct { + background-color: #243041; + color: #5eead4; + border-top-left-radius: 0; + border-top-right-radius: 12px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 12px; + padding: 0 10px; + font-size: 12px; + font-weight: 700; +} + +QTableWidget { + background-color: #0c1117; + alternate-background-color: #141b24; + gridline-color: #243041; + border: 1px solid #2b3544; + border-radius: 12px; + color: #e8eef4; + padding: 0; +} + +QTableWidget::item { + padding: 0; +} + +QTableWidget::item:selected { + background-color: #134e4a; + color: #ccfbf1; +} + +QTableWidget QHeaderView { + background-color: transparent; +} + +QHeaderView::section { + background-color: #171e28; + color: #8b9aab; + border: none; + border-bottom: 1px solid #2b3544; + padding: 10px 8px; + font-weight: 600; +} + +QHeaderView::section:first { + border-top-left-radius: 11px; +} + +QHeaderView::section:last { + border-top-right-radius: 11px; +} + +QTableCornerButton::section { + background-color: #171e28; + border: none; + border-top-left-radius: 11px; +} + +QTableWidget QScrollBar:vertical { + background: #0c1117; + width: 10px; + margin: 4px 2px 4px 0; + border: none; +} + +QTableWidget QScrollBar::handle:vertical { + background: #3d4a5c; + border-radius: 5px; + min-height: 24px; +} + +QTableWidget QScrollBar::handle:vertical:hover { + background: #54657a; +} + +QTableWidget QScrollBar::add-line:vertical, +QTableWidget QScrollBar::sub-line:vertical { + height: 0; + width: 0; + border: none; + background: none; +} + +QTableWidget QScrollBar::add-page:vertical, +QTableWidget QScrollBar::sub-page:vertical { + background: none; +} + +QTableWidget QScrollBar:horizontal { + background: #0c1117; + height: 10px; + margin: 0 4px 2px 4px; + border: none; +} + +QTableWidget QScrollBar::handle:horizontal { + background: #3d4a5c; + border-radius: 5px; + min-width: 24px; +} + +QTableWidget QScrollBar::handle:horizontal:hover { + background: #54657a; +} + +QTableWidget QScrollBar::add-line:horizontal, +QTableWidget QScrollBar::sub-line:horizontal { + width: 0; + height: 0; + border: none; + background: none; +} + +QTableWidget QScrollBar::add-page:horizontal, +QTableWidget QScrollBar::sub-page:horizontal { + background: none; +} + +QTableWidget QScrollBar::corner { + background: #0c1117; +} + +QCheckBox, +QRadioButton { + spacing: 8px; + color: #e8eef4; + padding: 2px 0; + background-color: transparent; +} + +QCheckBox::indicator, +QRadioButton::indicator { + width: 16px; + height: 16px; + border: 1px solid #3d4a5c; + background-color: #0c1117; +} + +QCheckBox::indicator { + border-radius: 4px; +} + +QRadioButton::indicator { + border-radius: 8px; +} + +QCheckBox::indicator:checked, +QRadioButton::indicator:checked { + background-color: #14b8a6; + border: 1px solid #5eead4; +} + +QScrollBar:vertical { + background: #10151c; + width: 10px; + margin: 4px; +} + +QScrollBar::handle:vertical { + background: #3d4a5c; + border-radius: 5px; + min-height: 24px; +} + +QScrollBar::add-line:vertical, +QScrollBar::sub-line:vertical { + height: 0; +} diff --git a/src/ui/icons.ts b/src/ui/icons.ts new file mode 100644 index 0000000..56dc137 --- /dev/null +++ b/src/ui/icons.ts @@ -0,0 +1,58 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import { QIcon, QSize, QSvgWidget } from "@nodegui/nodegui"; + +const cacheDir = path.join(os.tmpdir(), "des-key-cracker-icons"); +const iconCache = new Map(); +const svgKeepAlive: QSvgWidget[] = []; + +export const ICON_SIZE = new QSize(16, 16); +export const BUTTON_ICON_GAP = 4; +export const BUTTON_ICON_SIZE = new QSize(16 + BUTTON_ICON_GAP, 16); +export const LOGO_SIZE = 28; +export const ACCENT = "#5eead4"; +export const INK = "#e8eef4"; + +function lucideFile(name: string): string { + return require.resolve(`lucide-static/icons/${name}.svg`); +} + +export function tintedSvgPath(name: string, color: string, gapRightPx = 0): string { + const key = `${name}-${color.replace("#", "")}${gapRightPx ? `-g${gapRightPx}` : ""}`; + fs.mkdirSync(cacheDir, { recursive: true }); + const out = path.join(cacheDir, `${key}.svg`); + let svg = fs.readFileSync(lucideFile(name), "utf8").replace(/currentColor/g, color); + if (gapRightPx > 0) { + const extra = (gapRightPx * 24) / 16; + svg = svg + .replace(/viewBox="0 0 24 24"/, `viewBox="0 0 ${24 + extra} 24"`) + .replace(/width="24"/, `width="${24 + extra}"`); + } + fs.writeFileSync(out, svg); + return out; +} + +export function lucideIcon(name: string, color = INK, gapRightPx = 0): QIcon { + const key = `${name}:${color}:${gapRightPx}`; + let cached = iconCache.get(key); + if (!cached) { + cached = new QIcon(tintedSvgPath(name, color, gapRightPx)); + iconCache.set(key, cached); + } + return cached; +} + +export function buttonIcon(name: string, color = INK): QIcon { + return lucideIcon(name, color, BUTTON_ICON_GAP); +} + +export function lucideSvg(name: string, color: string, size: number): QSvgWidget { + const widget = new QSvgWidget(); + widget.setObjectName("svgIcon"); + widget.load(tintedSvgPath(name, color)); + widget.setFixedWidth(size); + widget.setFixedHeight(size); + svgKeepAlive.push(widget); + return widget; +} diff --git a/src/ui/mainWindow.ts b/src/ui/mainWindow.ts new file mode 100644 index 0000000..eec74b0 --- /dev/null +++ b/src/ui/mainWindow.ts @@ -0,0 +1,1166 @@ +import os from "os"; +import { + AlignmentFlag, + Direction, + EditTrigger, + FileMode, + QBoxLayout, + QCheckBox, + QComboBox, + QFileDialog, + QFrame, + QGridLayout, + QLabel, + QLineEdit, + QMainWindow, + QMessageBox, + QPlainTextEdit, + QProgressBar, + QPushButton, + QRadioButton, + QScrollArea, + QSpinBox, + QTabWidget, + QTableWidget, + QTableWidgetItem, + QWidget, + Shape, + Shadow, + ButtonSymbols, + QSizePolicyPolicy, + WidgetAttribute, +} from "@nodegui/nodegui"; +import { + DEFAULT_CHARSET, + DEFAULT_GPU_BATCH, + DEFAULT_GPU_BLOCK_SIZE, + DEFAULT_PADS, + DEFAULT_PORT, + defaultBlockSize, +} from "../crypto/constants"; +import { localIPv4 } from "../net/lan"; +import { MasterServer } from "../net/master"; +import { SlaveClient } from "../net/slave"; +import { defaultMedPath, defaultOutDir } from "../paths"; +import { formatNumber, formatRate, parsePads } from "../search/planner"; +import { + describeProgress, + localWorkerId, + MasterSearch, + remoteWorkerId, + type WorkerRow, +} from "../search/orchestrator"; +import { listComputeDevices, type ComputeDevice } from "../search/nativeRunner"; +import { ACCENT, BUTTON_ICON_SIZE, LOGO_SIZE, buttonIcon, lucideIcon, lucideSvg } from "./icons"; +import { loadAppStylesheet } from "./stylesheet"; + +const WORKER_HEADERS = [ + "Host", + "IP", + "Device", + "Current", + "Progress", + "Completed block", + "Completed keys", + "Rate", + "Average rate", +]; + +const BLOCK_SIZE_PRESETS: Array<{ value: number; label: string }> = [ + { value: 65_536, label: "64K" }, + { value: 262_144, label: "256K" }, + { value: 1_048_576, label: "1M" }, + { value: 4_194_304, label: "4M" }, + { value: 16_777_216, label: "16M" }, + { value: 67_108_864, label: "64M" }, + { value: 268_435_456, label: "256M" }, + { value: 1_073_741_824, label: "1G" }, +]; + +function formatBlockSizeLabel(value: number): string { + return BLOCK_SIZE_PRESETS.find((preset) => preset.value === value)?.label ?? value.toLocaleString("en-US"); +} + +function blockSizeChoices(current: number): Array<{ value: number; label: string }> { + const choices = BLOCK_SIZE_PRESETS.map((preset) => ({ ...preset })); + if (current > 0 && !choices.some((preset) => preset.value === current)) { + choices.push({ value: current, label: formatBlockSizeLabel(current) }); + choices.sort((a, b) => a.value - b.value); + } + return choices; +} + +function selectedBlockSize(combo: QComboBox, values: number[]): number { + return values[combo.currentIndex()] ?? values[0] ?? DEFAULT_GPU_BLOCK_SIZE; +} + +function configBlockSizeCombo(value: number): { combo: QComboBox; value: () => number } { + const combo = new QComboBox(); + const choices = blockSizeChoices(value); + const values = choices.map((choice) => choice.value); + combo.setMinimumWidth(108); + for (const choice of choices) { + combo.addItem(undefined, choice.label); + } + const selected = values.indexOf(value); + combo.setCurrentIndex(selected >= 0 ? selected : Math.max(0, values.indexOf(DEFAULT_GPU_BLOCK_SIZE))); + return { + combo, + value: () => selectedBlockSize(combo, values), + }; +} + +function alert(title: string, text: string): void { + const box = new QMessageBox(); + box.setWindowTitle(title); + box.setText(text); + box.exec(); + (global as typeof globalThis & { _alert?: QMessageBox })._alert = box; +} + +function vbox(margins: [number, number, number, number], spacing: number): { widget: QWidget; layout: QBoxLayout } { + const widget = new QWidget(); + const layout = new QBoxLayout(Direction.TopToBottom); + layout.setContentsMargins(...margins); + layout.setSpacing(spacing); + widget.setLayout(layout); + return { widget, layout }; +} + +function hbox(margins: [number, number, number, number], spacing: number): { widget: QWidget; layout: QBoxLayout } { + const widget = new QWidget(); + const layout = new QBoxLayout(Direction.LeftToRight); + layout.setContentsMargins(...margins); + layout.setSpacing(spacing); + widget.setLayout(layout); + return { widget, layout }; +} + +function label(text: string, objectName?: string): QLabel { + const l = new QLabel(); + l.setText(text); + if (objectName) { + l.setObjectName(objectName); + } + return l; +} + +function captioned(caption: string, widget: QWidget): QWidget { + const { widget: wrap, layout } = vbox([0, 0, 0, 0], 6); + layout.addWidget(label(caption.toUpperCase(), "fieldCaption")); + layout.addWidget(widget); + return wrap; +} + +function pageHeader( + title: string, + subtitle: string, + trailing?: QWidget, +): { widget: QWidget; subtitle: QLabel } { + const titleLabel = label(title, "sectionTitle"); + const subtitleLabel = label(subtitle, "muted"); + const { widget: text, layout: textLayout } = vbox([0, 0, 0, 0], 8); + textLayout.addWidget(titleLabel); + textLayout.addWidget(subtitleLabel); + if (!trailing) { + return { widget: text, subtitle: subtitleLabel }; + } + const { widget, layout } = hbox([0, 0, 0, 0], 16); + layout.addWidget(text, 1); + layout.addWidget(trailing, 0, AlignmentFlag.AlignVCenter); + return { widget, subtitle: subtitleLabel }; +} + +function toolbarGrid(cells: Array<{ caption: string; widget: QWidget; stretch?: number }>): QWidget { + const wrap = new QWidget(); + const grid = new QGridLayout(); + grid.setContentsMargins(0, 0, 0, 0); + grid.setHorizontalSpacing(12); + grid.setVerticalSpacing(6); + cells.forEach((cell, i) => { + grid.addWidget(label(cell.caption.toUpperCase(), "fieldCaption"), 0, i); + grid.addWidget(cell.widget, 1, i, 1, 1, AlignmentFlag.AlignBottom); + if (cell.stretch) { + grid.setColumnStretch(i, cell.stretch); + } + }); + wrap.setLayout(grid); + return wrap; +} + +function vSep(): QFrame { + const line = new QFrame(); + line.setObjectName("vSep"); + line.setFrameShape(Shape.VLine); + line.setFrameShadow(Shadow.Plain); + line.setLineWidth(1); + line.setFixedWidth(1); + return line; +} + +function button(text: string, objectName: string | undefined, iconName: string): QPushButton { + const btn = new QPushButton(); + btn.setText(text); + btn.setIcon(buttonIcon(iconName)); + btn.setIconSize(BUTTON_ICON_SIZE); + if (objectName) { + btn.setObjectName(objectName); + } + return btn; +} + +function spinBox(): QSpinBox { + const spin = new QSpinBox(); + spin.setButtonSymbols(ButtonSymbols.NoButtons); + return spin; +} + +function formatPct(value: number): string { + return `${(Math.min(1000, Math.max(0, value)) / 10).toFixed(1)}%`; +} + +function progressMeter(): { row: QWidget; setValue: (value: number) => void } { + const bar = new QProgressBar(); + bar.setObjectName("progressTrack"); + bar.setRange(0, 1000); + bar.setValue(0); + bar.setTextVisible(false); + + const pct = label("0.0%", "progressPct"); + pct.setAlignment(AlignmentFlag.AlignCenter); + pct.setFixedWidth(72); + bar.setFixedHeight(26); + pct.setFixedHeight(26); + + const { widget: row, layout } = hbox([0, 0, 0, 0], 0); + row.setObjectName("progressRow"); + layout.addWidget(bar, 1, AlignmentFlag.AlignVCenter); + layout.addWidget(pct, 0, AlignmentFlag.AlignVCenter); + + return { + row, + setValue(value: number) { + const clamped = Math.min(1000, Math.max(0, Math.round(value))); + bar.setValue(clamped); + pct.setText(formatPct(clamped)); + }, + }; +} + +function sizeStorageKey(host: string, deviceKey: string): string { + return `${host}|${deviceKey}`; +} + +export function createMainWindow(): QMainWindow { + const win = new QMainWindow(); + win.setWindowTitle("DES Key Cracker"); + win.resize(1100, 760); + win.setMinimumSize(1200, 780); + + const root = new QWidget(); + const rootLayout = new QBoxLayout(Direction.TopToBottom); + rootLayout.setContentsMargins(0, 0, 0, 0); + rootLayout.setSpacing(0); + root.setLayout(rootLayout); + win.setCentralWidget(root); + win.setStyleSheet(loadAppStylesheet()); + + const masterRadio = new QRadioButton(); + masterRadio.setText("Master"); + masterRadio.setChecked(true); + const slaveRadio = new QRadioButton(); + slaveRadio.setText("Slave"); + + const bindCombo = new QComboBox(); + bindCombo.setMinimumWidth(150); + for (const ip of localIPv4()) { + bindCombo.addItem(undefined, ip); + } + const portSpin = spinBox(); + portSpin.setRange(1, 65535); + portSpin.setValue(DEFAULT_PORT); + portSpin.setMinimumWidth(88); + const listenBtn = button("Start service", "primaryBtn", "radio"); + + const masterHostEdit = new QLineEdit(); + masterHostEdit.setPlaceholderText("192.168.1.10"); + masterHostEdit.setMinimumWidth(160); + const masterPortSpin = spinBox(); + masterPortSpin.setRange(1, 65535); + masterPortSpin.setValue(DEFAULT_PORT); + masterPortSpin.setMinimumWidth(88); + const connectBtn = button("Connect", "primaryBtn", "plug"); + const disconnectBtn = button("Disconnect", "ghostBtn", "unplug"); + disconnectBtn.setEnabled(false); + + const statusPill = label("Local only", "statusPill"); + + const fileEdit = new QLineEdit(); + fileEdit.setText(defaultMedPath()); + const browseBtn = button("Browse", "ghostBtn", "folder-open"); + const minSpin = spinBox(); + minSpin.setRange(1, 8); + minSpin.setValue(1); + const maxSpin = spinBox(); + maxSpin.setRange(1, 8); + maxSpin.setValue(5); + const charsetEdit = new QLineEdit(); + charsetEdit.setText(DEFAULT_CHARSET); + const padsEdit = new QLineEdit(); + padsEdit.setText(DEFAULT_PADS); + const startBtn = button("Start search", "primaryBtn", "play"); + const stopBtn = button("Stop", "dangerBtn", "circle-stop"); + stopBtn.setEnabled(false); + + const refreshBtn = button("Refresh", "ghostBtn", "refresh-cw"); + + const table = new QTableWidget(0, WORKER_HEADERS.length); + table.setHorizontalHeaderLabels(WORKER_HEADERS); + table.setEditTriggers(EditTrigger.NoEditTriggers); + table.setAlternatingRowColors(true); + table.horizontalHeader().setStretchLastSection(true); + table.verticalHeader().hide(); + table.setColumnWidth(0, 120); + table.setColumnWidth(1, 110); + table.setColumnWidth(2, 180); + table.setColumnWidth(3, 150); + table.setColumnWidth(4, 160); + table.setColumnWidth(5, 120); + table.setColumnWidth(6, 120); + table.setColumnWidth(7, 90); + table.setColumnWidth(8, 110); + + const log = new QPlainTextEdit(); + log.setReadOnly(true); + + const stageLabel = label("Current stage · idle", "muted"); + const stageMeter = progressMeter(); + const overallLabel = label("Overall · idle", "muted"); + const overallMeter = progressMeter(); + const stats = label("0/s elapsed -- ETA -- hits 0", "stats"); + + const toolbar = new QWidget(); + toolbar.setObjectName("toolbar"); + const toolbarLayout = new QBoxLayout(Direction.LeftToRight); + toolbarLayout.setContentsMargins(24, 16, 24, 16); + toolbarLayout.setSpacing(16); + toolbar.setLayout(toolbarLayout); + + const { widget: brandBox, layout: brandLayout } = hbox([0, 0, 0, 0], 12); + const { widget: brandText, layout: brandTextLayout } = vbox([0, 0, 0, 0], 0); + brandTextLayout.addWidget(label("DES Key Cracker", "brand")); + brandTextLayout.addWidget(label("SHORT-KEY SEARCH", "brandSub")); + brandLayout.addWidget(lucideSvg("key-round", ACCENT, LOGO_SIZE)); + brandLayout.addWidget(brandText); + + const { widget: roleRadios, layout: roleRadiosLayout } = hbox([0, 0, 0, 0], 16); + roleRadiosLayout.addWidget(masterRadio); + roleRadiosLayout.addWidget(slaveRadio); + + const { widget: slaveBtns, layout: slaveBtnsLayout } = hbox([0, 0, 0, 0], 8); + slaveBtnsLayout.addWidget(connectBtn); + slaveBtnsLayout.addWidget(disconnectBtn); + + const masterNet = toolbarGrid([ + { caption: "Listen address", widget: bindCombo, stretch: 1 }, + { caption: "Port", widget: portSpin }, + { caption: "Network", widget: listenBtn }, + ]); + const slaveNet = toolbarGrid([ + { caption: "Master host", widget: masterHostEdit, stretch: 1 }, + { caption: "Port", widget: masterPortSpin }, + { caption: "Network", widget: slaveBtns }, + ]); + + toolbarLayout.addWidget(brandBox); + toolbarLayout.addWidget(vSep()); + toolbarLayout.addWidget(captioned("Role", roleRadios)); + toolbarLayout.addWidget(vSep()); + toolbarLayout.addWidget(masterNet, 1); + toolbarLayout.addWidget(slaveNet, 1); + toolbarLayout.addSpacing(8); + toolbarLayout.addWidget(captioned("Status", statusPill)); + + const searchPage = new QWidget(); + searchPage.setObjectName("page"); + const searchLayout = new QBoxLayout(Direction.TopToBottom); + searchLayout.setContentsMargins(20, 20, 20, 20); + searchLayout.setSpacing(16); + searchPage.setLayout(searchLayout); + searchLayout.addWidget( + pageHeader("Search", "Recover an 8-byte DES key against the known pad ciphertext.").widget, + ); + + const searchForm = new QWidget(); + const form = new QGridLayout(); + form.setContentsMargins(0, 0, 0, 0); + form.setHorizontalSpacing(16); + form.setVerticalSpacing(16); + searchForm.setLayout(form); + + const { widget: fileRow, layout: fileRowLayout } = hbox([0, 0, 0, 0], 10); + fileRowLayout.addWidget(fileEdit, 1); + fileRowLayout.addWidget(browseBtn); + form.addWidget(captioned("Scan file", fileRow), 0, 0, 1, 2); + + const { widget: lenRow, layout: lenRowLayout } = hbox([0, 0, 0, 0], 16); + lenRowLayout.addWidget(captioned("Min length", minSpin)); + lenRowLayout.addWidget(captioned("Max length", maxSpin)); + lenRowLayout.addStretch(); + form.addWidget(lenRow, 1, 0, 1, 2); + + form.addWidget(captioned("Charset", charsetEdit), 2, 0, 1, 2); + form.addWidget(captioned("Pad bytes", padsEdit), 3, 0, 1, 2); + + const { widget: actionRow, layout: actionLayout } = hbox([0, 4, 0, 0], 10); + actionLayout.addWidget(startBtn); + actionLayout.addWidget(stopBtn); + actionLayout.addStretch(); + form.addWidget(actionRow, 4, 0, 1, 2); + + const slaveHint = label( + "This machine is a worker. Connect from the toolbar and expose devices on the Devices tab. The master assigns key ranges and block sizes.", + "hint", + ); + + searchLayout.addWidget(searchForm); + searchLayout.addWidget(slaveHint); + searchLayout.addStretch(); + + const devicesPage = new QWidget(); + devicesPage.setObjectName("page"); + const devicesLayout = new QBoxLayout(Direction.TopToBottom); + devicesLayout.setContentsMargins(20, 20, 20, 20); + devicesLayout.setSpacing(16); + devicesPage.setLayout(devicesLayout); + const { widget: devicesHeader, subtitle: devicesHint } = pageHeader( + "Compute devices", + "Local and connected devices. Set block size per device; CPU thread count applies only to CPU cards.", + refreshBtn, + ); + devicesLayout.addWidget(devicesHeader); + + const deviceListHost = new QWidget(); + deviceListHost.setObjectName("deviceList"); + const deviceListLayout = new QBoxLayout(Direction.TopToBottom); + deviceListLayout.setContentsMargins(0, 0, 8, 0); + deviceListLayout.setSpacing(12); + deviceListHost.setLayout(deviceListLayout); + deviceListLayout.addStretch(1); + + const deviceScroll = new QScrollArea(); + deviceScroll.setObjectName("deviceScroll"); + deviceScroll.setWidgetResizable(true); + deviceScroll.setFrameShape(Shape.NoFrame); + deviceScroll.setSizePolicy(QSizePolicyPolicy.Expanding, QSizePolicyPolicy.Expanding); + deviceScroll.setWidget(deviceListHost); + devicesLayout.addWidget(deviceScroll, 1); + + const workersPage = new QWidget(); + workersPage.setObjectName("page"); + const workersLayout = new QBoxLayout(Direction.TopToBottom); + workersLayout.setContentsMargins(20, 20, 20, 20); + workersLayout.setSpacing(16); + workersPage.setLayout(workersLayout); + const { widget: workersHeader, subtitle: workersHint } = pageHeader( + "Workers", + "Local and remote workers stay listed with current-block progress and a completed-block count.", + ); + workersLayout.addWidget(workersHeader); + workersLayout.addWidget(table, 1); + + const logPage = new QWidget(); + logPage.setObjectName("page"); + const logLayout = new QBoxLayout(Direction.TopToBottom); + logLayout.setContentsMargins(20, 20, 20, 20); + logLayout.setSpacing(16); + logPage.setLayout(logLayout); + logLayout.addWidget(pageHeader("Log", "Messages from this session.").widget); + logLayout.addWidget(log, 1); + + const tabs = new QTabWidget(); + tabs.addTab(searchPage, lucideIcon("search", ACCENT), "Search"); + tabs.addTab(devicesPage, lucideIcon("cpu", ACCENT), "Devices"); + tabs.addTab(workersPage, lucideIcon("users", ACCENT), "Workers"); + tabs.addTab(logPage, lucideIcon("terminal", ACCENT), "Log"); + + const body = new QWidget(); + body.setObjectName("body"); + const bodyLayout = new QBoxLayout(Direction.TopToBottom); + bodyLayout.setContentsMargins(20, 16, 20, 8); + bodyLayout.setSpacing(0); + body.setLayout(bodyLayout); + bodyLayout.addWidget(tabs, 1); + + const { widget: stageBlock, layout: stageBlockLayout } = vbox([0, 0, 0, 8], 8); + stageBlockLayout.addWidget(stageLabel); + stageBlockLayout.addWidget(stageMeter.row); + + const { widget: overallBlock, layout: overallBlockLayout } = vbox([0, 0, 0, 0], 8); + overallBlockLayout.addWidget(overallLabel); + overallBlockLayout.addWidget(overallMeter.row); + + const footer = new QWidget(); + footer.setObjectName("footer"); + const footerLayout = new QBoxLayout(Direction.TopToBottom); + footerLayout.setContentsMargins(24, 24, 24, 24); + footerLayout.setSpacing(8); + footer.setLayout(footerLayout); + footerLayout.addWidget(stageBlock); + footerLayout.addWidget(overallBlock); + footerLayout.addWidget(stats); + + rootLayout.addWidget(toolbar); + rootLayout.addWidget(body, 1); + rootLayout.addWidget(footer); + + const deviceCards = new Map< + string, + { + id: string; + local: boolean; + device: ComputeDevice; + widget: QWidget; + check?: QCheckBox; + blockCombo: QComboBox; + blockSize: () => number; + blockWrap: QWidget; + workersSpin?: QSpinBox; + } + >(); + let devices: ComputeDevice[] = []; + let search: MasterSearch | null = null; + let liveRows: WorkerRow[] | null = null; + let firstGpu = true; + + const tableOrder: string[] = []; + const rowWidgets = new Map }>(); + const savedBlockSizes = new Map(); + const savedCpuWorkers = new Map(); + + function setStatus(text: string, live: boolean): void { + statusPill.setText(text); + statusPill.setObjectName(live ? "statusPillLive" : "statusPill"); + } + + function appendLog(line: string): void { + const prev = log.toPlainText(); + log.setPlainText(prev ? `${prev}\n${line}` : line); + } + + function selectedDevices(): ComputeDevice[] { + return [...deviceCards.values()] + .filter((card) => card.local && (!card.check || card.check.isChecked())) + .map((card) => card.device); + } + + function rememberedBlockSize(host: string, deviceKey: string, deviceType: string, workerId: string): number { + const card = deviceCards.get(workerId); + if (card) { + return card.blockSize(); + } + return ( + savedBlockSizes.get(sizeStorageKey(host, deviceKey)) ?? + savedBlockSizes.get(workerId) ?? + defaultBlockSize(deviceType) + ); + } + + function rememberedCpuWorkers(host: string, deviceKey: string, workerId: string, cores?: number): number { + const spin = deviceCards.get(workerId)?.workersSpin; + if (spin) { + return spin.value(); + } + return savedCpuWorkers.get(sizeStorageKey(host, deviceKey)) ?? savedCpuWorkers.get(workerId) ?? cores ?? Math.max(1, os.cpus().length); + } + + function blockSizeFor(workerId: string, deviceType: string): bigint { + const card = deviceCards.get(workerId); + if (card) { + return BigInt(card.blockSize()); + } + const saved = savedBlockSizes.get(workerId); + if (saved) { + return BigInt(saved); + } + return BigInt(defaultBlockSize(deviceType)); + } + + function cpuWorkersFor(workerId: string): number { + const spin = deviceCards.get(workerId)?.workersSpin; + if (spin) { + return spin.value(); + } + return savedCpuWorkers.get(workerId) ?? Math.max(1, os.cpus().length); + } + + function localCpuWorkers(): number { + const cpu = [...deviceCards.values()].find((card) => card.local && card.device.type === "cpu"); + return cpu?.workersSpin?.value() ?? Math.max(1, os.cpus().length); + } + + function idleRoster(): WorkerRow[] { + const host = os.hostname(); + const rows: WorkerRow[] = selectedDevices().map((device) => { + const id = localWorkerId(device.key); + return { + id, + host, + ip: "local", + device: device.name, + deviceKey: device.key, + deviceType: device.type, + blockSize: rememberedBlockSize(host, device.key, device.type, id), + current: "idle", + pct: 0, + completedBlocks: 0, + completedKeys: 0n, + rate: "—", + rateNum: 0, + avgRate: "—", + avgRateNum: 0, + busy: false, + }; + }); + if (masterRadio.isChecked()) { + for (const remote of master.listWorkers()) { + const id = remoteWorkerId(remote.slaveId, remote.device.key); + const count = remote.progressCount; + const done = remote.progressDone; + const pct = !remote.idle && count > 0n ? Number((done * 1000n) / count) / 10 : 0; + rows.push({ + id, + host: remote.hostname, + ip: remote.ip, + device: remote.device.name, + deviceKey: remote.device.key, + deviceType: remote.device.type, + blockSize: rememberedBlockSize(remote.hostname, remote.device.key, remote.device.type, id), + current: remote.idle ? "idle" : `${formatNumber(done)} / ${formatNumber(count)}`, + pct, + completedBlocks: 0, + completedKeys: 0n, + rate: remote.idle ? "—" : formatRate(remote.rate), + rateNum: remote.idle ? 0 : remote.rate, + avgRate: "—", + avgRateNum: 0, + busy: !remote.idle, + }); + } + } + return rows; + } + + function clearWorkerTable(): void { + table.setRowCount(0); + tableOrder.length = 0; + rowWidgets.clear(); + } + + function configSpin(min: number, max: number, step: number, value: number): QSpinBox { + const spin = spinBox(); + spin.setRange(min, max); + spin.setSingleStep(step); + spin.setValue(value); + spin.setMinimumWidth(120); + return spin; + } + + function makeProgressBar(): QProgressBar { + const bar = new QProgressBar(); + bar.setObjectName("workerProgress"); + bar.setRange(0, 1000); + bar.setValue(0); + bar.setTextVisible(true); + bar.setFormat("idle"); + bar.setSizePolicy(QSizePolicyPolicy.Expanding, QSizePolicyPolicy.Expanding); + return bar; + } + + function setCell( + widgets: { cells: Array }, + row: number, + col: number, + text: string, + ): void { + const existing = widgets.cells[col]; + if (existing) { + if (existing.text() !== text) { + existing.setText(text); + } + return; + } + const item = new QTableWidgetItem(text); + widgets.cells[col] = item; + table.setItem(row, col, item); + } + + function createWorkerRow(index: number, row: WorkerRow): void { + const bar = makeProgressBar(); + table.setCellWidget(index, 4, bar); + rowWidgets.set(row.id, { bar, cells: [] }); + } + + function updateWorkerRow(index: number, row: WorkerRow): void { + const widgets = rowWidgets.get(row.id); + if (!widgets) { + return; + } + setCell(widgets, index, 0, row.host); + setCell(widgets, index, 1, row.ip); + setCell(widgets, index, 2, row.device); + 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"); + setCell(widgets, index, 5, formatNumber(row.completedBlocks)); + setCell(widgets, index, 6, formatNumber(row.completedKeys)); + setCell(widgets, index, 7, row.rate); + setCell(widgets, index, 8, row.avgRate); + } + + function syncWorkerTable(rows: WorkerRow[]): void { + const incoming = new Set(rows.map((r) => r.id)); + for (let i = tableOrder.length - 1; i >= 0; i--) { + if (!incoming.has(tableOrder[i])) { + table.removeRow(i); + rowWidgets.delete(tableOrder[i]); + tableOrder.splice(i, 1); + } + } + for (const row of rows) { + let index = tableOrder.indexOf(row.id); + if (index < 0) { + index = tableOrder.length; + table.insertRow(index); + tableOrder.push(row.id); + createWorkerRow(index, row); + } + updateWorkerRow(index, row); + } + } + + function applySlaveFooter(rows: WorkerRow[]): void { + const busy = rows.filter((r) => r.busy); + if (busy.length === 0) { + stageMeter.setValue(0); + stageLabel.setText("Assigned work · idle"); + stats.setText("0/s"); + return; + } + const avg = busy.reduce((sum, r) => sum + r.pct, 0) / busy.length; + stageMeter.setValue(avg * 10); + stageLabel.setText( + busy.length === 1 + ? `Assigned block · ${busy[0].current}` + : `Assigned work · ${busy.length} workers`, + ); + const rate = busy.reduce((sum, r) => sum + r.rateNum, 0); + stats.setText(`${rate.toFixed(0)} keys/s`); + } + + function paintWorkers(rows?: WorkerRow[]): void { + if (rows) { + liveRows = rows; + } + const view = liveRows ?? idleRoster(); + syncWorkerTable(view); + if (!masterRadio.isChecked()) { + applySlaveFooter(view); + } + } + + function insertCard(widget: QWidget): void { + const stretchIndex = Math.max(0, deviceListLayout.count() - 1); + deviceListLayout.insertWidget(stretchIndex, widget); + } + + function removeCard(id: string): void { + const card = deviceCards.get(id); + if (!card) { + return; + } + deviceListLayout.removeWidget(card.widget); + card.widget.hide(); + card.widget.close(); + deviceCards.delete(id); + } + + function applyCardRole(card: { + check?: QCheckBox; + blockWrap: QWidget; + blockCombo: QComboBox; + }): void { + const isMaster = masterRadio.isChecked(); + card.blockWrap.setVisible(isMaster); + card.blockCombo.setEnabled(isMaster); + } + + function createDeviceCard(opts: { + id: string; + host: string; + ip: string; + device: ComputeDevice; + local: boolean; + checked: boolean; + }): void { + const isCpu = opts.device.type === "cpu"; + const storeKey = sizeStorageKey(opts.host, opts.device.key); + const initialSize = rememberedBlockSize(opts.host, opts.device.key, opts.device.type, opts.id); + const { combo: blockCombo, value: blockSize } = configBlockSizeCombo(initialSize); + blockCombo.addEventListener("currentIndexChanged", () => { + const value = blockSize(); + savedBlockSizes.set(storeKey, value); + savedBlockSizes.set(opts.id, value); + }); + const blockWrap = captioned("Block size", blockCombo); + + let workersSpin: QSpinBox | undefined; + let workersWrap: QWidget | undefined; + if (isCpu) { + workersSpin = configSpin(1, 256, 1, rememberedCpuWorkers(opts.host, opts.device.key, opts.id, opts.device.cores)); + workersSpin.addEventListener("valueChanged", (value) => { + savedCpuWorkers.set(storeKey, value); + savedCpuWorkers.set(opts.id, value); + if (!masterRadio.isChecked() && opts.local) { + slave.setDevices(selectedDevices(), value); + } + }); + workersWrap = captioned("Workers", workersSpin); + } + + const check = opts.local ? new QCheckBox() : undefined; + if (check) { + check.setText(""); + check.setChecked(opts.checked); + check.addEventListener("toggled", () => onDevicesChanged()); + } + + const hostLabel = label(opts.host, "deviceHost"); + const nameLabel = label(opts.device.name, "deviceName"); + const metaBits = [opts.device.type.toUpperCase(), opts.local ? "this machine" : opts.ip]; + if (opts.device.memory) { + metaBits.push(`${(opts.device.memory / (1024 * 1024 * 1024)).toFixed(1)} GB`); + } + if (opts.device.cores) { + metaBits.push(`${opts.device.cores} cores`); + } + const metaLabel = label(metaBits.join(" · "), "deviceMeta"); + + const { widget: names, layout: namesLayout } = vbox([0, 0, 0, 0], 4); + namesLayout.addWidget(hostLabel); + namesLayout.addWidget(nameLabel); + namesLayout.addWidget(metaLabel); + + const { widget: fields, layout: fieldsLayout } = hbox([0, 0, 0, 0], 16); + if (workersWrap) { + fieldsLayout.addWidget(workersWrap); + } + fieldsLayout.addWidget(blockWrap); + + + const { widget: body, layout: bodyLayout } = hbox([0, 0, 0, 0], 16); + bodyLayout.addWidget(names, 1); + bodyLayout.addWidget(fields, 0, AlignmentFlag.AlignVCenter); + + const card = new QFrame(); + card.setObjectName("deviceCard"); + card.setFrameShape(Shape.NoFrame); + card.setAttribute(WidgetAttribute.WA_StyledBackground, true); + const cardLayout = new QBoxLayout(Direction.LeftToRight); + cardLayout.setContentsMargins(check ? 0 : 16, 16, 16, 16); + cardLayout.setSpacing(0); + card.setLayout(cardLayout); + card.setSizePolicy(QSizePolicyPolicy.Preferred, QSizePolicyPolicy.Maximum); + if (check) { + cardLayout.addSpacing(18); + cardLayout.addWidget(check, 0, AlignmentFlag.AlignVCenter); + cardLayout.addSpacing(18); + } + cardLayout.addWidget(body, 1); + + const record = { + id: opts.id, + local: opts.local, + device: opts.device, + widget: card, + check, + blockCombo, + blockSize, + blockWrap, + workersSpin, + }; + applyCardRole(record); + deviceCards.set(opts.id, record); + insertCard(card); + } + + function syncDeviceCards(): void { + const isMaster = masterRadio.isChecked(); + const host = os.hostname(); + const wanted = new Set(); + + for (const device of devices) { + const id = localWorkerId(device.key); + wanted.add(id); + if (!deviceCards.has(id)) { + const isGpu = device.type !== "cpu"; + const checked = device.type === "cpu" || firstGpu; + if (isGpu && checked) { + firstGpu = false; + } + createDeviceCard({ id, host, ip: "local", device, local: true, checked }); + } + } + + if (isMaster) { + for (const remote of master.listWorkers()) { + const id = remoteWorkerId(remote.slaveId, remote.device.key); + wanted.add(id); + if (!deviceCards.has(id)) { + createDeviceCard({ + id, + host: remote.hostname, + ip: remote.ip, + device: remote.device, + local: false, + checked: true, + }); + } + } + } + + for (const id of [...deviceCards.keys()]) { + if (!wanted.has(id)) { + removeCard(id); + } else { + applyCardRole(deviceCards.get(id)!); + } + } + } + + function applyRole(): void { + const isMaster = masterRadio.isChecked(); + masterNet.setVisible(isMaster); + slaveNet.setVisible(!isMaster); + searchForm.setVisible(isMaster); + slaveHint.setVisible(!isMaster); + overallBlock.setVisible(isMaster); + tabs.setTabText(2, isMaster ? "Workers" : "Assigned work"); + devicesHint.setText( + isMaster + ? "Local and connected slave devices. Set a block size on each card; CPU thread count is only on CPU devices." + : "Tick devices to expose. Set CPU threads on CPU cards. The master chooses each device’s block size.", + ); + workersHint.setText( + isMaster + ? "Local and remote workers stay listed with current-block progress and a completed-block count." + : "Exposed workers and the block the master currently assigned.", + ); + liveRows = null; + clearWorkerTable(); + if (!isMaster) { + setStatus("Offline", false); + } else if (!master.listening) { + setStatus("Local only", false); + } + syncDeviceCards(); + onDevicesChanged(); + } + + function onDevicesChanged(): void { + if (!search) { + liveRows = null; + } + if (!masterRadio.isChecked()) { + slave.setDevices(selectedDevices(), localCpuWorkers()); + } + if (!search) { + paintWorkers(); + } + } + + async function refreshDevices(): Promise { + appendLog("Probing compute devices…"); + devices = await listComputeDevices(); + firstGpu = true; + for (const [id, card] of [...deviceCards.entries()]) { + if (card.local) { + removeCard(id); + } + } + syncDeviceCards(); + if (devices.length === 0) { + appendLog("No devices found. Run npm run build:native."); + } else { + appendLog(`Found ${devices.map((d) => d.name).join(", ")}`); + } + onDevicesChanged(); + } + + const master = new MasterServer({ + onLog: appendLog, + onSlaves: () => { + syncDeviceCards(); + if (!search) { + liveRows = null; + paintWorkers(); + } + }, + }); + const slave = new SlaveClient({ + onLog: appendLog, + onStatus: (text) => { + setStatus(text, slave.connected); + }, + onWorkers: (rows) => paintWorkers(rows), + onHit: (_job, index, keyHex) => appendLog(`Slave HIT index=${index} key=${keyHex}`), + }); + + browseBtn.addEventListener("clicked", () => { + const dlg = new QFileDialog(); + dlg.setFileMode(FileMode.ExistingFile); + dlg.setNameFilter("MED scan (*.med);;All files (*.*)"); + dlg.exec(); + const files = dlg.selectedFiles(); + if (files[0]) { + fileEdit.setText(files[0]); + } + (global as typeof globalThis & { _dlg?: QFileDialog })._dlg = dlg; + }); + + masterRadio.addEventListener("clicked", applyRole); + slaveRadio.addEventListener("clicked", applyRole); + refreshBtn.addEventListener("clicked", () => { + void refreshDevices(); + }); + + listenBtn.addEventListener("clicked", () => { + if (master.listening) { + void master.close().then(() => { + listenBtn.setText("Start service"); + listenBtn.setObjectName("primaryBtn"); + listenBtn.setIcon(buttonIcon("radio")); + setStatus("Local only", false); + appendLog("Master service stopped"); + if (!search) { + liveRows = null; + syncDeviceCards(); + paintWorkers(); + } + }); + return; + } + const host = bindCombo.currentText(); + const port = portSpin.value(); + void master + .listen(host, port) + .then(() => { + listenBtn.setText("Stop service"); + listenBtn.setObjectName("dangerBtn"); + listenBtn.setIcon(buttonIcon("square")); + setStatus(`Listening :${port}`, true); + appendLog(`Workers can connect to this machine on port ${port}`); + }) + .catch((err: unknown) => { + alert("Listen failed", err instanceof Error ? err.message : String(err)); + }); + }); + + connectBtn.addEventListener("clicked", () => { + const host = masterHostEdit.text().trim(); + if (!host) { + alert("Master host", "Enter the master IP address."); + return; + } + slave.setDevices(selectedDevices(), localCpuWorkers()); + slave.connect(host, masterPortSpin.value()); + connectBtn.setEnabled(false); + disconnectBtn.setEnabled(true); + }); + disconnectBtn.addEventListener("clicked", () => { + slave.disconnect(); + connectBtn.setEnabled(true); + disconnectBtn.setEnabled(false); + setStatus("Offline", false); + liveRows = null; + paintWorkers(); + }); + + startBtn.addEventListener("clicked", () => { + if (search) { + return; + } + const filePath = fileEdit.text().trim(); + if (!filePath) { + alert("Scan file", "Choose a .med file first."); + return; + } + const pads = parsePads(padsEdit.text()); + if (pads.length === 0) { + alert("Pads", "Enter pad bytes as hex, e.g. 00,20"); + return; + } + const charset = charsetEdit.text(); + if (!charset) { + alert("Charset", "Charset cannot be empty."); + return; + } + const picked = selectedDevices(); + search = new MasterSearch( + master.listening ? master : null, + { + filePath, + minLen: minSpin.value(), + maxLen: maxSpin.value(), + charset, + pads, + cpuWorkersFor, + gpuBatch: DEFAULT_GPU_BATCH, + devices: picked, + outDir: defaultOutDir(), + blockSizeFor, + }, + { + onLog: appendLog, + onProgress: (snap) => { + stageMeter.setValue(snap.stagePct * 10); + overallMeter.setValue(snap.overallPct * 10); + stageLabel.setText( + `Current · DES len=${snap.length} pad=0x${snap.pad.toString(16).padStart(2, "0")} ` + + `${snap.stageDone.toString()} / ${snap.stageTotal.toString()}`, + ); + overallLabel.setText(`Overall · ${snap.overallDone.toString()} / ${snap.overallTotal.toString()}`); + stats.setText(describeProgress(snap)); + }, + onWorkers: (rows) => paintWorkers(rows), + onDone: (hits, cancelled) => { + search = null; + startBtn.setEnabled(true); + stopBtn.setEnabled(false); + appendLog(cancelled ? "Stopped." : `Finished. ${hits.length} pad match(es).`); + }, + }, + ); + startBtn.setEnabled(false); + stopBtn.setEnabled(true); + appendLog("Starting search…"); + tabs.setCurrentIndex(2); + void search.run().catch((err: unknown) => { + search = null; + startBtn.setEnabled(true); + stopBtn.setEnabled(false); + appendLog(`Error: ${err instanceof Error ? err.message : String(err)}`); + }); + }); + + stopBtn.addEventListener("clicked", () => { + search?.stop(); + appendLog("Stop requested."); + }); + + applyRole(); + void refreshDevices(); + appendLog("Ready. Use the toolbar to run as master or join as a slave."); + return win; +} diff --git a/src/ui/stylesheet.ts b/src/ui/stylesheet.ts new file mode 100644 index 0000000..5a8e44b --- /dev/null +++ b/src/ui/stylesheet.ts @@ -0,0 +1,16 @@ +import fs from "fs"; +import path from "path"; +import { projectRoot } from "../paths"; + +export function loadAppStylesheet(): string { + const candidates = [ + path.join(__dirname, "app.css"), + path.join(projectRoot(), "src", "ui", "app.css"), + ]; + for (const file of candidates) { + if (fs.existsSync(file)) { + return fs.readFileSync(file, "utf8"); + } + } + throw new Error("src/ui/app.css not found"); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2ff0ce4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "moduleResolution": "node", + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +}