Files
LlamaCasty/Distribution.md
gramps 85fad1ab79 docs: distribution route decided — Microsoft Store MSIX + Store IAP
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.
2026-09-27 14:23:51 -07:00

33 KiB
Raw Permalink Blame History

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. 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)

// 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:

<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:

# 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)

# .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

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 §2–3. The executable checklist is 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:

$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):

// 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)

// 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)

// 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)

// 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)

// 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)

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.


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)

// 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.