大更新,后端启动时生成密钥对,前端自动对接后端获取公钥。

This commit is contained in:
shuaiplus committed 2025-05-23 19:01:05 +08:00
1 parent 4fe2f0d57b
commit 0179377067
10 files changed
+199 -95

No files matched your search

+4 -9
View File
@@ -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"]
# 直接启动Node.js服务和Nginx
CMD node /app/server/server.js & nginx -g "daemon off;"
+112 -27
View File
@@ -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!
## 主要功能特性
### 🔒 安全特性
- 端到端加密聊天
- 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
```
+1 -1
View File
@@ -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",
-17
View File
@@ -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'));
+47 -5
View File
@@ -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
============ */
-12
View File
@@ -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;"
+2 -3
View File
@@ -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
}
},
+30 -7
View File
@@ -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: <client id>, username: <username> } ]
onClientMessage: callbacks.onClientMessage || null, // Parameters: message => { clientId: <client id>, username: <username>, type: <type>, data: <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') {
+1 -2
View File
@@ -6,7 +6,6 @@
<title>NodeCrypt</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<script>window.NODECRYPT_PUBLIC_KEY = 'NODECRYPT_PUBLIC_KEY'</script>
</head>
<body>
<!-- 登录页面 -->
@@ -92,6 +91,6 @@
</div>
</div>
</div> <script type="module" src="./main.js"></script>
<script src="./NodeCrypt2.js"></script>
<!-- <script src="./NodeCrypt2.js"></script> -->
</body>
</html>
+2 -12
View File
@@ -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;
// === 全局函数导出 ===
/**