From 01793770679fa9b355e76b835cccf4d90f420768 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Fri, 23 May 2025 19:01:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E6=9B=B4=E6=96=B0=EF=BC=8C=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E5=90=AF=E5=8A=A8=E6=97=B6=E7=94=9F=E6=88=90=E5=AF=86?= =?UTF-8?q?=E9=92=A5=E5=AF=B9=EF=BC=8C=E5=89=8D=E7=AB=AF=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E5=90=8E=E7=AB=AF=E8=8E=B7=E5=8F=96=E5=85=AC?= =?UTF-8?q?=E9=92=A5=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 13 ++-- README.md | 139 +++++++++++++++++++++++++++++++++-------- package-lock.json | 2 +- server/key-generate.js | 17 ----- server/server.js | 52 +++++++++++++-- start.sh | 12 ---- vite.config.js | 5 +- web/NodeCrypt.js | 37 ++++++++--- web/index.html | 3 +- web/main.js | 14 +---- 10 files changed, 199 insertions(+), 95 deletions(-) delete mode 100644 server/key-generate.js delete mode 100644 start.sh diff --git a/Dockerfile b/Dockerfile index b58bce4..eae6d8b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-alpine +FROM docker.1ms.run/node:18-alpine WORKDIR /app @@ -6,11 +6,7 @@ COPY server/ ./server/ RUN cd server && npm install COPY dist/ ./web/ -COPY start.sh /start.sh - -RUN chmod +x /start.sh && \ - dos2unix /start.sh && \ - apk add --no-cache nginx dos2unix && \ +RUN apk add --no-cache nginx && \ mkdir -p /etc/nginx RUN cat > /etc/nginx/nginx.conf <<'EOF' @@ -43,8 +39,7 @@ http { } EOF -RUN dos2unix /etc/nginx/nginx.conf - EXPOSE 80 -CMD ["/start.sh"] \ No newline at end of file +# 直接启动Node.js服务和Nginx +CMD node /app/server/server.js & nginx -g "daemon off;" \ No newline at end of file diff --git a/README.md b/README.md index 3244bc5..4ce6b2e 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,118 @@ -# NodeCrypt Web 聊天系统 +## 项目概览 -## 已完成功能 +这是一个现代化的实时聊天系统,采用客户端-服务器架构,具备端到端加密功能。 -- **登录与加入房间弹窗**:支持输入用户名、房间名、房间密码,禁止输入空格和特殊符号,支持通过分享链接自动填充房间信息。 -- **房间分享链接自动生成与复制**:可一键生成当前房间的专属链接(含密码),并复制到剪贴板,便于邀请他人加入。 -- **多房间(节点)支持**:可同时加入多个房间,左侧房间栏支持动态切换,房间状态独立保存。 -- **聊天消息实时同步**:支持文本与图片消息,消息自动同步到所有在线成员。 -- **图片粘贴、拖拽、附件上传发送**:支持直接粘贴、拖拽图片或通过附件按钮上传图片,自动压缩大图片,限制单张图片最大5MB。 -- **图片可点击全屏预览**:聊天图片点击后可全屏预览,支持缩放、拖动、双击还原,ESC或点击关闭。 -- **系统消息提示**:如用户加入/退出、房间状态变更等,系统消息以特殊气泡样式居中显示。 -- **在线成员列表实时更新**:右侧成员栏实时显示在线成员昵称与头像,自动区分自己与他人。 -- **支持自定义头像**:基于用户名自动生成 SVG 头像,所有头像样式统一,支持昵称变更自动刷新头像。 -- **输入框多行自适应高度**:输入区高度随内容自动扩展,支持多行输入,采用 Telegram 风格占位符。 -- **禁止用户名、房间名、密码输入空格和特殊符号**:输入时自动过滤,提升安全性和一致性。 -- **响应式布局**:支持侧边栏和右侧成员栏宽度拖拽调整,适配不同屏幕宽度。 -- **前端防止 XSS 攻击**:所有用户输入内容均进行 HTML 转义,防止脚本注入。 -- **退出房间彻底销毁连接**:退出房间时断开与服务器的连接,清理房间数据,支持全部房间关闭后自动刷新页面。 -- **主要交互均有动画与友好提示**:如菜单弹出、系统消息、按钮反馈等,提升用户体验。 +## 技术架构 -## 待完成目标 +### 前端技术栈 +- **框架**: 原生 JavaScript (ES6+模块化) +- **构建工具**: Vite +- **加密库**: 自研 NodeCrypt.js 和 NodeCrypt2.js +- **UI设计**: 类 Telegram 风格的响应式界面 -- **支持更多类型的消息**:如文件、表情包等,丰富聊天内容形式。 -- **聊天记录本地持久化与历史消息加载**:支持浏览历史消息,断线重连后自动恢复聊天记录。 -- **移动端适配与优化**:提升在手机、平板等移动设备上的使用体验。 -- **更完善的错误处理与网络异常提示**:如连接超时、消息发送失败等,提供明确的用户提示。 -- **多语言支持**:支持中英文等多语言界面切换。 -- **更丰富的用户资料与个性化设置**:如自定义昵称、头像、个性签名等。 -- **服务端权限与安全机制增强**:如房间管理员、踢人、禁言、消息撤回等权限控制。 +### 后端技术栈 +- **运行时**: Node.js +- **WebSocket**: 实时通信协议 +- **加密**: RSA + AES-256-CBC 混合加密 +- **部署**: Docker + Nginx ---- +### 核心加密机制 +- **密钥交换**: 椭圆曲线 Diffie-Hellman (ECDH) +- **对称加密**: AES-256-CBC +- **非对称加密**: RSA (服务器身份验证) +- **端到端**: 客户端间直接加密通信 -如有建议或问题,欢迎 Issue 或 PR! \ No newline at end of file +## 主要功能特性 + +### 🔒 安全特性 +- 端到端加密聊天 +- RSA 服务器身份验证 +- 客户端密钥生成和管理 +- 防 XSS 攻击(HTML 转义) + +### 💬 聊天功能 +- 多房间支持(可同时加入多个聊天室) +- 私聊模式(点击用户头像进入私聊) +- 图片发送和预览(支持拖拽缩放) +- 表情符号支持 +- 系统通知消息 + +### 🎨 用户体验 +- **自动头像生成**: 基于用户名的 SVG 头像(使用 DiceBear API) +- **响应式设计**: 完美适配桌面和移动端 +- **拖拽调整**: 侧边栏宽度可调 +- **智能输入框**: 自动高度调整,支持多行输入 +- **未读消息提醒**: 红色徽章显示未读数量 + +### 📱 移动端优化 +- 触摸友好的 UI 交互 +- 移动端专用侧边栏和遮罩 +- 自适应布局和字体大小 +- 防止页面拖动和缩放 + +## 核心模块分析 + +### 1. 房间管理 (room.js) +```javascript +// 多房间状态管理 +let roomsData = []; +let activeRoomIndex = -1; +``` +- 支持多房间同时在线 +- 房间切换和上下文恢复 +- 用户列表和消息历史管理 + +### 2. 聊天显示 (chat.js) +- 消息渲染和类型处理 +- 图片预览模态框 +- 输入框占位符和自动增高 +- 粘贴纯文本处理 + +### 3. 用户界面 (ui.js) +- 用户列表渲染 +- 移动端 UI 适配 +- 登录模态框管理 +- 设置面板交互 + +### 4. 工具库 +- **util.dom.js**: DOM 操作封装 +- **util.string.js**: 字符串处理和 HTML 转义 +- **util.avatar.js**: 头像生成 +- **util.image.js**: 图片处理 +- **util.emoji.js**: 表情选择器 +- **util.settings.js**: 设置管理 + +## 部署方案 + +### Docker 部署 +- **Dockerfile**: 容器化配置 +- **start.sh**: 启动脚本(动态生成 RSA 密钥对) +- **Nginx**: 静态文件服务和 WebSocket 代理 + +### 密钥管理 +```bash +# 动态生成 RSA 密钥对 +node /app/server/key-generate.js +``` + +## 项目亮点 + +1. **安全性**: 实现了真正的端到端加密,即使服务器也无法解密消息内容 +2. **用户体验**: 类 Telegram 的现代化 UI,流畅的交互体验 +3. **技术先进性**: 使用现代 JavaScript 特性,模块化架构 +4. **跨平台**: 完美支持桌面和移动端 +5. **可扩展性**: 清晰的模块划分,易于功能扩展 + +## 启动方式 + +```bash +# 开发模式 +npm run dev + +# 生产构建 +npm run build + +# Docker 部署 +docker build -t nodecrypt . +docker run -p 80:80 nodecrypt +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1548846..70c5d3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1219,7 +1219,7 @@ }, "node_modules/vite": { "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "resolved": "https://registry.npmmirror.com/vite/-/vite-6.3.5.tgz", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", diff --git a/server/key-generate.js b/server/key-generate.js deleted file mode 100644 index 527ddf1..0000000 --- a/server/key-generate.js +++ /dev/null @@ -1,17 +0,0 @@ - -const crypto = require('crypto'); - -const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { - modulusLength: 2048, - publicKeyEncoding: { - type: 'spki', - format: 'der' - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'der' - } -}); - -console.log('PRIVATE', Buffer.from(privateKey).toString('base64')); -console.log('PUBLIC', Buffer.from(publicKey).toString('base64')); diff --git a/server/server.js b/server/server.js index 4409d7d..97e5883 100644 --- a/server/server.js +++ b/server/server.js @@ -5,12 +5,43 @@ const crypto = require('crypto'); const ws = require('ws'); +// 生成RSA密钥对函数,整合自key-generate.js +const generateRSAKeyPair = () => { + try { + console.log('Generating new RSA keypair...'); + const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { + type: 'spki', + format: 'der' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'der' + } + }); + + return { + rsaPublic: Buffer.from(publicKey).toString('base64'), + rsaPrivate: crypto.createPrivateKey({ + key: privateKey, + format: 'der', + type: 'pkcs8' + }) + }; + } catch (error) { + console.error('Error generating RSA key pair:', error); + process.exit(1); + } +}; + +// 生成密钥并配置服务器 +const keyPair = generateRSAKeyPair(); +console.log('RSA key pair generated successfully'); + const config = { - rsaPrivate: crypto.createPrivateKey({ - key: Buffer.from(process.env.NODECRYPT_PRIVATE_KEY, 'base64'), - format: 'der', - type: 'pkcs8' - }), + rsaPrivate: keyPair.rsaPrivate, + rsaPublic: keyPair.rsaPublic, wsHost: '127.0.0.1', wsPort: 8088, seenTimeout: 60000, @@ -83,6 +114,17 @@ wss.on('connection', ( connection ) => { channel: null }; + // Send the server's public key to the client first + try { + logEvent('sending-public-key', clientId, 'debug'); + sendMessage(connection, JSON.stringify({ + type: 'server-key', + key: config.rsaPublic + })); + } catch (error) { + logEvent('sending-public-key', error, 'error'); + } + /* ============ ON MESSAGE ============ */ diff --git a/start.sh b/start.sh deleted file mode 100644 index aca7045..0000000 --- a/start.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -key_output=$(node /app/server/key-generate.js) -export NODECRYPT_PRIVATE_KEY=$(echo "$key_output" | grep '^PRIVATE' | cut -d' ' -f2) -export NODECRYPT_PUBLIC_KEY=$(echo "$key_output" | grep '^PUBLIC' | cut -d' ' -f2) -echo "PRIVATE_KEY=$NODECRYPT_PRIVATE_KEY" -echo "PUBLIC_KEY=$NODECRYPT_PUBLIC_KEY" -# 替换 main.js 里的 NODECRYPT_PUBLIC_KEY 为实际公钥 -PUB_ESC=$(printf "%s" "$NODECRYPT_PUBLIC_KEY" | sed "s/'/\\\\'/g") -sed -i "s|'NODECRYPT_PUBLIC_KEY'|'$PUB_ESC'|g" /app/web/index.html - -node /app/server/server.js & -nginx -g "daemon off;" diff --git a/vite.config.js b/vite.config.js index 5f9793d..03265db 100644 --- a/vite.config.js +++ b/vite.config.js @@ -9,11 +9,10 @@ export default defineConfig({ ], build: { outDir: '../dist', - emptyOutDir: true, - minify: 'terser', // 比默认的 esbuild 压缩更彻底 + emptyOutDir: true, minify: 'terser', // 比默认的 esbuild 压缩更彻底 terserOptions: { compress: { - drop_console: true, // 移除生产环境中的console语句 + drop_console: false, // 保留生产环境中的console语句 drop_debugger: true } }, diff --git a/web/NodeCrypt.js b/web/NodeCrypt.js index 5306edf..12b288f 100644 --- a/web/NodeCrypt.js +++ b/web/NodeCrypt.js @@ -1,14 +1,11 @@ - -import { sha256 } from 'js-sha256'; +import { sha256 } from 'js-sha256'; import { ec as elliptic } from 'elliptic'; import { ModeOfOperation } from 'aes-js'; import chacha from 'js-chacha20'; import './NodeCrypt.js';import { Buffer } from 'buffer'; window.Buffer = Buffer; -class ChatCrypt { - - constructor ( config = {}, callbacks = {} ) { +class ChatCrypt { constructor ( config = {}, callbacks = {} ) { // Update configuration @@ -29,6 +26,9 @@ class ChatCrypt { onClientList: callbacks.onClientList || null, // Parameters: clients => [ { clientId: , username: } ] onClientMessage: callbacks.onClientMessage || null, // Parameters: message => { clientId: , username: , type: , data: } }; + + // Initialize server key storage + this.SERVER_KEY_STORAGE = 'nodecrypt_server_key'; // Initialize client elliptic curve @@ -270,7 +270,6 @@ class ChatCrypt { /* ============ ON MESSAGE ============ */ - async onMessage ( event ) { if ( @@ -290,6 +289,19 @@ class ChatCrypt { 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) { + // 不是JSON或解析错误,按正常消息处理 + } + // Receive server ecdh public key if ( @@ -1073,7 +1085,18 @@ class ChatCrypt { : false ); } - + /* ================= + HANDLE SERVER 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') { diff --git a/web/index.html b/web/index.html index 9da323b..a2b0f03 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,6 @@ NodeCrypt - @@ -92,6 +91,6 @@ - + \ No newline at end of file diff --git a/web/main.js b/web/main.js index 1f8b6d8..26a6069 100644 --- a/web/main.js +++ b/web/main.js @@ -10,7 +10,7 @@ import { openSettingsPanel, closeSettingsPanel, initSettings, notifyMessage } fr import { escapeHTML, textToHTML } from './util.string.js'; import { $, $$, $id, on, off, addClass, removeClass } from './util.dom.js'; -//import './NodeCrypt.js'; +import './NodeCrypt.js'; // === 核心功能模块导入 === import { @@ -45,24 +45,14 @@ import { /** * 应用全局配置 - * rsaPublic: RSA公钥,用于加密通信 * wsAddress: WebSocket服务器地址 * debug: 是否开启调试模式 */ -/* -const config = { - rsaPublic: window.NODECRYPT_PUBLIC_KEY, +window.config = { wsAddress: '/ws', debug: true }; -*/ -window.config = { - rsaPublic: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzKm1a2iTsDM25z19sv2RlfX/nQuM5UQoT6oVNLFRQM5Pgk6B7jV0jUdcbeopP3Vka5Q81QYW12QfM/RvdPoLyLEMRLysGGpJlB7vvrbRFLjg31lWKgaavghb9cedde0w9/qVjU3qKNdcmL+/csQJ7bqMj20X9Jwm2JAr1NS0Ys0EAAPKlWYDq1UWmTaFIHxzuyxsokQy8QyeKSJtD0KCPs6509xGaEXB2mqLEgxpbRZ1xeguBgSj41lQB0FaN8nSalhUrGbcmTAVyOb7hz1GhutzzTAJ1l0Fd+NYLo/+FuKo/cL65hCBlBn7ddnIJqWW3SoDscIOzmxvYPj998C91wIDAQAB', - wsAddress: 'wss://crypt.works/ws', - debug: true -}; -window.config = config; // === 全局函数导出 === /**