@trap_stevo/lockline
Version:
The ultimate solution for secure, scalable, and tamper-resistant license key validation. Combine flexible validation strategies, encrypted / pluggable storage, smart fallback resilience, and real-time revocation control to lock down your software — and un
266 lines (265 loc) • 9.27 kB
JavaScript
"use strict";
const crypto = require("crypto");
const path = require("path");
const fs = require("fs");
function getStableFingerprint(storagePath, options = {}) {
const fingerPrintFile = options?.filename || ".fingerprint";
const fingerPrintPath = path.resolve(storagePath, fingerPrintFile);
if (fs.existsSync(fingerPrintPath)) {
return fs.readFileSync(fingerPrintPath, "utf-8");
}
const salt = crypto.randomBytes(options?.printDetail ?? 32).toString("hex");
fs.writeFileSync(fingerPrintPath, salt, {
encoding: "utf-8",
flag: "wx"
});
return salt;
}
;
function deriveSecret(offset = "", lockprint, storagePath, options = {}) {
const fingerprint = lockprint || getStableFingerprint(storagePath, options);
return crypto.createHash("sha256").update("Lockline::" + fingerprint + "::" + offset).digest();
}
;
function secureKeyName(k, keySecret) {
return crypto.createHash("sha256").update("key::" + k + "::" + keySecret).digest("hex");
}
;
function encrypt(value, key) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
return Buffer.concat([iv, tag, encrypted]).toString("base64");
}
;
function decrypt(encrypted, key) {
const raw = Buffer.from(encrypted, "base64");
const iv = raw.slice(0, 12);
const tag = raw.slice(12, 28);
const data = raw.slice(28);
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
decipher.setAuthTag(tag);
return decipher.update(data, null, "utf8") + decipher.final("utf8");
}
;
function Lockbox(storagePath, options = {}) {
const {
namespace = "default",
offset = "",
lockprintOptions = {},
lockprint = null,
encrypt: enableEncryption = true
} = options;
const shardCount = 16;
const namespacePath = path.resolve(storagePath, namespace);
fs.mkdirSync(storagePath, {
recursive: true
});
fs.mkdirSync(namespacePath, {
recursive: true
});
const key = enableEncryption ? deriveSecret(offset, lockprint, storagePath, lockprintOptions) : null;
const secretForKeyHashing = enableEncryption ? key.toString("hex") : "default";
const inMemoryIndex = {};
const writeQueues = {};
function getShardPath(shardID) {
return path.join(namespacePath, shardID.toString().padStart(4, "0") + ".ldb");
}
;
function getShardID(keyHash) {
return crypto.createHash("sha256").update(keyHash).digest().readUInt32BE(0) % shardCount;
}
;
function initializeShard(shardID) {
const filePath = getShardPath(shardID);
if (!fs.existsSync(filePath)) {
return;
}
const index = new Map();
const fd = fs.openSync(filePath, "r");
let offset = 0;
try {
const stat = fs.fstatSync(fd);
const fileSize = stat.size;
while (offset < fileSize) {
const header = Buffer.alloc(8);
fs.readSync(fd, header, 0, 8, offset);
const keyLen = header.readUInt32BE(0);
const valLen = header.readUInt32BE(4);
const keyBuf = Buffer.alloc(keyLen);
fs.readSync(fd, keyBuf, 0, keyLen, offset + 8);
const keyHash = keyBuf.toString("utf8");
index.set(keyHash, {
offset: offset + 8 + keyLen,
length: valLen
});
offset += 8 + keyLen + valLen;
}
} catch {} finally {
fs.closeSync(fd);
}
inMemoryIndex[shardID] = index;
}
;
function queueShardWrite(shardID, task) {
if (!writeQueues[shardID]) {
writeQueues[shardID] = Promise.resolve();
}
writeQueues[shardID] = writeQueues[shardID].then(() => task()).catch(() => {}).then(() => {
if (writeQueues[shardID] === task) {
delete writeQueues[shardID];
}
});
return writeQueues[shardID];
}
return {
get: async (k, {
locked = false
} = {}) => {
try {
const safeKey = locked ? k : secureKeyName(k, secretForKeyHashing);
const shardID = getShardID(safeKey);
if (!(shardID in inMemoryIndex)) {
initializeShard(shardID);
}
const index = inMemoryIndex[shardID];
if (!index || !index.has(safeKey)) {
return null;
}
const meta = index.get(safeKey);
const fd = fs.openSync(getShardPath(shardID), "r");
const buffer = Buffer.alloc(meta.length);
fs.readSync(fd, buffer, 0, meta.length, meta.offset);
fs.closeSync(fd);
const raw = buffer.toString("utf8");
const json = enableEncryption ? decrypt(raw, key) : raw;
return JSON.parse(json);
} catch {
return null;
}
},
set: async (k, value) => {
const safeKey = secureKeyName(k, secretForKeyHashing);
const shardID = getShardID(safeKey);
return queueShardWrite(shardID, async () => {
if (!(shardID in inMemoryIndex)) {
inMemoryIndex[shardID] = new Map();
}
const index = inMemoryIndex[shardID];
const raw = JSON.stringify(value);
const encrypted = enableEncryption ? encrypt(raw, key) : raw;
const keyBuf = Buffer.from(safeKey, "utf8");
const valBuf = Buffer.from(encrypted, "utf8");
const shardPath = getShardPath(shardID);
if (index.has(safeKey)) {
const entries = Array.from(index.entries());
const tempPath = shardPath + ".rewrite";
const fd = fs.openSync(tempPath, "w");
let offset = 0;
try {
const newIndex = new Map();
for (const [keyHash, meta] of entries) {
const target = keyHash === safeKey;
const kBuf = Buffer.from(keyHash, "utf8");
const vBuf = target ? valBuf : (() => {
const fdRead = fs.openSync(shardPath, "r");
const buf = Buffer.alloc(meta.length);
fs.readSync(fdRead, buf, 0, meta.length, meta.offset);
fs.closeSync(fdRead);
return buf;
})();
const header = Buffer.alloc(8);
header.writeUInt32BE(kBuf.length, 0);
header.writeUInt32BE(vBuf.length, 4);
const record = Buffer.concat([header, kBuf, vBuf]);
fs.writeSync(fd, record, 0, record.length);
newIndex.set(keyHash, {
offset: offset + 8 + kBuf.length,
length: vBuf.length
});
offset += record.length;
}
inMemoryIndex[shardID] = newIndex;
} finally {
fs.closeSync(fd);
fs.renameSync(tempPath, shardPath);
}
} else {
const header = Buffer.alloc(8);
header.writeUInt32BE(keyBuf.length, 0);
header.writeUInt32BE(valBuf.length, 4);
const record = Buffer.concat([header, keyBuf, valBuf]);
const fd = fs.openSync(shardPath, "a");
fs.writeSync(fd, record, 0, record.length);
fs.closeSync(fd);
index.set(safeKey, {
offset: fs.statSync(shardPath).size - valBuf.length,
length: valBuf.length
});
}
});
},
delete: async k => {
const safeKey = secureKeyName(k, secretForKeyHashing);
const shardID = getShardID(safeKey);
return queueShardWrite(shardID, async () => {
try {
if (!(shardID in inMemoryIndex)) {
initializeShard(shardID);
}
const index = inMemoryIndex[shardID];
if (!index || !index.has(safeKey)) {
return;
}
index.delete(safeKey);
const entries = Array.from(index.entries());
const shardPath = getShardPath(shardID);
if (entries.length === 0) {
if (fs.existsSync(shardPath)) {
fs.unlinkSync(shardPath);
}
delete inMemoryIndex[shardID];
return;
}
const tempPath = shardPath + ".delete";
const fd = fs.openSync(tempPath, "w");
try {
for (const [keyHash, meta] of entries) {
const fdRead = fs.openSync(shardPath, "r");
const buffer = Buffer.alloc(meta.length);
fs.readSync(fdRead, buffer, 0, meta.length, meta.offset);
fs.closeSync(fdRead);
const keyBuf = Buffer.from(keyHash, "utf8");
const header = Buffer.alloc(8);
header.writeUInt32BE(keyBuf.length, 0);
header.writeUInt32BE(meta.length, 4);
const record = Buffer.concat([header, keyBuf, buffer]);
fs.writeSync(fd, record, 0, record.length);
}
} finally {
fs.closeSync(fd);
fs.renameSync(tempPath, shardPath);
index.clear();
initializeShard(shardID);
}
} catch {}
});
},
keys: async () => {
const result = [];
for (let i = 0; i < shardCount; i++) {
if (!(i in inMemoryIndex)) {
initializeShard(i);
}
const index = inMemoryIndex[i];
if (index) {
result.push(...index.keys());
}
}
return result;
}
};
}
;
module.exports = Lockbox;