304 lines
11 KiB
HTML
304 lines
11 KiB
HTML
<!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-100A">EST-100A</option>
|
||
<option value="EST-100D">EST-100D</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----- ... -----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> |