
Hey folks! I want to share the story behind our B2B software licensing system. Honestly, on paper, it looked like a weekend job: the client pays, you fire off a key. Done. But reality hits hard. Suddenly, you're drowning in webhook race conditions, debugging payment gateways that act like total divas, and dealing with users whose warehouse PCs literally fall apart. Here’s how we engineered our way out of that mess. Here is a breakdown of how we solved this. The project is called DMtoCSV . It automates compliance workflows for a strict national track-and-trace system (state-mandated product tracking). We needed to ensure that license keys were issued automatically, the app could work entirely offline, and hacking the software would cost more time and money than simply buying it. The "Burnt Wi-Fi" Problem: Choosing Our HWID Strategy Our first thought? Let's just hash the MAC address alongside the CPU. Horrible idea. Seriously, please don't do this. The exact second a warehouse worker plugs in a cheap $5 USB Wi-Fi dongle or fires up a VPN, that MAC address rotates. The license instantly snaps. Now you have a furious client on hold and a support inbox on fire. Ultimately, we settled on a "trio" of hardware parameters that actually live long: Motherboard UUID — the foundation. System Drive Serial Number — changes very rarely. CPU ID — a solid constant. We dropped the MAC address completely. And just so we don't torture people when their hardware actually dies or gets significantly upgraded, I added a simple "Reset Activation" button in the admin panel. The product owner can unlink the old hardware from the key in one click. Offline Validation & RSA: What About Key Swapping? Warehouses are often concrete bunkers with zero internet connection. Because of this, the software must be able to validate licenses entirely offline. We use RSA-2048. The server signs a JSON payload with the license data using a private key, and the desktop app verifies it using a public key. Look, we understand that 100% desktop protection doesn't exist. Any software can be cracked given enough time. Our goal is strictly economic: make the cost of hacking higher than the cost of the license. To achieve this, we do two things: Obfuscate the public key inside the desktop code so it’s harder to find and replace with a fake one. Use VMProtect to pack the binary and shield it from debuggers. Here is an example of the payload the app stores in the license.lic file: \ { "lic_id": "uuid-v4", "client_id": "customer-email-hash", "valid_until": "2027-04-09", "hwid": "device-fingerprint-hash", "sig": "rsa-digital-signature-base64" } The Webhook Battle: Handling Race Conditions & Float Precision Here is where the real fun begins. Regional payment providers love sending webhooks multiple times. Sometimes, two identical payment notifications arrive 50 milliseconds apart. If your logic is just to SELECT and check if a license exists, and then INSERT a new one… well, welcome to a classic Race Condition. Both requests hit the DB, both see zero existing licenses, and boom — both generate a key. The client pays for one license but receives two. Not great. We solved this using database row locking and unique indexes. \ class LicenseService { public function issueLicense($orderData) { // 1. Race Condition fix: Use transactions and SELECT ... FOR UPDATE. // This locks the specific order row until the transaction finishes, // forcing the second webhook to wait in line. $this->db->query("BEGIN"); $existing = $this->db->get_row( "SELECT key FROM licenses WHERE order_id = ? FOR UPDATE", [$orderData->id] ); if ($existing) { $this->db->query("ROLLBACK"); return $existing->key; // Key already generated by the first webhook } // 2. The Float Precision Detective. // Our payment provider sometimes sends the amount as "100.00" and sometimes as "100". // If you hash "100.00" for the signature and then hash "100", they won't match. // Rule of thumb: always cast billing amounts to integer cents before hashing. $amountInCents = (int)round((float)$orderData->amount * 100); if (!$this->validator->checkSignature($orderData, $amountInCents)) { $this->db->query("ROLLBACK"); throw new Exception("Signature mismatch. Check float precision."); } $newKey = bin2hex(random_bytes(16)); // 3. Save to DB. A UNIQUE index on `order_id` is our final line of defense. $this->repository->save([ 'order_id' => $orderData->id, 'key' => $newKey, 'expires' => date('Y-m-d H:i:s', strtotime("+1 year")) ]); $this->db->query("COMMIT"); return $newKey; } } What We Learned Looking back, the biggest takeaway was that UX is actually security. We nuked all registrations and user accounts entirely. Also, if we hadn't logged raw webhooks… What’s the weirdest payment gateway bug you've ever encountered? Let’s discuss in the comments! \
View original source — Hacker Noon ↗



