From 140ef8726b11f243b6a10d98e148fd43fece51ce Mon Sep 17 00:00:00 2001 From: Hermes Date: Fri, 24 Jul 2026 21:47:31 -0700 Subject: [PATCH] [grade=B] refactor: remove stale duplicate license key generator --- dashcaddy-api/src/managers/license-keygen.js | 314 ------------------- 1 file changed, 314 deletions(-) delete mode 100644 dashcaddy-api/src/managers/license-keygen.js diff --git a/dashcaddy-api/src/managers/license-keygen.js b/dashcaddy-api/src/managers/license-keygen.js deleted file mode 100644 index ef2796b..0000000 --- a/dashcaddy-api/src/managers/license-keygen.js +++ /dev/null @@ -1,314 +0,0 @@ -#!/usr/bin/env node -/** - * DashCaddy License Code Generator - * - * Admin-only CLI tool for generating license codes. - * NOT shipped with the product — runs only on the developer's machine. - * - * Usage: - * node license-keygen.js --duration 365 --count 10 - * node license-keygen.js --duration 30 --count 1 --output codes.txt - * node license-keygen.js --verify DC-XXXXX-XXXXX-XXXXX-XXXXX - * node license-keygen.js --init-secret - */ - -const crypto = require('crypto'); -const fs = require('fs'); -const path = require('path'); -const platformPaths = require('../../platform-paths'); - -// Master secret file — lives only on admin machine, NEVER shipped -const SECRET_FILE = process.env.LICENSE_SECRET_FILE || path.join(platformPaths.dataDir, '.license-secret'); - -// License code format: DC-AAAAA-BBBBB-CCCCC-DDDDD -// Encodes: version(4bit) + duration_days(12bit) + code_id(32bit) + created_ts(32bit) + hmac(48bit) -// Total: 128 bits = 16 bytes, base32-encoded into 4 groups of 5 chars - -const VALID_DURATIONS = [30, 90, 180, 365]; -const LIFETIME_DURATION = 0; // Admin-only, not publicly available -const VERSION = 1; - -// Base32 alphabet (Crockford variant — no I/L/O/U to avoid confusion) -const BASE32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; - -function base32Encode(buffer) { - let bits = ''; - for (const byte of buffer) { - bits += byte.toString(2).padStart(8, '0'); - } - // Pad to multiple of 5 - while (bits.length % 5 !== 0) bits += '0'; - let result = ''; - for (let i = 0; i < bits.length; i += 5) { - const index = parseInt(bits.substring(i, i + 5), 2); - result += BASE32[index]; - } - return result; -} - -function base32Decode(str) { - let bits = ''; - for (const char of str.toUpperCase()) { - const index = BASE32.indexOf(char); - if (index === -1) throw new Error(`Invalid base32 character: ${char}`); - bits += index.toString(2).padStart(5, '0'); - } - const bytes = []; - for (let i = 0; i + 8 <= bits.length; i += 8) { - bytes.push(parseInt(bits.substring(i, i + 8), 2)); - } - return Buffer.from(bytes); -} - -function getSecret() { - if (!fs.existsSync(SECRET_FILE)) { - console.error('No master secret found. Run with --init-secret first.'); - process.exit(1); - } - return fs.readFileSync(SECRET_FILE, 'utf8').trim(); -} - -function initSecret() { - if (fs.existsSync(SECRET_FILE)) { - console.error('Master secret already exists at', SECRET_FILE); - console.error('Delete it first if you want to regenerate (WARNING: invalidates all existing codes).'); - process.exit(1); - } - const secret = crypto.randomBytes(32).toString('hex'); - fs.writeFileSync(SECRET_FILE, secret, { mode: 0o600 }); - console.log('Master secret generated and saved to', SECRET_FILE); - console.log('KEEP THIS FILE SAFE. It is needed to generate and validate all license codes.'); - console.log('DO NOT ship this file with the product.'); -} - -function generateCode(secret, durationDays, codeId) { - // Pack payload: version(4b) + duration_days(12b) + code_id(32b) + created_ts(32b) = 80 bits = 10 bytes - const payload = Buffer.alloc(10); - - // Byte 0-1: version (4 bits) + duration (12 bits) = 16 bits - const versionAndDuration = ((VERSION & 0x0F) << 12) | (durationDays & 0x0FFF); - payload.writeUInt16BE(versionAndDuration, 0); - - // Byte 2-5: code_id (32 bits) - payload.writeUInt32BE(codeId, 2); - - // Byte 6-9: created timestamp (32 bits, seconds since epoch) - const createdTs = Math.floor(Date.now() / 1000); - payload.writeUInt32BE(createdTs, 6); - - // HMAC the payload to get signature - const hmac = crypto.createHmac('sha256', secret).update(payload).digest(); - // Take first 5 bytes of HMAC (40 bits) — fits exactly in 25 base32 chars with 10-byte payload - const signature = hmac.subarray(0, 5); - - // Combine: payload (10 bytes) + signature (5 bytes) = 15 bytes = 120 bits - // 25 base32 chars = 125 bits, comfortably fits 120 bits - const combined = Buffer.concat([payload, signature]); - - let encoded = base32Encode(combined); - while (encoded.length < 25) encoded += '0'; - encoded = encoded.substring(0, 25); - const groups = []; - for (let i = 0; i < 25; i += 5) { - groups.push(encoded.substring(i, i + 5)); - } - - return `DC-${groups.join('-')}`; -} - -function parseCode(code) { - // Strip prefix and dashes - const cleaned = code.replace(/^DC-/, '').replace(/-/g, ''); - if (cleaned.length !== 25) { - throw new Error(`Invalid code length: expected 25 base32 chars, got ${cleaned.length}`); - } - - // Decode base32 — 25 chars = 125 bits = 15 full bytes - const decoded = base32Decode(cleaned); - if (decoded.length < 15) { - const padded = Buffer.alloc(15); - decoded.copy(padded); - return parsePayload(padded); - } - return parsePayload(decoded.subarray(0, 15)); -} - -function parsePayload(buffer) { - const payload = buffer.subarray(0, 10); - const signature = buffer.subarray(10, 15); - - const versionAndDuration = payload.readUInt16BE(0); - const version = (versionAndDuration >> 12) & 0x0F; - const durationDays = versionAndDuration & 0x0FFF; - const codeId = payload.readUInt32BE(2); - const createdTs = payload.readUInt32BE(6); - - return { version, durationDays, codeId, createdTs, payload, signature }; -} - -function verifyCode(secret, code) { - try { - const { version, durationDays, codeId, createdTs, payload, signature } = parseCode(code); - - // Verify HMAC (5-byte signature) - const expectedHmac = crypto.createHmac('sha256', secret).update(payload).digest(); - const expectedSig = expectedHmac.subarray(0, 5); - - if (!crypto.timingSafeEqual(signature, expectedSig)) { - return { valid: false, reason: 'Invalid signature — code is forged or corrupted' }; - } - - if (version !== VERSION) { - return { valid: false, reason: `Unsupported version: ${version}` }; - } - - // Accept lifetime (0) and standard durations - if (durationDays !== LIFETIME_DURATION && !VALID_DURATIONS.includes(durationDays)) { - return { valid: false, reason: `Invalid duration: ${durationDays} days` }; - } - - const createdDate = new Date(createdTs * 1000); - const isLifetime = durationDays === LIFETIME_DURATION; - const expiresDate = isLifetime ? null : new Date(createdTs * 1000 + durationDays * 86400000); - - return { - valid: true, - version, - durationDays, - codeId, - createdAt: createdDate.toISOString(), - expiresAt: isLifetime ? null : expiresDate.toISOString(), - expired: isLifetime ? false : Date.now() > expiresDate.getTime() - }; - } catch (error) { - return { valid: false, reason: error.message }; - } -} - -// CLI -function main() { - const args = process.argv.slice(2); - - if (args.includes('--help') || args.length === 0) { - console.log(` -DashCaddy License Code Generator - -Usage: - node license-keygen.js --init-secret Initialize master secret (first time only) - node license-keygen.js --duration [options] Generate license codes - node license-keygen.js --verify Verify a license code - node license-keygen.js --decode Decode and display code details - -Options: - --duration Code validity: 30, 90, 180, or 365 days (required for generation) - --count Number of codes to generate (default: 1) - --start-id Starting code ID (default: auto from counter file) - --output Write codes to file instead of stdout - --json Output as JSON - -Valid durations: ${VALID_DURATIONS.join(', ')} days - `); - process.exit(0); - } - - if (args.includes('--init-secret')) { - initSecret(); - return; - } - - if (args.includes('--verify') || args.includes('--decode')) { - const codeIndex = args.indexOf('--verify') !== -1 ? args.indexOf('--verify') : args.indexOf('--decode'); - const code = args[codeIndex + 1]; - if (!code) { - console.error('Please provide a code to verify.'); - process.exit(1); - } - const secret = getSecret(); - const result = verifyCode(secret, code); - if (args.includes('--json')) { - console.log(JSON.stringify(result, null, 2)); - } else if (result.valid) { - const isLifetime = result.durationDays === 0; - console.log('Code is VALID'); - console.log(` Version: ${result.version}`); - console.log(` Duration: ${isLifetime ? 'LIFETIME' : result.durationDays + ' days'}`); - console.log(` Code ID: ${result.codeId}`); - console.log(` Created: ${result.createdAt}`); - console.log(` Expires: ${isLifetime ? 'NEVER' : result.expiresAt}`); - console.log(` Status: ${isLifetime ? 'LIFETIME' : (result.expired ? 'EXPIRED' : 'ACTIVE')}`); - } else { - console.log('Code is INVALID'); - console.log(` Reason: ${result.reason}`); - } - return; - } - - // Generate codes - const isLifetime = args.includes('--lifetime'); - const durationIndex = args.indexOf('--duration'); - if (!isLifetime && durationIndex === -1) { - console.error('--duration is required. Use --help for usage.'); - process.exit(1); - } - const duration = isLifetime ? LIFETIME_DURATION : parseInt(args[durationIndex + 1]); - if (!isLifetime && !VALID_DURATIONS.includes(duration)) { - console.error(`Invalid duration: ${duration}. Valid: ${VALID_DURATIONS.join(', ')}`); - process.exit(1); - } - - const countIndex = args.indexOf('--count'); - const count = countIndex !== -1 ? parseInt(args[countIndex + 1]) : 1; - - // Load or create counter file for auto-incrementing code IDs - const counterFile = process.env.LICENSE_COUNTER_FILE || path.join(platformPaths.dataDir, '.license-counter'); - let startId; - const startIdIndex = args.indexOf('--start-id'); - if (startIdIndex !== -1) { - startId = parseInt(args[startIdIndex + 1]); - } else if (fs.existsSync(counterFile)) { - startId = parseInt(fs.readFileSync(counterFile, 'utf8').trim()) + 1; - } else { - startId = 1; - } - - const secret = getSecret(); - const codes = []; - - for (let i = 0; i < count; i++) { - const codeId = startId + i; - const code = generateCode(secret, duration, codeId); - codes.push({ code, codeId, durationDays: duration }); - } - - // Save counter - fs.writeFileSync(counterFile, String(startId + count - 1)); - - // Output - const outputIndex = args.indexOf('--output'); - if (args.includes('--json')) { - const output = JSON.stringify(codes, null, 2); - if (outputIndex !== -1) { - fs.writeFileSync(args[outputIndex + 1], output); - console.log(`${count} code(s) written to ${args[outputIndex + 1]}`); - } else { - console.log(output); - } - } else { - const lines = codes.map(c => `${c.code} (${c.durationDays === 0 ? 'LIFETIME' : c.durationDays + ' days'}, ID: ${c.codeId})`); - if (outputIndex !== -1) { - fs.writeFileSync(args[outputIndex + 1], codes.map(c => c.code).join('\n') + '\n'); - console.log(`${count} code(s) written to ${args[outputIndex + 1]}`); - } else { - lines.forEach(l => console.log(l)); - } - } - - console.log(`\nGenerated ${count} code(s) for ${duration === 0 ? 'LIFETIME' : duration + ' days'}. Next ID: ${startId + count}`); -} - -// Also export for use by license-manager.js -module.exports = { verifyCode, parseCode, VALID_DURATIONS, VERSION }; - -if (require.main === module) { - main(); -}