如何在 SJCL 图书馆解密 AES SubtleCrypto Web API

How to Decrypt AES SubtleCrypto Web API at SJCL Library

我们有一个使用加密的 Expo React Native 项目。我们目前的加密是基于SubtleCrypto / Web API [window.subtle.crypto],使用AES-GCM 128,现在我们需要使用一个在所有平台上通用的库[Web, iOS Android],根据我之前的问题,我们发现 SJCL 支持 GCM 模式,我们可以完全替换所有基于 Web 的代码,但挑战在于我们需要确保所有当前的加密数据都是在这个新库也解密了,我们必须这样做:

window.crypto.subtle.encrypt [AES-GCM 128] => (a) ---> SJCL.mode.gcm.decrypt(a)

一旦我们成功做到这一点,我们就可以完全替换库并拥有通用平台支持和向后兼容性。

这意味着我们不能完全改变加密的处理方式,因为这是要求,我们将完全按照下面的代码对其进行加密。

我得到了 Neneil94 a very good lead here,但我仍然面临编码/格式方面的问题;这是当前代码:

function arrayBufferToString(buffer){
    var str = "";
    for (var iii = 0; iii < buffer.byteLength; iii++){
        str += String.fromCharCode(buffer[iii]);
    }
    return str;
}

const generateKey = async () =>
{
    const key = await window.crypto.subtle.generateKey(
    {
        name: "AES-GCM",
        length: 128
    }, true, ["encrypt", "decrypt"]);
    const key_exported = await window.crypto.subtle.exportKey("jwk", key);
    return key_exported.k;
}

const text = "This is an encrypted message";
const printCurrent = async () =>
{
    let kkey = await generateKey();
    await window.crypto.subtle.importKey(
        "jwk",
        {
            k: kkey,
            alg: "A128GCM",
            ext: true,
            key_ops: ["encrypt", "decrypt"],
            kty: "oct",
        },
        {
            name: "AES-GCM",
            length: 128
        },
        false,
        ["encrypt", "decrypt"]
    ).then(function(key)
    {
        window.crypto.subtle.encrypt(
        {
            name: "AES-GCM",
            iv: new Uint8Array(12)
        }, key, new TextEncoder().encode(JSON.stringify(text))).then(function(encryptedObject)
        {
            console.log({kkey}); //{kkey: 'eKM_Cen2Z-jhedM284cltA'}

            let bkey = sjcl.codec.utf8String.toBits(kkey);
            console.log({bkey}); //{bkey: Array(6)}
            let cipher = new sjcl.cipher.aes(bkey);
            //let bdata = sjcl.codec.base64.toBits(encryptedObject); //gives error of a.replace
            let bdata = arrayBufferToString(encryptedObject);
            let ivvv = new Uint8Array(12);
            let ivv = Buffer.from(ivvv).toString('base64');
            let ive = sjcl.codec.base64.toBits(ivv);

            // decrypt
            let decbits = sjcl.mode.gcm.decrypt(cipher, bdata, ive);

            // convert into utf8string
            decryptedData = sjcl.codec.utf8String.fromBits(decbits);
        });
    });
}

我得到的错误是:

CORRUPT: gcm: tag doesn't match

我不确定上面链接的 Neneil 代码中的 aesKey 到底是什么,它是 CryptoKey 对象还是从 JsonWebKey 生成的 22 字符?对于 'encryptedObject' 我如何将其转换为 BitArray。对于 IV,我只是提供了 Encrypt 正在做的事情,但我也不确定..

尽管如此,任何见解都会很有帮助,感谢您的宝贵时间!

你的代码有两个问题:

  • kkey 是 Base64url 编码的原始密钥。这必须首先转换为 Base64,然后转换为 bitArray:
let kkeyB64 = kkey.replace(/-/g, '+').replace(/_/g, '/');   // Base64url -> Base64 (ignore optional padding)
let bkey = sjcl.codec.base64.toBits(kkeyB64);               // conert to bitArray
  • Ciphertext 和 IV 作为 ArrayBuffer 给出,因此使用 ArrayBuffer codec 是有意义的,例如:
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/sjcl@1.0.8/core/codecArrayBuffer.js"></script>
let bdata = sjcl.codec.arrayBuffer.toBits(encryptedObject)
let ive = sjcl.codec.arrayBuffer.toBits(new Uint8Array(12).buffer)

通过这些更改,使用 sjcl 解密成功。


完整代码:

const generateKey = async () =>
{
    const key = await window.crypto.subtle.generateKey(
    {
        name: "AES-GCM",
        length: 128
    }, true, ["encrypt", "decrypt"]);
    const key_exported = await window.crypto.subtle.exportKey("jwk", key);
    return key_exported.k;
}

const text = "This is an encrypted message";
const printCurrent = async () =>
{
    let kkey = await generateKey();
    await window.crypto.subtle.importKey(
        "jwk",
        {
            k: kkey,
            alg: "A128GCM",
            ext: true,
            key_ops: ["encrypt", "decrypt"],
            kty: "oct",
        },
        {
            name: "AES-GCM",
            length: 128
        },
        false,
        ["encrypt", "decrypt"]
    ).then(function(key)
    {
        window.crypto.subtle.encrypt(
        {
            name: "AES-GCM",
            iv: new Uint8Array(12)
        }, key, new TextEncoder().encode(JSON.stringify(text))).then(function(encryptedObject)
        {
            //console.log({kkey}); //{kkey: 'eKM_Cen2Z-jhedM284cltA'}

            let kkeyB64 = kkey.replace(/-/g, '+').replace(/_/g, '/'); // Fix 1
            let bkey = sjcl.codec.base64.toBits(kkeyB64); 
            //console.log({bkey}); //{bkey: Array(6)}
            let cipher = new sjcl.cipher.aes(bkey);
            
            
            let bdata = sjcl.codec.arrayBuffer.toBits(encryptedObject) // Fix 2
            let ive = sjcl.codec.arrayBuffer.toBits(new Uint8Array(12).buffer) // Fix 3
            
            // decrypt
            let decbits = sjcl.mode.gcm.decrypt(cipher, bdata, ive);

            // convert into utf8string
            decryptedData = sjcl.codec.utf8String.fromBits(decbits);
            document.getElementById("pt").innerHTML = JSON.parse(decryptedData);
        });
    });
}

printCurrent();
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/sjcl@1.0.8/sjcl.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/sjcl@1.0.8/core/codecArrayBuffer.js"></script>
<p style="font-family:'Courier New', monospace;" id="pt"></p>


如果 ArrayBuffer 编解码器 由于某种原因在您的环境中不可用(我不确定),那么 ArrayBuffer 也可以明确地进行十六进制或 Base64 编码,然后转换为 bitArray,例如:

// 
function buf2hex(buffer) { 
    return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
}
...
let bdata = sjcl.codec.hex.toBits(buf2hex(encryptedObject)) 
let ive = sjcl.codec.hex.toBits(buf2hex(new Uint8Array(12).buffer)) 

安全:
静态 IV(如此处的 new Uint8Array(12))是 GCM 上下文中的致命错误,s。例如here。相反,将为 each 加密生成一个 random IV,它将与密文(通常是串联的)一起传递。因此,除非静态 IV 仅用于测试目的,否则必须修复此问题。