482 lines
14 KiB
HTML
482 lines
14 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>🔐 端到端加密聊天</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
font-family: "Microsoft YaHei", sans-serif
|
||
}
|
||
|
||
body {
|
||
background: #f0f2f5;
|
||
padding: 20px;
|
||
max-width: 900px;
|
||
margin: 0 auto
|
||
}
|
||
|
||
.header {
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
padding: 15px;
|
||
background: #1a73e8;
|
||
color: white;
|
||
border-radius: 10px;
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1)
|
||
}
|
||
|
||
.chat-container {
|
||
display: flex;
|
||
gap: 20px
|
||
}
|
||
|
||
.panel {
|
||
background: white;
|
||
border-radius: 12px;
|
||
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.1);
|
||
padding: 20px;
|
||
flex: 1
|
||
}
|
||
|
||
.title {
|
||
font-size: 18px;
|
||
margin-bottom: 15px;
|
||
color: #1a73e8;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px
|
||
}
|
||
|
||
.title i {
|
||
font-size: 20px
|
||
}
|
||
|
||
#chatBox {
|
||
height: 400px;
|
||
overflow-y: auto;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
margin-bottom: 15px;
|
||
background: #fafafa;
|
||
line-height: 1.6
|
||
}
|
||
|
||
.msg {
|
||
margin-bottom: 12px;
|
||
padding: 10px 15px;
|
||
border-radius: 18px;
|
||
max-width: 80%
|
||
}
|
||
|
||
.sent {
|
||
background: #e3f2fd;
|
||
margin-left: auto;
|
||
border-bottom-right-radius: 5px
|
||
}
|
||
|
||
.received {
|
||
background: #f1f8e9;
|
||
border-bottom-left-radius: 5px
|
||
}
|
||
|
||
.sys {
|
||
background: #fff8e1;
|
||
color: #5d4037;
|
||
font-size: 13px;
|
||
text-align: center;
|
||
margin: 5px 0
|
||
}
|
||
|
||
.input-area {
|
||
display: flex;
|
||
gap: 10px
|
||
}
|
||
|
||
textarea {
|
||
flex: 1;
|
||
padding: 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 20px;
|
||
resize: none;
|
||
font-size: 15px;
|
||
height: 45px
|
||
}
|
||
|
||
button {
|
||
background: #1a73e8;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 20px;
|
||
padding: 0 25px;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
transition: all 0.3s
|
||
}
|
||
|
||
button:hover {
|
||
background: #0d5bbb;
|
||
transform: scale(1.03)
|
||
}
|
||
|
||
button:disabled {
|
||
background: #b0b0b0;
|
||
cursor: not-allowed;
|
||
transform: none
|
||
}
|
||
|
||
.status {
|
||
padding: 8px;
|
||
border-radius: 6px;
|
||
margin-top: 10px;
|
||
font-size: 14px;
|
||
text-align: center
|
||
}
|
||
|
||
.status.connected {
|
||
background: #e8f5e9;
|
||
color: #2e7d32
|
||
}
|
||
|
||
.status.connecting {
|
||
background: #e3f2fd;
|
||
color: #1565c0
|
||
}
|
||
|
||
.status.error {
|
||
background: #ffebee;
|
||
color: #c62828
|
||
}
|
||
|
||
.debug {
|
||
background: #2d2d2d;
|
||
color: #64dd17;
|
||
padding: 10px;
|
||
border-radius: 6px;
|
||
margin-top: 15px;
|
||
font-family: monospace;
|
||
font-size: 13px;
|
||
max-height: 150px;
|
||
overflow: auto;
|
||
display: none
|
||
}
|
||
|
||
.badge {
|
||
background: #e3f2fd;
|
||
color: #1565c0;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
font-size: 12px;
|
||
margin-left: 5px
|
||
}
|
||
|
||
@media (max-width:768px) {
|
||
.chat-container {
|
||
flex-direction: column
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="header">
|
||
<h1>🔐 端到端加密聊天 <span class="badge">微信级安全</span></h1>
|
||
<div style="margin-top:8px; font-size:14px; opacity:0.9">AES-256-CBC加密 | SHA1签名验证 | 防篡改防重放</div>
|
||
</div>
|
||
|
||
<div class="chat-container">
|
||
<div class="panel">
|
||
<div class="title">💬 聊天窗口</div>
|
||
<div id="chatBox"></div>
|
||
<div class="input-area">
|
||
<textarea id="messageInput" placeholder="输入消息... (按Ctrl+Enter发送)" disabled></textarea>
|
||
<button id="sendBtn" disabled>发送</button>
|
||
</div>
|
||
<div id="statusBar" class="status connecting">⏳ 正在连接服务器...</div>
|
||
<button id="toggleDebug" style="margin-top:10px; padding:5px 15px; background:#f5f5f5; color:#5f6368">显示调试日志</button>
|
||
<div id="debugLog" class="debug"></div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="title">⚙️ 连接配置</div>
|
||
<div style="margin-bottom:15px">
|
||
<div style="margin-bottom:8px"><strong>用户ID:</strong></div>
|
||
<input type="text" id="userId" value="user_<?= bin2hex(random_bytes(4)) ?>" style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px">
|
||
</div>
|
||
<div style="margin-bottom:15px">
|
||
<div style="margin-bottom:8px"><strong>服务器地址:</strong></div>
|
||
<input type="text" id="serverUrl" value="wss://dev.xixingwl.cn:2346" style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px">
|
||
</div>
|
||
<button id="connectBtn" style="width:100%; background:#34a853">🔌 连接服务器</button>
|
||
<div style="margin-top:20px; padding:12px; background:#e8f0fe; border-radius:8px; font-size:13px; line-height:1.5">
|
||
<strong>🔒 安全说明:</strong><br>
|
||
• 所有消息端到端加密,服务器无法查看内容<br>
|
||
• 每条消息独立签名,防篡改防伪造<br>
|
||
• 时间戳验证,防止消息重放攻击
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script src="/assets/ywxapp/modules/aescgm.js"></script>
|
||
<script>
|
||
|
||
// =============== 聊天客户端 ===============
|
||
class ChatClient {
|
||
constructor() {
|
||
this.ws = null;
|
||
this.crypto = null;
|
||
this.userId = '';
|
||
this.isConnected = false;
|
||
this.initUI();
|
||
}
|
||
|
||
initUI() {
|
||
// DOM元素
|
||
this.chatBox = document.getElementById('chatBox');
|
||
this.messageInput = document.getElementById('messageInput');
|
||
this.sendBtn = document.getElementById('sendBtn');
|
||
this.statusBar = document.getElementById('statusBar');
|
||
this.connectBtn = document.getElementById('connectBtn');
|
||
this.toggleDebug = document.getElementById('toggleDebug');
|
||
this.debugLog = document.getElementById('debugLog');
|
||
|
||
// 事件绑定
|
||
this.connectBtn.onclick = () => this.connect();
|
||
this.sendBtn.onclick = () => this.sendMessage();
|
||
this.messageInput.onkeypress = (e) => {
|
||
if (e.ctrlKey && e.key === 'Enter') this.sendMessage();
|
||
};
|
||
this.toggleDebug.onclick = () => {
|
||
this.debugLog.style.display = this.debugLog.style.display === 'none' ? 'block' : 'none';
|
||
};
|
||
|
||
// 初始化状态
|
||
this.updateStatus('⏳ 未连接,请配置后点击连接', 'connecting');
|
||
}
|
||
|
||
log(msg, type = 'info') {
|
||
const prefix = type === 'error' ? '❌' : type === 'success' ? '✅' : 'ℹ️';
|
||
this.debugLog.innerHTML += `<span style="color:${type === 'error' ? '#ff5252' : type === 'success' ? '#4caf50' : '#aaa'}">${prefix} ${msg}</span><br>`;
|
||
this.debugLog.scrollTop = this.debugLog.scrollHeight;
|
||
console.log(`[ChatClient] ${msg}`);
|
||
}
|
||
|
||
updateStatus(text, className) {
|
||
this.statusBar.textContent = text;
|
||
this.statusBar.className = `status ${className}`;
|
||
}
|
||
|
||
addMessage(text, type = 'received', user = '系统') {
|
||
const div = document.createElement('div');
|
||
div.className = type === 'sys' ? 'sys' : `msg ${type}`;
|
||
if (type === 'sys') {
|
||
div.innerHTML = text;
|
||
} else {
|
||
div.innerHTML = `<strong>${user}:</strong> ${this.escapeHtml(text)}`;
|
||
}
|
||
this.chatBox.appendChild(div);
|
||
this.chatBox.scrollTop = this.chatBox.scrollHeight;
|
||
}
|
||
|
||
escapeHtml(unsafe) {
|
||
return unsafe
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
async connect() {
|
||
try {
|
||
this.userId = document.getElementById('userId').value.trim() || 'anonymous';
|
||
const serverUrl = document.getElementById('serverUrl').value.trim();
|
||
|
||
if (!serverUrl.startsWith('ws://') && !serverUrl.startsWith('wss://')) {
|
||
throw new Error('服务器地址必须以 ws:// 或 wss:// 开头');
|
||
}
|
||
|
||
this.updateStatus('🔄 正在连接...', 'connecting');
|
||
this.connectBtn.disabled = true;
|
||
|
||
// 创建WebSocket
|
||
this.ws = new WebSocket(serverUrl);
|
||
|
||
this.ws.onopen = () => {
|
||
this.log('WebSocket连接已建立');
|
||
// 发送认证请求
|
||
this.ws.send(JSON.stringify({
|
||
action: 'auth',
|
||
token: 'your_secure_token_123456', // 与服务端约定
|
||
user_id: this.userId
|
||
}));
|
||
};
|
||
|
||
this.ws.onmessage = (event) => this.handleMessage(event.data);
|
||
|
||
this.ws.onerror = (error) => {
|
||
this.log(`连接错误: ${error.message}`, 'error');
|
||
this.log(error);
|
||
this.updateStatus('❌ 连接失败: ' + error.message, 'error');
|
||
this.connectBtn.disabled = false;
|
||
};
|
||
|
||
this.ws.onclose = () => {
|
||
this.isConnected = false;
|
||
this.log('连接已关闭', 'error');
|
||
if (this.isConnected) {
|
||
this.updateStatus('⚠️ 连接意外断开', 'error');
|
||
this.addMessage('连接已断开', 'sys');
|
||
}
|
||
this.connectBtn.disabled = false;
|
||
this.messageInput.disabled = true;
|
||
this.sendBtn.disabled = true;
|
||
};
|
||
|
||
} catch (e) {
|
||
this.log(`连接异常: ${e.message}`, 'error');
|
||
this.updateStatus('❌ ' + e.message, 'error');
|
||
this.connectBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
handleMessage(data) {
|
||
try {
|
||
const msg = JSON.parse(data);
|
||
|
||
// 认证响应
|
||
if (msg.action === 'auth_success') {
|
||
// 初始化加密模块 (使用服务端返回的密钥)
|
||
this.crypto = new ChatCrypto(
|
||
msg.encodingAesKey,
|
||
'your_secure_token_123456',
|
||
'CHAT_APP_2024'
|
||
);
|
||
|
||
|
||
this.isConnected = true;
|
||
this.updateStatus(`✅ 已连接 | 会话ID: ${msg.session_id.substring(0, 8)}...`, 'connected');
|
||
this.addMessage(`欢迎 ${this.userId}!端到端加密已启用`, 'sys');
|
||
this.messageInput.disabled = false;
|
||
this.sendBtn.disabled = false;
|
||
this.connectBtn.textContent = '🔌 已连接';
|
||
this.connectBtn.style.background = '#9e9e9e';
|
||
this.connectBtn.disabled = true;
|
||
this.log('认证成功,加密模块已初始化', 'success');
|
||
return;
|
||
}
|
||
|
||
// 错误处理
|
||
if (msg.error) {
|
||
this.log(`服务端错误: ${msg.error} (代码: ${msg.code})`, 'error');
|
||
if (msg.code === 401) {
|
||
this.updateStatus('❌ 认证失败,请检查token', 'error');
|
||
this.ws.close();
|
||
}
|
||
this.addMessage(`[系统] ${msg.error}`, 'sys');
|
||
return;
|
||
}
|
||
|
||
// =============== 接收聊天消息 ===============
|
||
// 验证签名
|
||
if (!this.crypto.verifySignature(
|
||
msg.signature,
|
||
msg.timestamp,
|
||
msg.nonce,
|
||
msg.encrypt
|
||
)) {
|
||
this.addMessage(`[警告] 消息签名验证失败,来源: ${msg.from_user}`, 'sys');
|
||
this.log(`收到无效签名消息 (来源: ${msg.from_user})`, 'error');
|
||
return;
|
||
}
|
||
|
||
// 防重放检查
|
||
if (Math.abs(Date.now() / 1000 - msg.timestamp) > 300) {
|
||
this.log(`收到过期消息 (时间差: ${Math.abs(Date.now() / 1000 - msg.timestamp)}秒)`, 'error');
|
||
return;
|
||
}
|
||
|
||
// 解密消息
|
||
try {
|
||
const plainText = this.crypto.decrypt(msg.encrypt);
|
||
const chatData = JSON.parse(plainText);
|
||
this.addMessage(chatData.content, 'received', chatData.from_user || msg.from_user);
|
||
this.log(`收到消息: "${chatData.content.substring(0, 20)}..." (来源: ${msg.from_user})`, 'success');
|
||
} catch (e) {
|
||
this.addMessage(`[系统] 消息解密失败: ${e.message}`, 'sys');
|
||
this.log(`解密异常: ${e.message}`, 'error');
|
||
}
|
||
|
||
} catch (e) {
|
||
this.log(`消息处理异常: ${e.message}`, 'error');
|
||
console.error(e);
|
||
}
|
||
}
|
||
|
||
sendMessage() {
|
||
const content = this.messageInput.value.trim();
|
||
if (!content || !this.isConnected || !this.crypto) return;
|
||
try {
|
||
// 构建消息体
|
||
const chatMsg = {
|
||
content: content,
|
||
type: 'text',
|
||
timestamp: Math.floor(Date.now() / 1000)
|
||
};
|
||
console.log('原始消息体:', chatMsg);
|
||
console.log('加密前消息体字符串:', JSON.stringify(chatMsg));
|
||
console.log('加密前消息体字符串:', window.encryptJson(chatMsg, 'your_secure_token_123456'));
|
||
// 加密
|
||
const encryptMsg = this.crypto.encrypt(JSON.stringify(chatMsg));
|
||
// 生成签名参数
|
||
const timestamp = Math.floor(Date.now() / 1000);
|
||
const nonce = this.crypto.getRandomStr(10);
|
||
const signature = this.crypto.generateSignature(timestamp, nonce, encryptMsg);
|
||
// 发送
|
||
this.ws.send(JSON.stringify({
|
||
encrypt: encryptMsg,
|
||
signature: signature,
|
||
timestamp: timestamp,
|
||
nonce: nonce
|
||
}));
|
||
// 显示到聊天框
|
||
this.addMessage(content, 'sent', this.userId);
|
||
this.messageInput.value = '';
|
||
this.log(`发送消息: "${content.substring(0, 20)}..."`, 'success');
|
||
|
||
} catch (e) {
|
||
this.log(`发送失败: ${e.message}`, 'error');
|
||
alert('发送失败: ' + e.message);
|
||
}
|
||
}
|
||
}
|
||
|
||
const aes = new AESGCM();
|
||
// 加密示例
|
||
aes.encrypt('Hello World!', 'mypassword')
|
||
.then(encrypted => {
|
||
console.log('加密结果:', encrypted);
|
||
|
||
// 解密示例
|
||
return aes.decrypt(encrypted, 'mypassword');
|
||
})
|
||
.then(decrypted => {
|
||
console.log('解密结果:', decrypted);
|
||
})
|
||
.catch(error => {
|
||
console.error('操作失败:', error);
|
||
});
|
||
// 初始化
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
console.log('页面加载完成,正在初始化聊天客户端...');
|
||
window.chatClient = new ChatClient();
|
||
document.getElementById('messageInput').focus();
|
||
});
|
||
</script>
|
||
</body>
|
||
|
||
</html> |