UNPKG

@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
"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;