Nodejs AES-256-GCM 通过 webcrypto api 解密加密的客户端消息
Posted
技术标签:
【中文标题】Nodejs AES-256-GCM 通过 webcrypto api 解密加密的客户端消息【英文标题】:Nodejs AES-256-GCM decrypt the encrypted client message by webcrypto api 【发布时间】:2021-11-07 08:32:15 【问题描述】:我已经通过 AES-256-GCM 算法通过客户端中的密钥加密了我的文本,我可以在客户端中解密它,但是当我将它发送到具有 SharedKey
的后端时(与客户端相同) ),它可以通过AES-256-CTR算法解密消息(我使用了这个算法,因为Nodejs中的AES-256-GCM需要authTag
,我没有在客户端创建它,iv
是我唯一的东西有)。
当我在后端解密消息时,它可以正常工作,但结果不是我在客户端加密的结果
这是我写的: 客户:
async function encrypt(text: string)
const encodedText = new TextEncoder().encode(text);
const aesKey = await generateAesKey();
const iv = window.crypto.getRandomValues(
new Uint8Array(SERVER_ENCRYPTION_IV_LENGTH)
);
const encrypted = await window.crypto.subtle.encrypt(
name: 'AES-GCM',
iv,
,
aesKey,
encodedText
);
const concatenatedData = new Uint8Array(
iv.byteLength + encrypted.byteLength
);
concatenatedData.set(iv);
concatenatedData.set(new Uint8Array(encrypted), iv.byteLength);
return arrayBufferToBase64(concatenatedData),
后端:
export function decrypt(sharedKey: string, message: string)
const messageBuffer = new Uint8Array(base64ToArrayBuffer(message));
const iv = messageBuffer.subarray(0, 16);
const data = messageBuffer.subarray(16);
const decipher = crypto.createDecipheriv(
'aes-256-ctr',
Buffer.from(sharedKey, 'base64'),
iv
);
const decrypted =
decipher.update(data, 'binary', 'hex') + decipher.final('hex');
return Buffer.from(decrypted, 'hex').toString('base64');
示例用法:
const encrypted = encrypt("Hi Everybody");
// send the encrypted message to the server
// Response is: Ô\tp\x8F\x03$\f\x91m\x8B B\x1CkQPQ=\x85\x97\x8AêsÌG0¸Ê
【问题讨论】:
看来您已经从对称加密部分的其他(现已删除)问题中找到了错误。我在删除之前注意到,在 ECDH 部分中,在服务器端导入公钥时,密钥转换中存在另一个错误。可能你自己已经找到了问题所在。否则,您可以在新问题中发布 ECDH 部分以及共享密钥和 AES 密钥的计算。 【参考方案1】:由于 GCM 是基于 CTR 的,因此原则上也可以使用 CTR 进行解密。但是,这在实践中通常不应该这样做,因为它跳过了密文的认证,这是 GCM 相对于 CTR 的附加值。 正确的方法是在 NodeJS 端使用 GCM 解密并适当考虑身份验证标签。 身份验证标签由 WebCrypto API 自动附加到密文中,而 NodeJS 的加密模块分别处理密文和标签。因此,NodeJS端不仅要分离nonce,还要分离authentication标签。
以下 javascript/WebCrypto 代码演示了加密:
(async () =>
var nonce = crypto.getRandomValues(new Uint8Array(12));
var plaintext = 'The quick brown fox jumps over the lazy dog';
var plaintextEncoded = new TextEncoder().encode(plaintext);
var aesKey = base64ToArrayBuffer('a068Sk+PXECrysAIN+fEGDzMQ3xlpWgE1bWXHVLb0AQ=');
var aesCryptoKey = await crypto.subtle.importKey('raw', aesKey, 'AES-GCM', true, ['encrypt', 'decrypt']);
var ciphertextTag = await crypto.subtle.encrypt(name: 'AES-GCM', iv: nonce, aesCryptoKey, plaintextEncoded);
ciphertextTag = new Uint8Array(ciphertextTag);
var nonceCiphertextTag = new Uint8Array(nonce.length + ciphertextTag.length);
nonceCiphertextTag.set(nonce);
nonceCiphertextTag.set(ciphertextTag, nonce.length);
nonceCiphertextTag = arrayBufferToBase64(nonceCiphertextTag.buffer);
document.getElementById("nonceCiphertextTag").innerhtml = nonceCiphertextTag; // ihAdhr6595oyQ3koj52cnZp7VeB1fzWuY1v7vqFdSQGxK0VQxIXUegB1mVG4rC5Aymij7bQ9rmnFWbpo7C2znN4ROnnChB0=
)();
// Helper
// https://***.com/a/9458996/9014097
function arrayBufferToBase64(buffer)
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++)
binary += String.fromCharCode(bytes[i]);
return window.btoa(binary);
// https://***.com/a/21797381/9014097
function base64ToArrayBuffer(base64)
var binary_string = window.atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++)
bytes[i] = binary_string.charCodeAt(i);
return bytes.buffer;
<p style="font-family:'Courier New', monospace;" id="nonceCiphertextTag"></p>
此代码与您的代码基本相同,但由于您未发布的方法(如 generateAesKey()
或 arrayBufferToBase64()
)需要进行一些更改。
示例输出:
ihAdhr6595oyQ3koj52cnZp7VeB1fzWuY1v7vqFdSQGxK0VQxIXUegB1mVG4rC5Aymij7bQ9rmnFWbpo7C2znN4ROnnChB0=
以下 NodeJS/crypto 代码演示了解密过程。注意setAuthTag()
的标签分离和显式传递:
var crypto = require('crypto');
function decrypt(key, nonceCiphertextTag)
key = Buffer.from(key, 'base64');
nonceCiphertextTag = Buffer.from(nonceCiphertextTag, 'base64');
var nonce = nonceCiphertextTag.slice(0, 12);
var ciphertext = nonceCiphertextTag.slice(12, -16);
var tag = nonceCiphertextTag.slice(-16); // Separate tag!
var decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
decipher.setAuthTag(tag); // Set tag!
var decrypted = decipher.update(ciphertext, '', 'utf8') + decipher.final('utf8');
return decrypted;
var nonceCiphertextTag = 'ihAdhr6595oyQ3koj52cnZp7VeB1fzWuY1v7vqFdSQGxK0VQxIXUegB1mVG4rC5Aymij7bQ9rmnFWbpo7C2znN4ROnnChB0=';
var key = 'a068Sk+PXECrysAIN+fEGDzMQ3xlpWgE1bWXHVLb0AQ=';
var decrypted = decrypt(key, nonceCiphertextTag);
console.log(decrypted);
输出:
The quick brown fox jumps over the lazy dog
为了完整性:通过在 12 个字节的 nonce (0x00000002) 上附加 4 个字节,也可以使用 CTR 解密 GCM 密文。对于其他随机数大小,关系更复杂,请参见例如Relationship between AES GCM and AES CTR。然而,正如已经说过的,这在实践中不应该这样做,因为它绕过了密文的认证,因此是不安全的。
【讨论】:
哇,它成功了。您可以在后端的加密阶段帮助我吗?我应该为此创建另一个问题吗? 如果我在 Client 中加密一个 Text 并将它发送到 Backend,它可以解密它,但是如果我在 Backend 中加密一条消息,则 Client 无法解密它。 @AliTorki - 在 SO 上,在已经回答完问题后,像您所做的那样扩展问题并不常见。它经常使帖子混乱,使后续读者难以理解问题。因此,请将您的问题回滚到以前的状态,并将您的新问题放在新帖子中。如有必要,您可以链接到此帖子。谢谢。 你说得对,我又问了一个问题,感谢您的帮助,非常感谢:***.com/questions/69142812/… @AliTorki - 即使使用更长更复杂的明文,我也无法重现此问题。如果您可以提供数据来重建问题,那将会很有帮助:明文、密文和密钥。您还应该检查 unmodified 密文和 same 密钥是否应用于解密端。以上是关于Nodejs AES-256-GCM 通过 webcrypto api 解密加密的客户端消息的主要内容,如果未能解决你的问题,请参考以下文章
使用 Java 的 AES-256-GCM 解密中的标签不匹配错误
跨平台AES 256 GCM Javascript和Elixir
有没有办法过滤 aes 256 gcm 加密数据库中的数据?
golang 在golang中使用AES256 GCM加密文本