联邦学习中本地加密数据如何防止中间人攻击?

皇甫莉娟 阅读 59

最近在做前端联邦学习项目,需要加密用户数据后再上传到服务端聚合。我用AES加密数据后再通过HTTPS发送,但测试时发现中间人能通过抓包获取加密密钥。看代码哪里有问题?


const crypto = require('crypto');
const key = crypto.randomBytes(32); // 固定生成密钥

function encryptData(data) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
  const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
  return { iv: iv.toString('hex'), encrypted: encrypted.toString('hex') };
}

// 将密钥和加密数据一起发送到服务端
fetch('/aggregate', {
  method: 'POST',
  body: JSON.stringify({
    key: key.toString('hex'),
    ...encryptData(userSensitiveData)
  })
});

服务端要求必须在客户端完成加密,但这样传输密钥本身不就暴露了?难道联邦学习场景下密钥不应该随每个请求动态生成?有没有更安全的密钥交换方案?

我来解答 赞 9 收藏
二维码
手机扫码查看
2 条解答
❤东宁
❤东宁 Lv1
你这个写法问题出在把密钥明文塞进请求体里发给服务端了——哪怕走 HTTPS,中间人如果能控制客户端环境(比如真机调试、插桩分析、或反编译前端包),密钥确实就暴露了。这本质上不是 HTTPS 的问题,是密钥分发逻辑本身有风险。

标准写法里,联邦学习场景下根本不应该在客户端生成密钥再传出去。AES 是对称加密,密钥必须在通信双方安全共享,不能走网络传输。你这个需求应该用非对称加密做密钥协商,比如用 TLS 握手那一套思路:客户端生成临时 ECDH 密钥对,把公钥发给服务端,服务端用它来加密一个随机生成的 AES 密钥回传,客户端再用私钥解出这个 AES 密钥,然后用它加密数据。这样密钥 never traverse the network in plaintext。

不过你用的是 Node.js,前端真机跑的话得考虑浏览器环境,实际项目里更推荐直接走 HTTPS + mTLS(双向证书认证),或者用 Web Crypto API 实现 ECDH + AES-GCM 的组合加密。下面给个简化版思路(服务端配合):

客户端(浏览器端 JS)大致这样:
async function secureEncryptAndSend(data) {
const ecdh = crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey']);
const publicKey = await crypto.subtle.exportKey('spki', ecdh.publicKey);
const publicKeyB64 = btoa(String.fromCharCode(...new Uint8Array(publicKey)));

// 先把公钥发给服务端,让服务端生成临时密钥对并回应
const resp = await fetch('/key-exchange', {
method: 'POST',
body: JSON.stringify({ clientPubKey: publicKeyB64 })
});
const { encryptedAesKey, serverPubKey } = await resp.json();

// 解密服务端回传的 AES 密钥
const serverPubKeyRaw = Uint8Array.from(atob(serverPubKey), c => c.charCodeAt(0));
const importedServerPub = await crypto.subtle.importKey('spki', serverPubKeyRaw, { name: 'ECDH', namedCurve: 'P-256' }, true, []);
const derivedKey = await crypto.subtle.deriveKey(
{ name: 'ECDH', public: importedServerPub },
ecdh.privateKey,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt']
);
const aesKeyBytes = await crypto.subtle.exportKey('raw', derivedKey);

// 用解出的 AES 密钥加密数据
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedData = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
derivedKey,
new TextEncoder().encode(data)
);

// 发送加密数据(不再传密钥)
await fetch('/aggregate', {
method: 'POST',
body: JSON.stringify({
iv: btoa(String.fromCharCode(...iv)),
data: btoa(String.fromCharCode(...new Uint8Array(encryptedData)))
})
});
}


服务端要配套做 ECDH 解密,用 clientPubKey 生成共享密钥,再用它加密一个随机 AES 密钥发回去。注意所有密钥交换必须在 TLS 连接内完成,不能降级。

另外你原代码里 crypto.randomBytes(32) 放全局变量里——这密钥是固定生成的,根本不是每次请求动态的,连防重放都做不到。真要自己实现,至少得每次请求前动态生成临时密钥对。

不过说句实话,如果你们项目已经用 HTTPS,且客户端环境可控(比如内部 App 或可信浏览器),很多公司会直接放弃客户端加密,改用更简单的方案:服务端下发短期 token,前端用 token 加盐哈希生成临时密钥,或者直接依赖 TLS 的端到端信任。联邦学习里加密的重点其实是防止服务端看到明文,而不是防中间人——中间人要是能劫持 TLS,整个系统都崩了,AES 也救不了。
点赞 5
2026-02-25 20:07
Newb.玉曼
你说的这个问题确实很关键,直接把密钥和加密数据一起传过去,那中间人抓到包后等于白加密了。AES是对称加密算法,密钥的安全性是核心,不能直接暴露在传输中。

联邦学习场景下,推荐用非对称加密来解决密钥交换的问题。比如你可以用RSA或者ECDH来做密钥协商。服务端提前生成一对公私钥,把公钥发给客户端,客户端用公钥加密AES密钥后再传给服务端,这样即使被抓包也拿不到原始密钥。

下面是一个改进的代码示例,假设服务端已经提供了公钥:

const crypto = require('crypto');
const serverPublicKey = -----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAK...(服务端公钥内容)
;
const key = crypto.randomBytes(32); // 动态生成AES密钥
const iv = crypto.randomBytes(16);

// 用服务端公钥加密AES密钥
function encryptKeyWithPublicKey(key, publicKey) {
const encryptedKey = crypto.publicEncrypt(
{ key: publicKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING },
key
);
return encryptedKey.toString('base64');
}

// 加密数据
function encryptData(data, key, iv) {
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
return encrypted.toString('hex');
}

// 发送数据
const encryptedKey = encryptKeyWithPublicKey(key, serverPublicKey);
const encryptedData = encryptData(userSensitiveData, key, iv);

fetch('/aggregate', {
method: 'POST',
body: JSON.stringify({
encryptedKey: encryptedKey,
iv: iv.toString('hex'),
encryptedData: encryptedData
})
});


服务端收到后用自己的私钥解密出AES密钥,再用这个密钥解密数据。这种方式能有效防止中间人攻击,因为公钥可以公开,但私钥只在服务端保存。

另外提醒一下,AES密钥每次请求都要动态生成,别复用!不然安全性会大打折扣。希望这些建议能帮到你,有问题咱们再交流!
点赞 5
2026-02-14 07:00