当前位置: 首页 > 知识库问答 >
问题:

用浏览器webcrypto api实现Nodejs AES-256-GCM客户端加解密

方昊
2023-03-14

我在客户端生成一对公钥/私钥,并将publickey发送到服务器,后端将在其一侧生成sharedkey,并响应我一个publickey,这也帮助我在客户端生成一个sharedkey,用于加密/解密。因此,我在Nodejs上用AES-256-GCM加密消息,并在客户端上解密消息。

后端:

export function encrypt(sharedKey: string, message: string) {
  const firstIv = getRandomIV();
  const cipher = crypto.createCipheriv(
    'aes-256-gcm',
    Buffer.from(sharedKey, 'base64'),
    firstIv
  );

  const encrypted = cipher.update(message, 'utf8');

  return Buffer.from(encrypted + cipher.final()).toString('base64');
}
function getRandomIV() {
  return crypto.randomBytes(12);
}

客户端:

async function decrypt(encryptedData: Uint8Array) {
    const aesKey = await generateAesKey();
    const nonce = encryptedData.subarray(0, SERVER_ENCRYPTION_IV_LENGTH);
    const data = encryptedData.subarray(SERVER_ENCRYPTION_IV_LENGTH);

    const decrypted = await crypto.subtle.decrypt(
      {
        name: 'AES-GCM',
        iv: nonce,
      },
      aesKey,
      data
    );
    return {
      decrypted: new Uint8Array(decrypted),
      decryptedString: new TextDecoder().decode(decrypted),
    };
  }

async function generateAesKey() {
    const publicKey = await getServerPublicKey();
    const privateKey = await getPrivateKey();
    const sharedSecret = await crypto.subtle.deriveBits(
      {
        name: 'ECDH',
        public: publicKey!,
      },
      privateKey,
      256
    );

    const aesSecret = await crypto.subtle.digest('SHA-256', sharedSecret);
    return crypto.subtle.importKey('raw', aesSecret, 'AES-GCM', true, [
      'encrypt',
      'decrypt',
    ]);
  }

现在,我无法解密客户端中的服务器加密响应,并且遇到domexception错误,我不知道为什么?

共有1个答案

曾丰茂
2023-03-14

GCM使用由NodeJS/crypto单独处理的身份验证标记,而WebCrypto自动将其与密文连接起来。
因此,在NodeJS代码中,必须显式地确定标记并将其附加到密文。这在当前的NodeJS代码中是缺失的,可以考虑如下。注意用cipher.getAuthTag()及其连接确定标记:

var crypto = require('crypto');

function encrypt(key, plaintext) {
  
    var nonce = getRandomIV();
    var cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
    var nonceCiphertextTag = Buffer.concat([
        nonce, 
        cipher.update(plaintext), 
        cipher.final(), 
        cipher.getAuthTag() // Fix: Get tag with cipher.getAuthTag() and concatenate: nonce|ciphertext|tag
    ]); 
    return nonceCiphertextTag.toString('base64');
}

function getRandomIV() {
    return crypto.randomBytes(12);
}

var message = Buffer.from('The quick brown fox jumps over the lazy dog', 'utf8');
var sharedKey = Buffer.from('MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=', 'base64');
var ciphertext = encrypt(sharedKey, message);
console.log(ciphertext); // wRE5KM6FG81QSMNvG0xR+iaIeF77cyyeBceGS5NkcYaD17K9nL0/helnqRBOkD9pLVoWM/nRAcaKg/YdvfNJcO1Zn/7ZM0k=

一个可能的输出是

wRE5KM6FG81QSMNvG0xR+iaIeF77cyyeBceGS5NkcYaD17K9nL0/helnqRBOkD9pLVoWM/nRAcaKg/YdvfNJcO1Zn/7ZM0k=

以下用于在WebCrypto端解密的代码基本上基于您的代码(没有从共享秘密派生密钥,这与当前问题无关):

(async () => {

    var nonceCiphertextTag = base64ToArrayBuffer('wRE5KM6FG81QSMNvG0xR+iaIeF77cyyeBceGS5NkcYaD17K9nL0/helnqRBOkD9pLVoWM/nRAcaKg/YdvfNJcO1Zn/7ZM0k=');
    var nonceCiphertextTag = new Uint8Array(nonceCiphertextTag);
    var decrypted = await decrypt(nonceCiphertextTag);
    console.log(decrypted); // The quick brown fox jumps over the lazy dog
})();

async function decrypt(nonceCiphertextTag) {
    
    const SERVER_ENCRYPTION_IV_LENGTH = 12; // For GCM a nonce length of 12 bytes is recommended!
    var nonce = nonceCiphertextTag.subarray(0, SERVER_ENCRYPTION_IV_LENGTH);
    var ciphertextTag = nonceCiphertextTag.subarray(SERVER_ENCRYPTION_IV_LENGTH);

    var aesKey = base64ToArrayBuffer('MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=');
    aesKey = await window.crypto.subtle.importKey('raw', aesKey, 'AES-GCM', true, ['encrypt', 'decrypt']);
    var decrypted = await crypto.subtle.decrypt({name: 'AES-GCM', iv: nonce}, aesKey, ciphertextTag);
    return new TextDecoder().decode(decrypted);
}

// Helper

// https://stackoverflow.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;
}
 类似资料:
  • 一面-2023年5月6日 ios客户端,大前端、object-c 自我介绍。有点背稿的感觉。 聊一个项目。说了OSG的项目。提到内存管理。 开始八股。内存:只能指针。 多态,静态、动态。模板、虚函数。静态的除了模板还有啥。析构函数为什么要添加虚函数。 空类size的大小。 struct/class的区别。 堆/栈/BSS几个内存类别 TCP四次挥手。 ipv4地址空间这么小怎么够用,NAT。用同一

  • 二面-2023年5月8日 第一次迟到了面试.. 自我介绍。刚开始没准备各种结巴,语言不顺。整体还是讲完了。 项目。疯狂问项目、但是又不是挖,就是让自己讲。讲了好几个项目。看起来不是特别满意。一个是我自己准备项目拿普通横向项目准备的亮点肯定不够不到位,另外岗位是客户端,没有什么相关的。 岗位匹配度上,问了好几遍和原技术栈不匹配,自己的个人想法。(一开始也是你捞的我,我有啥想法 手撕算法。快排。这两面

  • 本文向大家介绍php实现读取手机客户端浏览器的类,包括了php实现读取手机客户端浏览器的类的使用技巧和注意事项,需要的朋友参考一下 本文实例讲述了php实现读取手机客户端浏览器的类。分享给大家供大家参考。具体分析如下: 这里介绍的手机信息函数类有取手机号码,浏览器报头信息,取得手机类型,取得手机IP等功能。 希望本文所述对大家的php程序设计有所帮助。

  • 免责声明:我知道这个问题有重复(一、二、三),但是: null 我的代码: 这将打印: (所有3个请求返回200个http代码) 我有JVM选项: (在我的keystore.jks中添加了vk.com证书) 下面是http-client的内部日志 当然,上面的测试是在关闭内部日志记录的情况下执行的,因为它会给每个请求增加大约50毫秒的额外时间。 我已经读了三遍所有的apache http-clie

  • Spring Security文档显示: “当您使用CSRF保护时?我们建议对正常用户可以通过浏览器处理的任何请求使用CSRF保护。如果您只创建非浏览器客户端使用的服务,则可能需要禁用CSRF保护。” 如果我的服务将被"浏览器"和"非浏览器"客户端(如第三方外部服务)使用,Spring Security是否提供了一种仅针对某些类型的客户端禁用CSRF的方法?

  • 我知道这是不好的做法和特征识别应该是建设网站的方式。然而,这不是我的用例。 我有我的浏览器扩展的不同发行版,我想根据他们当前的浏览器更改下载按钮。 我试过使用,事实证明这是非常没用的,因为大多数浏览器都设置了所有流行的用户代理。例如chrome就有这个。 我见过很多网站的下载按钮上都有这个功能。如何做到这一点呢? 编辑:我现在了解了为什么“mozzila/x.x”位于userAgent字符串开头的