85fad1ab79
Creator ruling 2026-09-27. Criteria, verbatim: "zero headaches, minimal maintenance (for me) while still providing accountability and a reasonably easy upgrade flow." Route A is the only combination where all four are solved by handing the work to Microsoft rather than to a certificate vendor: $0/yr, no certificate, no HSM, no annual renewal, no SmartScreen ramp — plus Store auto-update, Store-side payments/entitlements/refunds/support, and Microsoft review as the accountability layer. The rejected options and their reasons stay in research-store-certification.md §3 so a later session reads the ruling instead of re-deriving it. What this deletes: - The entire licensing backend. PolarLicenseService, PolarLicense, MainViewModel.License.cs (PremiumUrl, customer portal, the OfflineGracePeriod = 14 days subscription-era artifact, renewal/lapse copy) and the wrong "Polar unlocks alerts" string all become dead code. IsPremium is derived from the Store entitlement instead of an HTTP call, which also removes the whole "network flaky -> app thinks I'm expired" bug class. - Velopack, the update URL, and the self-hosted droplet — the Store updates. - Distribution.md's premise: Polar as the distribution backbone, Polar file hosting, and code signing as our problem. The IP-protection sections (1, 5, 6, 7) still stand and the build-posture ceiling is unchanged. What does NOT change: the entitlement. Free gets everything; the branding flash stays the only paid delta. Store IAP changes how IsPremium is obtained, never what it gates. Still open, deliberately: the price. The Store revenue share is unverified (do not assume a percentage), and MONETIZATION.md's $29 -> $49 one-time decision is re-opened against a fresh instinct toward ~$99/yr. No price encoded yet. The first code unit is unchanged: bundle ffmpeg (TASK 48 item 1). That clears the one hard certification gate and fixes a real user-facing 404. MARCOM.md and MONETIZATION.md were edited too but are gitignored by design, so those changes stayed local.
706 lines
33 KiB
Markdown
706 lines
33 KiB
Markdown
# Distribution & IP Protection Plan
|
||
|
||
> 360-degree protection strategy for LlamaCasty (.NET 8 / WPF).
|
||
|
||
> ## ⛔ SUPERSEDED IN PART — 2026-09-27
|
||
>
|
||
> **Distribution is now the Microsoft Store: MSIX package + Store IAP.** The creator's criteria
|
||
> were *"zero headaches, minimal maintenance (for me) while still providing accountability and
|
||
> a reasonably easy upgrade flow"*, and that ruling deleted this document's central premise.
|
||
>
|
||
> **What still stands below:** §1 (code architecture / IP protection), §5 (obfuscation posture),
|
||
> §6 (legal), §7 (hardening) — those are about protecting the *binary*, and an MSIX package is
|
||
> still a binary. **§1.2's build-posture ceiling (HARDENED + MOCK_REWARDS, additive-only)
|
||
> remains the agreed ceiling.**
|
||
>
|
||
> **What is dead:**
|
||
> - **Polar as the distribution backbone** — the entire section below. Store IAP handles
|
||
> checkout, entitlements, refunds, tax, and the customer portal. So does **file hosting**:
|
||
> the Store is the delivery mechanism, so "upload `LlamaCasty.exe` to Polar as a File
|
||
> Download benefit" (§2) no longer exists as a step.
|
||
> - **Code signing as our problem** — MSIX is signed by **Microsoft**. There is no certificate
|
||
> to buy, no HSM, no annual renewal. See §4.3, which was already corrected to say this, and
|
||
> `TASKS/research-store-certification.md` §2–3.
|
||
> - **Velopack / the update URL / the DO droplet** — Store auto-update.
|
||
>
|
||
> **The authoritative plan is now [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md).**
|
||
> Keep this file for the protection sections; do not re-derive a delivery strategy from it.
|
||
|
||
---
|
||
|
||
## Polar.sh as the Distribution Backbone — ⛔ OBSOLETE (2026-09-27)
|
||
|
||
> **Historical record only.** Retained because the fee tables and the reasoning about *why*
|
||
> one-time beat subscription were worth working out. **None of the "How We Use It" rows are
|
||
> current**, with one exception worth keeping: the Merchant-of-Record point — *someone else
|
||
> collects and remits VAT/GST* — is still true under Store IAP, because **Microsoft** is now
|
||
> that someone. That fact alone was the strongest argument for the ruling.
|
||
|
||
Polar handled everything between "customer wants to pay" and "customer has a working license key." We didn't build any of that. What Polar *was* going to give us:
|
||
|
||
| Capability | How We Use It |
|
||
|------------|---------------|
|
||
| **License key management** | Polar generates, validates, expires, rotates, and revokes keys. Activation limits (device caps) are built-in — no custom device registry. |
|
||
| **File hosting** (up to 10GB) | Upload `LlamaCasty.exe` (self-contained, ~80-120MB) as a "File Download" benefit. Customers get signed, personal download URLs. SHA-256 checksums included. |
|
||
| **Checkout** | Polar's hosted checkout page handles payment. No Stripe integration, no PCI scope, no custom checkout UI. |
|
||
| **Customer portal** | Customers self-serve: view/copy license keys, see expiry, deactivate devices, rotate compromised keys. We don't build a portal. |
|
||
| **One-time orders** | **Chosen model (2026-09-21):** perpetual license via a one-time Polar order — no subscription. Polar also supports recurring billing if ever needed, but we do not use it. |
|
||
| **Merchant of Record** | Polar collects and remits VAT/GST/sales tax globally. We never touch tax compliance. |
|
||
| **Webhooks** | Polar notifies our backend on purchase, cancellation, key rotation. Used for optional telemetry (section 6.3). |
|
||
|
||
**Polar pricing (2026):** 5% + $0.50 per transaction (Starter plan). No monthly fee. ⛔ *No
|
||
longer paid to anyone — the equivalent cost is now inside the Store revenue share, whose rate
|
||
is unverified (see `TASKS/research-store-certification.md` §11).*
|
||
|
||
**What Polar did NOT handle:** Obfuscation, code signing, anti-tamper, runtime protection, EULA, DMCA. That's all us — sections 1-3, 5-7 below.
|
||
|
||
---
|
||
|
||
## 1. Code Architecture for Protection
|
||
|
||
### 1.1 Assembly Splitting (est. 2–3 days)
|
||
|
||
Split the monolith into three assemblies with different protection tiers:
|
||
|
||
| Assembly | Contents | Protection Level |
|
||
|----------|----------|-----------------|
|
||
| `LlamaCasty.Core.dll` | Models, DTOs, interfaces, enums, constants | Light obfuscation (symbol renaming only) |
|
||
| `LlamaCasty.Services.dll` | YouTube auth, stream service, encoder, audio pipeline, layout store | Full obfuscation + control flow + string encryption |
|
||
| `LlamaCasty.exe` | WPF UI, ViewModels, XAML | No obfuscation (XAML constraints) |
|
||
|
||
**Why:** Obfuscators break WPF XAML/BAML bindings when they rename types referenced in `DataContext`, `Binding Path=`, or `x:Name`. Keeping the UI assembly separate lets you protect the business logic aggressively while leaving the presentation layer safe.
|
||
|
||
**How:**
|
||
- `LlamaCasty.Core` → Class library, `net8.0`, no WPF dependency. Pure C#.
|
||
- `LlamaCasty.Services` → Class library, `net8.0`, references Core. No XAML.
|
||
- `LlamaCasty` → WPF app, references both. All `MainWindow.xaml`, `Themes/`, ViewModels live here.
|
||
|
||
### 1.2 Obfuscation-Friendly Patterns (est. 1–2 days, ongoing)
|
||
|
||
**Do:**
|
||
- Use `public`/`internal` properties with `{ get; set; }` for data binding (XAML `Binding Path=StringLiteral` resolves at runtime by string match — obfuscators handle this if configured).
|
||
- Use `[Obfuscation(Exclude = true)]` on anything bound to XAML via `Binding`, `CommandParameter`, or `x:Name`.
|
||
- Prefer `nameof()` for property names passed to `INotifyPropertyChanged` so renaming works.
|
||
- Use `RelayCommand` (already in use) — it takes lambdas, not string names.
|
||
|
||
**Avoid:**
|
||
- `FindName("SomeControl")` in code-behind (breaks if XAML name is mangled).
|
||
- `x:Name` references accessed from code-behind (use `Binding` instead).
|
||
- `Activator.CreateInstance(Type.GetType("SomeString"))` — obfuscator renames the type string.
|
||
- `Assembly.Load("LlamaCasty.Core")` by string — use direct references.
|
||
- `[Serializable]` DTOs that get binary-serialized (renamed fields break deserialization).
|
||
|
||
### 1.3 Strategic Attribute Placement (est. 1 day)
|
||
|
||
```csharp
|
||
// Exclude from obfuscation — XAML binding relies on property name
|
||
[Obfuscation(Exclude = true)]
|
||
public string RecordFolderDisplay { get; }
|
||
|
||
// Exclude — DTO serialized to SQLite
|
||
[Obfuscation(Exclude = true)]
|
||
public class StreamHealth { ... }
|
||
|
||
// Exclude — ViewModels bound by name in XAML
|
||
[Obfuscation(Exclude = true)]
|
||
public partial class MainViewModel : ViewModelBase { ... }
|
||
|
||
// Safe to obfuscate — internal service, no XAML binding
|
||
public class FfmpegEncoder { ... }
|
||
```
|
||
|
||
**Rules:**
|
||
- All ViewModels: `[Obfuscation(Exclude = true)]` (XAML `Binding Path=` resolves by name at runtime).
|
||
- All Models/DTOs used in SQLite: `[Obfuscation(Exclude = true)]` (column names are strings).
|
||
- All public service interfaces: `[Obfuscation(Exclude = true)]` (DI resolution).
|
||
- Internal service implementations: safe to obfuscate.
|
||
- Helpers/Converters in XAML: `[Obfuscation(Exclude = true)]` on the class.
|
||
|
||
---
|
||
|
||
## 2. Build Pipeline
|
||
|
||
### 2.1 Build Configuration (est. 0.5 day)
|
||
|
||
In `ytLive.csproj`:
|
||
|
||
```xml
|
||
<PropertyGroup>
|
||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||
<OutputType>WinExe</OutputType>
|
||
<PublishSingleFile>true</PublishSingleFile>
|
||
<SelfContained>true</SelfContained>
|
||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||
<PublishTrimmed>true</PublishTrimmed>
|
||
<TrimMode>partial</TrimMode> <!-- 'full' breaks WPF reflection; 'partial' is safe -->
|
||
<AssemblyName>LlamaCasty</AssemblyName>
|
||
<Version>0.9.0</Version>
|
||
<Company>LlamaCasty</Company>
|
||
<Product>LlamaCasty</Product>
|
||
</PropertyGroup>
|
||
```
|
||
|
||
**Key decisions:**
|
||
- `PublishTrimmed` + `TrimMode=partial`: Trims unused BCL code but preserves WPF infrastructure. `full` trimming breaks `Binding`, `DataTemplate`, and XAML reflection.
|
||
- `PublishSingleFile`: One `.exe` to distribute. Self-contained = no .NET runtime dependency on user machines.
|
||
- `win-x64`: Target the dominant platform. ARM64 is a separate RID if needed later.
|
||
|
||
### 2.2 Obfuscator Integration (est. 2–3 days)
|
||
|
||
**Recommended: Eazfuscator.NET** (free for projects under $1M revenue, excellent WPF support, .NET 8 compatible).
|
||
|
||
Alternative commercial: **ArmDot** ($399/yr, better control flow, built-in anti-tamper).
|
||
|
||
**Integration as post-publish step:**
|
||
|
||
```powershell
|
||
# publish.ps1 — called after dotnet publish
|
||
param([string]$PublishDir = "publish")
|
||
|
||
dotnet publish ytLive.csproj -c Release -r win-x64 `
|
||
--self-contained true `
|
||
-p:PublishSingleFile=true `
|
||
-p:PublishTrimmed=true `
|
||
-p:TrimMode=partial `
|
||
-o $PublishDir
|
||
|
||
# Obfuscate the Services assembly (core business logic)
|
||
eazfuscator.exe "$PublishDir\LlamaCasty.Services.dll" `
|
||
--target-framework net8.0 `
|
||
--Renaming-mode unprintable `
|
||
--Control-flow-obfuscation true `
|
||
--String-encryption true `
|
||
--Resource-encryption true
|
||
|
||
# Obfuscate the Core assembly (lighter touch)
|
||
eazfuscator.exe "$PublishDir\LlamaCasty.Core.dll" `
|
||
--target-framework net8.0 `
|
||
--Renaming-mode unprintable
|
||
|
||
# Do NOT obfuscate LlamaCasty.exe (WPF entry point + XAML)
|
||
```
|
||
|
||
### 2.3 XAML/BAML Handling (est. 1 day research)
|
||
|
||
**Problem:** WPF compiles XAML into BAML (binary XAML) embedded as resources. Obfuscators that rename types break BAML because it references types by assembly-qualified name.
|
||
|
||
**Solution:**
|
||
- Do NOT obfuscate the main `.exe` assembly (contains all BAML).
|
||
- Only obfuscate `Core` and `Services` DLLs (no XAML).
|
||
- If an obfuscator must touch the exe: use Eazfuscator.NET's `[Obfuscation(Exclude = true, Feature = "XAML")]` attribute on types referenced in BAML, or configure the obfuscator to skip the `LlamaCasty.g.resources` manifest resource.
|
||
|
||
**Testing:** After obfuscation, launch the app and verify:
|
||
- All screens render correctly
|
||
- `Binding` expressions resolve (no blank/missing data)
|
||
- `DataContext` inheritance works
|
||
- Storyboards/animations play
|
||
- `ContextMenu` items bind correctly
|
||
- Dialogs open/close properly
|
||
|
||
### 2.4 CI/CD Pipeline (est. 1 day)
|
||
|
||
```yaml
|
||
# .github/workflows/release.yml
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: actions/setup-dotnet@v4
|
||
with:
|
||
dotnet-version: 8.0.x
|
||
|
||
- name: Test
|
||
run: dotnet test --no-restore -c Release
|
||
|
||
- name: Publish
|
||
run: dotnet publish ytLive.csproj -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -p:PublishTrimmed=true -p:TrimMode=partial -o publish
|
||
|
||
- name: Obfuscate
|
||
run: |
|
||
eazfuscator.exe publish/LlamaCasty.Services.dll --target-framework net8.0 --Renaming-mode unprintable --Control-flow-obfuscation true --String-encryption true
|
||
eazfuscator.exe publish/LlamaCasty.Core.dll --target-framework net8.0 --Renaming-mode unprintable
|
||
|
||
- name: Sign
|
||
run: |
|
||
$cert = Get-Content -Raw -Path secrets/certificate.pfx | ConvertTo-SecureString -AsPlainText -Force
|
||
Set-AuthenticodeSignature -FilePath publish/LlamaCasty.exe -Certificate $cert -TimestampServer http://timestamp.digicert.com -HashAlgorithm SHA256
|
||
|
||
- name: Upload to Polar
|
||
run: |
|
||
# Upload the signed .exe to Polar as a file download benefit
|
||
# Update the product's file benefit with the new version
|
||
curl -X POST "https://api.polar.sh/v1/products/{product_id}/benefits" \
|
||
-H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
|
||
-F "type=file_download" \
|
||
-F "file=@publish/LlamaCasty.exe"
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Obfuscation Strategy
|
||
|
||
### 3.1 Recommended Obfuscator
|
||
|
||
**Eazfuscator.NET** (primary):
|
||
- Free for projects under $1M revenue — fits solo dev.
|
||
- Excellent WPF support: knows to skip BAML resources, handles `INotifyPropertyChanged` patterns.
|
||
- .NET 8 support confirmed.
|
||
- Symbol renaming (unprintable Unicode), control flow obfuscation, string encryption, resource encryption.
|
||
- Simple CLI integration.
|
||
|
||
**ArmDot** (commercial, $399/yr):
|
||
- Better control flow (virtualization of critical methods).
|
||
- Anti-tamper and anti-debug built in.
|
||
- Better at handling complex WPF scenarios.
|
||
|
||
**What NOT to use:**
|
||
- ConfuserEx — abandoned, doesn't support .NET 8.
|
||
- Obfuscar — limited WPF support, BAML breakage issues.
|
||
- Dotfuscator Community — too basic, no string encryption.
|
||
|
||
### 3.2 What to Obfuscate
|
||
|
||
| Technique | Where | Effect |
|
||
|-----------|-------|--------|
|
||
| Symbol renaming (unprintable) | `Services.dll`, `Core.dll` | All type/method/field names → Unicode garbage. 10x decompilation difficulty. |
|
||
| Control flow obfuscation | `Services.dll` | Flattens `if/else/switch` into state machines. Decompiler produces spaghetti. |
|
||
| String encryption | `Services.dll` | All string literals encrypted; decrypted at runtime. Hides API URLs, error messages, config values. |
|
||
| Resource encryption | `Services.dll` | Embedded resources encrypted. Prevents extraction of embedded data. |
|
||
| Anti-tamper | `Services.dll` | CRC check on method bodies at runtime. Detects binary patching. |
|
||
|
||
### 3.3 What to Exclude
|
||
|
||
| Item | Reason |
|
||
|------|--------|
|
||
| `LlamaCasty.exe` (WPF entry point) | BAML resources break if types are renamed. |
|
||
| All ViewModels | `Binding Path=` resolves property names at runtime. |
|
||
| All Models/DTOs | SQLite column mapping uses property names as strings. |
|
||
| `LayoutStore` (public interface) | Called by ViewModels via interface — renaming breaks DI. |
|
||
| All `IValueConverter` implementations | XAML references converters by type name in `StaticResource`. |
|
||
| All `MarkupExtension` subclasses | XAML parser resolves by type name. |
|
||
| `App.xaml.cs` entry point | Must be discoverable. |
|
||
|
||
### 3.4 Testing Obfuscated Build (est. 1 day per release)
|
||
|
||
**Automated checks:**
|
||
1. App launches without crash (startup log at `%APPDATA%\ytLlive\startup.log`).
|
||
2. All 5 scenes load (Starting/Live/BRB/Chat/Ending).
|
||
3. Social bar renders with entries.
|
||
4. TRAX button click opens file picker.
|
||
5. Recording starts/stops correctly.
|
||
6. YouTube OAuth flow completes (if signed in).
|
||
7. Settings panel opens from Gear menu.
|
||
8. All 247 unit tests pass against the unobfuscated assemblies (tests never run against obfuscated code).
|
||
|
||
**Manual smoke test:**
|
||
- Start a recording, verify file appears in configured folder.
|
||
- Open social dialog, add/edit/delete a handle.
|
||
- Toggle mic mute, verify meter responds.
|
||
- Switch scenes via thumbnail strip.
|
||
|
||
---
|
||
|
||
## 4. Distribution Artifacts
|
||
|
||
### 4.1 Files to Ship
|
||
|
||
```
|
||
LlamaCasty.exe (single-file, self-contained, ~80-120MB)
|
||
```
|
||
|
||
Single-file means everything is inside the `.exe`. No DLLs, no runtime dependency.
|
||
|
||
**Why not an installer?**
|
||
- Single `.exe` is simpler to distribute and update.
|
||
- No admin rights required (installs to `%LOCALAPPDATA%`).
|
||
- No installer framework to maintain (WiX, Inno Setup, etc.).
|
||
- User drops it anywhere and runs it.
|
||
|
||
**Caveat:** Self-contained single-file means ~80-120MB due to bundled .NET runtime. For smaller size, use framework-dependent publishing (requires .NET 8 runtime on user machines) — drops to ~5-10MB.
|
||
|
||
### 4.2 Distribution via Polar
|
||
|
||
**Product setup on Polar:**
|
||
|
||
1. Create a product: "LlamaCasty" — **one-time purchase** (perpetual license; $29 founder / $49 list).
|
||
2. Add two benefits:
|
||
- **License Key** — brandable prefix `LLAMA-****`, activation limit (e.g., 3 devices), optional expiry.
|
||
- **File Download** — upload the signed `LlamaCasty.exe` (~80-120MB). Polar generates signed, personal download URLs. SHA-256 checksum included.
|
||
|
||
**Customer flow:**
|
||
1. Customer visits `buy.polar.sh/polar_cl_...` (the Premium URL already in `MainViewModel.PremiumUrl`).
|
||
2. Customer pays via Polar checkout.
|
||
3. Polar generates a license key + provides a signed download URL.
|
||
4. Customer downloads `LlamaCasty.exe`, runs it, enters license key in Settings.
|
||
5. App calls `PolarLicenseService.ValidateAsync(key)` → Polar API validates → `IsValid = true`.
|
||
6. Premium features unlock locally (persisted in `LayoutStore.Settings`).
|
||
|
||
**Activation limits:**
|
||
- Polar supports `limit_activations` per license key (e.g., 3 devices).
|
||
- Client calls `/v1/customer-portal/license-keys/activate` before first validation.
|
||
- Polar tracks activations; customer can deactivate via Polar's customer portal.
|
||
- No custom device registry needed — Polar handles it.
|
||
|
||
**Key rotation:**
|
||
- If a key is compromised, customer or merchant can rotate it via Polar.
|
||
- Previous key stops validating immediately. Status, usage, limits, expiry preserved.
|
||
|
||
### 4.3 Code Signing
|
||
|
||
> ⚠️ **The cert choice below is a live decision, and the old plan's EV recommendation is
|
||
> dead.** Microsoft **removed the SmartScreen instant-bypass for EV certificates in March
|
||
> 2024** — an EV-signed file now accrues reputation exactly like an OV-signed one, so paying
|
||
> $400+/yr buys precisely what paying ~$150 buys. **Do not buy EV.**
|
||
>
|
||
> **But the more interesting finding is that you may not need a certificate at all.** Three
|
||
> costs, current as of 2026-09-27:
|
||
>
|
||
> | Route | Cert/yr | SmartScreen | Note |
|
||
> |---|---|---|---|
|
||
> | **Microsoft Store (MSIX)** | **$0** — Microsoft re-signs | **No warning** | Requires MSIX packaging; Store review; public listing |
|
||
> | **Microsoft Store (EXE/MSI)** | $120–300 — *your* cert | Warning ramp | Store does **not** re-sign; silent install mandatory. Strictly dominated — cert required anyway |
|
||
> | **Direct + OV cert** | $150–300 | Warning ramp | Worldwide availability |
|
||
> | **Direct + Azure Artifact Signing** | ~$9.99/mo ≈ $120 | Warning ramp | **Individuals: USA/Canada only.** Identity validation required |
|
||
>
|
||
> Signing does **not** clear the warning on day one either way: a valid cert stops the
|
||
> *malware* warning immediately, but "unrecognized publisher" clears as SmartScreen accrues
|
||
> reputation from real download volume. Budget cert validity at **460 days** (CA/B Forum
|
||
> CSC-31) — this is a recurring line item, not a one-time purchase — and note that private
|
||
> keys must sit on an **HSM or hardware token**.
|
||
>
|
||
> **The full comparison, the four real combinations, and the policy analysis live in
|
||
> [`TASKS/research-store-certification.md`](TASKS/research-store-certification.md) §2–3.
|
||
> The executable checklist is [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md),
|
||
> and the route decision is the creator's.**
|
||
|
||
**Certificate:** Buy an **OV (Organization Validation)** code-signing certificate from DigiCert
|
||
or Sectigo (~$150–300/year), or an **Azure Artifact Signing** profile (~$9.99/month,
|
||
USA/Canada individuals only) if the geography allows. ⛔ **Not EV** — no SmartScreen benefit
|
||
since March 2024. **Or buy nothing at all** if the route is the Microsoft Store.
|
||
|
||
**Signing process:**
|
||
```powershell
|
||
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(
|
||
"path\to\certificate.pfx",
|
||
"password",
|
||
[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
|
||
)
|
||
Set-AuthenticodeSignature -FilePath "LlamaCasty.exe" `
|
||
-Certificate $cert `
|
||
-TimestampServer "http://timestamp.digicert.com" `
|
||
-HashAlgorithm SHA256
|
||
```
|
||
|
||
**Storage:** Certificate `.pfx` file stored in GitHub Secrets (or Azure DevOps secure files). Never committed to the repo.
|
||
|
||
### 4.4 Update Mechanism
|
||
|
||
**Polar-native approach (no custom infrastructure):**
|
||
|
||
```csharp
|
||
// In Services.dll (obfuscated)
|
||
public class UpdateCheckService
|
||
{
|
||
// Polar's customer portal API — we check if a newer file is available
|
||
// by validating the license key and checking the benefit's file checksum
|
||
private const string ValidateUrl = "https://api.polar.sh/v1/customer-portal/license-keys/validate";
|
||
|
||
public async Task<UpdateInfo?> CheckForUpdateAsync(string currentVersion, string licenseKey)
|
||
{
|
||
// Validate the key — Polar returns the current benefit state
|
||
// If the file benefit's checksum changed, a new version is available
|
||
// The download URL is Polar's signed URL — we never hardcode it
|
||
// ... implementation in PolarLicenseService ...
|
||
}
|
||
}
|
||
```
|
||
|
||
**Design:**
|
||
- App validates license key on startup (already implemented in `PolarLicenseService`).
|
||
- Polar's validation response includes benefit metadata — if the file benefit's SHA-256 changed, a new version exists.
|
||
- App prompts user to download from Polar's customer portal (or provides the signed URL).
|
||
- No custom update manifest, no self-hosted endpoints, no signing key infrastructure.
|
||
- Version obsolescence: Polar can disable old file benefits, making old versions unsupported.
|
||
|
||
**Alternative (if Polar's API doesn't expose file version metadata):**
|
||
- Host a lightweight `version.json` on GitHub Releases (public, no secrets).
|
||
- App checks GitHub Releases API for latest version.
|
||
- Download link points to Polar's customer portal (not direct download).
|
||
- This adds one external dependency (GitHub) but keeps Polar as the source of truth.
|
||
|
||
### 4.5 Licensing Logic (already implemented)
|
||
|
||
```csharp
|
||
// Services/PolarLicenseService.cs — already ships
|
||
// Calls Polar's customer-portal validation endpoint (no auth required)
|
||
// Survives obfuscation — property names excluded via [Obfuscation(Exclude = true)]
|
||
[Obfuscation(Exclude = true)]
|
||
public class LicenseValidationResult
|
||
{
|
||
public bool IsValid { get; set; }
|
||
public string? Status { get; set; } // "granted", "revoked", "disabled"
|
||
public DateTime? ExpiresAt { get; set; }
|
||
public string? Error { get; set; }
|
||
}
|
||
```
|
||
|
||
**What's already done:**
|
||
- `PolarLicenseService` validates keys against Polar API.
|
||
- `OrganizationId` is hardcoded (safe — it's public knowledge, tied to our Polar account).
|
||
- License state persists in `LayoutStore.Settings` (SQLite) with offline grace period.
|
||
- `PremiumUrl` points to Polar checkout.
|
||
|
||
**What survives obfuscation:** The validation logic in `Services.dll` is obfuscated. An attacker who decompiles the exe sees only the UI calling an interface — the actual Polar API call is in the obfuscated DLL.
|
||
|
||
---
|
||
|
||
## 5. Runtime Protection
|
||
|
||
### 5.1 Anti-Debugging (est. 1–2 days)
|
||
|
||
```csharp
|
||
// In Services.dll (obfuscated)
|
||
internal static class AntiTamper
|
||
{
|
||
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
||
private static extern bool IsDebuggerPresent();
|
||
|
||
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
||
private static extern bool CheckRemoteDebuggerPresent(IntPtr hProcess, ref bool isDebuggerPresent);
|
||
|
||
internal static void Validate()
|
||
{
|
||
if (IsDebuggerPresent())
|
||
Environment.FailFast("Tamper detected.");
|
||
|
||
bool debuggerPresent = false;
|
||
CheckRemoteDebuggerPresent(IntPtr.Zero, ref debuggerPresent);
|
||
if (debuggerPresent)
|
||
Environment.FailFast("Tamper detected.");
|
||
|
||
// Timing check: if a method takes >10x expected time, a debugger is stepping
|
||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||
SomeFrequentlyCalledMethod();
|
||
sw.Stop();
|
||
if (sw.ElapsedMilliseconds > 100)
|
||
Environment.FailFast("Tamper detected.");
|
||
}
|
||
}
|
||
```
|
||
|
||
**Trade-offs:**
|
||
- `IsDebuggerPresent()` is trivially patched (NOP the call). Useful against casual inspection, not determined attackers.
|
||
- Timing checks can false-positive on slow machines. Use conservative thresholds.
|
||
- `Environment.FailFast()` terminates the process immediately — no data corruption, no recovery.
|
||
|
||
### 5.2 Anti-Tampering (est. 1 day)
|
||
|
||
```csharp
|
||
// Method body CRC check — runs at startup
|
||
// Obfuscator can inject this automatically (ArmDot)
|
||
internal static void VerifyMethodBodies()
|
||
{
|
||
var assembly = Assembly.GetExecutingAssembly();
|
||
var location = assembly.Location;
|
||
var bytes = File.ReadAllBytes(location);
|
||
|
||
// Check known CRC offsets (computed at build time)
|
||
foreach (var (offset, expectedCrc) in KnownChecksums)
|
||
{
|
||
var actualCrc = Crc32.Compute(bytes, offset, MethodBodyLength);
|
||
if (actualCrc != expectedCrc)
|
||
Environment.FailFast("Binary integrity check failed.");
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.3 Environment Validation (est. 0.5 day)
|
||
|
||
```csharp
|
||
// Detect common analysis environments — NOT bulletproof, raises the bar
|
||
internal static bool IsAnalysisEnvironment()
|
||
{
|
||
var suspiciousProcesses = new[] { "dnSpy", "ILSpy", "x64dbg", "x32dbg", "ollydbg", "procmon", "wireshark" };
|
||
var running = System.Diagnostics.Process.GetProcesses()
|
||
.Select(p => p.ProcessName.ToLowerInvariant());
|
||
if (running.Any(p => suspiciousProcesses.Contains(p)))
|
||
return true;
|
||
|
||
var vmIndicators = new[] { "VMware", "VirtualBox", "Hyper-V", "Sandboxie" };
|
||
var wmi = new System.Management.ManagementObjectSearcher("SELECT * FROM Win32_ComputerSystem");
|
||
foreach (var obj in wmi.Get())
|
||
{
|
||
var manufacturer = obj["Manufacturer"]?.ToString() ?? "";
|
||
if (vmIndicators.Any(v => manufacturer.Contains(v, StringComparison.OrdinalIgnoreCase)))
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
```
|
||
|
||
**Privacy note:** Environment checks that query WMI or enumerate processes require the app to have no network exfiltration of this data. Log locally only. Do NOT send "user is in a VM" telemetry — that's a privacy violation and potential legal issue.
|
||
|
||
### 5.4 Preventing Memory Dumps (est. 0.5 day, limited effectiveness)
|
||
|
||
```csharp
|
||
internal static void ProtectSensitiveMemory()
|
||
{
|
||
// Encrypt license keys in memory when not in use
|
||
// Zero memory after use
|
||
// Use SecureString for transient secrets
|
||
}
|
||
```
|
||
|
||
**Reality check:** Against a determined attacker with admin rights, memory protection is bypassable. The value is in raising the bar, not creating an impenetrable wall. Focus effort on:
|
||
1. String encryption (hides API endpoints, keys, config).
|
||
2. Control flow obfuscation (makes logic hard to follow).
|
||
3. Polar server-side validation (license checks hit Polar's API, not our code).
|
||
|
||
### 5.5 Secrets Management
|
||
|
||
| Secret | Storage | Notes |
|
||
|--------|---------|-------|
|
||
| YouTube OAuth ClientSecret | Never in binary. Server-side only. | Client uses OAuth device flow; the secret lives on your auth server. |
|
||
| Code-signing certificate | CI/CD secrets only. | Never in source. |
|
||
| Polar access token (for uploads) | CI/CD secrets only. | Used in pipeline to upload new .exe to Polar. |
|
||
| User tokens (DPAPI) | `%APPDATA%\ytLlive\ytLlive.auth` | OS-level encryption. Never sent anywhere. |
|
||
|
||
**Principle:** The client binary contains zero secrets that matter. Polar handles license validation server-side. The binary is a consumer of Polar's API, not a holder of credentials. The `OrganizationId` in `PolarLicenseService` is not a secret — it's required by Polar's public validation endpoint.
|
||
|
||
---
|
||
|
||
## 6. Legal and Operational Measures
|
||
|
||
### 6.1 EULA (est. 1 day, lawyer review recommended)
|
||
|
||
Key clauses:
|
||
|
||
```
|
||
1. LICENSE GRANT: Limited, non-exclusive, non-transferable license to use the software.
|
||
2. RESTRICTIONS: No reverse engineering, decompilation, disassembly, or circumvention
|
||
of technical protection measures. No redistribution, sublicensing, or rental.
|
||
3. INTELLECTUAL PROPERTY: All rights reserved. The software is protected by copyright
|
||
law and international treaties.
|
||
4. TERMINATION: License terminates automatically upon breach. Upon termination,
|
||
you must destroy all copies.
|
||
5. DISCLAIMER: Software provided "AS IS" without warranty.
|
||
6. LIMITATION OF LIABILITY: Not liable for damages arising from use.
|
||
7. GOVERNING LAW: [Your jurisdiction].
|
||
```
|
||
|
||
**Enforceability:** EULAs that prohibit reverse engineering have been upheld in the US (DMCA §1201) and EU (Copyright Directive Art. 6). They're less enforceable in some jurisdictions (e.g., Germany has compulsory decompilation rights under §69e UrhG for interoperability). A lawyer should review for your target markets.
|
||
|
||
### 6.2 DMCA / Takedown
|
||
|
||
1. **Register copyright** with the US Copyright Office ($65 per work). Required for statutory damages in infringement suits.
|
||
2. **Monitor** GitHub, torrent sites, and software sharing sites for pirated copies. Set up Google Alerts for "LlamaCasty download", "LlamaCasty crack".
|
||
3. **DMCA takedown** to GitHub/hosting provider when found. Template:
|
||
```
|
||
Subject: DMCA Takedown Notice — Copyright Infringement
|
||
I am the copyright owner of the software "LlamaCasty".
|
||
The following repository contains unauthorized copies:
|
||
[URL]
|
||
I have a good faith belief that the use is not authorized.
|
||
I swear under penalty of perjury that this information is accurate.
|
||
```
|
||
4. **DMCA to Google** to delist pirate download pages.
|
||
|
||
### 6.3 Telemetry for Unauthorized Use (est. 0.5 day, optional)
|
||
|
||
```csharp
|
||
// Optional: anonymized heartbeat that detects unauthorized distribution
|
||
// Only send: installation GUID (random, stored locally), version, license tier
|
||
// DO NOT send: file paths, user names, screen content, keystrokes
|
||
internal static async Task SendHeartbeatAsync()
|
||
{
|
||
var id = GetOrCreateInstallationId(); // Random GUID, stored in Settings
|
||
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
||
// POST to your API — response can include "deactivate" flag
|
||
}
|
||
```
|
||
|
||
**Privacy:** Heartbeat must be disclosed in the EULA and privacy policy. Must not collect PII. Must be opt-out-able in EU (GDPR). Consider whether this is worth the complexity for a solo dev — Polar already tracks validation counts per key, which gives you usage data without custom telemetry.
|
||
|
||
---
|
||
|
||
## 7. Fallback and Recovery
|
||
|
||
### 7.1 If Protection Is Bypassed
|
||
|
||
**Tier 1 — Casual piracy (someone shares the .exe):**
|
||
- Monitor download sites, issue DMCA takedowns.
|
||
- Polar can disable the file benefit — old download URLs stop working.
|
||
- Push a new version to Polar; old versions become obsolete.
|
||
- No kill-switch needed — version obsolescence is sufficient.
|
||
|
||
**Tier 2 — Cracked version distributed (license check bypassed):**
|
||
- Revoke the compromised license key via Polar dashboard (or API: `POST /v1/license-keys/{id}/revoke`).
|
||
- Rotate the key — previous key stops validating immediately.
|
||
- Push emergency update with new validation logic.
|
||
- The cracked version is now outdated; Polar's API rejects old keys.
|
||
|
||
**Tier 3 — Full decompilation and source reconstruction:**
|
||
- This means obfuscation failed. Audit your obfuscation settings.
|
||
- Switch from Eazfuscator to ArmDot (virtualization is harder to reverse).
|
||
- Add server-side validation for critical features (license, updates).
|
||
- Accept that a determined attacker with enough time will always win — focus on making it not worth their time.
|
||
|
||
### 7.2 Emergency Update Push
|
||
|
||
**Via Polar (no custom infrastructure):**
|
||
1. Upload new `.exe` to Polar dashboard as updated file benefit.
|
||
2. Disable old file benefit — existing customers lose access to old download URL.
|
||
3. Customers with valid license keys see the new version in their Polar purchase page.
|
||
4. Optional: Use Polar webhook to notify a lightweight endpoint → push in-app notification.
|
||
|
||
**Via GitHub Releases (fallback):**
|
||
1. Create a new release on GitHub with the updated `.exe`.
|
||
2. App checks GitHub Releases API for latest version (if Polar's file version metadata isn't exposed).
|
||
3. Download link points to Polar's customer portal (not direct GitHub download).
|
||
|
||
### 7.3 Kill-Switch Ethics
|
||
|
||
A kill-switch is legally and ethically complex:
|
||
- **Legal:** You can deactivate your own software per your EULA. Users agreed to it.
|
||
- **Ethical:** Don't deactivate without warning. Give 30 days notice. Provide data export.
|
||
- **Practical:** A kill-switch that bricks the app without notice will generate bad press and potential legal action.
|
||
- **Recommendation:** Use Polar's key revocation + version obsolescence instead of kill-switches. Let the version go stale rather than actively disabling it.
|
||
|
||
---
|
||
|
||
## Summary: Feasibility Assessment
|
||
|
||
| Step | Time | Difficulty | Solo Dev? | Polar Handles? |
|
||
|------|------|------------|-----------|----------------|
|
||
| Assembly split | 2–3 days | Medium | Yes | No |
|
||
| Obfuscation-friendly patterns | 1–2 days | Low | Yes | No |
|
||
| Attribute placement | 1 day | Low | Yes | No |
|
||
| Build pipeline | 1 day | Medium | Yes | No |
|
||
| Obfuscator integration | 2–3 days | Medium | Yes | No |
|
||
| XAML/BAML testing | 1 day | Medium | Yes | No |
|
||
| CI/CD pipeline | 1 day | Medium | Yes | No |
|
||
| Code signing setup | 0.5 day | Low | Yes | No |
|
||
| License key setup (Polar) | 0.5 day | Low | Yes | **Yes** |
|
||
| File hosting (Polar) | 0.5 day | Low | Yes | **Yes** |
|
||
| Checkout (Polar) | 0 | None | Yes | **Yes** |
|
||
| Customer portal (Polar) | 0 | None | Yes | **Yes** |
|
||
| Activation limits (Polar) | 0.5 day | Low | Yes | **Yes** |
|
||
| Key rotation/revocation (Polar) | 0 | None | Yes | **Yes** |
|
||
| Update mechanism | 1 day | Medium | Yes | Partial |
|
||
| Anti-debug/anti-tamper | 1–2 days | Medium | Yes | No |
|
||
| Environment validation | 0.5 day | Low | Yes | No |
|
||
| EULA (draft + review) | 1–2 days | Low | Lawyer recommended | No |
|
||
| DMCA preparation | 0.5 day | Low | Yes | No |
|
||
| **Total** | **~12–18 days** | | **Yes** | **~5 days saved** |
|
||
|
||
**Bottom line:** Polar eliminates ~5 days of custom infrastructure (licensing backend, checkout, customer portal, device management, tax compliance, file hosting). What remains is purely our protection layer (obfuscation, code signing, anti-tamper) and legal measures.
|
||
|
||
The most cost-effective approach for a solo dev:
|
||
|
||
1. **Do (core):** Assembly split + Eazfuscator (Services.dll only) + code signing + Polar license keys + Polar file hosting. Stops 95% of casual piracy. (~7 days)
|
||
2. **Do (hardening):** Anti-debug checks + Polar activation limits (device caps). Adds another layer. (~3 days)
|
||
3. **Skip (for now):** Environment VM detection, memory protection, complex kill-switches. The cost/benefit doesn't work at indie scale. These are for enterprise or high-value targets.
|
||
|
||
The determined reverse engineer with IDA Pro and unlimited time will always win. The goal is to make the cost of cracking exceed the value of the software — for a solo dev product, that bar is low. Focus on making a great product that people want to pay for.
|