feat: encrypt sftp connect data
This commit is contained in:
1 parent
4cf4d8939f
commit
1f97292aeb
7 files changed
+141
-3
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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 };
|
||||
Generated
+7
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user