Files

625 lines
18 KiB
JavaScript

// NodeCrypt core cryptographic client for secure chat
// NodeCrypt 安全聊天的核心加密客户端
import {
sha256
} from 'js-sha256';
import {
ec as elliptic
} from 'elliptic';
import {
ModeOfOperation
} from 'aes-js';
import chacha from 'js-chacha20';
import {
Buffer
} from 'buffer';
window.Buffer = Buffer;
// Main NodeCrypt class for secure communication
// 用于安全通信的 NodeCrypt 主类
class NodeCrypt {
// Initialize NodeCrypt instance
// 初始化 NodeCrypt 实例
constructor(config = {}, callbacks = {}) {
this.config = {
rsaPublic: config.rsaPublic || '',
wsAddress: config.wsAddress || '',
reconnectDelay: config.reconnectDelay || 3000,
pingInterval: config.pingInterval || 20000,
debug: config.debug || false,
};
this.callbacks = {
onServerClosed: callbacks.onServerClosed || null,
onServerSecured: callbacks.onServerSecured || null,
onClientSecured: callbacks.onClientSecured || null,
onClientList: callbacks.onClientList || null,
onClientMessage: callbacks.onClientMessage || null,
};
this.SERVER_KEY_STORAGE = 'nodecrypt_server_key';
try {
this.clientEc = new elliptic('curve25519')
} catch (error) {
this.logEvent('constructor', error, 'error')
}
this.serverKeys = null;
this.serverShared = null;
this.credentials = null;
this.connection = null;
this.reconnect = null;
this.ping = null;
this.channel = {};
this.setCredentials = this.setCredentials.bind(this);
this.connect = this.connect.bind(this);
this.destruct = this.destruct.bind(this);
this.onOpen = this.onOpen.bind(this);
this.onMessage = this.onMessage.bind(this);
this.onError = this.onError.bind(this);
this.onClose = this.onClose.bind(this);
this.logEvent = this.logEvent.bind(this);
this.isOpen = this.isOpen.bind(this);
this.isClosed = this.isClosed.bind(this);
this.startReconnect = this.startReconnect.bind(this);
this.stopReconnect = this.stopReconnect.bind(this);
this.startPing = this.startPing.bind(this);
this.stopPing = this.stopPing.bind(this);
this.disconnect = this.disconnect.bind(this);
this.sendMessage = this.sendMessage.bind(this);
this.sendChannelMessage = this.sendChannelMessage.bind(this);
this.encryptServerMessage = this.encryptServerMessage.bind(this);
this.decryptServerMessage = this.decryptServerMessage.bind(this);
this.encryptClientMessage = this.encryptClientMessage.bind(this);
this.decryptClientMessage = this.decryptClientMessage.bind(this)
}
// Set user credentials (username, channel, password)
// 设置用户凭证(用户名、频道、密码)
setCredentials(username, channel, password) {
this.logEvent('setCredentials');
try {
this.credentials = {
username: username,
channel: sha256(channel),
password: sha256(password)
}
} catch (error) {
this.logEvent('setCredentials', error, 'error');
return (false)
}
return (true)
}
// Connect to the server
// 连接到服务器
connect() {
if (!this.credentials) {
return (false)
}
this.logEvent('connect', this.config.wsAddress);
this.stopReconnect();
this.stopPing();
this.serverKeys = null;
this.serverShared = null;
this.channel = {};
try {
this.connection = new WebSocket(this.config.wsAddress);
this.connection.onopen = this.onOpen;
this.connection.onmessage = this.onMessage;
this.connection.onerror = this.onError;
this.connection.onclose = this.onClose
} catch (error) {
this.logEvent('connect', error, 'error');
return (false)
}
return (true)
}
// Clean up and disconnect
// 清理并断开连接
destruct() {
this.logEvent('destruct');
this.stopReconnect();
this.stopPing();
this.reconnect = null;
this.ping = null;
this.config = {
rsaPublic: '',
wsAddress: '',
reconnectDelay: 3000,
pingInterval: 15000,
debug: false,
};
this.callbacks.onServerClosed = null;
this.callbacks.onServerSecured = null;
this.callbacks.onClientSecured = null;
this.callbacks.onClientList = null;
this.callbacks.onClientMessage = null;
this.clientEc = null;
this.serverKeys = null;
this.serverShared = null;
this.credentials = null;
this.connection.onopen = null;
this.connection.onmessage = null;
this.connection.onerror = null;
this.connection.onclose = null;
try {
this.connection.removeAllListeners()
} catch (error) {
this.logEvent('destruct', error, 'error')
}
try {
this.connection.close()
} catch (error) {
this.logEvent('destruct', error, 'error')
}
this.connection = null;
this.channel = {};
return (true)
}
// WebSocket open event handler
// WebSocket 连接打开事件处理
async onOpen() {
this.logEvent('onOpen');
this.startPing();
try {
this.serverKeys = await crypto.subtle.generateKey({
name: 'ECDH',
namedCurve: 'P-384'
}, false, ['deriveKey', 'deriveBits']);
this.serverShared = null;
this.sendMessage(Buffer.from(await crypto.subtle.exportKey('raw', this.serverKeys.publicKey)).toString('hex'))
} catch (error) {
this.logEvent('onOpen', error, 'error')
}
}
// WebSocket message event handler
// WebSocket 消息事件处理
async onMessage(event) {
if (!event || !this.isString(event.data)) {
return
}
if (event.data === 'pong') {
return
}
this.logEvent('onMessage', event.data);
try {
const data = JSON.parse(event.data);
if (data.type === 'server-key') {
const result = await this.handleServerKey(data.key);
if (!result) {
return
}
}
} catch (e) {}
if (!this.serverShared) {
const parts = event.data.split('|');
if (!parts[0] || !parts[1]) {
return
}
try {
if (await crypto.subtle.verify({
name: 'RSASSA-PKCS1-v1_5'
}, await crypto.subtle.importKey('spki', Buffer.from(this.config.rsaPublic, 'base64'), {
name: 'RSASSA-PKCS1-v1_5',
hash: {
name: 'SHA-256'
}
}, false, ['verify']), Buffer.from(parts[1], 'base64'), Buffer.from(parts[0], 'hex')) === true) {
this.serverShared = Buffer.from(await crypto.subtle.deriveBits({
name: 'ECDH',
namedCurve: 'P-384',
public: await crypto.subtle.importKey('raw', Buffer.from(parts[0], 'hex'), {
name: 'ECDH',
namedCurve: 'P-384'
}, true, [])
}, this.serverKeys.privateKey, 384)).slice(8, 40);
this.sendMessage(this.encryptServerMessage({
a: 'j',
p: this.credentials.channel
}, this.serverShared));
if (this.callbacks.onServerSecured) {
try {
this.callbacks.onServerSecured()
} catch (error) {
this.logEvent('onMessage-server-secured-callback', error, 'error')
}
}
}
} catch (error) {
this.logEvent('onMessage', error, 'error')
}
return
}
const serverDecrypted = this.decryptServerMessage(event.data, this.serverShared);
this.logEvent('onMessage-server-decrypted', serverDecrypted);
if (!this.isObject(serverDecrypted) || !this.isString(serverDecrypted.a)) {
return
}
if (serverDecrypted.a === 'l' && this.isArray(serverDecrypted.p)) {
try {
for (const clientId in this.channel) {
if (serverDecrypted.p.indexOf(clientId) < 0) {
delete(this.channel[clientId])
}
}
let payloads = {};
for (const clientId of serverDecrypted.p) {
if (!this.channel[clientId]) {
this.channel[clientId] = {
username: null,
keys: this.clientEc.genKeyPair(),
shared: null,
};
payloads[clientId] = this.channel[clientId].keys.getPublic('hex')
}
}
if (Object.keys(payloads).length > 0) {
this.sendMessage(this.encryptServerMessage({
a: 'w',
p: payloads,
}, this.serverShared))
}
} catch (error) {
this.logEvent('onMessage-list', error, 'error')
}
if (this.callbacks.onClientList) {
let clients = [];
for (const clientId in this.channel) {
if (this.channel[clientId].shared && this.channel[clientId].username) {
clients.push({
clientId: clientId,
username: this.channel[clientId].username
})
}
}
try {
this.callbacks.onClientList(clients)
} catch (error) {
this.logEvent('onMessage-client-list-callback', error, 'error')
}
}
return
}
if (!this.isString(serverDecrypted.p) || !this.isString(serverDecrypted.c)) {
return
}
if (serverDecrypted.a === 'c' && (!this.channel[serverDecrypted.c] || !this.channel[serverDecrypted.c].shared)) {
try {
if (!this.channel[serverDecrypted.c]) {
this.channel[serverDecrypted.c] = {
username: null,
keys: this.clientEc.genKeyPair(),
shared: null,
};
this.sendMessage(this.encryptServerMessage({
a: 'c',
p: this.channel[serverDecrypted.c].keys.getPublic('hex'),
c: serverDecrypted.c
}, this.serverShared))
}
this.channel[serverDecrypted.c].shared = Buffer.from(this.xorHex(this.channel[serverDecrypted.c].keys.derive(this.clientEc.keyFromPublic(serverDecrypted.p, 'hex').getPublic()).toString('hex').padEnd(64, '8').substr(0, 64), this.credentials.password), 'hex');
this.sendMessage(this.encryptServerMessage({
a: 'c',
p: this.encryptClientMessage({
a: 'u',
p: this.credentials.username
}, this.channel[serverDecrypted.c].shared),
c: serverDecrypted.c
}, this.serverShared))
} catch (error) {
this.logEvent('onMessage-client', error, 'error')
}
return
}
if (serverDecrypted.a === 'c' && this.channel[serverDecrypted.c] && this.channel[serverDecrypted.c].shared) {
const clientDecrypted = this.decryptClientMessage(serverDecrypted.p, this.channel[serverDecrypted.c].shared);
this.logEvent('onMessage-client-decrypted', clientDecrypted);
if (!this.isObject(clientDecrypted) || !this.isString(clientDecrypted.a)) {
return
}
if (clientDecrypted.a === 'u' && this.isString(clientDecrypted.p) && clientDecrypted.p.match(/\S+/) && !this.channel[serverDecrypted.c].username) {
this.channel[serverDecrypted.c].username = clientDecrypted.p.replace(/^\s+/, '').replace(/\s+$/, '');
if (this.callbacks.onClientSecured) {
try {
this.callbacks.onClientSecured({
clientId: serverDecrypted.c,
username: this.channel[serverDecrypted.c].username
})
} catch (error) {
this.logEvent('onMessage-client-secured-callback', error, 'error')
}
}
return
} if (!this.channel[serverDecrypted.c].username) {
return
}
if (clientDecrypted.a === 'm' && this.isString(clientDecrypted.t) && (this.isString(clientDecrypted.d) || this.isObject(clientDecrypted.d))) {
if (this.callbacks.onClientMessage) {
try {
this.callbacks.onClientMessage({
clientId: serverDecrypted.c,
username: this.channel[serverDecrypted.c].username,
type: clientDecrypted.t,
data: clientDecrypted.d
})
} catch (error) {
this.logEvent('onMessage-client-message-callback', error, 'error')
}
}
return
}
}
}
// WebSocket error event handler
// WebSocket 错误事件处理
async onError(event) {
this.logEvent('onError', event, 'error');
this.disconnect();
if (this.credentials) {
this.startReconnect()
}
if (this.callbacks.onServerClosed) {
try {
this.callbacks.onServerClosed()
} catch (error) {
this.logEvent('onError-server-closed-callback', error, 'error')
}
}
}
// WebSocket close event handler
// WebSocket 关闭事件处理
async onClose(event) {
this.logEvent('onClose', event);
this.disconnect();
if (this.credentials) {
this.startReconnect()
}
if (this.callbacks.onServerClosed) {
try {
this.callbacks.onServerClosed()
} catch (error) {
this.logEvent('onClose-server-closed-callback', error, 'error')
}
}
}
// Log events for debugging
// 记录事件日志用于调试
logEvent(source, message, level) {
if (this.config.debug) {
const date = new Date(),
dateString = date.getFullYear() + '-' + ('0' + (date.getMonth() + 1)).slice(-2) + '-' + ('0' + date.getDate()).slice(-2) + ' ' + ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2) + ':' + ('0' + date.getSeconds()).slice(-2);
console.log('[' + dateString + ']', (level ? level.toUpperCase() : 'INFO'), source + (message ? ':' : ''), (message ? message : ''))
}
}
// Check if connection is open
// 检查连接是否已打开
isOpen() {
return (this.connection && this.connection.readyState && this.connection.readyState === WebSocket.OPEN ? true : false)
}
// Check if connection is closed
// 检查连接是否已关闭
isClosed() {
return (!this.connection || !this.connection.readyState || this.connection.readyState === WebSocket.CLOSED ? true : false)
}
// Start reconnect timer
// 启动重连定时器
startReconnect() {
this.stopReconnect();
this.logEvent('startReconnect');
this.reconnect = setTimeout(() => {
this.reconnect = null;
this.connect()
}, this.config.reconnectDelay)
}
// Stop reconnect timer
// 停止重连定时器
stopReconnect() {
if (this.reconnect) {
this.logEvent('stopReconnect');
clearTimeout(this.reconnect);
this.reconnect = null
}
}
// Start ping timer
// 启动心跳定时器
startPing() {
this.stopPing();
this.logEvent('startPing');
this.ping = setInterval(() => {
this.sendMessage('ping')
}, this.config.pingInterval)
}
// Stop ping timer
// 停止心跳定时器
stopPing() {
if (this.ping) {
this.logEvent('stopPing');
clearInterval(this.ping);
this.ping = null
}
}
// Disconnect from server
// 从服务器断开连接
disconnect() {
this.stopReconnect();
this.stopPing();
if (!this.isClosed()) {
try {
this.logEvent('disconnect');
this.connection.close()
} catch (error) {
this.logEvent('disconnect', error, 'error')
}
}
}
// Send a message to the server
// 向服务器发送消息
sendMessage(message) {
try {
if (this.isOpen()) {
this.connection.send(message);
return (true)
}
} catch (error) {
this.logEvent('sendMessage', error, 'error')
}
return (false)
}
// Send a message to all channels
// 向所有频道发送消息
sendChannelMessage(type, data) {
if (this.serverShared) {
try {
let payloads = {};
for (const clientId in this.channel) {
if (this.channel[clientId].shared && this.channel[clientId].username) {
payloads[clientId] = this.encryptClientMessage({
a: 'm',
t: type,
d: data
}, this.channel[clientId].shared);
if (payloads[clientId].length === 0) {
return (false)
}
}
}
if (Object.keys(payloads).length > 0) {
const payload = this.encryptServerMessage({
a: 'w',
p: payloads,
}, this.serverShared);
if (!this.isOpen() || payload.length === 0 || payload.length > (8 * 1024 * 1024)) {
return (false)
}
this.connection.send(payload)
}
return (true)
} catch (error) {
this.logEvent('sendChannelMessage', error, 'error')
}
}
return (false)
}
// Encrypt a message for the server
// 加密发送给服务器的消息
encryptServerMessage(message, key) {
let encrypted = '';
try {
message = Buffer.from(JSON.stringify(message), 'utf8');
if ((message.length % 16) !== 0) {
message = Buffer.from([...message, ...Buffer.alloc(16 - (message.length % 16))])
}
const iv = Buffer.from(crypto.getRandomValues(new Uint8Array(16)));
const cipher = new ModeOfOperation.cbc(key, iv);
encrypted = iv.toString('base64') + '|' + Buffer.from(cipher.encrypt(message)).toString('base64')
} catch (error) {
this.logEvent('encryptServerMessage', error, 'error')
}
return (encrypted)
}
// Decrypt a message from the server
// 解密来自服务器的消息
decryptServerMessage(message, key) {
let decrypted = {};
try {
const parts = message.split('|');
const decipher = new ModeOfOperation.cbc(key, Buffer.from(parts[0], 'base64'));
decrypted = JSON.parse(Buffer.from(decipher.decrypt(Buffer.from(parts[1], 'base64'))).toString('utf8').replace(/\0+$/, ''))
} catch (error) {
this.logEvent('decryptServerMessage', error, 'error')
}
return (decrypted)
}
// Encrypt a message for a client
// 加密发送给客户端的消息
encryptClientMessage(message, key) {
let encrypted = '';
try {
message = Buffer.from(JSON.stringify(message), 'utf8');
if ((message.length % 16) !== 0) {
message = Buffer.from([...message, ...Buffer.alloc(16 - (message.length % 16))])
}
const iv = Buffer.from(crypto.getRandomValues(new Uint8Array(12)));
const counter = Buffer.from(crypto.getRandomValues(new Uint8Array(4)));
const cipher = new chacha(key, iv, counter.reduce((a, b) => a * b));
encrypted = iv.toString('base64') + '|' + counter.toString('base64') + '|' + Buffer.from(cipher.encrypt(message)).toString('base64')
} catch (error) {
this.logEvent('encryptClientMessage', error, 'error')
}
return (encrypted)
}
// Decrypt a message from a client
// 解密来自客户端的消息
decryptClientMessage(message, key) {
let decrypted = {};
try {
const parts = message.split('|');
const decipher = new chacha(key, Buffer.from(parts[0], 'base64'), Buffer.from(parts[1], 'base64').reduce((a, b) => a * b));
decrypted = JSON.parse(Buffer.from(decipher.decrypt(Buffer.from(parts[2], 'base64'))).toString('utf8').replace(/\0+$/, ''))
} catch (error) {
this.logEvent('decryptClientMessage', error, 'error')
}
return (decrypted)
}
// XOR two hex strings
// 对两个十六进制字符串进行异或
xorHex(a, b) {
let result = '',
hexLength = Math.min(a.length, b.length);
for (let i = 0; i < hexLength; ++i) {
result += (parseInt(a.charAt(i), 16) ^ parseInt(b.charAt(i), 16)).toString(16)
}
return (result)
}
// Check if value is a string
// 检查值是否为字符串
isString(value) {
return (value && Object.prototype.toString.call(value) === '[object String]' ? true : false)
}
// Check if value is an array
// 检查值是否为数组
isArray(value) {
return (value && Object.prototype.toString.call(value) === '[object Array]' ? true : false)
}
// Check if value is an object
// 检查值是否为对象
isObject(value) {
return (value && Object.prototype.toString.call(value) === '[object Object]' ? true : false)
}
// Handle server public key
// 处理服务器公钥
async handleServerKey(serverKey) {
this.logEvent('handleServerKey', 'Received server key');
localStorage.removeItem(this.SERVER_KEY_STORAGE);
localStorage.setItem(this.SERVER_KEY_STORAGE, serverKey);
this.config.rsaPublic = serverKey;
return true
}
};
if (typeof window !== 'undefined') {
window.NodeCrypt = NodeCrypt
}