feat: encrypt sftp connect data

This commit is contained in:
dalaolala committed 2026-03-03 12:29:31 +08:00
1 parent 4cf4d8939f
commit 1f97292aeb
7 files changed
+141 -3

No files matched your search

+17 -1
View File
@@ -35,7 +35,23 @@ const sftpConnections = new Map();
// 连接SFTP - 快速连接专属
router.post('/connect', authenticateToken, async (req, res) => {
try {
const { host, port, username, password, privateKey } = req.body;
let connectionInfo = req.body;
// 如果请求体包含 key 和 data,说明是加密传输
if (connectionInfo.key && connectionInfo.data) {
try {
const cryptoUtil = require('../utils/crypto');
connectionInfo = cryptoUtil.decryptPayload(connectionInfo.key, connectionInfo.data);
} catch (err) {
console.error('解密载荷失败:', err);
return res.status(400).json({
success: false,
message: '无效的加密数据结构'
});
}
}
const { host, port, username, password, privateKey } = connectionInfo;
const userId = req.user.userId;
if (!host || !username) {
+12
View File
@@ -19,6 +19,9 @@ const db = require('./config/database');
// Socket handlers
const socketHandler = require('./socket/socketHandler');
// Utilities
const cryptoUtil = require('./utils/crypto');
// Load environment variables
dotenv.config();
@@ -44,6 +47,11 @@ app.use('/api/sftp/quick', sftpQuickRoutes);
app.use('/api/users', userRoutes);
app.use('/api/admin', adminRoutes);
// Crypto Endpoint
app.get('/api/crypto/public-key', (req, res) => {
res.json({ publicKey: cryptoUtil.getPublicKey() });
});
// Socket.io connection handling
socketHandler(io);
@@ -65,6 +73,10 @@ db.authenticate()
console.error('Unable to connect to the database:', err);
});
// Initialize RSA keys
cryptoUtil.initKeys();
console.log('RSA Keys initialized for secure payload transmission.');
const PORT = process.env.PORT || 3000;
httpServer.listen(PORT, () => {
console.log(`WebSSH server running on port ${PORT}`);
+41
View File
@@ -0,0 +1,41 @@
const crypto = require('crypto');
let publicKey = null;
let privateKey = null;
function initKeys() {
if (publicKey && privateKey) return;
const { publicKey: pub, privateKey: priv } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
publicKey = pub;
privateKey = priv;
}
function getPublicKey() {
if (!publicKey) initKeys();
return publicKey;
}
function decryptPayload(encryptedAesKeyBase64, encryptedDataBase64) {
if (!privateKey) throw new Error('Keys not initialized');
// 1. RSA 解密 AES 密钥
const encryptedAesKeyBuffer = Buffer.from(encryptedAesKeyBase64, 'base64');
const aesKeyStr = crypto.privateDecrypt(
{ key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING },
encryptedAesKeyBuffer
).toString('utf8');
const { key, iv } = JSON.parse(aesKeyStr);
// 2. AES 解密数据
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key, 'hex'), Buffer.from(iv, 'hex'));
let decrypted = decipher.update(encryptedDataBase64, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
module.exports = { initKeys, getPublicKey, decryptPayload };
+7
View File
@@ -13,6 +13,7 @@
"axios": "^1.6.2",
"crypto-js": "^4.2.0",
"element-plus": "^2.3.14",
"jsencrypt": "^3.5.4",
"pinia": "^2.1.7",
"socket.io-client": "^4.7.5",
"vue": "^3.3.4",
@@ -1137,6 +1138,12 @@
"node": ">= 0.4"
}
},
"node_modules/jsencrypt": {
"version": "3.5.4",
"resolved": "https://registry.npmmirror.com/jsencrypt/-/jsencrypt-3.5.4.tgz",
"integrity": "sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA==",
"license": "MIT"
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz",
+1
View File
@@ -13,6 +13,7 @@
"axios": "^1.6.2",
"crypto-js": "^4.2.0",
"element-plus": "^2.3.14",
"jsencrypt": "^3.5.4",
"pinia": "^2.1.7",
"socket.io-client": "^4.7.5",
"vue": "^3.3.4",
+7 -2
View File
@@ -1,5 +1,6 @@
import { ref } from 'vue';
import axios from 'axios';
import { encryptPayload } from '../utils/crypto';
// 这是一个组合式函数(Composable)而不是全局 Pinia Store
// 这样每次调用 useQuickSftp() 都会生成完全独立的响应式状态,非常适合多标签页互不干扰。
@@ -18,13 +19,17 @@ export function useQuickSftp() {
loading.value = true;
error.value = null;
const response = await axios.post('/api/sftp/quick/connect', {
// 使用 RSA + AES 混合加密传输载荷
const rawPayload = {
host: connectionInfo.host,
port: connectionInfo.port,
username: connectionInfo.username,
password: connectionInfo.password,
privateKey: connectionInfo.privateKey
});
};
const secureData = await encryptPayload(rawPayload);
const response = await axios.post('/api/sftp/quick/connect', secureData);
if (response.data.success) {
isConnected.value = true;
+56
View File
@@ -0,0 +1,56 @@
import axios from 'axios';
import JSEncrypt from 'jsencrypt';
import CryptoJS from 'crypto-js';
let publicKeyCache = null;
/**
* 获取后端 RSA 公钥
*/
export async function getPublicKey() {
if (publicKeyCache) return publicKeyCache;
try {
const response = await axios.get('/api/crypto/public-key');
publicKeyCache = response.data.publicKey;
return publicKeyCache;
} catch (error) {
console.error('获取公钥失败:', error);
throw error;
}
}
/**
* 混合加密传输数据 (RSA + AES)
* @param {Object} data 原始对象数据
* @returns {Promise<{key: string, data: string}>} 加密后的 payload
*/
export async function encryptPayload(data) {
const pubKey = await getPublicKey();
// 1. 生成一次性 AES-256 密钥 (32字节) 和 IV (16字节)
const aesKeyStr = CryptoJS.lib.WordArray.random(32).toString(CryptoJS.enc.Hex);
const ivStr = CryptoJS.lib.WordArray.random(16).toString(CryptoJS.enc.Hex);
// 2. 使用 AES 加密真实数据
const payloadStr = JSON.stringify(data);
const encryptedData = CryptoJS.AES.encrypt(payloadStr, CryptoJS.enc.Hex.parse(aesKeyStr), {
iv: CryptoJS.enc.Hex.parse(ivStr),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString(); // 自动输出 base64
// 3. 使用 RSA 加密 AES 密钥和 IV
const encryptor = new JSEncrypt();
encryptor.setPublicKey(pubKey);
const sessionKeyObj = JSON.stringify({ key: aesKeyStr, iv: ivStr });
const encryptedKey = encryptor.encrypt(sessionKeyObj);
if (!encryptedKey) {
throw new Error('RSA 加密失败,可能是数据过长或公钥无效');
}
return {
key: encryptedKey,
data: encryptedData
};
}