AES GCM 使用网络微妙加密加密并使用颤振加密解密

问题描述

我正在尝试使用 SubtleCrypto 加密 webextension 中的某些内容,并使用 cryptography 对其进行解密。我想使用密码来加密消息,将其发送到应用程序并使用相同的密码解密。为此,我使用带有 pbkdf2 的 AES GCM

我能够在 Mozilla 文档页面上找到一个加密片段。但是,我很难在颤振中解密它。

我也遇到了术语问题。 SubtleCrypto 使用 iv、salt 和标签,而 Flutter 加密使用 nonce 和 mac。

Javascript 代码

test(){
  // const salt = window.crypto.getRandomValues(new Uint8Array(16));
  // const iv = window.crypto.getRandomValues(new Uint8Array(12));
  const salt = new Uint8Array([0,72,16,170,232,145,179,47,241,92,75,146,25,193,176]);
  const iv = new Uint8Array([198,253,245,140,79,236,215,255,0]);

  console.log('salt: ',salt);
  console.log('iv: ',iv);
  console.log('salt: ',btoa(String.fromCharCode(...salt)));
  console.log('iv: ',btoa(String.fromCharCode(...iv)));

  this.encrypt('value',salt,iv).then(x => console.log('got encrypted: ',x));
}

getKeyMaterial(): Promise<CryptoKey> {
  const password = 'key';
  const enc = new TextEncoder();
  return window.crypto.subtle.importKey(
    'raw',enc.encode(password),'PBKDF2',false,['deriveBits','deriveKey']
  );
}

async encrypt(plaintext: string,salt: Uint8Array,iv: Uint8Array): Promise<string> {
  const keyMaterial = await this.getKeyMaterial();
  const key = await window.crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',iterations: 100000,hash: 'SHA-256'
    },keyMaterial,{ name: 'aes-gcm',length: 256},true,[ 'encrypt','decrypt' ]
  );

  const encoder = new TextEncoder();
  const tes = await window.crypto.subtle.encrypt(
    {
      name: 'aes-gcm',iv
    },key,encoder.encode(plaintext)
  );

  return btoa(String.fromCharCode(...new Uint8Array(tes)));
}

颤动飞镖代码

void decrypt(){
final algorithm = AesGcm.with256bits();

final encrypted = base64Decode('1MdEsqwqh4bUTlfpIk12SeziA9Pw');

final secretBox = SecretBox.fromConcatenation(encrypted,nonceLength: 12,macLength: 0);

// // Encrypt
final data = await algorithm.decrypt(
  secretBox,secretKey: await getKey(),);


String res = utf8.decode(data);
}

Future<SecretKey> getKey() async{
  final pbkdf2 = Pbkdf2(
    macAlgorithm: Hmac.sha256(),bits: 128,);

  // Password we want to hash
  final secretKey = SecretKey(utf8.encode('key'));

  // A random salt 
  final salt = [0,176];

  // Calculate a hash that can be stored in the database
  final newSecretKey = await pbkdf2.deriveKey(
    secretKey: secretKey,nonce: salt,);

  return Future<SecretKey>.value(newSecretKey);
}

我做错了什么?

解决方法

Dart 代码中存在以下问题:

  • WebCryptoAPI 代码按照密文| 密文的顺序将GCM 标记与密文连接起来。标记。在 Dart 代码中,这两个部分必须相应地分开。
    此外,在 Dart 代码中,没有考虑 nonce/IV。 decrypt() 的可能修复方法是:
   //final secretBox = SecretBox.fromConcatenation(encrypted,nonceLength: 12,macLength: 0);
   Uint8List ciphertext  = encrypted.sublist(0,encrypted.length - 16);
   Uint8List mac = encrypted.sublist(encrypted.length - 16);
   Uint8List iv = base64Decode('xgBc/QD1jE/s1/8A'); // should als be concatenated,e.g. iv | ciphertext | tag
   SecretBox secretBox = new SecretBox(ciphertext,nonce: iv,mac: new Mac(mac));
  • 此外,WebCryptoAPI 代码使用 AES-256,因此在 getKey() 中的 Dart 代码中,必须相应地应用 256 位作为 PBKDF2 调用中的密钥大小。

  • 此外,由于 decrypt() 包含异步方法调用,因此必须使用 async 关键字进行标记。

通过这些更改,decrypt() 可以在我的机器上运行并为来自 WebCryptoAPI 代码的数据返回 value

function test(){
    // const salt = window.crypto.getRandomValues(new Uint8Array(16));
    // const iv = window.crypto.getRandomValues(new Uint8Array(12));
    const salt = new Uint8Array([0,72,16,170,232,145,179,47,241,92,75,146,25,193,176]);
    const iv = new Uint8Array([198,253,245,140,79,236,215,255,0]);

    console.log('salt: ',salt);
    console.log('iv:   ',iv);
    console.log('salt:         ',btoa(String.fromCharCode(...salt)));
    console.log('iv:           ',btoa(String.fromCharCode(...iv)));

    encrypt('value',salt,iv).then(x => console.log('got encrypted:',x));
}


function getKeyMaterial() {
    const password = 'key';
    const enc = new TextEncoder();
    return window.crypto.subtle.importKey(
        'raw',enc.encode(password),'PBKDF2',false,['deriveBits','deriveKey']
    );
}


async function encrypt(plaintext,iv) {
    const keyMaterial = await getKeyMaterial();
    const key = await window.crypto.subtle.deriveKey(
        {
            name: 'PBKDF2',iterations: 100000,hash: 'SHA-256'
        },keyMaterial,{ name: 'AES-GCM',length: 256},true,[ 'encrypt','decrypt' ]
    );

    const encoder = new TextEncoder();
    const tes = await window.crypto.subtle.encrypt(
        {
            name: 'AES-GCM',iv
        },key,encoder.encode(plaintext)
    );

    return btoa(String.fromCharCode(...new Uint8Array(tes)));
}

test();

salt:          AEgQquiRsy/xXEuSGQDBsA== 
iv:            xgBc/QD1jE/s1/8A 
got encrypted: 1MdEsqwqh4bUTlfpIk12SeziA9Pw

请注意,静态随机数/IV 和盐通常是不安全的(当然,出于测试目的,这很好)。通常,它们是为每个加密/密钥派生随机生成的。由于 salt 和 nonce/IV 不是秘密的,它们通常与密文和标签连接,例如盐 |随机数 |密文|标签,并在收件人端分开。

实际上 SecretBox 提供了 fromConcatenation() 方法,它应该将 nonce、密文和标签的串联分开。然而,这个实现返回(至少在早期版本中)一个损坏的密文,这可能是一个错误。


关于 GCM 和 PBKDF2 上下文中的术语 nonce/IV、salt 和 MAC/tag:

GCM 模式使用 12 字节的随机数,在 WebCryptoAPI(有时在其他库中)称为 IV,s。 here。 PBKDF2 在密钥推导中应用了盐,在 Dart 中称为 nonce。

命名nonce是合适的,一个IV(与相同的密钥组合)和一个salt(与相同的密码组合)只能使用一次。前者对于 GCM 安全性尤其重要。 here

MAC 和标签是 GCM 身份验证标签的同义词。