Files
est-api/farmeworkapi/license_issuer.html
2025-12-15 07:55:31 +00:00

304 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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-----&#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>