Part 3 of the Encryption Series. Part 1: Encryption Demystified | Part 2: Database Encryption


We had built the product. Internally, we call it DevOps Genie - the market name is still under wraps. Features were solid, the UI was clean, customers were evaluating it.

Then came two questions we should have asked much earlier:

  1. What stops a customer from copying the binary to ten machines?
  2. What stops someone from emailing a database backup and exposing every credential stored in the system?

We had built a product without building the locks.

This is the story of how we designed the licensing and encryption system - from choosing algorithms to handling the hardest question in any encryption architecture: who holds the master key?


The Constraints That Shaped Everything

DevOps Genie is deployed at customer sites, not in our cloud. That single fact changes every design decision:

  • No phone-home for license checks - the customer’s network, their firewall, their rules
  • No server-side password reset - we do not have access to their database
  • Customers must own their backups - they back up, they restore, on their schedule
  • We need a recovery path - when a customer calls at 11 PM saying they lost access, “sorry, we cannot help” is not acceptable
  • Per-customer isolation - one customer must never be able to decrypt another’s data

These constraints are not theoretical. They came from real customer conversations and real deployment scenarios.


The License System: Ed25519 Digital Signatures

The first decision: how to enforce licensing without a license server.

Why Signing, Not Encrypting

A license file does not contain secrets. The customer should read it - their organization name, expiry date, user limits, feature entitlements. What we need to prevent is tampering.

A customer should not be able to:

  • Change max_users: 10 to max_users: 1000
  • Extend their expiry date
  • Enable features they have not paid for
  • Copy a license from one instance to another

Digital signatures solve this. We sign the license payload with our private key. The binary verifies the signature with the embedded public key. If any byte changes, verification fails.

The Key Hierarchy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
SkillMine (us - the publisher):
  Ed25519 master private key (master.key)
     Stored offline, NEVER on any server
     Used only for signing new licenses and emergency recovery
  Ed25519 master public key
     Embedded in the compiled Go binary
     Anyone can have it - it only verifies, it cannot sign

Per customer:
  LICENSE_KEY file = JSON { payload, signature }
  Payload: {
    organization: "Acme Corp",
    email: "[email protected]",
    expiry: "2027-01-15",
    max_users: 25,
    features: ["channels", "connectors", "backup"],
    instance_id: "acme-prod-01"
  }
  Signature: Ed25519(master_private_key, JSON(payload))

Startup Validation Flow

Every time the binary starts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
1. Read LICENSE_KEY from /app/LICENSE_KEY (mounted volume) or env var
2. Parse JSON  extract payload + signature
3. Verify Ed25519 signature against embedded public key
4. Check expiry:
   - Valid  full access
   - Expired < 30 days  grace period (warning logged)
   - Expired > 30 days  restricted mode
   - Invalid signature  rejected
   - No license  demo mode (warning, does not block startup)
5. Apply feature flags from payload

Why we chose not to hard-block on missing license:

During UAT and proof-of-concept deployments, forcing a license creates friction. Demo mode with visible warnings lets customers evaluate freely. Production enforcement can be tightened per customer agreement.

Why Ed25519 over RSA:

We covered this in Part 1, but the practical reasons bear repeating:

  • 64-byte signatures vs RSA’s 256-512 bytes - cleaner license files
  • Verification is fast - the binary checks the license on every boot
  • Deterministic - same payload + same key = same signature, always (makes testing predictable)
  • The Go standard library crypto/ed25519 is clean and well-audited

What about quantum? Ed25519, like all elliptic curve algorithms, is theoretically vulnerable to Shor’s algorithm on a future quantum computer. But for license validation, the “harvest now, decrypt later” threat does not apply - the signature is verified locally and instantly, not transmitted over a network to be intercepted. When post-quantum signatures (ML-DSA/CRYSTALS-Dilithium) mature in Go’s standard library, we have a clean migration path: update the signing algorithm, reissue licenses. The architecture does not change.

What About Decompilation?

If someone decompiles the binary, they will find the public key. That is fine. The public key can only verify signatures, not create them. To forge a valid license, an attacker needs the private key, which never leaves our offline storage.

Could someone patch the binary to skip verification? Yes. But that is true of any client-side check. The license system is a business control, not a DRM fortress. It keeps honest customers honest and makes unauthorized use a deliberate act rather than an accident.


The Backup Encryption: Dual-Envelope AES-256-GCM

The second and harder problem: encrypted backups.

Customers need to back up their data, including database credentials, API keys, and connection strings. These backups must be encrypted. But encrypted with whose key?

The Three Options We Considered

Option 1: Customer’s key only

The customer derives an encryption key from their license. They encrypt and decrypt their own backups.

Problem: if they lose their license file, the key is gone. We cannot recover their data. For a product deployed at customer sites, this is a real scenario - servers get rebuilt, volumes get wiped, people leave the company.

Option 2: Our master key only

We encrypt every customer’s backup with a key derived from our master key.

Problem: we can decrypt every customer’s data. Some customers will not accept this. And frankly, we should not want that level of access as a default.

Option 3: Dual envelope - both

The backup can be decrypted by either the customer OR us. The customer has self-service access. We have a recovery path for support scenarios.

We chose option 3. Here is how it works.

The Dual-Envelope Flow

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Step 1: Generate a random 32-byte session key (unique per backup)

Step 2: Encrypt the entire SQL dump with the session key
        Algorithm: AES-256-GCM
        → ciphertext + nonce + auth tag

Step 3: Encrypt the session key with customer's license-derived key
        Customer key = SHA-256(license_signature_bytes)
        → customer_encrypted_session_key

Step 4: Encrypt the session key with our master-derived key
        Master key = SHA-256("devops-genie-master-backup-key:" + master_pub_hex)
        → master_encrypted_session_key

Step 5: Bundle into .enc file:
        {
          "version": 1,
          "customer_key": "<base64>",
          "master_key": "<base64>",
          "nonce": "<base64>",
          "ciphertext": "<base64>"
        }

Why a Session Key?

The actual data (potentially gigabytes of SQL) is encrypted only once, with a random session key. That session key is then encrypted twice - once for each authorized party. This is far more efficient than encrypting the data twice.

This is the same envelope pattern used by PGP, age, and AWS KMS. The data encryption key (DEK) is symmetric and fast. The key encryption keys (KEKs) handle access control.

A fresh session key per backup means:

  • Compromising one backup does not compromise others
  • No long-lived data encryption key to rotate
  • Each backup is cryptographically independent

Decryption Paths

1
2
3
4
5
6
7
8
9
Customer recovery:
  LICENSE_KEY → extract signature → SHA-256 → AES key
  → decrypt customer_encrypted_session_key → session key
  → decrypt ciphertext → SQL dump

SkillMine recovery (support scenario):
  master.key → derive master AES key
  → decrypt master_encrypted_session_key → session key
  → decrypt ciphertext → SQL dump

The customer can always decrypt their own backups without contacting us. We can only decrypt if a customer needs help - and they must explicitly send us the .enc file.

Key Derivation: No Key Storage Needed

A design decision we are particularly pleased with: we do not store encryption keys anywhere.

1
2
3
4
5
6
// Customer key derived from their unique license signature
customerKey := sha256.Sum256(licenseSignatureBytes)  // 32 bytes = AES-256 key

// Master key derived from a fixed prefix + master public key
masterInput := "devops-genie-master-backup-key:" + hex.EncodeToString(masterPubKey)
masterKey := sha256.Sum256([]byte(masterInput))

Every customer has a unique license signature (Ed25519 is deterministic per input + key). SHA-256 of that signature gives a uniformly distributed 32-byte key - perfect for AES-256. Different license = different key. No key database, no key server, no key rotation infrastructure.

The trade-off: if a license file leaks, the derived key leaks. But the dual-envelope design means we can still recover the backup via the master key, and we can reissue a new license (which changes the derived key for future backups).

For systems needing dedicated key management at scale, HashiCorp Vault is the right tool. Our approach works because the license file is already a secret the customer must protect.

Two Backup Modes

Not every backup needs encryption. We built two modes:

ModeCredentialsFormatUse Case
StandardREDACTED.sqlConfig migration, sharing, support tickets
EncryptedIncluded, encrypted.encFull disaster recovery

Standard mode strips all passwords, API keys, and connection strings, replacing them with [REDACTED]. Safe to email, store in a ticket, hand to a consultant. The encrypted mode includes everything - but only the right key can unlock it.

In practice, customers use standard backups for day-to-day work and encrypted backups for quarterly DR drills.


The Master Key: The Hardest Trade-Off

Every encryption system eventually faces this question: who guards the master key?

Our Ed25519 master private key can:

  • Sign a valid license for any organization
  • Derive the master AES key to decrypt any encrypted backup

It is the single most sensitive asset in our system.

Our Approach

  • Stored offline - not on any server, not in any cloud, not in any CI/CD pipeline
  • Never in version control - the development keypair in the codebase is clearly marked test-only
  • Multiple secure backups - physically separate locations
  • Used only for two operations - issuing new licenses and emergency backup recovery
  • Access restricted - only authorized personnel, with documented procedures

The Trust Model

If this key is compromised, an attacker could:

  • Forge valid licenses for any organization
  • Decrypt any encrypted backup they have access to

That is the trade-off. The alternative - no master key - means we cannot help customers who lose access to their data. We chose recoverability over zero-trust purity, with extreme diligence on key protection.

This is the same tension BitLocker’s escrow model faces. Microsoft holds recovery keys and can be compelled to release them under legal order. Our model is similar in structure but different in trust boundaries - the customer knows we hold a master key, and the recovery process requires them to explicitly share the encrypted file with us.

Transparency about who holds the keys and under what conditions they are used - that is the difference between a trust model and a trust trap.


Lessons from Building This System

After designing, implementing, and deploying this in production:

  1. Start with the threat model, not the algorithm - “We need AES-256” is not a requirement. “A customer should not be able to read another customer’s backups” is.

  2. Signing and encrypting solve different problems - We initially considered encrypting the license. Signing was the right answer. The license has no secrets - it has integrity requirements.

  3. Key derivation over key storage, when you can - Deriving keys from existing secrets (the license signature) eliminated an entire category of key management complexity.

  4. The master key is a business decision, not a technical one - Zero-trust purists would say “no master key.” Our customers say “help me when I lose access.” We chose the customer.

  5. Build two modes, not one - The standard (redacted) backup mode gets used far more than encrypted mode. Not every operation needs the complexity of encryption.

  6. Use standard crypto primitives - We used Go’s crypto/ed25519, crypto/aes, crypto/cipher (GCM), and crypto/sha256. We did not invent anything. The creativity was in the architecture, not the cryptography.

  7. Plan for the 11 PM call - The master key recovery path exists because of a real scenario: a customer rebuilds a server, loses the license mount, and needs their encrypted backup. Design for that call.


Frequently Asked Questions

Why not use a license server instead of file-based licensing?

Our product deploys at customer sites. Many customers have strict network policies - no outbound calls to vendor servers. A file-based license works in air-gapped environments, behind firewalls, and without internet access. It trades revocation capability for deployment flexibility.

Can a customer decrypt another customer’s backup?

No. Each customer’s encryption key is derived from their unique license signature. Different license = different SHA-256 hash = different AES key. A customer would need another customer’s license file to derive their key.

What happens when a license is renewed?

A renewed license has a new payload (new expiry date, possibly new features) and therefore a new Ed25519 signature. The derived AES key changes. Backups encrypted under the old license can still be decrypted with the old license file. We recommend customers keep their previous license file until they have re-encrypted their backups with the new one.

Why SHA-256 for key derivation instead of a proper KDF like Argon2?

The license signature is already a high-entropy, 64-byte cryptographic output. There is no low-entropy password to protect against brute force. SHA-256 of a high-entropy input produces a uniformly distributed key. Argon2/PBKDF2 add computational cost that is only necessary for low-entropy inputs (passwords).

Is the dual-envelope pattern standard?

Yes. It is the same approach used by PGP (encrypt to multiple recipients), age (multiple identity files), and AWS KMS (envelope encryption). Encrypt data once with a random DEK, then wrap the DEK for each authorized party. It is well-understood, well-tested, and efficient.

What if I want to implement something similar in my product?

Start with Go’s standard crypto library (or your language’s equivalent). Use Ed25519 for signatures, AES-256-GCM for authenticated encryption, and SHA-256 for key derivation from high-entropy sources. Do not invent algorithms. The architecture decisions - who holds which keys, what the recovery model is, what the trust boundaries are - matter far more than algorithm choice.


This is Part 3 of the Encryption Series. Part 1: Encryption Demystified - symmetric, asymmetric, hashing fundamentals. Part 2: Database Encryption - what “encrypted at rest” actually means for PostgreSQL, MongoDB, and S3.