Spice-html5 改造
https://github.com/spice-gm/spice-html5-gm
https://github.com/spice-gm/sm-crypto-plugin-spice-html5
改造方案
对应C/S口令验证改造,Spice-html5也需兼容SM2/RSA两种验证方案。通过Webassembly或Node.js Addons复用SM2-EVP,产出wasm或node插件。对于Webassembly,需要安装配置Emscripten等编译环境,对于Node.js Addons需在WSL中利用NVM安装Node及npm环境。
使用Webassembly,在编译复用前,需对依赖OpenSSL/BabaSSL使用emconfigure及emmake编译动态链接库。再通过emcc对复用逻辑指定上述链接库及头文件路径进行编译,对需导出函数使用EMSCRIPTEN_KEEPALIVE声明,产出wasm及js胶水文件。
使用Node.js Addons,利用node-gyp进行编译,将函数通过NODE_SET_METHOD进行导出。并使用express搭建简单后台服务,接入编译后node插件提供改造所需还原公钥、数据加密等能力。特别的,在使用Node.js Addons时需将参数类型进行转换,使两者能进行数据传输。
口令身份验证的控制参数由Option标签实现,默认指定SM2算法。Spice-html5口令验证逻辑处理主要存在于SpiceConn类的process_inbound函数及SpiceLinkReply类的from_buffer函数,在两环节中通过ticket-handler值判断选用SM2/RSA。
核心代码
spice.html
<div id="Sidenav" class="SidenavClosed" style="width: 0;">
<p class="closebtn" onclick="close_nav()">✖</p>
<label for="host">Host:</label> <input type='text' id='host' value='localhost'> <!-- localhost --><br>
<label for="port">Port:</label> <input type='text' id='port' value='5959'><br>
<label for="password">Password:</label> <input type='password' id='password' value=''><br>
<label for="if_websocket_secure">Protocol:</label> <select id="if_websocket_secure"> </select> <br>
<label for="if_ntls">Safety Option:</label>
<select id="if_ntls">
<option value="ntls">NTLS</option>
<option value="tls">TLS</option>
</select> <br>
<label for="if_sm2">Ticket Handler:</label>
<select id="if_sm2">
<option value="sm2">SM2</option>
<option value="rsa">RSA</option>
</select> <br>
<button id="connectButton">Start Connection</button><br>
<button id="sendCtrlAltDel">Send Ctrl-Alt-Delete</button>
<button id="debugLogs">Toggle Debug Logs</button>
<div id="message-div" class="spice-message" style="display: none;"></div>
<div id="debug-div">
<!-- If DUMPXXX is turned on, dumped images will go here -->
</div>
</div>
if (window.location.protocol == 'https:') {
document.getElementById("if_websocket_secure").append(new Option("wss", "wss://"));
} else {
document.getElementById("if_websocket_secure").append(new Option("ws", "ws://"));
}
function connect() {
var host, port, password, scheme = "ws://", uri, tls_option, ticket_handler_option;
host = document.getElementById("host").value;
port = document.getElementById("port").value;
password = document.getElementById("password").value;
var select_websocket_scheme = document.getElementById("if_websocket_secure");
scheme = select_websocket_scheme.options[select_websocket_scheme.selectedIndex].value;
var select_websocket_tls_option = document.getElementById("if_ntls");
tls_option = select_websocket_tls_option.options[select_websocket_tls_option.selectedIndex].value;
var select_ticket_handler_option = document.getElementById("if_sm2");
ticket_handler_option = select_ticket_handler_option.options[select_ticket_handler_option.selectedIndex].value;
if ((!host) || (!port)) {
console.log("must set host and port");
return;
}
if (sc) {
sc.stop();
}
uri = scheme + host + ":" + port + "/" + tls_option;
document.getElementById('connectButton').innerHTML = "Stop Connection";
document.getElementById('connectButton').onclick = disconnect;
try {
sc = new SpiceHtml5.SpiceMainConn({
uri: uri, screen_id: "spice-screen", dump_id: "debug-div", ticket_handler: ticket_handler_option,
message_id: "message-div", password: password, onerror: spice_error, onagent: agent_connected
});
}
catch (e) {
alert(e.toString());
disconnect();
}
}
spiceconn.js SpiceConn.process_inbound
process_inbound: async function (mb, saved_header) {
... ...
case "link":
this.reply_link = new SpiceLinkReply(mb, 0, { ticket_handler: this.ticket_handler });
await this.reply_link.from_buffer(mb);
if (this.reply_link.error) {
this.state = "error";
var e = new Error('Error: reply link error ' + this.reply_link.error);
this.report_error(e);
}
else {
if (this.ticket_handler == "rsa") {
this.send_ticket(rsa_encrypt(this.reply_link.pub_key, this.password + String.fromCharCode(0)));
} else {
let that = this;
let encryptedPassword = "";
var url = 'https://xxx.xxx.xxx.xxx:18887/sm2-plugin/encrypt-password-pubKey'
await $.ajax({
url: url,
data: {
pubKey: that.reply_link.pub_key,
password: that.password + String.fromCharCode(0),
},
dataType: 'json',
type: 'POST',
success: function (data) {
console.log(data);
var dataArr = data.encryptedPassword.split(',');
for (let i = 0; i < dataArr.length; i++) dataArr[i] = parseInt(dataArr[i]);
console.log(dataArr);
encryptedPassword = dataArr;
}
})
this.send_ticket(encryptedPassword);
}
this.state = "ticket";
this.wire_reader.request(SpiceLinkAuthReply.prototype.buffer_size());
}
break;
... ...
}
spicemsg.js SpiceLinkReply.from_buffer
from_buffer: async function (a, at) {
at = at || 0;
var i;
var orig_at = at;
var dv = new SpiceDataView(a);
this.error = dv.getUint32(at, true); at += 4;
if (this.options.ticket_handler == "rsa") {
this.pub_key = create_rsa_from_mb(a, at);
} else {
var keyData = dv.u8.slice(4, 4 + 162);
let that = this;
var url = 'https://xxx.xxx.xxx.xxx:18887/sm2-plugin/repair-pubKey'
await $.ajax({
url: url,
data: {
keyData: keyData.toString(),
},
dataType: 'json',
type: 'POST',
success: function (data) {
console.log(data);
that.pub_key = data.pubKey;
}
})
at = 4;
}
at += Constants.SPICE_TICKET_PUBKEY_BYTES;
var num_common_caps = dv.getUint32(at, true); at += 4;
var num_channel_caps = dv.getUint32(at, true); at += 4;
var caps_offset = dv.getUint32(at, true); at += 4;
at = orig_at + caps_offset;
this.common_caps = [];
for (i = 0; i < num_common_caps; i++) {
this.common_caps.unshift(dv.getUint32(at, true)); at += 4;
}
this.channel_caps = [];
for (i = 0; i < num_channel_caps; i++) {
this.channel_caps.unshift(dv.getUint32(at, true)); at += 4;
}
}