大更新,后端启动时生成密钥对,前端自动对接后端获取公钥。
This commit is contained in:
1 parent
4fe2f0d57b
commit
0179377067
10 files changed
+199
-95
No files matched your search
+4
-9
@@ -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;"
|
||||
@@ -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
|
||||
```
|
||||
Generated
+1
-1
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
============ */
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
// === 全局函数导出 ===
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user