DoneDeal0
DoneDeal0

Reputation: 6257

How to fix this OperationError error when decrypting data with crypto API?

I've successfully encrypted data with crypto API. Once it's done, I save the initialization-vector and the encrypted data as a single base64 string.

When decrypting, I revert these two information to Uint8Array that match the originals. But the decryption always fails with the following error:

error decrypt Error: OperationError

Here is the code:

// generate key
generateKey (){
  crypto.subtle.generateKey(
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt", "decrypt"]
  );
}

// encrypt
  async encrypt(data, secretKey) {
    const initializationVector = crypto.getRandomValues(new Uint8Array(96));
    const encodedData = new TextEncoder().encode(JSON.stringify(data));

    const encryptedBuffer = await crypto.subtle.encrypt(
      {
        name: "AES-GCM",
        iv: initializationVector,
        tagLength: 128,
      },
      secretKey,
      encodedData
    );

    const encryptedDataBase64 = btoa(new Uint8Array(encryptedBuffer));
    const initializationVectorBase64 = btoa(initializationVector);
    return `${encryptedDataBase64}.${initializationVectorBase64}`;
  }

// convert base64 string to uint8array
  base64ToUint8Array(base64String) {
    return new Uint8Array(
      atob(base64String)
        .split(",")
        .map((n) => +n)
    );
  }

//decrypt
  async decrypt(encryptedData, secretKey) {
    const { 0: data, 1: iv } = encryptedData.split(".");
    const initializationVector = base64ToUint8Array(iv);
    const _data = base64ToUint8Array(data);
    const decryptedData = await crypto.subtle.decrypt(
      {
        name: "AES-GCM",
        iv: initializationVector,
        tagLength: 128,
      },
      secretKey,
      _data
    );
    return new TextDecoder().decode(decryptedData)
  }

I've checked the initialization-vector and the data Uint8Array during the encryption and during the decryption. They match their original versions. So I don't know where I'm doing something wrong here.

Thanks for your help!

Upvotes: 1

Views: 2501

Answers (1)

Topaco
Topaco

Reputation: 49251

The conversion from ArrayBuffer to Base64 and vice versa is not correct. Also, when creating the IV or instantiating the Uint8Array, the length must be specified in bytes and not bits. A possible fix is:

(async () => {
    var key = await generateKey();
    
    var plaintext = {"data": "The quick brown fox jumps over the lazy dog"};
    var ciphertext = await encrypt(plaintext, key);
    console.log(ciphertext.replace(/(.{48})/g,'$1\n'));
    
    var decrypted = await decrypt(ciphertext, key);
    console.log(JSON.parse(decrypted));
})();

// generate key
function generateKey (){                                            
    return crypto.subtle.generateKey(                                   
        { name: "AES-GCM", length: 256 },
        false,
        ["encrypt", "decrypt"]
    );
}

// encrypt
async function encrypt(data, secretKey) {                                   
    const initializationVector = crypto.getRandomValues(new Uint8Array(12)); // Fix: length in bytes
    const encodedData = new TextEncoder().encode(JSON.stringify(data));

    const encryptedBuffer = await crypto.subtle.encrypt(
        {
            name: "AES-GCM",
            iv: initializationVector,
            tagLength: 128,
        },
        secretKey,
        encodedData
    );

    const encryptedDataBase64 = ab2b64(encryptedBuffer); // Fix: Apply proper ArrayBuffer to Base64 conversion
    const initializationVectorBase64 = ab2b64(initializationVector); // Fix: Apply proper ArrayBuffer to Base64 conversion 
    return `${encryptedDataBase64}.${initializationVectorBase64}`;
}

// decrypt
async function decrypt(encryptedData, secretKey) {                      
    const { 0: data, 1: iv } = encryptedData.split(".");
    const initializationVector = b642ab(iv); // Fix: Apply proper Base64 to ArrayBuffer conversion
    const _data = b642ab(data); // Fix: Apply proper Base64 to ArrayBuffer conversion
    const decryptedData = await crypto.subtle.decrypt(
        {
            name: "AES-GCM",
            iv: initializationVector,
            tagLength: 128,
        },
        secretKey,
        _data
    );
    return new TextDecoder().decode(decryptedData)
}

// https://stackoverflow.com/a/11562550/9014097 or https://stackoverflow.com/a/9458996/9014097
function ab2b64(arrayBuffer) {
      return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}

// https://stackoverflow.com/a/41106346 or https://stackoverflow.com/a/21797381/9014097
function b642ab(base64string){
      return Uint8Array.from(atob(base64string), c => c.charCodeAt(0));
}

Upvotes: 1

Related Questions