linkCryptographic license files
Keygen's API supports a couple different license file types, namely "license" files and "machine" files, representing a snapshot of a license or a machine, respectively, at the time of checkout. For brevity, we'll refer to both of these as license files, as they are largely the same. Each license file consists of an encoded certificate.
All license files have a time-to-live (TTL) that must be respected. Once a license file's TTL has been met, i.e. it has expired, a new one should be checked out. We recommend starting out with a TTL of 30 days (which is the default), and going up or down from there based on your use case. A license file's expiry is separate from the license's expiry.
Why even set a time-to-live? In short: eventual consistency. During checkout, a license file can be set to expire after a certain amount of time (default 30 days) via a TTL. This gives room for any changes made to the license to propagate into subsequent license files. A good way to think of these would be in terms of a Let's Encrypt TLS certificate, having a short expiry and then re-upping after e.g. 90 days.
For most applications, we recommend setting an expiry, so that license files are eventually consistent when it comes to changes in its license's state, e.g. an update, renewal, expiration, or suspension event, since each time a new license file is checked out, a new snapshot of the license is taken.
Below, you will find information on each certificate's format, and how to decode a license file certificate and cryptographically verify its contents.
Want to see an example? Check out these examples on our GitHub:
- Verifying and decrypting machine files in Python
- Verifying and decrypting license files in Rust
- Verifying and decrypting license files in Java
- Verifying and decrypting license files in C++
- Verifying and decrypting license files in C#
- Verifying and decrypting license files in Dart
- Implementing air-gapped activation in Node
- Utilizing license files with our Go SDK
linkLicense file disposition
We recommend saving license files with the .lic
file extension. This is a widely used file
extension for licensing purposes, and it's perfect for our license files. These files can
be distributed in offline or air-gap environments using email or USB drives.
When downloading a license file, the Content-Disposition
header will contain our recommended
filename. But feel free to choose your own.
linkLicense file format
License file certificates consist of a few parts:
- The certificate header.
- The certificate body, an encoded payload.
- The certificate footer.
Certificate header
The header of the certificate will be as below, where <type>
is either LICENSE
or
MACHINE
, depending on the checked out resource.
-----BEGIN <type> FILE-----
The header will end in a newline.
Certificate body
Between the header and footer, you will find the base64 encoded certificate payload.
-----BEGIN LICENSE FILE-----eyJlbmMiOiJsSTc4N0QwcGZua1RvRDVOSjFpRXlaU093Q09QQ0NOdktKZHpC MlpSYlZBQzVsQUhjdzJSUi8xTEhrcXc0ZG5rUEl3TFVYRzhmUzk1R0JWTmtzd2JDTmllWm1uOElHeGpkbUY2T1RmNjRzOHlpbFRpL3FlUzJSTlhBdGJBWjUw
QWtpVnBudmFWTFhVdkY1UGJJYjNFRXA0YlZNOU1xWjBhNjhQa1R1MW5VS0E0QWRnV1FHWU1tU2hKdmIxbys0UFozYXU3a1FuWkZ1bWNPUUdUcllYRng1YVpZWkw3SzhGMWFFRWh1clNmNzdwa25yWXRScnBmS2tUT2QvN3RaUXhQSW16bWpjOTVGeWJlY0ladndEU2NTNGpRM2VCYlU0cHRFd056dlc5ay9JRFBja2N2eWdBNFhhUnFsRkNZYlRDa1R3LzBZbUVxQnJVTThCYWpFZ3dTRWdvNG44c2N0UTNJVncxYkx6cnVFWDEzY1B3eXE1UDM5SVY5TFlhUjhVYmZBR0tBT0YzbGlPZUppZStmKy96cDFGK2NrcFBIM3J6OW5XbTVjOEFzWkxKeUR3NmFBK1hDRHdmRThlWXdrQlpLTFB4SUJ5Qm9QemIwaU5NWmwya25sU2R1d2hZNUhIVktKbTVKWXR1eGVHK1FOcjFEZXg2NlVXWGJuVHNKT1M3Mlo4NVBBZFFSTjV4SmlHVW92akE0anI2RVJDcDBtWlhMQnM2RXljb2tnYi91cFRWRDV0Y0lRZ2YvSkZGcC96VnNrUjNDdEl5UHk2YUZUNGZIM1N3djFxZ1p6OHBtcUM0aDB6OWR1WlhXeUdzM0p1aGJSWmFpMG9IOTA5Z1V0anR4MHJ4SEg2OUdBamtHWCtQQ2w4QlN4ZlBPMVJsTktPSmw2ckZZVzVac0pIUWhzNnQvZkx5eGJUTEg5YVlUNm5sV0s0eUZ3M3JIVC9oMnlia0NJSjFtMGRQV0pFTkhqTGFzLzFZME5XK28rbXYvR2UyT2NlZ3FjQ2QxSmFLTis3dk13eTlSQ050d1IyeUt1dWIrQzU2aVdhVjQvanNqbmZyVHFZOU0reFZuNlFNM3djVUhwODJ3TzlySWNQOCtxcWtVQmQrYjUxMnVyUW5EbFRObi9JRE1jWGs5SUVWV09QQXdNMDBCV0U5TTFmU2xaTjRldlh6akx6N0c5L3dEbGsxZThMU0FDTEhzcUFSVEZVNzhLMmp3bmlodW9UeHE4TlNSLyswS013elVaVE03WGVHQnVHZVhZT25XUG5YUzlsRFJ6QlMrbE1sT0tZQTJxZzMrSEJSRDR0bS9XaGpRWFRQN250bEQ1MUFKRDRrc3U1NER2dWlqaXJoQmNwaVNYbjliRzV4dE8vQ0dhNlJQbGJJWkxaTjRnY1FjeDhkckdXV0Z2QnA4YVVqVlBWNlFoZ0lqREpDNG9QcDBXNkgyUEdFTC9jbXRMV3JaOVMzTUcvWFRHazFVZnZtNGFMekdQaEoyRGw4TFV3TEhmbGNvVEYvUFg3dDQ2aUlMQnpXdlV4MmU4MEpMakNMaXkrU1VOcnJTMXhqdUQ2R0NqWU9DeVh5TUhJYmlwd09PVEJKbXdvYmorWEh4QkZQY0J5bnBYVlM5ZWVlbEE5Nk8yV0xRZVdFYkFMQXJTcyt1RXJqSjFDZ3o5RjdDVjFnNGp5M2VPQVRDTUJHK1JlVE9KSUdyQ2xmZkt2N2laMmxKS1p4VTRodnExTjVwaEVwZ09KUC91K1M1Zi8zaFBHaXRIVUw0VzFJbS9CR1E3WDFQMHNmbkkySDJlVm8vRmU0L0J0VHQ0eUVUK1lpUkJyWGNsZWxUTWM0R0pENzRocVI4RGxxY0VBLy8xWThnSldCQlNqa0hHYlpiMHRsZ0ZnenpuYzAxUDRJVXByQ21ueEkzeXVVVXJWNDlQL25UcGRkVzJoNDFpUzNoSitSdzVuaHk0QWhZVDdOTVFER1h1cEw3Q29BUWphb0FTc3NZdGJSWDhheGdocTk5a1BYaVRCVVIycEM4ZHpaMnBtbGVqZjY1ck15TktabkRtWkNDanBIS1R3MnpnUzU2V3hoUXdxbnRoRlR0R2prTllKWDEyMm5McVF6T0crTThzWitqUDEzNWRGUUo3L1Y2eFNWOHFnRkJGaHVueHk3N3NoNXZadzVNaDNlOFQvMzh6d3FqYWpCWlYraDVPU05veEpjMlJrMGgxTjFlaUc2TUtUMDVlNDNVRkczcTBKNkJGdHkvL25qYU9EM1ZpVVBadkxGTEVpZFo0eG4vRzg0WDhjNDFtUjd4SEZmbVIxcTFTeTBYRFZLT2laRnhxdDJyYU5RV0wrWlQ2bWRDMEliNkg3Rm55cjdpbHFzRzNCWjNVbjE4UkRmdlkrb3NjNlN0bDA1bEoxdTJYTUNvZ2xEZDJpcnNzSjBZL29DU0Q1ZVE0SnRtL0xrSk81T0Z0ckJ5VnZEUGZkZ2ZTUnk4UU5pamNSNFBJUXBsNkNUQjJXQjBQWFN5VmRiaFVpRWZncE8xeXJRYnJobFFya0pPTFlDdnMwNCtMVjdUclVoRTF2aXFLY2ZVUXMwQnVvNjZOSy8yZDBJOWprRnM4ZzlWSzFqcE9RUU9BVjNoZTA1T01Lck5nNW5NUktPYTQxTUF5U1FpVlg3NHZhdEF3Y0lSelBTTmlOUFR3emFDdllBNUx3d0ovUW1kblpHeEVBSFZRV0VwKzBzNDNVc2lRMFNTdDRMVjVlMWlZMDl1aks4b2thazB6ZGUrRHd0RTlqdzlzTEhTdzRzZGpUUzNIZEN6aFZmVms0ZXZDZVB4NDhwQjk2QllFUHh5SCs5SUppcXE2eU8xTnRIZHBlWUk3TWpueTV6RkEzTk5UdkR1N0FOMmhnT0R2OXBUM2ZNWklZNnNua0RiU0RyVXhhMnBRUldaU3ZtK2hHeUowbHo2ZnhoU0FmM3hRaGpCb0V6bnRlblNTZm1QWEpLbmJoMmJhTnNrdVVIb1lOMVNmL2orRUxCU1dTd0NSbWtOZGI1UkhCQWswdk41QmVLSDVPM3ZlZXRBc2NjdGZMV0R3aDliWnB4ZHVLaFVMeTdLQXl4S1pFbjloWFVUMzhaKzFGTVdzY3RJbDB2anZSSXdpbkMwNFRPUHVqSkh0NXJkMVZRZ0pkTGNDdzQ5TThuSzRSbjlINkwwVWM2dUc4cWRHZXhLY2xyZU1KWmxCTlJsVm5pTzc0VnVoMkVMajk5UWJ2VUhTZ205QllHZmk4aDAwNUFGb0VvNDRxNnpYK0ZNc3daR0txQUpQd0ZCd0x1VGNsa1BIemYvUjlzazZQSkNrOWF1M0t4end3R1ArMkM0RzRaSEhGcUI4dVhldlYyZmR6SExTbTc2cTNkYlRVZ285YXJWLzZBc2ZTaFJacW9HZlhHNVdGRjBsYThMNU0zb21XY3JSeUR6R2ZwQ1BwUXJJNFQyUnN5Vmp6NDFwNHB0dEpUd0lla0tucEtjOWsvcFliR1hodi82cHFpY1hhN1Q5bUplT2Uxc1AxYm15UmJPTWorWXJadmVDaGlrTFFCQlI2U2gwMG5UNHpYL1l2RjRjNVNNM3NKK1pTejVaVXRJSGZiOFRrNWdsYWlzN2VYNTdjYXlxOHdmeVFSOVJlQzNPR0l5YWUyblZ4ZzE3RDBOeXBxTFJBOXFRMU1TSEFka2ZSMEdsWUpueFF4NThtT1J4Z2Fia2V5MExLeE5hRzdzaytKMVljRnUyTEhCNnp3S0t0dThLZGdtVTdHdHZoZz0uQmFHSEZJTExMQ3dNb3RkRC5pejZ5N1NxS0R0QXhGWTNsUlluMVZBPT0iLCJzaWciOiJyMWFjNjFocUcramZsc0p1RjFTUWRxUUQvYTd3L1Y4QndDMWNvdU5iaHo5ai9s TlJWWGp6Ym5DRkF6V3lOU3NUeG9xZm9MV2FlWlhITEZnR21Ub2VBdz09IiwiYWxnIjoiYWVzLTI1Ni1nY20rZWQyNTUxOSJ9 -----END LICENSE FILE-----
The encoded certificate payload has a newline every 80 characters. These may need to be stripped from the string before base64 decoding, depending on programming language support. Once base64 decoded, you now have a license file payload.
Certificate footer
The footer of the certificate will be as below, where <type>
is either LICENSE
or
MACHINE
, depending on the checked out resource.
-----END <type> FILE-----
The footer will end in a newline.
linkLicense file payload
Once the certificate payload has been decoded, you will find that it contains a
JSON payload with the following properties, enc
, sig
and alg
:
{ "enc": "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.xtP6YWsJgGJ4z7SB.lGc+q7XNAZ1DeHUKYsp1xA==", "sig": "wZR6+KJt73+02dtT1a8n10X0o5vwR+Lfk/ZiiWk0hAv+ZBXm1644vIEhbYfrQrejdgI93fGo0jnGr1jDjoupCw==", "alg": "aes-256-gcm+ed25519"}
The enc
property
This will contain a string, either an encoded JSON object or an encrypted JSON object,
depending on if the encrypt
parameter was used during checkout. The JSON object itself
will be a license or machine document under the data
property (as is standard in API
responses from us), as well as any included
relationships.
Lastly, and most importantly, is the meta
property — this will include information on
the license file's ttl
, issued
date, and expiry
date. These should be respected
when asserting that a license file is still valid.
{ "data": { "id": "c34c6e96-9b39-44a9-809c-4394552e5b4d", "type": "machines", "attributes": {
"fingerprint": "4d:Eq:UV:D3:XZ:tL:WN:Bz:mA:Eg:E6:Mk:YX:dK:NC", "cores": null, "ip": null, "hostname": null, "platform": null, "name": "Example Machine", "maxProcesses": null, "requireHeartbeat": true, "heartbeatStatus": "NOT_STARTED", "heartbeatDuration": 600, "lastHeartbeat": null, "nextHeartbeat": null, "lastCheckOut": null, "metadata": {}, "created": "2022-03-28T16:15:20.963Z", "updated": "2022-03-28T16:15:20.963Z" }, "relationships": {
"account": { "links": { "related": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52" }, "data": { "type": "accounts", "id": "1fddcec8-8dd3-4d8d-9b16-215cac0f9b52" } }, "product": { "links": { "related": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/machines/c34c6e96-9b39-44a9-809c-4394552e5b4d/product" }, "data": { "type": "products", "id": "d555e27f-4948-4938-8728-2ec2b1ecc6bb" } }, "group": { "links": { "related": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/machines/c34c6e96-9b39-44a9-809c-4394552e5b4d/group" }, "data": null }, "license": { "links": { "related": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/machines/c34c6e96-9b39-44a9-809c-4394552e5b4d/license" }, "data": { "type": "licenses", "id": "f5a618af-7076-407c-93bc-495caafa65c2" } }, "owner": { "links": { "related": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/machines/c34c6e96-9b39-44a9-809c-4394552e5b4d/owner" }, "data": null } }, "links": {
"self": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/machines/c34c6e96-9b39-44a9-809c-4394552e5b4d" } }, "included": [ { "id": "f5a618af-7076-407c-93bc-495caafa65c2", "type": "licenses", "attributes": {
"name": "Example License", "key": "6DFB15-6597FC-B7DBB6-E34DAB-9D77C0-V3", "expiry": null, "status": "ACTIVE", "uses": 0, "suspended": false, "scheme": null, "encrypted": false, "strict": false, "floating": false, "protected": true, "version": "1.0.0", "maxMachines": 1, "maxProcesses": null, "maxUsers": null, "maxCores": null, "maxUses": null, "requireHeartbeat": true, "requireCheckIn": false, "lastValidated": null, "lastCheckOut": null, "lastCheckIn": null, "nextCheckIn": null, "metadata": {}, "created": "2022-03-25T21:39:08.443Z", "updated": "2022-03-28T16:15:20.973Z" }, "relationships": {
"account": { "links": { "related": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52" }, "data": { "type": "accounts", "id": "1fddcec8-8dd3-4d8d-9b16-215cac0f9b52" } }, "product": { "links": { "related": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/f5a618af-7076-407c-93bc-495caafa65c2/product" }, "data": { "type": "products", "id": "d555e27f-4948-4938-8728-2ec2b1ecc6bb" } }, "policy": { "links": { "related": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/f5a618af-7076-407c-93bc-495caafa65c2/policy" }, "data": { "type": "policies", "id": "a7357c4c-fea5-4184-ad88-511ee984760c" } }, "group": { "links": { "related": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/f5a618af-7076-407c-93bc-495caafa65c2/group" }, "data": null }, "owner": { "links": { "related": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/f5a618af-7076-407c-93bc-495caafa65c2/owner" }, "data": null }, "users": { "links": { "related": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/f5a618af-7076-407c-93bc-495caafa65c2/users" }, "meta": { "count": 0 } }, "machines": { "links": { "related": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/f5a618af-7076-407c-93bc-495caafa65c2/machines" }, "meta": { "cores": 0, "count": 1 } }, "tokens": { "links": { "related": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/f5a618af-7076-407c-93bc-495caafa65c2/tokens" } }, "entitlements": { "links": { "related": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/f5a618af-7076-407c-93bc-495caafa65c2/entitlements" } } }, "links": {
"self": "/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/f5a618af-7076-407c-93bc-495caafa65c2" } } ], "meta": { "issued": "2022-03-28T16:15:40.674Z", "expiry": "2022-04-28T16:15:40.674Z", "ttl": 2629746 }}
As mentioned — the most important bits here are meta.issued
and meta.expiry
. These should
be checked any time a license file's contents are decoded and verified, before being used
elsewhere in your application. Not checking these could result in a license file being
used longer than its TTL allows.
You should assert the following:
- That
issued
is not greater than the current time, indicating the user has set their system clock to the past, also known as clock tampering. - That
expiry
is not less than the current time, indicating an expired license file.
As an example, that would look something like this:
const dec = Buffer.from(enc, 'base64').toString()const { meta, data, included } = JSON.parse(dec)const { issued, expiry } = metaif (new Date(issued).getTime() > Date.now() || new Date(expiry).getTime() < Date.now()) { throw new Error('License file has expired.')}
The sig
property
This will consist of a base64 encoded signature of the enc
property. The signing algorithm
will depend on the license's cryptographic scheme, set through the license's policy. When
the license's scheme is not set, we will default to signing with Ed25519. The license file
signature allows you to assert that the data within enc
has not been tampered with.
For instructions on verifying the signature, see license file verification.
The alg
property
This will provide more information on the cryptographic algorithms used on the license file.
The chosen algorithm will depend on the license policy's scheme
, and will try to be as
compatible as possible with any existing cryptographic key settings.
The following algorithms are supported:
Algorithm | Description |
---|---|
aes-256-gcm+ed25519 |
Encrypted using AES-256-GCM, signed using Ed25519. |
aes-256-gcm+rsa-pss-sha256 |
Encrypted using AES-256-GCM, signed using RSA PKCS1-PSS padding, with a SHA256 digest, max salt length, and a SHA256 MGF1. |
aes-256-gcm+rsa-sha256 |
Encrypted using AES-256-GCM, signed using RSA PKCS1 v1.5 padding. |
base64+ed25519 |
Encoded using Base64, signed using Ed25519. |
base64+rsa-pss-sha256 |
Encoded using Base64, signed using RSA PKCS1-PSS padding, with a SHA256 digest, max salt length, and a SHA256 MGF1. |
base64+rsa-sha256 |
Encoded using Base64, signed using RSA PKCS1 v1.5 padding. |
For example, for an encrypted license file signed with Ed25519, this will be equal to:
aes-256-gcm+ed25519
But for a non-encrypted license file signed using RSA PSS, this will equal:
base64+rsa-pss-sha256
Below is a mapping of license key scheme
to license file algorithm
:
Scheme | Algorithm | |
---|---|---|
None (null i.e. plaintext) |
-> | {aes-256-gcm,base64}+ed25519 |
ED25519_SIGN |
-> | {aes-256-gcm,base64}+ed25519 |
RSA_2048_PKCS1_PSS_SIGN_V2 |
-> | {aes-256-gcm,base64}+rsa-pss-sha256 |
RSA_2048_PKCS1_SIGN_V2 |
-> | {aes-256-gcm,base64}+rsa-sha256 |
RSA_2048_PKCS1_ENCRYPT |
-> | {aes-256-gcm,base64}+rsa-sha256 |
RSA_2048_JWT_RS256 |
-> | {aes-256-gcm,base64}+rsa-sha256 |
RSA_2048_PKCS1_PSS_SIGN |
-> | {aes-256-gcm,base64}+rsa-pss-sha256 |
RSA_2048_PKCS1_SIGN |
-> | {aes-256-gcm,base64}+rsa-sha256 |
For example, a plaintext license would use the ed25519
algorithm.
linkLicense file verification
Now that we understand how to decode a license file, how do we cryptographically verify
its contents? Verification will assert that the license file has not been tampered with,
i.e. that the content you currently have is the content our API originally sent. For
example, verification would fail if the meta.expiry
value was tampered with by a bad
actor attempting to extend the license file's validity period.
enc
contents until you've cryptographically
verified the value. Doing so could put your application's licensing at risk.
To cryptographically verify a license file, you will want to first check the alg
and assert that it matches your expected algorithm, e.g. base64+ed25519
. Since
these differ so widely implementation-wise, this assert is absolutely required.
Next, you will want to use your account's public key, the enc
value, and the sig
value to cryptographically verify enc
. Depending on your crypto library, you may need
to base64 decode sig
to obtain the signature's raw bytes.
Perform the verification like so, making sure to prefix enc
with either license/
or
machine/
, depending on your license file type.
# Strip the header and footer from the license file certificatepayload = LICENSE_FILE.delete_prefix("-----BEGIN LICENSE FILE-----\n") .delete_suffix("-----END LICENSE FILE-----\n") # Decode the payload and parse the JSON objectjson = JSON.parse(Base64.decode64(payload)) # Retrieve the enc and sig propertiesenc = json['enc']sig = json['sig']sig_bytes = Base64.strict_decode64(sig) # Verify using Ed25519verify_key = Ed25519::VerifyKey.new([PUBLIC_KEY].pack('H*')) ok = verify_key.verify(sig_bytes, "license/#{enc}")
If the license file is not encrypted, you can now base64 decode enc
and freely use the data,
remembering to check enc.meta.expiry
. You may also wish to assert that the decoded data
matches the license and/or machine you expect (see encryption below for more on this).
If the license file is encrypted, you can now move onto decrypting the license file.
linkLicense file decryption
License files can optionally be encrypted. License files are encrypted with the license's key, and machine files are encrypted with the license's key and the machine's fingerprint.
This not only hides the content of the license file to only those who possess the encryption secret,
e.g. the license key value, it also allows you to skip asserting that the license file's decoded data
matches the resource you expect. E.g. current_machine.fingerprint == machine_file.fingerprint
.
For example, if you checked out Machine A, encrypted with Machine A's fingerprint, the
machine file could not be used on Machine B, because Machine A's fingerprint is not
known by Machine B, which means the enc
property cannot be decrypted.
Encrypted license files use AES-256-GCM encryption. To decrypt enc
, as mentioned earlier,
you will need the encryption secret. For license files, this will be the license's key
attribute hashed using SHA256, and for machine files, this will be the license's key attribute
concatenated with the machine's fingerprint attribute hashed using SHA256. Hashing these
values with SHA256 is required.
For example, the encryption secret for a license file will be:
Digest::SHA256.digest(license.key)
While the encryption secret for a machine file will be:
Digest::SHA256.digest(license.key + machine.fingerprint)
For encrypted license files, the enc
attribute will consist of 3 parts, delimited by
a .
character:
- The base64 encoded ciphertext, i.e. the encryption result.
- The base64 encoded initialization vector (96-bit IV).
- The base64 encoded authentication tag (128-bit).
You will need to split the enc
string by the .
delimiter, then base64 decode each
part before moving onto decrypting. These values must be decoded.
To decrypt a license file, you can do so as follows:
aes = OpenSSL::Cipher::AES256.new(:GCM)aes.decrypt # Hash the license key using SHA256secret = OpenSSL::Digest::SHA256.digest(LICENSE_KEY) # Split and decode the enc valueparts = enc.split('.').map { Base64.strict_decode64(_1) }ciphertext = parts[0]iv = parts[1]tag = parts[2] # Set key and IVaes.key = secretaes.iv = iv # Set auth tag and set auth data to an empty stringaes.auth_tag = tagaes.auth_data = '' # Decrypt the ciphertextplaintext = aes.update(ciphertext) + aes.final # Parse the plaintext as JSONlic = JSON.parse(plaintext)
The decrypted value will be a JSON string. You can now JSON parse the plaintext string
and freely use the decrypted data, remembering to check lic.meta.expiry
.