【问题标题】:Encrypt with 'window.crypto.subtle', decrypt in c#用 \'window.crypto.subtle\' 加密,用 c# 解密
【发布时间】:2022-12-03 12:51:06
【问题描述】:

我想用window.crypto.subtle加密,用C#解密。

js 中的 crypt / decrypt 正在运行。

在 C# 中,计算的身份验证标记与输入不匹配。

我不知道我是否可以将任何 12 个字节作为盐,也不知道我是否需要导出密码。

export async function deriveKey(password, salt) {
  const buffer = utf8Encoder.encode(password);
  const key = await crypto.subtle.importKey(
    'raw',
    buffer,
    { name: 'PBKDF2' },
    false,
    ['deriveKey'],
  );

  const privateKey = crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',
      hash: { name: 'SHA-256' },
      iterations,
      salt,
    },
    key,
    {
      name: 'AES-GCM',
      length: 256,
    },
    false,
    ['encrypt', 'decrypt'],
  );

  return privateKey;
}
const buff_to_base64 = (buff) => btoa(String.fromCharCode.apply(null, buff));
const base64_to_buf = (b64) => Uint8Array.from(atob(b64), (c) => c.charCodeAt(null));

export async function encrypt(key, data) {
  const salt = crypto.getRandomValues(new Uint8Array(12));
  const iv = crypto.getRandomValues(new Uint8Array(12));

  console.log('encrypt');
  console.log('iv', iv);
  console.log('salt', salt);

  const buffer = new TextEncoder().encode(data);

  const privatekey = await deriveKey(key, salt);

  const encrypted = await crypto.subtle.encrypt(
    {
      name: 'AES-GCM',
      iv,
      tagLength: 128,
    },
    privatekey,
    buffer,
  );

  const bytes = new Uint8Array(encrypted);
  console.log('concat');

  const buff = new Uint8Array(iv.byteLength + encrypted.byteLength + salt.byteLength);
  buff.set(iv, 0);
  buff.set(salt, iv.byteLength);
  buff.set(bytes, iv.byteLength + salt.byteLength);

  console.log('iv', iv);
  console.log('salt', salt);
  console.log('buff', buff);

  const base64Buff = buff_to_base64(buff);
  console.log(base64Buff);
  return base64Buff;
}

export async function decrypt(key, data) {
  console.log('decryption');
  console.log('buff', base64_to_buf(data));

  const d = base64_to_buf(data);
  const iv = d.slice(0, 12);
  const salt = d.slice(12, 24);
  const ec = d.slice(24);

  console.log('iv', iv);
  console.log('salt', salt);
  console.log(ec);

  const decrypted = await window.crypto.subtle.decrypt(
    {
      name: 'AES-GCM',
      iv,
      tagLength: 128,
    },
    await deriveKey(key, salt),
    ec,
  );

  return new TextDecoder().decode(new Uint8Array(decrypted));
}
Span<byte> encryptedData = Convert.FromBase64String(enc).AsSpan();
Span<byte> nonce = encryptedData[..12];
Span<byte> salt = encryptedData.Slice(12, 12);
Span<byte> data = encryptedData.Slice(12 + 12, encryptedData.Length - 16 - 12 - 12);
Span<byte> tag = encryptedData[^16..];

Span<byte> result = new byte[data.Length];

using Rfc2898DeriveBytes pbkdf2 = new(Encoding.UTF8.GetBytes(password), salt.ToArray(), 1000, HashAlgorithmName.SHA256);
using AesGcm aes = new(pbkdf2.GetBytes(16));

aes.Decrypt(nonce, data, tag, result);

【问题讨论】:

  • 关于 PBKDF2 的迭代似乎有所不同。此外,在 C# 代码中,数据确定不正确(Slice() 的第一个参数应该是 12 而不是 11,或者使用范围运算符 [12..^16] 而不是 Slice(),至于 nonce 和 @ 987654331@)。
  • 以为这是一个索引..谢谢!
  • 此外,密钥大小似乎不同,盐应该与密文连接在一起,如 IV(也应该是随机的)。
  • const salt= crypto.getRandomValues(new Uint8Array(12));不见了。我会尝试传递盐。
  • 两种代码都派生出不同长度的密钥,因此应用不同的 AES 变体,因此不兼容。我已经在我的回答中更详细地描述了这一点。

标签: c# cryptography aes-gcm subtlecrypto window.crypto


【解决方案1】:

两种代码都存在一些不一致和/或小缺陷。关于 JavaScript 代码:

  • 盐应该像 IV 一样与密文/标签连接(密文/标签 = 实际密文和标签的隐式连接),例如盐|IV|密文|标签。 IV 应该像盐一样随机生成。
  • 在两个代码中,相同的迭代计数必须用于 PBKDF2 的密钥派生,例如25000(在实践中,该值应设置得尽可能高,同时保持可接受的性能)。
  • 在这两种代码中,PBKDF2 密钥派生必须生成相同长度的 AES 密钥,以便使用相同的 AES 变体,例如AES-256 的 32 字节密钥。

通过这些更改,JavaScript 代码

(async () => {

    const utf8Encoder = new TextEncoder('utf-8');
    const salt = crypto.getRandomValues(new Uint8Array(16)); // Fix 1: consider salt
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const iterations = 25000; // Fix 2: apply the same iteration count

    async function deriveKey(password) {
        const buffer = utf8Encoder.encode(password);
        const key = await crypto.subtle.importKey(
            'raw',
            buffer,
            { name: 'PBKDF2' },
            false,
            ['deriveKey'],
        );

        const privateKey = crypto.subtle.deriveKey(
            {
                name: 'PBKDF2',
                hash: { name: 'SHA-256' },
                iterations,
                salt,
            },
            key,
            {
                name: 'AES-GCM',
                length: 256, // Fix 3: use the same key size
            },
            false,
            ['encrypt', 'decrypt'],
        );

        return privateKey;
    }
    
    const buff_to_base64 = (buff) => btoa(String.fromCharCode.apply(null, buff));
    const base64_to_buf = (b64) => Uint8Array.from(atob(b64), (c) => c.charCodeAt(null));

    async function encrypt(key, data, iv, salt) {
        const buffer = new TextEncoder().encode(data);

        const privatekey = await deriveKey(key);
        const encrypted = await crypto.subtle.encrypt(
            {
                name: 'AES-GCM',
                iv,
                tagLength: 128,
            },
            privatekey,
            buffer,
        );

        const bytes = new Uint8Array(encrypted);
        let buff = new Uint8Array(salt.byteLength + iv.byteLength + encrypted.byteLength);
        buff.set(salt, 0); // Fix 1: consider salt
        buff.set(iv, salt.byteLength);
        buff.set(bytes, salt.byteLength + iv.byteLength);

        const base64Buff = buff_to_base64(buff);
        return base64Buff;
    }

    async function decrypt(key, data) {
        const d = base64_to_buf(data);
        const salt = d.slice(0, 16); // Fix 1: consider salt
        const iv = d.slice(16, 16 + 12)
        const ec = d.slice(16 + 12);

        const decrypted = await window.crypto.subtle.decrypt(
            {
                name: 'AES-GCM',
                iv,
                tagLength: 128,
            },
            await deriveKey(key),
            ec
        );

        return new TextDecoder().decode(new Uint8Array(decrypted));
    }

    var data = 'The quick brown fox jumps over the lazy dog';
    var passphrase = 'my passphrase';
    var ct = await encrypt(passphrase, data, iv, salt);
    var dt = await decrypt(passphrase, ct);
    console.log(ct);
    console.log(dt);

})();

回报,例如:

P/y3nrZU70XtanEUvubyVUp+LzOVHLGAl55cd+N6T0c9ak15KVXh5UxFEjMYGsvGWzf286wAGc5HgEjmwxWCkdjSt5vt42Anb4jwKlVMdLyYoP9Gg/be

C#代码中salt、IV、ciphertext/tag必须正确分离,必须使用JavaScript代码的keysize和iteration count:

string ciphertext = "P/y3nrZU70XtanEUvubyVUp+LzOVHLGAl55cd+N6T0c9ak15KVXh5UxFEjMYGsvGWzf286wAGc5HgEjmwxWCkdjSt5vt42Anb4jwKlVMdLyYoP9Gg/be";
Span<byte> encryptedData = Convert.FromBase64String(ciphertext).AsSpan();
Span<byte> salt = encryptedData[..16]; // Fix 1: consider salt (and apply the correct parameters)
Span<byte> nonce = encryptedData[16..(16 + 12)];  
Span<byte> data = encryptedData[(16 + 12)..^16]; 
Span<byte> tag = encryptedData[^16..];

string password = "my passphrase";
using Rfc2898DeriveBytes pbkdf2 = new(Encoding.UTF8.GetBytes(password), salt.ToArray(), 25000, HashAlgorithmName.SHA256); // Fix 2: apply the same iteration count

using AesGcm aes = new(pbkdf2.GetBytes(32)); // Fix 3: use the same key size (e.g. 32 bytes for AES-256)
Span<byte> result = new byte[data.Length];
aes.Decrypt(nonce, data, tag, result);

Console.WriteLine(Encoding.UTF8.GetString(result)); // The quick brown fox jumps over the lazy dog

然后用C#代码就可以成功解密JavaScript代码的密文了。

【讨论】:

  • pbkdf2.GetBytes(32) 做到了!非常感谢您的帮助!
猜你喜欢
  • 1970-01-01
  • 2013-03-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多