Cryptonoob 尝试在 .NET 上加密并在 Javascript 上解密

Cryptonoob tries to encrypt on .NET and decrypt on Javascript

对于特定的应用程序,我需要在我的 .NET 服务器上对称加密并在浏览器中解密。

我通常可以自由选择算法,所以我尝试了 AES-GCM,因为它在 .NET 上具有更好的内置 API,并且也受 crypto.subtle 支持。

虽然我没有让它工作,但我对从 crypto.subtle.decrypt 的调用中得到一个无用的异常感到难过,它不包含关于 Chrome 的消息并说“操作失败出于特定操作的原因”在 Firefox 上。

解密密码为(也here in codesandbox):

import "./styles.css";
import { Base64 } from "js-base64";

let nonce = Base64.toUint8Array("o/YcD/yZVU2egcGd");

async function importKey() {
  const keyData = Base64.toUint8Array("3NraMtQP10qKGL3HLloObA==");

  const key = await crypto.subtle.importKey(
    "raw",
    keyData,
    { name: "AES-GCM" },
    true,
    ["decrypt", "encrypt"]
  );

  return key;
}

var cypherText = Base64.toUint8Array("Is+l7cojlfbuU3vUN0gWMw==");

async function decrypt() {
  const key = await importKey();
  try {
    return await crypto.subtle.decrypt(
      { name: "AES-GCM", iv: nonce },
      key,
      cypherText
    );
  } catch (ex) {
    console.error("Error: " + ex.message);
  }
}

async function work() {
  const decrypted = await decrypt();

  const result = new TextDecoder().decode(decrypted);

  document.getElementById("app").innerText = result;
}

work();

不完全确定 .NET 调用的 nonce 是不是 JS 调用的 iv。

在任何情况下,总是会到达 catch 处理程序。

为了比较,生成密文的 .NET 代码是(也是 here as a LINQPad query):

AesGcm.NonceByteSizes.Dump();
AesGcm.TagByteSizes.Dump();

var key = Guid.Parse("32dadadc-0fd4-4ad7-8a18-bdc72e5a0e6c")
    .ToByteArray()
    .ToArray();

var nonce = Guid.Parse("0f1cf6a3-99fc-4d55-9e81-c19d09003e9b")
    .ToByteArray()
    .Take(12)
    .ToArray();

Convert.ToBase64String(key).Dump("key");

var aes = new AesGcm(key);

Convert.ToBase64String(nonce).Dump("nonce");

var text = Encoding.UTF8.GetBytes("Hello, world 123");

text.Length.Dump("cypher text size");

var buffer = new Byte[text.Length];
var tag = new Byte[16];

aes.Encrypt(nonce, text, buffer, tag, null);

String.Join(" ", from b in buffer select b.ToString("d")).Dump("cypher text");

Convert.ToBase64String(buffer).Dump("cypher text");

var text2 = new Byte[text.Length];

aes.Decrypt(nonce, buffer, tag, text2, null);

Encoding.UTF8.GetString(text2).Dump("check");

在.NET代码中,密文和标签是分开处理的,而在JavaScript代码中,两者必须连在一起处理:ciphertext | tag.

在 .NET 代码中生成的身份验证标记根本没有应用到 JavaScript 代码中,它单独阻止了解密。

此外,我无法用 .NET 代码重现 JavaScript 代码中使用的密文。然而,密钥和随机数是可以复制的。当我 运行 .NET 代码时,我得到以下数据(Base64 编码):

nonce:      o/YcD/yZVU2egcGd
key:        3NraMtQP10qKGL3HLloObA==
ciphertext: 1dupqLQFLXe31Pq48udCFw==
tag:        kfMFJS+cy4VoDuFX1t7Reg==

如果JavaScript代码中使用了正确的密文,并且将密文和tag拼接,则解密成功:

// Concatenate ciphertext and tag!
const ciphertext = Base64.toUint8Array("1dupqLQFLXe31Pq48udCFw==");
const tag = Base64.toUint8Array("kfMFJS+cy4VoDuFX1t7Reg==");
const ciphertextTag = new Uint8Array(ciphertext.length + tag.length);
ciphertextTag.set(ciphertext);
ciphertextTag.set(tag, ciphertext.length);

let nonce = Base64.toUint8Array("o/YcD/yZVU2egcGd");

async function importKey() {
    const keyData = Base64.toUint8Array("3NraMtQP10qKGL3HLloObA==");
    const key = await crypto.subtle.importKey(
        "raw",
        keyData,
        { name: "AES-GCM" },
        true,
        ["decrypt", "encrypt"]
    );
    return key;
}

async function decrypt() {
    const key = await importKey();
    try {
        return await crypto.subtle.decrypt(
            { name: "AES-GCM", iv: nonce },
            key,
            ciphertextTag // Use the concatenated data!
        );
    } catch (ex) {
        console.error("Error: " + ex.message);
    }
}

async function work() {
    const decrypted = await decrypt();
    const result = new TextDecoder().decode(decrypted);
    console.log(result);
}

work(); 
<script src="https://cdn.jsdelivr.net/npm/js-base64@3.2.4/base64.min.js"></script>

请注意,出于安全原因,密钥/随机数对只能使用一次(参见 GCM / Security)。通常为每次加密创建一个新的随机随机数。由于 nonce 不是秘密的,它通常放在密文之前:nonce | ciphertext | tag。这被发送给接收者,接收者将随机数分开(并取决于 API,标签),因此拥有解密所需的所有信息。