diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index 4af744f..4fe9d9d 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -86,13 +86,13 @@ Completed: 2026-05-13 **v0.2 NcProgramManager** (in progress) Status: In Progress -Phases: 2 of 4 complete +Phases: 3 of 4 complete | Phase | Name | Plans | Status | |-------|------|-------|--------| | 06 | rename-cleanup | 1 | ✅ Complete | | 07 | license-core | 1 | ✅ Complete | -| 08 | license-integration | 1 | Not started | +| 08 | license-integration | 1 | ✅ Complete | 2026-05-17 | | 09 | license-generator | 1 | Not started | ### Phase 06: rename-cleanup ✅ @@ -105,13 +105,14 @@ Phases: 2 of 4 complete **Completed:** 2026-05-17 **Plans:** [x] 07-01: RSA core + interfaces + 6 unit tests -### Phase 08: license-integration (proposed) -**Goal:** Wire license system into Program.cs, replace `checkLicense`, replace `-chiave=` with `-licfile=` -**Depends on:** Phase 07 +### Phase 08: license-integration ✅ +**Goal:** Wire license system into Program.cs, delete `checkLicense`, remove `-chiave=`, hardcode `license.lic` next to EXE +**Completed:** 2026-05-17 +**Plans:** [x] 08-01: Program.cs wiring ### Phase 09: license-generator (proposed) **Goal:** Separate generator console app — reads machine fingerprint, signs, writes `.lic` file **Depends on:** Phase 07 --- -*Roadmap updated: 2026-05-17 — Phase 07 license-core complete* +*Roadmap updated: 2026-05-17 — Phase 08 license-integration complete* diff --git a/.paul/STATE.md b/.paul/STATE.md index ed5d67f..6a1340a 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -5,27 +5,25 @@ See: .paul/PROJECT.md (updated 2026-05-17) **Core value:** Machinists transfer CNC programs to/from any controller without manual steps or internet -**Current focus:** v0.1 complete — ready for next milestone planning +**Current focus:** v0.2 — Phase 08 complete, Phase 09 (license-generator) next ## Current Position Milestone: v0.2 NcProgramManager -Phase: 3 of 4 (08-license-integration) — Not started -Plan: Not started -Status: Ready to plan Phase 08 -Last activity: 2026-05-17 — Phase 07 complete, transitioned to Phase 08 +Phase: 4 of 4 (09-license-generator) — Not started +Status: Phase 08 complete, ready to plan Phase 09 +Last activity: 2026-05-17 — Phase 08 license-integration complete (UNIFY done) Progress: - Milestone v0.1: [██████████] 100% (complete) -- Milestone v0.2: [█████░░░░░] 50% -- Phase 08: [░░░░░░░░░░] 0% +- Milestone v0.2: [███████░░░] 75% +- Phase 09: [░░░░░░░░░░] 0% ## Loop Position -Current loop state: ``` PLAN ──▶ APPLY ──▶ UNIFY - ✓ ✓ ✓ [Loop complete — ready for next PLAN] + ✓ ✓ ✓ [Phase 08 closed] ``` ## What Was Built (v0.1 Multi-CN Release) @@ -58,44 +56,72 @@ PLAN ──▶ APPLY ──▶ UNIFY - `NcProgramValidatorTests` — 30+ NUnit tests ### Test Infrastructure -- `FanucProgramManager.Tests/` — NUnit 3.13.3 + Moq 4.18.4, .NET 4.7.2 +- `NcProgramManager.Tests/` — NUnit 3.13.3 + Moq 4.18.4, .NET 4.7.2 - `FtpServerStub` + `Lsv2ServerStub` — in-process TCP stubs - Unit + integration tests; Fanuc FOCAS tests `[Ignore]`d (Windows-only DLL) -## CLI Args (v0.1) +## What Was Built (v0.2 NcProgramManager — in progress) + +### Phase 06 — Rename Cleanup +- Namespace: `FanucProgramManager.*` → `NcProgramManager.*` (all files) +- Project renamed, test folder renamed, dead `FANUCMachine.cs` deleted + +### Phase 07 — License Core +- `IMachineFingerprint` / `ILicenseValidator` interfaces +- `MachineFingerprint` — WMI CPU/BIOS fingerprint, SHA256 hash +- `LicenseFile.TryRead` — reads `.lic` file, returns base64 signature +- `LicenseValidator.Validate` — RSA-SHA256 verify; PLACEHOLDER public key until Phase 09 +- `LicenseValidatorTests` — 6 unit tests (testable via internal ctor injection) + +### Phase 08 — License Integration +- `Program.cs` — `checkLicense` deleted; RSA check wired at startup: + ```csharp + string _licPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "license.lic"); + if (!LicenseFile.TryRead(_licPath, out _licBase64) + || !new LicenseValidator().Validate(_licFingerprint, _licBase64)) { ... } + ``` +- `InputArgs.cs` — `chiave` field removed +- `-chiave=` CLI arg removed; license file hardcoded next to EXE as `license.lic` +- `MostraAiuto` updated: instructs user to place `license.lic` next to executable + +## CLI Args (current) - `-manufacturer=fanuc|heidenhain|siemens|mitsubishi` (default: fanuc) +- `-azione=` — action (Scarica/Invia/etc.) +- `-ip=` — CNC IP +- `-porta=` — port - `-username=` (Heidenhain/Siemens/Mitsubishi) - `-password=` (Heidenhain/Siemens/Mitsubishi) +- License: place `license.lic` next to the EXE (no CLI arg) ## Accumulated Context ### Decisions - .NET 4.7.2 locked — max compatibility - RSA asymmetric offline licensing: private key vendor-only, public key embedded in binary -- PublicKeyXml = PLACEHOLDER until Phase 09 generates real key pair +- `license.lic` always at `AppDomain.CurrentDomain.BaseDirectory` — no CLI override - `RSACryptoServiceProvider` + `SHA256CryptoServiceProvider` (not HashAlgorithmName) — .NET 4.7.2 compat - Constructor injection pattern: internal ctor accepts key XML — testable without Windows WMI +- PublicKeyXml = PLACEHOLDER until Phase 09 generates real key pair - Heidenhain: LSV2 port 19000, needs real hardware testing - Siemens: FTP `/_N_MPF_DIR/`, needs Sinumerik FTP option enabled on machine - `#` = Warning in Fanuc/Mitsubishi — macro variable indicator ### Deferred Issues -- License wiring into Program.cs — Phase 08 target -- License generator tool — Phase 09 target - Real RSA key pair — Phase 09 output; copy PublicKeyXml into LicenseValidator.cs +- `AesCrypt.cs` dead code — remove in future cleanup phase - Heidenhain: `ReadActiveProgramAsync` / `SelectMainProgram` not implemented - Siemens: same as Heidenhain - Build + integration not verifiable on Linux (fwlib32 Windows-only) - MachineFingerprint WMI — code-review verified, needs real Windows hardware test ### Blockers/Concerns -None — Phase 08 can start. +None — Phase 09 can start. ## Session Continuity Last session: 2026-05-17 -Stopped at: Phase 07 complete (UNIFY done) -Next action: /paul:plan for Phase 08 (license-integration) +Stopped at: Phase 08 UNIFY complete +Next action: /paul:plan phase 09 license-generator Resume file: .paul/ROADMAP.md --- diff --git a/.paul/paul.json b/.paul/paul.json index 3d3c378..386d245 100644 --- a/.paul/paul.json +++ b/.paul/paul.json @@ -7,8 +7,8 @@ "status": "in_progress" }, "phase": { - "number": 7, - "name": "license-core", + "number": 8, + "name": "license-integration", "status": "complete" }, "loop": { @@ -17,7 +17,7 @@ }, "timestamps": { "created_at": "2026-05-13T00:00:00Z", - "updated_at": "2026-05-17T21:00:00Z" + "updated_at": "2026-05-17T22:00:00Z" }, "satellite": { "groom": true diff --git a/.paul/phases/08-license-integration/08-01-PLAN.md b/.paul/phases/08-license-integration/08-01-PLAN.md new file mode 100644 index 0000000..2881e5e --- /dev/null +++ b/.paul/phases/08-license-integration/08-01-PLAN.md @@ -0,0 +1,199 @@ +--- +phase: 08-license-integration +plan: 01 +type: execute +wave: 1 +depends_on: ["07-01"] +files_modified: + - Program.cs +autonomous: true +--- + + +## Goal +Wire the Phase 07 RSA license core into Program.cs: replace the hardcoded AES `checkLicense` call with `MachineFingerprint` + `LicenseFile` + `LicenseValidator`. Keep `-chiave=` as the CLI arg — it now carries a path to a `.lic` file instead of the AES key string. + +## Purpose +The current `checkLicense` embeds a hardcoded AES key/IV in source — anyone with the source or a decompiler can generate licenses for any machine. The Phase 07 RSA core is built but not yet wired in. This phase closes the gap: the app will reject any run where the `.lic` file (passed via `-chiave=`) doesn't contain a valid RSA-SHA256 signature matching the current machine's fingerprint. + +## Output +- `Program.cs` only — `checkLicense` method deleted; license check replaced with `MachineFingerprint` + `LicenseFile.TryRead` + `LicenseValidator.Validate`; `inputArgs.chiave` now treated as `.lic` file path; help text for `-chiave=` updated to describe the file path; dead `using System.Management` and `using System.Security.Cryptography` removed; `using NcProgramManager.Licensing` added +- `InputArgs.cs` — NO CHANGE (`chiave` field stays as-is) +- Docker build + all 118 tests still pass + + + +## Project Context +@.paul/PROJECT.md +@.paul/STATE.md + +## Phase 07 Output (direct inputs) +@Licensing/IMachineFingerprint.cs +@Licensing/ILicenseValidator.cs +@Licensing/LicenseFile.cs +@Licensing/MachineFingerprint.cs +@Licensing/LicenseValidator.cs + +## File Being Modified +@Program.cs + + + + +## AC-1: -chiave= arg still parsed, value treated as .lic file path +```gherkin +Given Program.cs parseArgomenti is called with args containing "-chiave=C:\license.lic" +When the arg is parsed +Then inputArgs.chiave equals "C:\license.lic" +And the parsing code is unchanged from the current implementation +``` + +## AC-2: RSA license check used at startup +```gherkin +Given Program.cs Main is called +When the license check runs +Then it calls MachineFingerprint().GetFingerprint(), LicenseFile.TryRead(inputArgs.chiave), and LicenseValidator.Validate(fingerprint, base64) +And the old checkLicense method no longer exists +``` + +## AC-3: License failure exits with -100 +```gherkin +Given the .lic file is missing OR the signature does not match the machine fingerprint +When Main runs +Then it prints "Licenza ERRATA", calls vediErrore(-100), and returns -1 +``` + +## AC-4: Help text updated for -chiave= +```gherkin +Given MostraAiuto is called +When its output is inspected +Then -chiave= is documented as the path to the .lic license file +``` + + + + + + + Task 1: Wire RSA license check into Program.cs + Program.cs + + **1. Replace usings at top of file:** + + Remove these two lines: + ```csharp + using System.Management; + using System.Security.Cryptography; + ``` + Add: + ```csharp + using NcProgramManager.Licensing; + ``` + Keep all other existing usings unchanged. + + **2. Replace the license check call in Main:** + + Remove: + ```csharp + if (!checkLicense(inputArgs.chiave)) + { + Console.WriteLine("Licenza ERRATA"); + vediErrore(-100); + return -1; + } + ``` + Replace with: + ```csharp + string _licFingerprint = new MachineFingerprint().GetFingerprint(); + string _licBase64; + if (!LicenseFile.TryRead(inputArgs.chiave, out _licBase64) + || !new LicenseValidator().Validate(_licFingerprint, _licBase64)) + { + Console.WriteLine("Licenza ERRATA"); + vediErrore(-100); + return -1; + } + ``` + `inputArgs.chiave` carries the `.lic` file path — parsing in `parseArgomenti` is unchanged. + + **3. Delete the entire `checkLicense` static method:** + ```csharp + static Boolean checkLicense(string chiave) + { + ... + } + ``` + Remove from first line to closing brace entirely. + + **4. Update MostraAiuto — update the description for `-chiave=`:** + + Remove: + ```csharp + Console.WriteLine("-chiave="); + Console.WriteLine(" la stringa chiave per eseguire il software."); + ``` + Replace with: + ```csharp + Console.WriteLine("-chiave="); + Console.WriteLine(" Path del file di licenza (.lic) per autenticare il software."); + ``` + + Avoid: touching `parseArgomenti` chiave parsing, `Scarica`, `Invia`, `vediErrore`, or any other part of the file. + + + ```bash + grep -n "checkLicense\|System\.Management\|System\.Security\.Cryptography" Program.cs + grep -n "MachineFingerprint\|LicenseFile\|LicenseValidator\|NcProgramManager\.Licensing" Program.cs + grep -n "chiave" Program.cs InputArgs.cs + ``` + First grep: zero hits. + Second grep: 4+ hits. + Third grep: `chiave` present in both files (parsing in Program.cs unchanged, field in InputArgs.cs unchanged). + + AC-2 satisfied: RSA check wired. AC-3 satisfied: failure path unchanged (-1 return). AC-4 satisfied: help text updated. AC-1 satisfied: -chiave= parsing untouched. + + + + + + +## DO NOT CHANGE +- `InputArgs.cs` — `chiave` field stays as-is; no rename +- `-chiave=` parsing in `parseArgomenti` — keep exactly as-is +- `AesCrypt.cs` — now dead code but removal is out of scope; leave for a cleanup phase +- `Licensing/*.cs` — Phase 07 output, must not be modified +- All `Cnc/` files +- All test files + +## SCOPE LIMITS +- No changes to `Scarica`, `Invia`, `vediErrore` logic +- No changes to any other arg parsing in `parseArgomenti` +- No new test files (license core already tested in Phase 07) +- `AesCrypt.cs` stays in csproj and on disk + + + + +Before declaring plan complete: +- [ ] `grep "checkLicense" Program.cs` — zero hits +- [ ] `grep "System.Management\|System.Security.Cryptography" Program.cs` — zero hits +- [ ] `grep "MachineFingerprint\|LicenseFile\|LicenseValidator" Program.cs` — 3 hits +- [ ] `grep "NcProgramManager.Licensing" Program.cs` — 1 hit +- [ ] `grep "chiave" InputArgs.cs` — still present (field unchanged) +- [ ] `grep "\-chiave=" Program.cs` — still present in parseArgomenti AND in MostraAiuto +- [ ] Docker build passes, all 118 tests pass + + + +- All tasks completed +- All verification checks pass +- `checkLicense` method gone +- `-chiave=` CLI arg and `inputArgs.chiave` field unchanged +- RSA license check wired at startup using `inputArgs.chiave` as .lic path +- Help text describes `-chiave=` as the .lic file path +- No test regressions + + + +After completion, create `.paul/phases/08-license-integration/08-01-SUMMARY.md` + diff --git a/.paul/phases/08-license-integration/08-01-SUMMARY.md b/.paul/phases/08-license-integration/08-01-SUMMARY.md new file mode 100644 index 0000000..559e4f3 --- /dev/null +++ b/.paul/phases/08-license-integration/08-01-SUMMARY.md @@ -0,0 +1,63 @@ +--- +phase: 08-license-integration +plan: 01 +type: summary +status: complete +completed: 2026-05-17 +--- + +# Phase 08 Summary: license-integration + +## What Was Built + +RSA license check wired into `Program.cs`. Old AES-based `checkLicense` deleted. License now validated at startup against `license.lic` in the EXE directory. + +**Files modified:** +- `Program.cs` — license check replaced, `checkLicense` deleted, `-chiave=` parsing removed, `MostraAiuto` updated +- `InputArgs.cs` — `chiave` field removed + +## Acceptance Criteria Results + +| AC | Result | Notes | +|----|--------|-------| +| AC-1: -chiave= arg still parsed | **SUPERSEDED** | User directed removal mid-APPLY (see Deviations) | +| AC-2: RSA license check at startup | PASS | `MachineFingerprint` + `LicenseFile.TryRead` + `LicenseValidator.Validate` wired | +| AC-3: License failure exits -100 | PASS | `Console.WriteLine("Licenza ERRATA")` + `vediErrore(-100)` + `return -1` | +| AC-4: Help text updated | PASS | `-chiave=` block replaced with `license.lic` location note | + +## Deviations + +**Plan AC-1 superseded by user decision.** +Plan stated: keep `-chiave=` as the CLI arg carrying the `.lic` file path. +User directed mid-APPLY: remove `-chiave=` entirely; hardcode `license.lic` next to the EXE. + +Actual license path: +```csharp +string _licPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "license.lic"); +``` + +`inputArgs.chiave` field removed from `InputArgs.cs`. All `-chiave=` parsing removed from `parseArgomenti`. + +## Verification (post-apply grep) + +``` +grep "checkLicense|chiave|System.Management|System.Security.Cryptography" Program.cs → 0 hits +grep "MachineFingerprint|LicenseFile|LicenseValidator|NcProgramManager.Licensing" Program.cs → 5 hits +grep "chiave" InputArgs.cs → 0 hits +``` + +## Decisions Made + +- License file always at `AppDomain.CurrentDomain.BaseDirectory + "license.lic"` — no CLI override +- `InputArgs.chiave` field deleted (not just unused) — cleaner than leaving dead field +- `AesCrypt.cs` left on disk (still in csproj, no cleanup in scope) + +## Deferred Issues + +- Real RSA key pair — Phase 09 generates; copy `PublicKeyXml` into `LicenseValidator.cs` +- Phase 09: separate `LicenseGenerator` console app — reads WMI fingerprint, signs, writes `.lic` +- `AesCrypt.cs` dead code — remove in future cleanup phase + +## Tests + +No new tests (license core tested in Phase 07). Docker build + 116 tests pass (2 Fanuc FOCAS ignored, Windows-only DLL). diff --git a/InputArgs.cs b/InputArgs.cs index 8edcc06..628bd7b 100755 --- a/InputArgs.cs +++ b/InputArgs.cs @@ -11,7 +11,6 @@ namespace NcProgramManager public string porta = ""; public short pathCNC = 1; public int nodoHssb = -1; - public string chiave = ""; public string username = ""; public string password = ""; public string commentoProgramma = ""; diff --git a/Program.cs b/Program.cs index 483d97d..b2a80f9 100755 --- a/Program.cs +++ b/Program.cs @@ -1,11 +1,10 @@ using System; using System.Collections.Generic; using System.IO; -using System.Management; -using System.Security.Cryptography; using System.Text.RegularExpressions; using NcProgramManager.Cnc; using NcProgramManager.Cnc.Models; +using NcProgramManager.Licensing; namespace NcProgramManager { @@ -35,7 +34,11 @@ namespace NcProgramManager return 0; } - if (!checkLicense(inputArgs.chiave)) + string _licFingerprint = new MachineFingerprint().GetFingerprint(); + string _licBase64; + string _licPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "license.lic"); + if (!LicenseFile.TryRead(_licPath, out _licBase64) + || !new LicenseValidator().Validate(_licFingerprint, _licBase64)) { Console.WriteLine("Licenza ERRATA"); vediErrore(-100); @@ -275,47 +278,6 @@ namespace NcProgramManager Console.ReadLine(); } - static Boolean checkLicense(string chiave) - { - string pcInfo = string.Empty; - ManagementClass mc = new ManagementClass("win32_processor"); - ManagementObjectCollection moc = mc.GetInstances(); - - foreach (ManagementObject mo in moc) - { - //Get only the first CPU's ID - pcInfo = mo.Properties["processorID"].Value.ToString(); - } - - ManagementObjectSearcher baseboardSearcher = new ManagementObjectSearcher("root\\CIMV2", "SELECT * FROM Win32_BaseBoard"); - foreach (ManagementObject queryObj in baseboardSearcher.Get()) - { - //Get motherboardSerialNumber - pcInfo += queryObj["SerialNumber"].ToString(); - break; - } - - using (Aes myAes = Aes.Create()) - { - if (chiave == "") - return false; - myAes.IV = Convert.FromBase64String("qqxNM8Li99Cwx61VX/QoWA=="); - myAes.Key = Convert.FromBase64String("7/12mP3dFLGqPGjujlwBCVblURxBaBcGGQ2c2FatfP4="); - byte[] encrypted = AesCrypt.EncryptStringToBytes_Aes(pcInfo, myAes.Key, myAes.IV); - string roundtrip = AesCrypt.DecryptStringFromBytes_Aes(encrypted, myAes.Key, myAes.IV); - string decoded = ""; - try - { - decoded = AesCrypt.DecryptStringFromBytes_Aes(Convert.FromBase64String(chiave), myAes.Key, myAes.IV); - } - catch (Exception ex) - { - Console.WriteLine(ex.ToString()); - } - return pcInfo == decoded; - } - } - static void MostraAiuto() { Console.WriteLine("Utilizzo del Software gestore Programmi FANUC"); @@ -326,8 +288,7 @@ namespace NcProgramManager Console.WriteLine(); Console.WriteLine("Lista dei parametri da fornire all'eseguibile\nDevono essere separati da uno spazio tra l'uno e l'altro,\nnon andare a capo e non lasciare spazi dopo l'= "); Console.WriteLine(); - Console.WriteLine("-chiave="); - Console.WriteLine(" la stringa chiave per eseguire il software."); + Console.WriteLine("Il file 'license.lic' deve essere posizionato nella stessa cartella dell'eseguibile."); Console.WriteLine("-azione="); Console.WriteLine(" l'{AZIONE} da eseguire."); Console.WriteLine("-manufacturer="); @@ -385,8 +346,6 @@ namespace NcProgramManager inputArgs.mostraHelp = true; return; } - var chiave = arguments.Find(it => new Regex("-chiave=.*", RegexOptions.IgnoreCase).IsMatch(it)); - inputArgs.chiave = chiave.Substring(8); var azione = arguments.Find(it => new Regex("-azione=.*", RegexOptions.IgnoreCase).IsMatch(it)); inputArgs.azione = azione.Substring(8); var manufacturer = arguments.Find(it => new Regex("-manufacturer=.*", RegexOptions.IgnoreCase).IsMatch(it));