This commit is contained in:
2025-10-21 08:10:15 +00:00
commit 8eaf13f445
1983 changed files with 292430 additions and 0 deletions

View File

@@ -0,0 +1,303 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>License 发行端(浏览器)</title>
<style>
body{font-family:system-ui,-apple-system,'Helvetica Neue',Arial;padding:20px;background:#0f172a;color:#e6eef8}
.card{background:#0b1220;padding:18px;border-radius:8px;box-shadow:0 6px 18px rgba(2,6,23,0.6);max-width:900px;margin:12px auto}
label{display:block;margin-top:10px;font-size:14px}
input, select, textarea{width:100%;padding:8px;margin-top:6px;border-radius:6px;border:1px solid #223;}
button{margin-top:12px;padding:10px 12px;border-radius:8px;border:none;cursor:pointer;background:#4f46e5;color:white}
.row{display:flex;gap:12px}
.col{flex:1}
pre{background:#071026;padding:12px;border-radius:6px;overflow:auto}
.small{font-size:13px;color:#9fb0d4}
</style>
</head>
<body>
<div class="card">
<h2>License 发行端(浏览器版)</h2>
<p class="small">在本页面生成公私钥对、序列号、激活码,并签名生成 .lic 文件。私钥仅保存在浏览器生成后允许你下载,<strong>请妥善保管私钥</strong></p>
<label>型号 (model)
<select id="inputModel" class="form-select">
<option value="">请选择型号</option>
<option value="EST-05E">EST-05E</option>
<option value="EST-10E">EST-10E</option>
<option value="EST-100E">EST-100E</option>
<option value="EST-05C">EST-05C</option>
<option value="EST-10C">EST-10C</option>
<option value="EST-100C">EST-100C</option>
<option value="EST-10A">EST-10A</option>
<option value="EST-100">EST-100</option>
</select>
</label>
<label>硬件码 (hardware_id)
<input id="inputHardwareId" placeholder="请输入硬件码" />
</label>
<label>用户 (user)
<input id="inputUser" placeholder="请输入最终用户" />
</label>
<div class="row">
<div class="col">
<label>激活天数(默认无限期)
<input id="inputDays" type="number" value="99999" readonly />
</label>
</div>
<div class="col">
<label>金牌服务有效期
<select id="inputGoldDays">
<option value="90">3个月</option>
<option value="180">6个月</option>
<option value="365">1年</option>
<option value="730">2年</option>
<option value="1095">3年</option>
<option value="1825">5年</option>
</select>
</label>
</div>
</div>
<label>序列号 生成规则
<div class="small">自动生成SN-随机8字符-时间戳</div>
<button id="btnGenSerial" type="button">生成序列号</button>
<input id="inputSerial" placeholder="点击生成或手动输入" />
</label>
<label>激活码
<button id="btnGenAct" type="button">生成激活码</button>
<input id="inputAct" placeholder="点击生成或手动输入" />
</label>
<hr style="border:none;border-top:1px solid #173047;margin:14px 0" />
<div class="row">
<div class="col">
<button id="btnCreateLicense">生成并签名 .lic 文件</button>
</div>
</div>
<div class="row" style="margin-top:10px">
<div class="col">
<label>导入私钥 (PKCS8 PEM 格式)
<input type="file" id="inputPrivKeyFile" accept=".pem,.key" />
</label>
</div>
<div class="col">
<label>或粘贴私钥内容
<textarea id="inputPrivKeyText" rows="3" placeholder="-----BEGIN PRIVATE KEY-----&#10;...&#10;-----END PRIVATE KEY-----"></textarea>
</label>
<button id="btnImportPrivKey" style="margin-top:6px">导入私钥</button>
</div>
</div>
<p class="small">生成后你可以下载license 文件。私钥默认从 priv.pem 自动导入,也可以手动导入已存在的私钥用于签名。</p>
<h3>调试输出</h3>
<pre id="out">准备就绪</pre>
<h3>私钥(仅供发行方保存)</h3>
<pre id="privpem">(请导入私钥)</pre>
</div>
<script>
// helpers
function rndStr(len=8){const chars='ABCDEFGHJKLMNPQRSTUVWXYZ23456789';let s='';for(let i=0;i<len;i++)s+=chars[Math.floor(Math.random()*chars.length)];return s}
function ab2b64(buf){return btoa(String.fromCharCode(...new Uint8Array(buf)));}
function b642ab(b64){const bin=atob(b64);const len=bin.length;const arr=new Uint8Array(len);for(let i=0;i<len;i++)arr[i]=bin.charCodeAt(i);return arr.buffer;}
// PEM parsing
function parsePem(pem) {
// 移除头尾和换行符获取base64内容
const pemContent = pem.replace(/-----BEGIN [^-]+-----/, '')
.replace(/-----END [^-]+-----/, '')
.replace(/[\r\n]/g, '');
return b642ab(pemContent);
}
// 从PEM导入私钥
async function importPrivateKeyFromPem(pemString) {
try {
const binaryDer = parsePem(pemString);
return await window.crypto.subtle.importKey(
'pkcs8',
binaryDer,
{
name: 'RSASSA-PKCS1-v1_5',
hash: {name: 'SHA-256'},
},
true,
['sign']
);
} catch (error) {
console.error('导入私钥失败:', error);
throw new Error('导入私钥失败: ' + error.message);
}
}
// generate RSA keypair
let issuerKeyPair = null;
// 导入私钥
document.getElementById('btnImportPrivKey').addEventListener('click', async () => {
try {
// 检查是否有文件上传
const fileInput = document.getElementById('inputPrivKeyFile');
const textInput = document.getElementById('inputPrivKeyText');
let pemContent = '';
if (fileInput.files.length > 0) {
// 从文件读取
const file = fileInput.files[0];
pemContent = await file.text();
} else if (textInput.value.trim()) {
// 从文本框读取
pemContent = textInput.value.trim();
} else {
throw new Error('请上传私钥文件或粘贴私钥内容');
}
setOut('正在导入私钥...');
// 导入私钥
const privateKey = await importPrivateKeyFromPem(pemContent);
// 设置私钥
if (!issuerKeyPair) {
issuerKeyPair = {};
}
issuerKeyPair.privateKey = privateKey;
// 更新UI
document.getElementById('privpem').textContent = pemContent;
setOut('私钥导入成功现在可以使用此私钥生成license文件。');
} catch (error) {
setOut('导入私钥失败: ' + error.message);
console.error(error);
}
});
// 从文件读取私钥
document.getElementById('inputPrivKeyFile').addEventListener('change', async (event) => {
try {
if (event.target.files.length > 0) {
const file = event.target.files[0];
const pemContent = await file.text();
document.getElementById('inputPrivKeyText').value = pemContent;
}
} catch (error) {
setOut('读取私钥文件失败: ' + error.message);
console.error(error);
}
});
function downloadText(text, filename){
const a=document.createElement('a');
const blob=new Blob([text],{type:'application/octet-stream'});
a.href=URL.createObjectURL(blob);
a.download=filename;
document.body.appendChild(a);
a.click();
a.remove();
}
// generate serial
document.getElementById('btnGenSerial').addEventListener('click', ()=>{
const sn = `SN-${rndStr(8)}-${Date.now().toString().slice(-6)}`;
document.getElementById('inputSerial').value = sn;
setOut('已生成序列号: '+sn);
});
// generate activation code
document.getElementById('btnGenAct').addEventListener('click', ()=>{
const act = `ACT-${rndStr(6)}-${rndStr(4)}`;
document.getElementById('inputAct').value = act;
setOut('已生成激活码: '+act);
});
function isoNow(){return new Date().toISOString();}
function isoAddDays(days){const d=new Date();d.setUTCDate(d.getUTCDate()+Number(days));return d.toISOString();}
// sign payload using private key
async function signPayload(payloadStr){
if(!issuerKeyPair) throw new Error('请先生成/导入私钥');
const enc = new TextEncoder().encode(payloadStr);
const signature = await window.crypto.subtle.sign({name:'RSASSA-PKCS1-v1_5'}, issuerKeyPair.privateKey, enc);
return ab2b64(signature);
}
// create license file and download
document.getElementById('btnCreateLicense').addEventListener('click', async ()=>{
try{
const model = document.getElementById('inputModel').value.trim();
const user = document.getElementById('inputUser').value.trim();
const hardware_id = document.getElementById('inputHardwareId').value.trim();
const serial = document.getElementById('inputSerial').value.trim() || `SN-${rndStr(8)}-${Date.now().toString().slice(-6)}`;
const activation_code = document.getElementById('inputAct').value.trim() || `ACT-${rndStr(6)}-${rndStr(4)}`;
const days = Number(document.getElementById('inputDays').value || 365);
const goldDays = Number(document.getElementById('inputGoldDays').value || 365);
if(!model||!user||!hardware_id){setOut('请填写型号、用户和硬件码');return}
setOut('生成 license 中,请稍候...');
const payloadObj = {
model,
user,
hardware_id,
serial,
activation_code,
activated_at: isoNow(),
expires_at: isoAddDays(days),
gold_service_expires_at: isoAddDays(goldDays),
issued_at: isoNow(),
issuer: '上海朗坤信息系统有限公司'
};
const payloadStr = JSON.stringify(payloadObj);
function base64EncodeUnicode(str) {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (_, p1) =>
String.fromCharCode('0x' + p1)
)
);
}
const payloadBase64 = base64EncodeUnicode(JSON.stringify(payloadObj));
const signatureB64 = await signPayload(payloadStr);
const lic = {
payload: payloadBase64,
signature: signatureB64
};
const licText = JSON.stringify(lic, null, 2);
// download license file
downloadText(licText, `${serial}.lic`);
// update UI
setOut('license 已生成并下载:'+serial+'.lic\n\npayload:\n'+payloadStr);
}catch(err){
setOut('生成失败: '+err.message);
console.error(err);
}
});
function setOut(text){document.getElementById('out').textContent = text}
// allow importing an existing private key (pkcs8 PEM)
// (optional) not implemented UI; could add in future
</script>
</body>
</html>