#!/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'); // Master secret file — lives only on admin machine, NEVER shipped const SECRET_FILE = path.join(__dirname, '.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 = path.join(__dirname, '.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(); }