← Blog 2026-04-03 · 5 min read

Why I moved the license secret off the client.

A code review caught a critical issue: the signing secret was compiled into every .exe. Here's what happened and how I fixed it.

The problem

Web Iron Shield used a classic offline activation scheme. When a customer bought a license, my keygen tool would take their email and hardware ID, run them through SHA-256 with a secret, and output a serial key. The installed app would do the same computation on first activation and compare — if the keys matched, license valid.

The secret: "WIS_2024_SECRET_KEY_TAQI". Hardcoded. In license_system.py. Inside the compiled .exe that every customer downloads.

PyInstaller-packaged executables are not obfuscated. Anyone with pyinstxtractor can extract the Python source in 30 seconds. Once they have the secret, they can generate valid keys for any email + hardware ID combination — forever, without ever paying.

How I missed it

Honestly? I built the licensing early in the project when I wasn't thinking about scale. One-person shop, small sales, who's going to bother reverse-engineering a $99 tool? That logic works until one copy gets shared on a keygen forum and suddenly you have 5,000 "customers" who paid zero.

During a code review I asked for exactly the kind of question I should have asked myself earlier: "if one copy of this leaks, how bad is it?" The answer was "catastrophic", so it became the first thing to fix.

The fix: server-side generation

The signing secret now lives only on webironshield.com. When a payment webhook fires, the server (not the client) generates the serial key using the secret. The secret never leaves the server, never gets shipped, never appears in any binary.

Two PHP endpoints do the work:

/api/generate_license.php   (admin-only, mints keys)
/api/verify_license.php     (public, validates keys)

The admin endpoint is protected by a separate admin token (different from the signing secret). Only my internal ordering system knows the admin token, so even if someone finds the endpoint URL they can't generate keys without the token.

Verification flow

Before: client computes expected key locally, compares.

After: client sends email + hardware ID + serial to the verify endpoint. Server re-computes expected key using the server-side secret. Responds valid or invalid.

For the offline-use case (travelling, no internet), the client caches a successful verification for 14 days. If the server is unreachable after that, the client keeps working — network problems don't lock out paying customers. It just re-verifies when possible.

Trade-offs

The main cost is that first-time activation now requires internet. That's fine — you're buying it on the website, you have internet. The other cost is that I need to keep the verification endpoint alive forever. But it's a 2 KB PHP file with no dependencies, so "forever" is extremely cheap.

The benefit is that the cost of a leaked .exe drops from "catastrophic" to "someone might have one pirated copy until they activate it" — and the server-side verification blocks activation for non-matching keys.

The broader lesson

If you're building a licensed product with offline activation: assume every bit of logic and every constant you ship will be read by a determined attacker. That's what "running on someone else's computer" means. The only trustworthy secrets are the ones that never leave your infrastructure.

This applies beyond licensing:

Migration for existing customers

Existing v2.6 customers had keys generated with the OLD secret. Those keys won't match the new server's secret, so they'd show as invalid after upgrading. The fix was a 30-day transition period where the verify endpoint accepts either secret. After 30 days, I'll drop the old secret and anyone still on v2.6 will need to request a new key (free, auto-issued on request).


← Back to blog