AES GCM 使用 web subtlecrypto 加密并使用 flutter cryptography 解密

AES GCM encrypt with web subtlecrypto and decrypt with flutter cryptography

我正在尝试使用 SubtleCrypto and decrypt it in flutter with cryptography 加密网络扩展中的内容。我想使用密码来加密消息,将其发送到应用程序并使用相同的密码对其进行解密。为此,我将 AES GCM 与 pbkdf2

一起使用

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

我在术语方面也有问题。 SubtleCrypto 使用 iv、salt 和 tags 而 flutter cryptography 使用 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, 0, 193, 176]);
  const iv = new Uint8Array([198, 0, 92, 253, 0, 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',
      salt,
      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(),
    iterations: 100000,
    bits: 128,
  );

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

  // A random salt 
  final salt = [0, 72, 16, 170, 232, 145, 179, 47, 241, 92, 75, 146, 25, 0, 193, 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 代码按照 ciphertext | 顺序将 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代码中,PBKDF2调用中的密钥大小必须相应地应用256位。

  • 另外,由于decrypt()包含异步方法调用,所以必须用async关键字标记。

通过这些更改,decrypt() 在我的机器上工作,returns value 用于来自 WebCryptoAPI 代码的数据:

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, 0, 193, 176]);
    const iv = new Uint8Array([198, 0, 92, 253, 0, 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, salt, iv) {
    const keyMaterial = await getKeyMaterial();
    const key = await window.crypto.subtle.deriveKey(
        {
            name: 'PBKDF2',
            salt,
            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

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

实际上SecretBox提供了方法fromConcatenation(),它应该分离随机数、密文和标签的串联。但是,此实现 returns(至少在早期版本中)是一个损坏的密文,这可能是一个错误。


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

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

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

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