Initial commit

This commit is contained in:
Peter Sykora 2018-03-27 23:07:46 +02:00
commit 471d666de2
18 changed files with 2698 additions and 0 deletions

17
.eslintrc Normal file
View File

@ -0,0 +1,17 @@
{
"extends": ["eslint:recommended", "react-app"],
"parserOptions": {
"ecmaVersion": 8,
"ecmaFeatures": {
"experimentalObjectRestSpread": true
}
},
"plugins": ["react"],
"env": {
"es6": true,
"node": true
},
"rules": {
"no-console": ["error", { "allow": ["log", "warn", "error"] }]
}
}

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
.env
node_modules
.projectile
.vscode
.idea
/.vs/
/bin/

182
README.md Normal file
View File

@ -0,0 +1,182 @@
# License server
This is reference implementation of license server that allows per-machine software licensing while it also manages software and data updates for the
client machines. The protocol is described below.
## Preactivation request
When application is run for first time it is not activated. First it sends its system params (Fletcher64 hashes) to the server to check
if the computer has not been preactivated
```http
POST /activate0 HTTP/1.1
```
Example request body:
```json
{
"appId": "coc",
"systemParams": {
"biosSerialNum": "8690a8fb436070a9",
"computerUUID": "13cfc3b6f8f7fdd2",
"diskSerialNum": "63a58b9728485155",
"nicMac": "4b2856a1e9e8f43e",
"osId": "ec4fe2f3023d1f21"
}
}
```
The server checks if the system is preactivated by checking if any of the system parameters exists in `PreactivationParams` table.
For example for license `XXXX-YYYY` there can be single entry for `biosSerialNum` parameters which matches the system of the requestor.
Therefore the activation will happen using the license number `XXXX-YYYY` and the system will response with license file that the client
system should store locally. We will describe the structure later.
Example Response body:
```json
{
"success": true,
"licenseFile": "fG2HUWhE5kT10Ono3qgO/pzHNU1KAnUlEszz3B1pP5FdSQlIukg3P3xUgfHDQX1OBuAFH68WeXe2T0YP1dbjSsAyDJfpUltJnncwMMLOkfR3YbvyAVNmScgASLWwyQxQAVID6GOQ2weVNo3tAcbj2Ted7rx0HL36seSzyY5xuV/SnfJN6q5acqyJmibQcsrPQLZBvIb00Cy9KENnkVFVH70kC406pKxVZ3Ghqg8vTxgBK6sdbwqd7XrpupcQ4frwwm/NPesCIBYRG+6C/9oMroQoPZ+NEzOYQq2PADOZgOSvaMp4FUNe30IqNckobQO7N2TmW4BJDKVzhG5OYQRfs+xvzG0lF5gOBtmBhDf3yStgJj++jc2pNwe0rDSP2J+Yjs+V8wepyuCJ08cfZOh1feqnzmAarzAD80W8wRFd9U2lHDsyFt7Ke/3aPX454jxqITn/Wu6MK0paX8YAATWWAONyUsWTdA3UkJm45gNHJibcQcZLwKErZEPr2XBNy8nrImHRGFFtr3KC"
}
```
If the Preactivation is not available for given client, the system will response with `{ success: false }`
## Activation request
If the system is not preactivated the client application should ask use to enter the license key (Or the license key should be somehow
received by the client system. The license key is 24 characters in base32 (RFC 4648) format. For simplicity it can be sent to user as
e.g. `T3HZ-IFAT-HLN5-2I57-HAGL-V24R` which is easy to read and enter manually. Dashes in between the characters are ignored.
The client is supposed to send JSON request similar as in preactivation with just one parameter added `licenseNumber` which should be
uppercase without dashes so the server can directly query the database.
### Example
```http
POST /activate HTTP/1.1
Host: localhost:3000
Accept: application/json
Content-Type: application/json
Content-Length: 219
Charsets: utf-8
```
Request data:
```json
{
"appId": "coc",
"systemParams": {
"biosSerialNum": "8690a8fb436070a9",
"computerUUID": "13cfc3b6f8f7fdd2",
"diskSerialNum": "63a58b9728485155",
"nicMac": "4b2856a1e9e8f43e",
"osId": "ec4fe2f3023d1f21"
},
"licenseNumber": "JK33BTBSBKSKV63YEVLMQMBZ"
}
```
Response:
```
{
"success": true,
"licenseFile": "CmiN19MDaeCtA4L+cqhrnL71GWjBqTA6cqP8pUyfwRY+r/s/CyODkgHnO9eg3yaLLNxnNFMOnZOtxgtz8hNMNUIsTAKzus068sz9dJhV2yLrmkvhi1KjEJdOua4ZXuKSzjGKxM+VFXokFfFTqxvVpPt5sMkwq9kG/cZSwpBw7POhR+ncHeF11jjkbKVUnVgjGq8EDHDQFANYAVB3qbo7PY9CG3Gm25nORMUMpqwKieadVmklBZYs09EUqqwxAxxpD44Hw2DaRwoaVMuKTC//wH+3oS3zoL2mx+panJ/HPCN7ZtdBje+v6HSlfUoHgCHpFrr0+9/YqvxAQBWz0Q8dSyzyHkuzEkb4Ob7uWSeVhmKJ1TfzX8CAchth9f4CLCuLdzmCpRVvIAM6ZS4o0t3n/3AAJAmBuKFr9OrHTnN1EgXUYB1TVjiHqvBXR1jEyvYyvkAC4CTel7/Y1LRK2y6mq5is/uLcuKyKpWItMr5p9/3qJAove7tZpT1KrNLYaIPGudIwcnYin8EBAK09uUrqRg=="
}
```
## Check for updates
Another functionality that server supports is check for updates.
Client should send versions of its application modules (e.g. application, data) together with its activation ID, system parameters.
Server checks whether the system with given activation ID and system parameters is still valid and if yes it looks for updates
of the licensed modules. The typical request should look like this:
```json
{
"systemParams": {
"biosSerialNum": "8690a8fb436070a9",
"computerUUID": "13cfc3b6f8f7fdd2",
"diskSerialNum": "63a58b9728485155",
"nicMac": "4b2856a1e9e8f43e",
"osId": "ec4fe2f3023d1f21"
},
"activationId": "f0d68f64-4bc5-33b0-6ab3-e9b446baea08",
"moduleVersions": {
"coc-testdata": 2
}
}
```
The response contains `success: true` if the system is still activated with supplied activation number and system parameters.
If the number of licensed modules has changed since the activation, the server can reactivate the installation and return back also new `licenseFile`.
If there are newer versions available there should be list of updates attached. Here is the explanation of update parameters that are not obvious:
* flag - bit flag (bit 1: signals whether it is incremental update, bit 2: signals whether restart is required after update)
* checksum - SHA256 checksum of the zip file
```json
{
"success": true,
"moduleUpdates": [
{
"moduleId": "coc-testdata",
"version": 3,
"flag": 0,
"checksum": "6c878854d349752eceb0d52658e8838c2ae3cca53962c942a276e8944da25731",
"updateUri": "http://localhost:3000/static/testsite-v3.zip",
"instPath": "data"
},
{
"moduleId": "coc-testdata",
"version": 4,
"flag": 1,
"checksum": "a515353daae35dc1b3e9e06e52b95a53690984cc3172bb4e6b44c6b516afa040",
"updateUri": "http://localhost:3000/static/testsite-v4-incremental.zip",
"instPath": "data"
}
]
}
```
## License file
License file is zlib deflated and AES256-GCM encrypted of following structure
```json
{
"data": "{\"activationId\":\"e8cb99bc-9827-5c3a-b944-94ac97c81366\",\"appId\":\"coc\",\"systemParams\":{\"biosSerialNum\":\"8690a8fb436070a9\",\"computerUUID\":\"13cfc3b6f8f7fdd2\",\"diskSerialNum\":\"63a58b9728485155\",\"nicMac\":\"4b2856a1e9e8f43e\",\"osId\":\"ec4fe2f3023d1f21\"},\"licensedModules\":[\"coc-engine\",\"coc-testdata\"],\"nonce\":\"pSvFZJ8q3tqBzp0pLEmCSg==\"}",
"signature": "304c02240163a4b1a6e9a366672df1a17418ab44fb3471fefc4234b4220de8079d0a59ee3c05ce3502240346e0d66d388e3f6cef1cffe14c6be6930a858c72190359fa57aa755c5767b3688d9d88"
}
```
The signature is ECDSA+SHA256 signature of the serialized data node. The data node extracts further below:
```json
{
"activationId": "e8cb99bc-9827-5c3a-b944-94ac97c81366",
"appId": "coc",
"systemParams": {
"biosSerialNum": "8690a8fb436070a9",
"computerUUID": "13cfc3b6f8f7fdd2",
"diskSerialNum": "63a58b9728485155",
"nicMac": "4b2856a1e9e8f43e",
"osId": "ec4fe2f3023d1f21"
},
"licensedModules": [
"coc-engine",
"coc-testdata"
]
}
```
As you can see the license file specifies activation number with all system parameters the license is activated to.
It also lists modules that are licensed for the given system.
Client system should save the license file received from the server localy and should upon every start check whether
the system parameters are the same as specified in the license file. The license file is protected from tempering by
ECDSA (public/private key cryptography) signature while the client only knows the public key of the server.
## Data model

13
config/index.js Normal file
View File

@ -0,0 +1,13 @@
// Ensure require('dotenv').config() is run before this module is required
exports.NODE_ENV = process.env.NODE_ENV || 'development'
exports.PORT = Number.parseInt(process.env.PORT, 10) || 3000
exports.DATABASE_FILE =
process.env.DATABASE_URL || './data/license-server.db'
// //////////////////////////////////////////////////////////
// Output config object in development to help with sanity-checking
if (exports.NODE_ENV === 'development' || exports.NODE_ENV === 'test') {
console.log(exports)
}

23
config/supportedApps.js Normal file
View File

@ -0,0 +1,23 @@
/**
* Pick parameters which are significant for including into license file
*/
exports.pickParams = function (appId, systemParams) {
// if (appId === 'coc') {
const appParams = ['biosSerialNum', 'osId', 'diskSerialNum', 'nicMac'];
const requiredParams = 2;
// }
let resParams = {};
console.log(systemParams)
for (paramId of appParams) {
if (systemParams.hasOwnProperty(paramId)) {
resParams[paramId] = systemParams[paramId];
}
if (Object.keys(resParams).length === requiredParams) {
return resParams;
}
}
return null;
};

1
index.js Normal file
View File

@ -0,0 +1 @@
require('./src').start()

92
license-server.njsproj Normal file
View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{8dfa872b-4368-4176-811a-6d3d9665e54b}</ProjectGuid>
<ProjectHome />
<ProjectView>ShowAllFiles</ProjectView>
<StartupFile />
<WorkingDirectory>.</WorkingDirectory>
<OutputPath>.</OutputPath>
<ProjectTypeGuids>{3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'" />
<PropertyGroup Condition="'$(Configuration)' == 'Release'" />
<ItemGroup>
<Content Include="package-lock.json" />
<Content Include="package.json" />
</ItemGroup>
<ItemGroup>
<Folder Include="backend" />
<Folder Include="license-server" />
<Folder Include="sql\" />
<Folder Include="src\" />
<Folder Include="src\db\" />
<Folder Include="src\routes\" />
<Folder Include="src\util\" />
</ItemGroup>
<ItemGroup>
<Compile Include="config.js">
<SubType>Code</SubType>
</Compile>
<Compile Include="index.js">
<SubType>Code</SubType>
</Compile>
<Compile Include="sql\reset-db.js" />
<Compile Include="src\util\licenseUtil.js">
<SubType>Code</SubType>
</Compile>
<Compile Include="src\db\pool.js" />
<Compile Include="src\index.js">
<SubType>Code</SubType>
</Compile>
<Compile Include="src\routes\index.js">
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<!--Do not delete the following Import Project. While this appears to do nothing it is a marker for setting TypeScript properties before our import that depends on them.-->
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets" Condition="False" />
<Import Project="$(VSToolsPath)\Node.js Tools\Microsoft.NodejsTools.targets" />
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>0</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>http://localhost:48022/</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://localhost:1337</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
<WebProjectProperties>
<StartPageUrl>
</StartPageUrl>
<StartAction>CurrentPage</StartAction>
<AspNetDebugging>True</AspNetDebugging>
<SilverlightDebugging>False</SilverlightDebugging>
<NativeDebugging>False</NativeDebugging>
<SQLDebugging>False</SQLDebugging>
<ExternalProgram>
</ExternalProgram>
<StartExternalURL>
</StartExternalURL>
<StartCmdLineArguments>
</StartCmdLineArguments>
<StartWorkingDirectory>
</StartWorkingDirectory>
<EnableENC>False</EnableENC>
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>

25
license-server.sln Normal file
View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2036
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "license-server", "license-server.njsproj", "{8DFA872B-4368-4176-811A-6D3D9665E54B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8DFA872B-4368-4176-811A-6D3D9665E54B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8DFA872B-4368-4176-811A-6D3D9665E54B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8DFA872B-4368-4176-811A-6D3D9665E54B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8DFA872B-4368-4176-811A-6D3D9665E54B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C030B8B7-C3CC-4C52-97CA-1C3852CB83D9}
EndGlobalSection
EndGlobal

1421
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "license-server",
"version": "1.0.0",
"description": "License Server",
"main": "index.js",
"dependencies": {
"content-type": "^1.0.4",
"dotenv": "^5.0.1",
"es6-denodeify": "^0.1.5",
"guid": "0.0.12",
"koa": "^2.5.0",
"koa-better-static2": "^1.0.2",
"koa-bodyparser": "^4.2.0",
"koa-bouncer": "^6.0.4",
"koa-compress": "^2.0.0",
"koa-helmet": "^3.3.0",
"koa-logger": "^3.1.0",
"koa-mount": "^3.0.0",
"koa-route": "^3.2.0",
"koa-router": "^7.4.0",
"sqlite": "^2.9.1"
},
"devDependencies": {},
"scripts": {
"start": "node index.js",
"reset-db": "node ./sql/reset-db",
"test": "echo \"Error: no test specified\" && exit 1"
},
"engines": {
"node": ">=9.x"
},
"author": "Peter Sykora",
"license": "ISC"
}

55
sql/reset-db.js Normal file
View File

@ -0,0 +1,55 @@
require('dotenv').config()
// Node
const path = require('path')
const { promisify } = require('util')
const readFile = promisify(require('fs').readFile)
// 1st
const config = require('../config')
const { pool } = require('../src/db/pool')
// //////////////////////////////////////////////////////////
// Sanity check: Ensure this isn't being run in production
if (config.NODE_ENV !== 'development') {
throw new Error('[reset_db] May only reset database in development mode')
}
// //////////////////////////////////////////////////////////
function slurpSql(filePath) {
const relativePath = '../sql/' + filePath
const fullPath = path.join(__dirname, relativePath)
return readFile(fullPath, 'utf8')
}
async function seed() {
console.log('Resetting the database...')
await (async () => {
const sql = await slurpSql('schema.sql')
console.log('-- Executing schema.sql...')
const conn = await pool;
console.log(conn)
console.log(await conn.get('SELECT 1'))
console.log(await conn.exec(sql));
console.log(await conn.get('SELECT * FROM Activation'));
})()
// await (async () => {
// const sql = await slurpSql('seeds.sql')
// console.log('-- Executing seeds.sql...')
// await pool._query(sql)
// })()
}
seed().then(
() => {
console.log('Finished resetting db')
process.exit(0)
},
err => {
console.error('Error:', err, err.stack)
process.exit(1)
}
)

301
sql/schema.sql Normal file
View File

@ -0,0 +1,301 @@
--
-- File generated with SQLiteStudio v3.1.1 on Tue Mar 13 19:16:57 2018
--
-- Text encoding used: UTF-8
--
PRAGMA foreign_keys = off;
BEGIN TRANSACTION;
-- Table: Activation
DROP TABLE IF EXISTS Activation;
CREATE TABLE Activation (
activationId VARCHAR (40) PRIMARY KEY,
appId VARCHAR (20) NOT NULL,
licenseNum VARCHAR (24) NOT NULL,
activatedOn DATETIME NOT NULL,
deactivatedOn DATETIME,
FOREIGN KEY (
appId,
licenseNum
)
REFERENCES License (appId,
licenseNum)
);
-- Table: ActivationParams
DROP TABLE IF EXISTS ActivationParams;
CREATE TABLE ActivationParams (
activationId VARCHAR (40) NOT NULL,
paramId VARCHAR (20) NOT NULL,
paramValue VARCHAR NOT NULL,
flag INTEGER NOT NULL
DEFAULT (0),
PRIMARY KEY (
activationId,
paramId
),
FOREIGN KEY (
activationId
)
REFERENCES Activation (activationId)
);
-- Table: CheckLog
DROP TABLE IF EXISTS CheckLog;
CREATE TABLE CheckLog (
checkLogId VARCHAR (40) PRIMARY KEY
NOT NULL,
activationId VARCHAR (40) NOT NULL,
checkedOn DATETIME NOT NULL,
FOREIGN KEY (
activationId
)
REFERENCES Activation (activationId)
);
-- Table: CheckLogParams
DROP TABLE IF EXISTS CheckLogParams;
CREATE TABLE CheckLogParams (
checkLogId VARCHAR (40) REFERENCES CheckLog (checkLogId)
NOT NULL,
paramId VARCHAR (20) NOT NULL,
paramValue VARCHAR NOT NULL,
PRIMARY KEY (
checkLogId,
paramId
)
);
-- Table: CheckLogVersion
DROP TABLE IF EXISTS CheckLogVersion;
CREATE TABLE CheckLogVersion (
checkLogId VARCHAR (40) NOT NULL
REFERENCES CheckLog (checkLogId),
moduleId VARCHAR (40) NOT NULL,
version INTEGER NOT NULL,
PRIMARY KEY (
checkLogId,
moduleId
)
);
-- Table: License
DROP TABLE IF EXISTS License;
CREATE TABLE License (
appId VARCHAR (20) NOT NULL,
licenseNum VARCHAR (24) NOT NULL,
customerId VARCHAR (20) NOT NULL,
PRIMARY KEY (
appId,
licenseNum
)
);
INSERT INTO License (
appId,
licenseNum,
customerId
)
VALUES (
'coc',
'T3HZIFATHLN52I57HAGLV24R',
'111'
);
INSERT INTO License (
appId,
licenseNum,
customerId
)
VALUES (
'coc',
'JK33BTBSBKSKV63YEVLMQMBZ',
'111'
);
-- Table: LicensedModule
DROP TABLE IF EXISTS LicensedModule;
CREATE TABLE LicensedModule (
appId VARCHAR (20) NOT NULL,
licenseNum VARCHAR (24) NOT NULL,
moduleId VARCHAR (40) NOT NULL,
PRIMARY KEY (
appId,
licenseNum,
moduleId
),
FOREIGN KEY (
appId,
licenseNum
)
REFERENCES License (appId,
licenseNum)
);
INSERT INTO LicensedModule (
appId,
licenseNum,
moduleId
)
VALUES (
'coc',
'JK33BTBSBKSKV63YEVLMQMBZ',
'coc-engine'
);
INSERT INTO LicensedModule (
appId,
licenseNum,
moduleId
)
VALUES (
'coc',
'JK33BTBSBKSKV63YEVLMQMBZ',
'coc-testdata'
);
INSERT INTO LicensedModule (
appId,
licenseNum,
moduleId
)
VALUES (
'coc',
'T3HZIFATHLN52I57HAGLV24R',
'coc-testdata'
);
INSERT INTO LicensedModule (
appId,
licenseNum,
moduleId
)
VALUES (
'coc',
'T3HZIFATHLN52I57HAGLV24R',
'coc-engine'
);
-- Table: ModuleUpdate
DROP TABLE IF EXISTS ModuleUpdate;
CREATE TABLE ModuleUpdate (
moduleId VARCHAR (40) NOT NULL,
version INTEGER NOT NULL,
checksum VARCHAR (32) NOT NULL,
updateUri VARCHAR NOT NULL,
instPath VARCHAR NOT NULL,
flag INTEGER NOT NULL
DEFAULT (0),
PRIMARY KEY (
moduleId,
version
)
);
INSERT INTO ModuleUpdate (
moduleId,
version,
checksum,
updateUri,
instPath,
flag
)
VALUES (
'coc-testdata',
2,
'a515353daae35dc1b3e9e06e52b95a53690984cc3172bb4e6b44c6b516afa040',
'http://localhost:3000/static/testsite-v2-incremental.zip',
'data',
1
);
INSERT INTO ModuleUpdate (
moduleId,
version,
checksum,
updateUri,
instPath,
flag
)
VALUES (
'coc-testdata',
1,
'6c878854d349752eceb0d52658e8838c2ae3cca53962c942a276e8944da25731',
'http://localhost:3000/static/testsite-v1.zip',
'data',
0
);
-- Table: PreactivationParams
DROP TABLE IF EXISTS PreactivationParams;
CREATE TABLE PreactivationParams (
appId VARCHAR (20) NOT NULL,
licenseNum VARCHAR (24) NOT NULL,
paramId VARCHAR (20) NOT NULL,
paramValue VARCHAR (20) NOT NULL,
paramOrig VARCHAR (255),
PRIMARY KEY (
appId,
licenseNum,
paramId
),
FOREIGN KEY (
appId,
licenseNum
)
REFERENCES License (appId,
licenseNum)
);
INSERT INTO PreactivationParams (
appId,
licenseNum,
paramId,
paramValue,
paramOrig
)
VALUES (
'coc',
'T3HZIFATHLN52I57HAGLV24R',
'biosSerialNum',
'8690a8fb436070a9',
'R80CW80'
);
-- View: ActiveActivationView
DROP VIEW IF EXISTS ActiveActivationView;
CREATE VIEW ActiveActivationView AS
SELECT *
FROM Activation
WHERE deactivatedOn IS NULL;
-- View: LastFullVersionView
DROP VIEW IF EXISTS LastFullVersionView;
CREATE VIEW LastFullVersionView AS
SELECT moduleId,
MAX(version) AS lastFullVersion
FROM ModuleUpdate
WHERE flag <> 1
GROUP BY moduleId;
COMMIT TRANSACTION;
PRAGMA foreign_keys = on;

9
src/db/pool.js Normal file
View File

@ -0,0 +1,9 @@
const sqlite = require('sqlite')
const config = require('../../config')
async function connect() {
return sqlite.open(config.DATABASE_FILE, { verbose:true, promise:Promise })
}
exports.pool = connect()

71
src/index.js Normal file
View File

@ -0,0 +1,71 @@
// Load env vars from .env, always run this early
require('dotenv').config()
// 3rd party
const debug = require('debug')('app:index')
const Koa = require('koa')
const helmet = require('koa-helmet')
const compress = require('koa-compress')
const serveStatic = require('koa-better-static2')
const logger = require('koa-logger')
const bodyParser = require('koa-bodyparser')
const bouncer = require('koa-bouncer')
const mount = require('koa-mount')
// 1st party
const config = require('../config')
const mw = require('./middleware')
// //////////////////////////////////////////////////////////
const app = new Koa()
app.poweredBy = false
app.proxy = config.TRUST_PROXY
// //////////////////////////////////////////////////////////
// Middleware
// //////////////////////////////////////////////////////////
app.use(helmet())
app.use(compress())
// TODO: You would set a high maxage on static assets if they had their hash in their filename.
// This project currently has no static asset build system setup.
app.use(mount('/static', serveStatic('static', { maxage: 0 })))
app.use(logger())
app.use(bodyParser())
/*var getRawBody = require('raw-body')
var contentType = require('content-type')
app.use(function* (next) {
console.log('data before ' + this.req.headers['content-length']);
this.text = yield getRawBody(this.req, {
length: this.req.headers['content-length'],
limit: '1mb',
encoding: contentType.parse(this.req).parameters.charset
})
console.log(this.text.toString());
console.log('data after');
yield next
}) */
app.use(mw.methodOverride()) // Must come after body parser
app.use(mw.removeTrailingSlash())
app.use(bouncer.middleware())
app.use(mw.handleBouncerValidationError()) // Must come after bouncer.middleware()
// //////////////////////////////////////////////////////////
// Routes
// //////////////////////////////////////////////////////////
app.use(require('./routes').routes())
// //////////////////////////////////////////////////////////
app.start = function (port = config.PORT) {
app.listen(port, () => {
console.log(`Listening on http://localhost:${port}`)
})
}
module.exports = app

57
src/middleware.js Normal file
View File

@ -0,0 +1,57 @@
// 3rd
const debug = require('debug')('app:middleware')
const bouncer = require('koa-bouncer')
// 1st
const config = require('../config')
exports.methodOverride = function() {
return async (ctx, next) => {
if (typeof ctx.request.body === 'undefined') {
throw new Error(
'methodOverride middleware must be applied after the body is parsed and ctx.request.body is populated'
)
}
if (ctx.request.body && ctx.request.body._method) {
ctx.method = ctx.request.body._method.toUpperCase()
delete ctx.request.body._method
}
return next()
}
}
exports.removeTrailingSlash = function() {
return async (ctx, next) => {
if (ctx.path.length > 1 && ctx.path.endsWith('/')) {
ctx.redirect(ctx.path.slice(0, -1))
return
}
return next()
}
}
exports.handleBouncerValidationError = function() {
return async (ctx, next) => {
try {
await next()
} catch (err) {
if (err instanceof bouncer.ValidationError) {
ctx.flash = {
message: ['danger', err.message || 'Validation error'],
// CAVEAT: Max cookie size is 4096 bytes. If the user sent us a
// body that exceeds that (for example, a large message), then
// the cookie will not get set (silently).
// TODO: Consider using localStorage to persist request bodies
// so that it scales.
params: ctx.request.body,
bouncer: err.bouncer,
}
return ctx.redirect('back')
}
throw err
}
}
}

105
src/routes/index.js Normal file
View File

@ -0,0 +1,105 @@
// 3rd party
const assert = require('better-assert')
const router = require('koa-router')()
const debug = require('debug')('app:routes:index')
// 1st party
const config = require('../../config')
const licenseUtil = require('../util/licenseUtil')
//
// The index.js routes file is mostly a junk drawer for miscellaneous
// routes until it's accumulated enough routes to warrant a new
// routes/*.js module.
//
function checkSystemParams(systemParams) {
if (systemParams !== null && typeof systemParams === 'object') {
if (Object.keys(systemParams).length > 1) {
let valid = true
Object.entries(systemParams).forEach(([key, value]) => {
if (typeof key !== 'string' || typeof value !== 'string') {
valid = false
}
})
if (valid) {
return systemParams
}
}
}
throw new Error('Invalid parameters provided')
}
function checkLicenseNumber(licenseNumber) {
if (licenseNumber !== null && typeof licenseNumber === 'string') {
if (licenseNumber.length === 24) {
return licenseNumber
}
}
throw new Error('Invalid license number')
}
function checkActivationId(activationId) {
if (activationId !== null && typeof activationId === 'string') {
if (/^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$/.test(activationId)) {
return activationId
}
}
throw new Error('Invalid activation id')
}
function checkAppId(appId) {
if (appId !== null && typeof appId === 'string') {
if (appId.length > 0) {
return (appId)
}
}
throw new Error('Invalid application id')
}
function isInt(value) {
return !isNaN(value) &&
parseInt(Number(value)) == value &&
!isNaN(parseInt(value, 10))
}
function checkModuleVersions(moduleVersions) {
if (moduleVersions !== null && typeof moduleVersions === 'object') {
let valid = true;
Object.entries(moduleVersions).forEach(([key, value]) => {
if (typeof key !== 'string' || !isInt(value)) {
valid = false;
}
})
if (valid) {
return moduleVersions;
}
}
throw new Error('Invalid parameters provided')
}
router.post('/activate0', async ctx => {
console.log(ctx.request)
console.log(ctx.request.body)
ctx.body = await licenseUtil.preactivate(
checkAppId(ctx.request.body.appId),
checkSystemParams(ctx.request.body.systemParams)
)
})
router.post('/activate', async ctx => {
ctx.body = await licenseUtil.activate(
checkAppId(ctx.request.body.appId),
checkLicenseNumber(ctx.request.body.licenseNumber),
checkSystemParams(ctx.request.body.systemParams)
)
})
router.post('/check', async ctx => {
ctx.body = await licenseUtil.check(
checkSystemParams(ctx.request.body.systemParams),
checkActivationId(ctx.request.body.activationId),
checkModuleVersions(ctx.request.body.moduleVersions)
)
})
module.exports = router

41
src/util/index.js Normal file
View File

@ -0,0 +1,41 @@
const crypto = require('crypto')
const zlib = require('zlib')
const denodeify = require('es6-denodeify')(Promise)
exports.generateLicenseFile = async function (activationId, appId, systemParams, licensedModules) {
const activationData = {
activationId,
appId,
systemParams,
licensedModules
};
const randomBytes = denodeify(crypto.randomBytes)
const deflateRaw = denodeify(zlib.deflateRaw)
const algorithm = 'aes-256-gcm';
const password = Buffer.from('e73db572349005f1c41979baf8166a0900745119fa096b9c3efbcee11ddd8b88', 'hex');
const privateKey = '-----BEGIN EC PRIVATE KEY-----' + "\n" +
'MIGAAgEBBCQBPIZnOt/mEsgtH3S9XZMGRuHkB5hYbMJ/BxcGmAc/pZLdxDWgBwYF' + "\n" +
'K4EEABGhTANKAAQHyyrnJFywb+B0pcaVRHIOcEao3OtSMSJJZiluIMme1aE+20UA' + "\n" +
'0c0+2u+M6bMi072XrXLf8KudcAxihG/aqCqbVVZS6i10SSM=' + "\n" +
'-----END EC PRIVATE KEY-----';
const nonce = await randomBytes(16)
activationData.nonce = nonce.toString('base64');
let data = JSON.stringify(activationData);
const sign = crypto.createSign('SHA256');
sign.write(data);
sign.end();
const signature = sign.sign(privateKey, 'hex');
data = JSON.stringify({ data, signature });
compressed = await deflateRaw(data)
const iv = await randomBytes(16)
const cipher = crypto.createCipheriv(algorithm, password, iv);
let encrypted = cipher.update(compressed);
encrypted = Buffer.concat([encrypted, cipher.final()]);
const tag = cipher.getAuthTag();
const output = Buffer.concat([iv, tag, encrypted]);
console.log(output.toString('hex'));
return output
}

243
src/util/licenseUtil.js Normal file
View File

@ -0,0 +1,243 @@
const Guid = require('guid')
const config = require('../../config')
const { pool } = require('../db/pool')
const { generateLicenseFile } = require('.')
const { pickParams } = require("../../config/supportedApps")
let dbCache;
const getDb = async function () {
if (!dbCache) {
dbCache = await pool
dbCache.on("trace", console.log)
}
return dbCache
}
const findPreactivatedLicense = async function (appId, systemParams) {
console.log(appId);
console.log(systemParams);
const db = await getDb()
let counts = await Promise.all(Object.entries(systemParams).map(([param, val]) =>
db.get('SELECT paramId, paramValue, COUNT(DISTINCT(licenseNum)) as count' +
' FROM PreactivationParams' +
' WHERE appId = ? AND paramId = ? AND paramValue = ?' +
' GROUP BY paramId, paramValue' +
' ORDER BY paramId, paramValue',
appId, param, val)
))
// If no licenses can be preactivated using any of given parameters
counts = counts
.filter((c) => (typeof c !== 'undefined' && c.count > 0))
const totalCount = counts
.reduce((a, b) => a.count + b.count, 0)
if (totalCount < 1) {
return null;
}
// Pick the most distinguishing parametere
const mostSignificantParam = counts
.sort((a, b) => (a.count - b.count))[0];
// Sanity check: Make sure that there are not too many licenses for the parameter
if (mostSignificantParam > 100) return null;
const licensesToCheck = await db.all('SELECT licenseNum' +
' FROM PreactivationParams' +
' WHERE appId = ? AND paramId = ? AND paramValue = ?' +
' ORDER BY licenseNum',
appId, mostSignificantParam.paramId, mostSignificantParam.paramValue)
const availableLicenses = []
for (let license of licensesToCheck) {
const paramPairs = await db.all('SELECT paramId, paramValue FROM PreactivationParams WHERE licenseNum = ?', license.licenseNum)
const allMatch = true
for (let paramPair of paramPairs) {
if (systemParams[paramPair.paramId] !== paramPair.paramValue) {
allMatch = false
break
}
}
if (allMatch) {
availableLicenses.push(license.licenseNum)
}
}
return (availableLicenses.length > 0) ? availableLicenses[0] : null
}
exports.activate = async function (appId, licenseNum, systemParams) {
const db = await getDb()
const activatedParams = await db.all('SELECT A1.paramId, A1.paramValue' +
' FROM ActiveActivationView A0' +
' INNER JOIN ActivationParams A1 on A1.activationId = A0.activationId' +
' WHERE A0.appId = ? AND A0.licenseNum = ?' +
' ORDER BY A1.paramId, A1.paramValue',
appId, licenseNum)
if (activatedParams.length > 1) {
// Already activated! Check if system params still matches so we can reactivate
for (let activatedParam of activatedParams) {
if (systemParams[activatedParam.paramId] !== activatedParam.paramValue) {
return { success: false, reason: 'License already activated for different system' }
}
}
}
const activationId = Guid.create();
await db.run('UPDATE Activation SET deactivatedOn = date(?) WHERE licenseNum = ? and deactivatedOn IS NULL', 'now', licenseNum)
await db.run('INSERT INTO Activation (activationId, appId, licenseNum, activatedOn)' +
' VALUES(?, ?, ?, date(?))', activationId.value, appId, licenseNum, 'now')
const pickedParams = pickParams(appId, systemParams);
await Promise.all(Object.keys(systemParams).map((d) => {
flag = (d.paramId in pickedParams) ? 1 : 0
return db.run('INSERT INTO ActivationParams (activationId, paramId, paramValue, flag)' +
' VALUES (?,?,?,?)', activationId.value, d, systemParams[d], flag)
}))
const licensedModulesRows = await db.all('SELECT moduleId' +
' FROM LicensedModule' +
' WHERE appId = ? AND licenseNum = ?' +
' ORDER BY moduleId',
appId, licenseNum)
const licensedModules = licensedModulesRows.map((d) => d.moduleId)
return {
success: true,
licenseFile: (await generateLicenseFile(activationId, appId, pickedParams, licensedModules)).toString("base64")
}
}
exports.preactivate = async function(appId, systemParams) {
const preactivatedLicense = await findPreactivatedLicense(appId, systemParams)
if (preactivatedLicense === null) {
return {
success: false,
}
}
return await exports.activate(appId, preactivatedLicense, systemParams);
}
exports.check = async function (systemParams, activationId, moduleVersions) {
const db = await getDb()
const activatedParams = await db.all('SELECT A0.appId, A1.paramId, A1.paramValue' +
' FROM ActiveActivationView A0' +
' INNER JOIN ActivationParams A1 on A1.activationId = A0.activationId' +
' WHERE A0.activationId = ?' +
' ORDER BY A1.paramId, A1.paramValue',
activationId)
if (activatedParams.length < 1) {
return { success: false, reason: 'Not active' }
}
const checkLogId = Guid.create().value;
await db.run('INSERT INTO CheckLog (checkLogId, activationId, checkedOn)' +
' VALUES(?, ?, date(?))', checkLogId, activationId, 'now')
await Promise.all(Object.keys(systemParams).map((d) =>
db.run('INSERT INTO CheckLogParams (checkLogId, paramId, paramValue)' +
' VALUES (?,?,?)', checkLogId, d, systemParams[d])))
await Promise.all(Object.keys(moduleVersions).map((d) =>
db.run('INSERT INTO CheckLogVersion (checkLogId, moduleId, version)' +
' VALUES (?,?,?)', checkLogId, d, moduleVersions[d])))
for (let activatedParam of activatedParams) {
if (systemParams[activatedParam.paramId] !== activatedParam.paramValue) {
return { success: false, reason: 'Invalid activation' }
}
}
const lastModuleVersions = await db.all(
'SELECT MU2.moduleId, MU2.version, MU2.flag, MU2.checksum, MU2.updateUri, MU2.instPath ' +
' FROM (SELECT MU.moduleId, MAX(MU.version) as lastVersion' +
' FROM ModuleUpdate MU' +
' INNER JOIN LicensedModule LM ON LM.moduleId = MU.moduleId' +
' INNER JOIN ActiveActivationView A ON A.appId = LM.appId AND A.licenseNum = LM.licenseNum' +
' WHERE A.activationId = ?' +
' GROUP BY MU.moduleId) LV' +
' INNER JOIN ModuleUpdate MU2 ON MU2.moduleId = LV.moduleId AND MU2.version = LV.lastVersion' +
' ORDER BY LV.moduleId',
activationId);
let updateLicenseFile = (Object.keys(moduleVersions).length > lastModuleVersions.length);
const modulesToUpdate = [];
for (let m of lastModuleVersions) {
if (!(m.moduleId in moduleVersions)) {
if (Object.keys(moduleVersions).length) {
updateLicenseFile = true;
}
modulesToUpdate.push(m);
} else if(m.version > moduleVersions[m.moduleId]) {
modulesToUpdate.push({
...m,
currentVersion: moduleVersions[m.moduleId],
})
}
}
let updates = modulesToUpdate.filter((d) => !(d.flag & 1))
const lastIncrementalUpdates = modulesToUpdate.filter((d) => d.flag & 1)
if (lastIncrementalUpdates.length > 0) {
const conds = lastIncrementalUpdates.map((d) => {
let result = 'MU.moduleId = ?'
let params = [d.moduleId]
if (typeof (d.currentVersion) !== 'undefined') {
result += ' AND MU.version > ?'
params.push(d.currentVersion)
}
return {
cond: result,
params
}
})
const queryConds = conds.map((d) => d.cond).join(' OR ')
const queryParams = [].concat.apply([], conds.map((d) => d.params))
const incrementalUpdates = await db.all(
'SELECT MU.moduleId, MU.version, MU.flag, MU.checksum, MU.updateUri, MU.instPath ' +
' FROM ModuleUpdate MU' +
' INNER JOIN LastFullVersionView LFV ON LFV.moduleId = MU.moduleId AND LFV.lastFullVersion <= MU.version' +
' WHERE ' + queryConds +
' ORDER BY MU.moduleId, MU.version',
queryParams)
updates = updates.concat(incrementalUpdates)
}
if (updateLicenseFile) {
// Update license file as some modules received license since last activation
const licensedModulesRows = await db.all('SELECT LM.moduleId' +
' FROM LicensedModule LM' +
' INNER JOIN ActiveActivationView A ON A.appId = LM.appId AND A.licenseNum = LM.licenseNum' +
' WHERE A.activationId = ?' +
' ORDER BY LM.moduleId',
activationId)
const licensedModules = licensedModulesRows.map((d) => d.moduleId)
let appId = null;
let activatedParamObj = {}
for (elm of activatedParams) {
activatedParamObj[elm.paramId] = elm.paramValue;
appId = elm.appId;
}
return {
success: true,
licenseFile: (await generateLicenseFile(activationId, appId, activatedParamObj, licensedModules)).toString("base64"),
moduleUpdates: updates
}
}
return {
success: true,
moduleUpdates: updates
}
}