How to Generate Secure License Keys in 2023

Generating and verifying license keys is a common requirement for a lot commercial software
these days. From desktop applications such as those built on frameworks like Electron
or Qt, to dual-licensed open source packages and libraries like Sidekiq,
to a variety of other on-premise software applications and dependencies.

They all have one common denominator: the need for secure licensing.

But as an independent software vendor (ISV), you may be asking yourself — “what technology should
I choose for software licensing in 2021?”

Well, before we get to the answer, we need to understand a bit of history.

The 2 big attack vectors for licensing

When it comes to software licensing, the key generation and verification algorithms vendors
choose can make or break a licensing system. After an algorithm has been compromised, a vendor
can no longer trust any previously generated license keys, including those belonging to legit
end-users.

When this happens, there are a couple things we can do:

  1. Move to an online licensing server, where we can check keys against a list of valid keys.
    If you choose to build an in-house licensing system, this can be incredibly expensive,
    both up-front and in the long-term. This can also limit our application’s license checks
    to online-only, which may be less than ideal.
  2. Move to a modern license key algorithm. This resolution has the rather unfortunate side-effect
    of invalidating all previously generated license keys, including those belonging to legit
    end-users, requiring a migration plan.

Both of these solutions can come at a huge cost, both in terms of end-user trust, support costs,
as well as engineering resources. Suffice it to say, it’s a bad situation. And ideally, what we
want to do is avoid the situation entirely, by choosing a modern, secure license key
algorithm from the get-go.

But before we cover how, let’s take a quick look at a couple of the largest attack vectors
for licensing, which may help us understand how to defend against them.

Software cracking

Software “cracking” is the act of directly modifying the source code of a software application
to bypass its licensing system entirely. As much as vendors hate to hear it: all applications
installed on an end-users device are susceptible to cracking.

(After all, an application is simply made up of bits and bytes, ones and zeroes, stored locally,
and those can always be modified no matter how hard a vendor may try.)

But doesn’t that mean licensing is a fool’s errand? Not at all. Licensing can be a powerful
deterrent against casual piracy, especially for applications that utilize a modern licensing
system. More importantly, a good licensing system can help you get visibility into application
usage, it can provide distribution and update services, and a great system can help you maximize
your software’s revenue potential by highlighting natural up-sell opportunities, such as when an
end-user attempts to utilize more device seats
than they’ve been allotted.

Software cracks usually only work for a single version of a particular application, since
the application code itself is modified to bypass any license checks (meaning a software
update often requires an updated crack for the new application code.) Distributing a
cracked version of an application falls on the bad actor.

Cracks are one of the more common attack vectors for software applications.

Software keygens

The other major attack vector is known as a software “keygen”, which is much more ominous. As
its name may imply, a keygen is a form of software, often a separate program or webpage, that
generates valid license keys, i.e. a key-generator, or “keygen.”

Most software vendors have some type of license keygen, which they keep secret. For example, after
a user submits a successful purchase order, part of the order process calls a key generator, which
generates a valid, legitimate license key for the new customer.

But when it comes to vulnerable, legacy license key algorithms, a bad actor may also be able to
accomplish a similar feat — generating valid, albeit illegitimate, license keys, granted they put
in some effort to reverse-engineer the algorithm.

Depending on your key generation algorithm, a keygen like this may only be able to generate valid
key for a single version of an application. But in the worst case, a bad actor can create a keygen
that generates valid license keys that work across all versions of an application, requiring
a complete upheaval of the product’s licensing system.

Fun fact: we chose the name “Keygen” for it’s true meaning — a license key generator.
We don’t use proprietary algorithms that can be reverse-engineered, like the olden days. Rather,
we lean on modern cryptography — the same algorithms used for the Internet’s security and used by
government agencies such as the NSA — to generate secure license keys.

It’s also worth mentioning that keygens are much more valuable to bad actors than cracks, because
a keygen can be used on the real application, vs the bad actor having to distribute a modified,
cracked version of the application.

The legacy license key algorithm

Now, we’ve alluded to this legacy algorithm, which is actually still in use to this day by a
number of software vendors. It’s called Partial Key Verification, and although it may seem
like a good-enough system, it is security through obscurity.

Why? Let’s dive in and find out.

Partial key verification

These days, writing a partial key verification (PKV) algorithm is actually more work than simply
doing it the right way. But for the sake of understanding, let’s write our own partial key
verification system. And then we’re going to break it.

A quick definition of PKV

Partial Key Verification
is a software license key algorithm that partitions a product key into multiple “subkeys.”
With each new version of your product, your license key verification algorithm will check a different
subset of a license’s subkeys.

It’s called partial key verification because the verification algorithm never tests the
full license key, it only tests a subset of subkeys. (Or so they say.)

I’d recommend reading the above blog post by Brandon from 2007, with his partial serial number
verification system being written in Delphi. But if you’re not into Delphi, we’ll be porting
the partial key verification algorithm to Node.

An implementation of PKV

The main components of a PKV key are the seed value and its subkeys (together referred
to as the serial), and then a checksum. The subkeys are derived from the unique seed value,
accomplished using bit twiddling, and the checksum
is to ensure that the serial (seed + subkeys) does not contain a typo. (Yes… in the olden
days, a person actually had to input license keys by-hand.)

We’re not going to get into the specifics on each of these components, e.g. how the checksum
works, since Brandon’s post covers all of that in detail.

With that said, let’s assume the role of a business that is about to release a new application.
We’re going to write a keygen that we, the business, can use to generate legitimate keys for
our end-users after they purchase our product.

Our PKV keygen should be a tightly kept trade secret, because with it comes the power to craft
license keys at-will. But we’ll soon realize, much to our demise, keeping a PKV keygen secret
is actually not possible.

So, without further ado — let’s begin.

Here’s what a PKV keygen looks like:

const

crypto

=

require

(

'crypto'

)

 

// Format a number to a fixed-length hexadecimal string

function

toFixedHex

(

num

,

len

) {

return

num.

toString

(

16

).

toUpperCase

().

padStart

(len,

'0'

).

substring

(

0

, len)

}

 

// Derive a subkey from the seed (a, b, c being params for bit twiddling)

function

getSubkeyFromSeed

(

seed

,

a

,

b

,

c

) {

if

(

typeof

seed

===

'string'

) {

seed

=

parseInt

(seed,

16

)

}

 

a

=

a

%

25

b

=

b

%

3

 

let

subkey

if

(a

%

2

===

0

) {

subkey

=

((seed

>>

a)

&

0x000000ff

)

^

((seed

>>

b)

|

c)

&

0xff

}

else

{

subkey

=

((seed

>>

a)

&

0x000000ff

)

^

((seed

>>

b)

&

c)

&

0xff

}

 

return

toFixedHex

(subkey,

2

)

}

 

// Get the checksum for a given serial string

function

getChecksumForSerial

(

serial

) {

let

right

=

0x00af

// 175

let

left

=

0x0056

// 101

 

for

(

var

i

=

0

; i

<

serial.

length

; i

++

) {

right

+=

serial.

charCodeAt

(i)

if

(right

>

0x00ff

) {

right

-=

0x00ff

}

 

left

+=

right

if

(left

>

0x00ff

) {

left

-=

0x00ff

}

}

 

return

toFixedHex

((left

<<

8

)

+

right,

4

)

}

 

// Format the key (XXXX-XXXX-XXXX-XXXX-XXXX)

function

formatKey

(

key

) {

return

key.

match

(

/

.

{4}

/

g

).

join

(

'-'

)

}

 

// Generate a 4-byte hexadecimal seed value

function

generateSeed

(

n

) {

const

seed

=

crypto.

randomBytes

(

4

).

toString

(

'hex'

)

 

return

seed.

toUpperCase

()

}

 

// Generate a (legitimate) license key

function

generateKey

(

seed

) {

// Build a list of subkeys (bit twiddling params are arbitrary but can never change)

const

subkeys

=

[

getSubkeyFromSeed

(seed,

24

,

3

,

200

),

getSubkeyFromSeed

(seed,

10

,

0

,

56

),

getSubkeyFromSeed

(seed,

1

,

2

,

91

),

getSubkeyFromSeed

(seed,

7

,

1

,

100

),

]

 

// Build the serial (seed + subkeys)

const

serial

=

seed

+

subkeys.

join

(

''

)

 

// Build the key (serial + checksum)

const

key

=

serial

+

getChecksumForSerial

(serial)

 

return

formatKey

(key)

}

Yeah — it’s a lot to take in. Most readers won’t be comfortable with all of those
magic numbers and the nifty bit-twiddling. (And rightly so — it is confusing, even
to me, as I port over the Delphi code and write this post.)

But with that, let’s generate our first license key:

const

seed

=

generateSeed

()

const

key

=

generateKey

(seed)

 

console.

log

({ key })

// => { key: 'ECE4-4EDB-37E8-7FF9-BC96' }

Next, let’s break down this new key, ECE4-4EDB-37E8-7FF9-BC96. Let’s recall
the components of a key: the seed, its subkeys, and the checksum.

In this case, we can strip away the dashes and see our components:

Seed

:

'ECE44EDB'

Subkeys

:

'37'

,

'E8'

,

'7F'

,

'F9'

Checksum

:

'BC96'

Now, a keygen for production-use may have more subkeys, or the subkeys may be arranged
or intermingled differently, but the algorithm is still going to be more or less the
same. As will the algorithm’s vulnerabilities.

So, how do we verify these license keys?

Well, let’s remember, the “P” in “PKV” stands for “partial” — Partial Key Verification.
Our license key verification algorithm should only verify one subkey at a time, which
we can rotate as-needed, or per version of our app.

Here’s what the verification algorithm looks like:

function

isKeyFormatValid

(

key

) {

return

key.

length

===

24

&&

key.

replace

(

/

-

/

g

,

''

).

length

===

20

}

 

function

isSeedFormatValid

(

seed

) {

return

seed.

match

(

/

[A-F0-9]

{8}

/

)

!=

null

}

 

function

isSerialChecksumValid

(

serial

,

checksum

) {

const

c

=

getChecksumForSerial

(serial)

 

return

c

===

checksum

}

 

function

isKeyValid

(

key

) {

if

(

!

isKeyFormatValid

(key)) {

return

false

}

 

const

[,

serial

,

checksum

]

=

key.

replace

(

/

-

/

g

,

''

).

match

(

/

(

.

{16}

)(

.

{4}

)

/

)

if

(

!

isSerialChecksumValid

(serial, checksum)) {

return

false

}

 

const

seed

=

serial.

substring

(

0

,

8

)

if

(

!

isSeedFormatValid

(seed)) {

return

false

}

 

// Verify 0th subkey

const

expected

=

getSubkeyFromSeed

(seed,

24

,

3

,

200

)

const

actual

=

serial.

substring

(

8

,

10

)

if

(actual

!==

expected) {

return

false

}

 

return

true

}

The gist of the verification algorithm is that we firstly check key formatting, then we’ll
verify the checksum is valid. Next, we’ll verify the format of the seed value, which if we
recall is the first 8 characters of the serial.

And then we hit the meat and potatoes of PKV: verifying the nth subkey.

// Verify 0th subkey

const

expected

=

getSubkeyFromSeed

(seed,

24

,

3

,

200

)

const

actual

=

serial.

substring

(

8

,

10

)

if

(actual

!==

expected) {

return

false

}

If you notice, getSubkeyFromSeed(seed, 24, 3, 200) is deriving an expected 0th subkey from the
seed value. We then compare the expected 0th subkey to our license key’s actual 0th subkey. If the
subkeys don’t match, the license key is not valid.

But if we’re including the exact 0th subkey parameters, which are used by our secret keygen, in
our application code, isn’t that bad? Absolutely! This is how we break PKV. And thus comes the
moment we’ve all been waiting for — let’s write an ‘illegal’ keygen.

Writing a keygen

Let’s assume the role of a bad actor. And let’s review what we know so far:

  1. The current version of the application verifies the 0th subkey.
  2. The 0th subkey is located at indices 8 and 9: 0000-0000-XX00-0000-0000.
  3. We possess the parameters to generate a valid 0th subkey: 24, 3, 200.

Let’s write a keygen, using only the operations contained within the verification code, that
generates a license key with a valid 0th subkey.

const

seed

=

'00000000'

const

subkey

=

getSubkeyFromSeed

(seed,

24

,

3

,

200

)

const

serial

=

`${

seed

}${

subkey

}000000`

const

checksum

=

getChecksumForSerial

(serial)

const

key

=

`${

serial

}${

checksum

}`

.

match

(

/

.

{4}

/

g

).

join

(

'-'

)

 

console.

log

({ key })

// => { key: '0000-0000-C800-0000-BBCD' }

That’s a lot of zeroes. Further, the only components that are not zeroed out are the 0th
subkey, and the checksum. But this can’t possibly be a valid key, right?

isKeyValid

(

'0000-0000-C800-0000-BBCD'

)

// => true

Shoot — that’s not good.

Well, actually… it’s good for ‘us’, the bad actor; bad for the business whose application
we just wrote a working keygen for. We need only increment the hexadecimal seed value to
generate more valid license keys:

-

const

seed

=

'00000000'

+

const

seed

=

'00000001'

const

subkey

=

getSubkeyFromSeed

(seed,

24

,

3

,

200

)

const

serial

=

`${

seed

}${

subkey

}000000`

const

checksum

=

getChecksumForSerial

(serial)

const

key

=

formatKey

(

`${

serial

}${

checksum

}`

)

Which we can then inspect,

console.

log

({ key })

// => { key: '0000-0001-C900-0000-CBCF' }

And as expected,

isKeyValid

(

'0000-0001-C900-0000-CBCF'

)

// => true

Well, that’s doubly not good, for them. And as Murphy’s Law would predict, this keygen has
just been submitted to a popular online message board that the business has no control over.
The keygen grows in popularity, sales dip, stakeholders are unhappy.

What can be done?

Verifying the next subkey

Let’s assume the role of the business once again. We need to fix this. Luckily, our chosen
key algorithm lets us go from verifying the 0th subkey, to verifying the 1st subkey. All
we have to do is adjust the subkey parameters:

-

// Verify 0th subkey

-

const

expected

=

getSubkeyFromSeed

(seed,

24

,

3

,

200

)

-

const

actual

=

serial.

substring

(

8

,

10

)

+

// Verify 1st subkey

+

const

expected

=

getSubkeyFromSeed

(seed,

10

,

0

,

56

)

+

const

actual

=

serial.

substring

(

10

,

12

)

if

(actual

!==

expected) {

return

false

}

Let’s quickly make this change and push out a silent update to limit any further damage
this bad actor can inflict. Luckily, our app auto-updates so this should be a fast fix.

Problem solved, right?

Not quite.

Writing another keygen

Let’s reclaim our role as bad actor. Users of our keygen are claiming that it no longer
works, which is weird because it was most definitely working before. They’re paying us
in cryptocurrency, and even though we’re a bad guy, we like to keep our customers happy.

We note: the first variable that has changed is that the application seems to have
updated itself. After poking around the new version, we reassess the situation.

And these are the facts:

  1. The new version of the application no longer verifies the 0th subkey.
  2. The new version of the application now verifies the 1st subkey.
  3. The 1st subkey is located at indices 10 and 11: 0000-0000-00XX-0000-0000.
  4. We possess the parameters to generate a valid 1st subkey: 10, 0, 56.

See a pattern?

All ‘they’ did was move from verifying the 0th subkey to the 1st subkey. Let’s adjust
our keygen program so that it generates valid product keys once again:

const

seed

=

'00000000'

-

const

subkey

=

getSubkeyFromSeed

(seed,

24

,

3

,

200

)

-

const

serial

=

`${

seed

}${

subkey

}000000`

+

const

subkey

=

getSubkeyFromSeed

(seed,

10

,

0

,

56

)

+

const

serial

=

`${

seed

}00${

subkey

}0000`

const

checksum

=

getChecksumForSerial

(serial)

const

key

=

`${

serial

}${

checksum

}`

.

match

(

/

.

{4}

/

g

).

join

(

'-'

)

Which produces a new license key,

console.

log

({ key })

// => { key: '0000-0000-0038-0000-25BD' }

And, once again, as expected,

isKeyValid

(

'0000-0000-0038-0000-25BD'

)

// => true

We can do this all day. All we need is some 90s KeyGen music.

As the business using PKV, we can continue adjusting our nth subkey verification as-needed
to combat these keygens as they pop up. We have alerts set up for various indicator keywords
and everything.

But there’s a major problem. (It may not be a problem now, but it will be soon.)

Do you see it?

It’s simple: once we start verifying the 2nd subkey, which the bad actor will once again
write a keygen for, and then the 3rd subkey, we’ll eventually run out of subkeys.
Even if we use 100 subkeys, running out is inevitable.

What does that mean, to “run out”?

It means that after we’ve rotated through verifying each of our subkeys, in our clever attempt
at combatting the keygens, we’ll soon have no more recourse. Sure, we can start blacklisting seed
values directly in our application code, but that’s a fool’s errand when there’s something
worse than running out of subkeys.

What’s “worse”?

Well, at the end of this scenario, once all subkey parameters have been leaked, the bad actor
can fully replicate our secret keygen! (After all, we’ve literally given them the keys to our
castle. It was a slow trickle, but they were patient.)

P0

:

24, 3, 200

P1

:

10, 0, 56

P2

:

1, 2, 91

P3

:

7, 1, 100

It’s game over after they get those.

So, what’s the point?

To be frank, Partial Key Verification is a lot of work, especially for a key algorithm that
we will eventually leak in its entirety. PKV is flawed by its very nature. Sure, the more
subkeys there are, the longer it will take to leak the entire algorithm. Maybe that’s awhile.
Or maybe the bad actor isn’t sophisticated enough to keep a record of subkey parameters.

But at the end of the day, we’re still leaking our secrets!

Things that are wrong with PKV:

  1. You leak the license key generation algorithm over time. (I can’t stress this enough!)
  2. You eventually have to maintain a blacklist of leaked/illegitimate keys.
  3. Given enough legitimate keys, your algorithm can be deduced.
  4. It’s hard to embed data into a key (e.g. max app version).
  5. It’s incredibly complex.

A quick tangent —

Most application licensing boils down to code that looks like this:

if

(

isKeyValid

(key)) {

// … do something

}

else

{

// … do something else

}

Some applications will have a central point in the bytecode where this check happens, but others
harden their system by inlining the license key checks, making the work of a bad actor wanting to
crack the software much, much harder. But licensing is all essentially the same: it’s a series
of conditionals.

With that in mind, there’s no benefit to using PKV, a licensing scheme that will eventually
leak its secrets to any bad actor that is looking, vs. modern cryptography. It’s not more secure,
it’s not easier to distribute, and it doesn’t protect you from keygens. PKV is, by design,
security through obscurity. And it should no longer be used.

So what’s the alternative?

Modern license key algorithms

When choosing a modern license key algorithm, we have a quite a few solid options. For example, our
API supports a variety of cryptographic schemes for license keys, from elliptic-curve signatures,
to RSA signatures and even encryption. Today, we’ll be covering elliptic-curve and RSA-2048 signatures.

Cryptography is a wide space, but we’re going to focus on asymmetric, or public-key,
cryptography. The way these asymmetric cryptographic schemes work is that they have a private key,
and a public key. You take some data and create a signature of it using the private key, which
can be verified using the public key. Verification is essentially an authenticity check, “was this
data signed by the private key?”

As the names imply, the private key is our secret (i.e. it’s never shared), the public key is
public. In our case, we’re going to embed the public key into our application code.

There are symmetric schemes, such as AES-128, which forego public and private keys, and
instead use a shared secret key. But those aren’t useful for license keys because we’d have
to embed our secret key into our code to verify license keys, which would give a bad actor
the ability to write a keygen (which was the end result of our PKV implementation).

At the end of today, our cryptographic license keys are going to end up looking like this:

${ENCODED_DATA}.${ENCODED_SIGNATURE}

The license keys we generate may differ in length, depending on the cryptographic scheme we use,
but the format is going to stay the same: some encoded data, a delimiter “.”, and a cryptographic
signature of the data. (This is more or less the same format our API uses for cryptographic keys.)

Let’s start with asymmetric elliptic-curves.

Elliptic-curve cryptography

We aren’t going to be doing a deep-dive into elliptic-curve cryptography
(ECC) today, but that link should curb the curious. Within the ECC category, there are a myriad of
different algorithms. Today, we’ll be exploring Ed25519, a modern implementation of a Schnorr
signature system using elliptic-curve groups.

Ed25519 provides a 128-bit security level, the same security level as AES-128, NIST P-256,
and RSA-3072. (Meaning, yes, it’s good.)

An implementation of ECC

Now, rather than write our own crypto, we’re going to be using Node’s standard crypto module,
which as of Node 12, supports Ed25519.

Let’s generate our private and public keys, or more succinctly, our keypair.

const

crypto

=

require

(

'crypto'

)

 

const

{

privateKey

,

publicKey

}

=

crypto.

generateKeyPairSync

(

'ed25519'

)

const

signingKey

=

privateKey.

export

({ type:

'pkcs8'

, format:

'der'

}).

toString

(

'hex'

)

const

verifyKey

=

publicKey.

export

({ type:

'spki'

, format:

'der'

}).

toString

(

'hex'

)

 

console.

log

({ signingKey, verifyKey })

// => {

// signingKey: '302e020100300506032b657004220420a9466527e2b4dd30f202742abe38e8d75c9756a4f3d22daf1e37a317c22e2197',

// verifyKey: '302a300506032b657003210092f97e92cf06959a8b469d9da95609a4419fa2cc4f03a7009cd3a7c6bc1423e9'

// }

After generating our keypair, we’re going to want to keep those encoded keys in a safe
place. We’ll use the private signing key for our keygen, and we’ll use the public
verify key to verify authenticity of license keys within our application.

Let’s write our license keygen. Thankfully, it’s a lot simpler than our PKV code:

// Some data we're going to embed into the license key

const

data

=

[email protected]'

 

// Generate a signature of the data

const

signature

=

crypto.

sign

(

null

, Buffer.

from

(data), privateKey)

 

// Encode the signature and the dataset using our signing key

const

encodedSignature

=

signature.

toString

(

'base64'

)

const

encodedData

=

Buffer.

from

(data).

toString

(

'base64'

)

 

// Combine the encoded data and signature to create a license key

const

licenseKey

=

`${

encodedData

}.${

encodedSignature

}`

 

console.

log

({ licenseKey })

// => { licenseKey: 'dXNlckBjdXN0b21lci5leGFtcGxl.kANuXAhc8b7rDNgbFBpoSUsmfkM7msQC0tNkeUed4b5W15xF6zxmoV3AYF54zaWFMHznSNY7M9bLloInknvlDw==' }

Once again, we can strip away any delimiters and see our components:

Dataset

:

dXNlckBjdXN0b21lci5leGFtcGxl

Signature

:

kANuXAhc8b7rDNgbFBpoSUsmfkM7msQC0tNkeUed4b5W15xF6zxmoV3AYF54zaWF

MHznSNY7M9bLloInknvlDw==

What’s great about this license key format is that we can embed any dataset into
it that we need. Right now, we’re embedding the customer’s email, but we could include
other information as well, such as order ID, key expiration date, entitlements, and
more. (It could even be a JSON object, which is actually the default for our API.)

One downside is that the more data you embed, the larger the license keys will become.
But in the real world, this isn’t really an issue, since the majority of users will
copy-and-paste their license keys, as opposed to typing them in by hand.

So, what about verifying the keys? Again, it’s pretty simple:

// Split the license key by delimiter

const

[

encodedData

,

encodedSignature

]

=

licenseKey.

split

(

'.'

)

 

// Decode the embedded data and its signature

const

signature

=

Buffer.

from

(encodedSignature,

'base64'

)

const

data

=

Buffer.

from

(encodedData,

'base64'

).

toString

()

 

// Verify the data and its signature using our verify key

const

valid

=

crypto.

verify

(

null

, Buffer.

from

(data), publicKey, signature)

 

console.

log

({ valid, data })

// => { valid: true, data: ' [email protected] ' }

That’s all the code you would need to add to your application to verify license keys
(minus implementation details like prompting the user for their license key, etc.)

Another major downside is that Ed25519 may not be supported in many programming languages,
outside of third-party dependencies. Most modern programming languages, given the
version is up-to-date should support it. For example, Node introduced support
in Node 12; but Ruby, however, still lacks support in the standard library.

So, what’s another alternative?

RSA cryptography

RSA (Rivest–Shamir–Adleman),
is a widely supported cryptography system, and it’s one of the oldest systems still in
use. Like ECC, it’s an asymmetric cryptography system, meaning it uses public-private
keypairs to verify and sign data, respectively.

Due to its age, you may find outdated advice online recommending 512-bit keys, or even
smaller. A modern use of RSA should utilize 2048-, 3072- or 4096-bit keys. The higher
the bit size, the higher the security level. (Though, we should also keep in mind: the
higher the bit size, the longer the signatures will be.)

For our implementation, we’re going to use RSA-2048.

An implementation of RSA

We’re going to be returning to our old friend, Node’s crypto module. It has full
support for RSA, like most programming languages do. Our RSA license keygen will be
very similar to its ECC counterpart.

So, let’s generate an RSA keypair: (Brace yourself.)

const

crypto

=

require

(

'crypto'

)

 

// Generate a new keypair

const

{

privateKey

,

publicKey

}

=

crypto.

generateKeyPairSync

(

'rsa'

, {

modulusLength:

2048

,

privateKeyEncoding: { type:

'pkcs1'

, format:

'pem'

},

publicKeyEncoding: { type:

'pkcs1'

, format:

'pem'

},

})

 

console.

log

({ privateKey, publicKey })

// => {

// privateKey: '-----BEGIN RSA PRIVATE KEY-----\n' +

// 'MIIEpAIBAAKCAQEAqYYo2aSU3EPASo3vAb1pXyU3vdAP1V73qGKcPvWJ1+DlCZJh\n' +

// 'BTlY/IyBYJCoaHiized1ynjiUNOvC5zbEGUOjRJBnUzX8ep53BeoCyWRfA1wRo5S\n' +

// 'easxpNyNE9yJKT/Cyv91jCxH4aOpt8jjTTbvZLh1YnqWI1Bc4YCUKVTA1wV63qzp\n' +

// 'A/ghBCsB6Hhq7Phlngcs+gQYMH4WRkGxwMAeqOJ/UmaK1cMCSZ1yMgN/+Svp1B8a\n' +

// 'iBZ5FAm/0uqh89TWr6ABihIRu9Q6Lan7nr33vB0Fl36KOlPSpJyonWUlFx4crAdR\n' +

// 'NnOO+vfmhMCjQnqNLEzltqrmY7sAZNT2gkmTdwIDAQABAoIBAGCKYHUhfwy5IKbU\n' +

// 'kYoCHiHrBgV4mau/e3ZPQf+wwSFJl+WNkObys7SPJ5agiueD2+M6rx/xG6FAC+2n\n' +

// 'FDIP+utnvCoies/v4hnu9unyKRnmZUwo/NsBHTJvz3/CFfKBtyL3vC9pgD4FgD+D\n' +

// 'jb6JTGeljGPav+m4eEyLdtTayT8pmE2ZQ6qelGW1M+Jd9JN7XYNKzmR8kI4rhug0\n' +

// 'cFf87FGZ7PI8uEXeTWIysS/ZMkxk+dDifaFIpBEFKPdinaotl9raE+pcgeV/+ktQ\n' +

// 'T7RsjEYstRH1VFA9z8Lf1w7RLwdGNMRzv6y69eac/N+FpUsDgRyfXS+JD+KnS6xV\n' +

// 'bW9hAeECgYEA2I8VcZDwPu2qRnyrU4ZWH7Dskv05KFTmxw8/E2oUVnhSnYOaBzAY\n' +

// 'O6DLUNObY3QPFaKzsQv+i5yDL5HHqIDuPEapiZ0g0DDbRQ5+mzxJy92jgP/qouXn\n' +

// 'yCWIun62VjoZqHU4PKWorPoSYrZRzDUtdiNSB+ulmhlt4sbWvB9fXt8CgYEAyGYb\n' +

// 'Ss9vxBjvC4MqLaf2J5cWSEkFA4LjEaz+TqGluuSWbAim4Yu/vjEV7l+jguPNZcm0\n' +

// 'u8+KeyrGtggEDz3z4eTQOtkWwrA0icQaKqT1iBRaoREpYMS4bOFjEoYsVTuAaeEs\n' +

// 'v8E94VkSmONdH7pQEObtb/T71hSO1qr6FjELlmkCgYBaaGGrZ7bkjpPnmWRtGkga\n' +

// 'MuKQ+uZB0DAIKnVKxZ53+wOCfs5u8cUsH5TByZW1j148yg/6eedqoYyi71lLH4hV\n' +

// '4aolqVNplvvzeHmilSi5023PDQgHubNp+0F5mizFErxjd4xixUYF8OB8FWFQv2Kb\n' +

// 'T2OPqvEXxEX7xscfAnnuQQKBgQDCI7kY9nDuZsFeQ8mexXMA06vwh1zmE+zq+M69\n' +

// 'Wnh14HGhY5hYNMyi8mausdR0P0CC9a+zqtIblEtBme5k3b3g/4yDFkCoh4++T06S\n' +

// 'NZDwLdfG5htR9gI86PTTw0w7nhM/f7ecZRcPsv0DRHC5BgP++9jWd11p/iyK5sS0\n' +

// 'rvrs0QKBgQC8I8nSwZIMu+jIMoMbL22hrwrghQ7g48MeDcxBBxAlnLbBdDl3bDeD\n' +

// 'FkDlFFTu8EqMz7MslbHH44iHU/WG1HBe7tGZqVEraKKlsQB3ULHkhi/m1pjKmFek\n' +

// 'QtiGtNPLB5zZk434moQb2/n772N+2OayxGbULkWhVqlw1OGXWVOnkw==\n' +

// '-----END RSA PRIVATE KEY-----\n',

// publicKey: '-----BEGIN RSA PUBLIC KEY-----\n' +

// 'MIIBCgKCAQEAqYYo2aSU3EPASo3vAb1pXyU3vdAP1V73qGKcPvWJ1+DlCZJhBTlY\n' +

// '/IyBYJCoaHiized1ynjiUNOvC5zbEGUOjRJBnUzX8ep53BeoCyWRfA1wRo5Seasx\n' +

// 'pNyNE9yJKT/Cyv91jCxH4aOpt8jjTTbvZLh1YnqWI1Bc4YCUKVTA1wV63qzpA/gh\n' +

// 'BCsB6Hhq7Phlngcs+gQYMH4WRkGxwMAeqOJ/UmaK1cMCSZ1yMgN/+Svp1B8aiBZ5\n' +

// 'FAm/0uqh89TWr6ABihIRu9Q6Lan7nr33vB0Fl36KOlPSpJyonWUlFx4crAdRNnOO\n' +

// '+vfmhMCjQnqNLEzltqrmY7sAZNT2gkmTdwIDAQAB\n' +

// '-----END RSA PUBLIC KEY-----\n'

// }

Right off the bat, we can see that RSA’s keys are much, much larger the Ed25519’s.
But that’s okay, they both get us to our end goal: a cryptographically secure
licensing system. Again, you’ll want to store these keys in a safe place. As
before, and as the names imply, the private key is private, and the public
key can be public.

// Some data we're going to embed into the license key

const

data

=

[email protected]'

 

// Create an RSA signer

const

signer

=

crypto.

createSign

(

'rsa-sha256'

)

signer.

update

(data)

 

// Encode the original data

const

encoded

=

Buffer.

from

(data).

toString

(

'base64'

)

 

// Generate a signature for the data using our private key

const

signature

=

signer.

sign

(privateKey,

'base64'

)

 

// Combine the encoded data and signature to create a license key

const

licenseKey

=

`${

encoded

}.${

signature

}`

 

console.

log

({ licenseKey })

// => {

// licenseKey: 'dXNlckBjdXN0b21lci5leGFtcGxl.mEuxvjm1wlrv02ujafM63shjrjZ3edR07adIvR4vQoaJlQ0PSgiCX6DlLFeP6Qzaz1YZDLHvh3hALEujKZCutJlFhrxhuHJ+H2cAGyMHLoxeCNJHrGBwcW4IP3sGeSVxgWFwUl1twEw5Xb9jdEbxadszCP34YQrKrf/NlmHCDLIP/5eEla02nUGnHOkZ0b3HJAM20sJbulZFfrqqKakkYziJDBiQ0DFjvpTp4xwHEDHsNORle128CMrnpN1PcPuteoKoiMFBha3+hLo9zkUyYBa34KaIZ2RKttF+cOj6MqK1zbC1SVQz2znwEletZdC4a9MMGd0UWNEL9jNzdhMAGw=='

// }

And as expected, like our keypair, our license keys are also much larger. But they’re
secure. And remember, most users copy-and-paste, so length doesn’t really matter.
(You could even wrap license keys in a license.dat file, which makes distribution
a breeze. But that’s just an implementation detail.)

Let’s break down the license key into its dataset and signature components:

Dataset

:

dXNlckBjdXN0b21lci5leGFtcGxl

Signature

:

mEuxvjm1wlrv02ujafM63shjrjZ3edR07adIvR4vQoaJlQ0PSgiCX6DlLFeP6Qza

z1YZDLHvh3hALEujKZCutJlFhrxhuHJ+H2cAGyMHLoxeCNJHrGBwcW4IP3sGeSVx

gWFwUl1twEw5Xb9jdEbxadszCP34YQrKrf/NlmHCDLIP/5eEla02nUGnHOkZ0b3H

JAM20sJbulZFfrqqKakkYziJDBiQ0DFjvpTp4xwHEDHsNORle128CMrnpN1PcPut

eoKoiMFBha3+hLo9zkUyYBa34KaIZ2RKttF+cOj6MqK1zbC1SVQz2znwEletZdC4

a9MMGd0UWNEL9jNzdhMAGw==

That signature, aye? If we were to use a smaller key size, the signature size
could be reduced, but we shouldn’t sacrifice security for such a thing. RSA-512
can be broken within days, for less than $100 in compute-power. Similarly,
even RSA-1024 can be broken, though for a much larger sum. RSA-2048 would take
around a billion years to break on modern systems (quantum computing aside.)

Suffice it to say — RSA-2048 is a safe choice in 2021. RSA-3072, even moreso.

But I digress —

Similarly to ECC, verifying an RSA license key is a rather painless process:

// Split the license key's data and the signature by delimiter

const

[

encoded

,

signature

]

=

licenseKey.

split

(

'.'

)

 

// Decode the embedded data

const

data

=

Buffer.

from

(encoded,

'base64'

).

toString

()

 

// Create an RSA verifier

const

verifier

=

crypto.

createVerify

(

'rsa-sha256'

)

verifier.

update

(data)

 

// Verify the signature for the data using our public key

const

valid

=

verifier.

verify

(publicKey, signature,

'base64'

)

 

console.

log

({ valid, data })

// => { valid: true, data: ' [email protected] ' }

Once again, it takes less than 10 lines of code to verify license keys within
your application. Our RSA implementation can be improved by using a more modern
non-deterministic padding scheme, PKCS1-PSS (which our API also supports.)

Caveats and summary

We’ve learned how legacy licensing systems, such as Partial Key Verification, can
be compromised by a bad actor, and how PKV is insecure by-design. We even wrote a
PKV keygen ourselves. We then wrote a couple secure licensing systems using modern
cryptography, implementing Ed25519 and RSA-2048 signature verification.

Okay, okay — after all we’ve been through with PKV…

You may be asking yourself —

“What about keygens?”

The good news is that unless a bad actor can break Ed25519 or RSA-2048, writing
a keygen is effectively impossible. Besides, if a bad actor can break Ed25519 or
RSA-2048 in 2021, we’ll have much bigger things to worry about, anyways.

But remember, a crack != a keygen, so your application’s licensing always runs
the risk of being circumvented via code modification. But license keys cannot
be forged when you utilize a licensing system built on modern cryptography.

(When it comes to cracking, we can defend against some low-hanging-fruit, but
we’ll leave that topic for another day.)

Now, where does a licensing server fit in?

Generating and verifying the authenticity of cryptographically signed license keys
like we’ve covered will work great for a lot of licensing needs. The implementation
is straight forward, it’s secure, and these types of license keys work especially
great for offline-first perpetual licenses (or a timed license with an embedded,
immutable expiry).

But coupled with a modern licensing server, cryptographic keys can be used to implement
more complex licensing models, such as these
popular ones:

  • An entitlement-based model that gates access to certain features or versions of an
    application by a license’s entitlements. For example, Sublime Text 4
    allows for a few years of updates, but after a license expires, only the versions within
    that 3 year window can be accessed, according to the license’s entitlements.
  • A node-locked or floating model where a license is limited to the number of devices
    it can be used on at one time. For example, Sketch
    allows you to purchase licenses by seat-count, where a user can activate and deactivate
    device “seats.”
  • Device-locked timed trials where a device can sign up for a single trial, without
    risk of the user signing up for a second, third or fourth trial.

But rather than build a licensing server in-house, that’s where our
software licensing API can come in and save your team time and money.

Until next time.

If you find any errors in my code, or if you can think of ways to improve things, ping me via Twitter.

Xổ số miền Bắc