Ty Everett Ty Everett - 28 days ago 13
Javascript Question

AES-GCM decryption rejecting promise

I have a string which I would like to encrypt with AES-GCM using the JavaScript Web Cryptography API. I'm able to encrypt it OK, but when I go to decrypt it the promise gets rejected and I'm not getting a vary descriptive error message.

function aes_encrypt(key, IV, data){
return new Promise(function(resolve, reject){
window.crypto.subtle.encrypt(
{
name: "AES-GCM",

//Don't re-use initialization vectors!
//Always generate a new iv every time your encrypt!
//Recommended to use 12 bytes length
iv: sta(IV),

//Tag length (optional)
tagLength: 128, //can be 32, 64, 96, 104, 112, 120 or 128 (default)
},
key, //from generateKey or importKey above
sta(data) //ArrayBuffer of data you want to encrypt
)
.then(function(encrypted){
//returns an ArrayBuffer containing the encrypted data
resolve(ats(encrypted));
})
.catch(function(err){
console.error(err);
});
});
}

function aes_decrypt(key, IV, data){
return new Promise(function(resolve, reject){
window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: sta(IV), //The initialization vector you used to encrypt
tagLength: 128 //The tagLength you used to encrypt (if any)
},
key, //from generateKey or importKey above
sta(data) //ArrayBuffer of the data
)
.then(function(decrypted){
//returns an ArrayBuffer containing the decrypted data
alert(decrypted);
resolve(ats(new Uint8Array(decrypted)));
//resolve(ats(decrypted));
})
.catch(function(err){
console.error(err);
});
});
}



function ecdh_generate_keypair(){
return new Promise(function(resolve, reject){
window.crypto.subtle.generateKey(
{
name: "ECDH",
namedCurve: "P-384" //can be "P-256", "P-384", or "P-521"
},
true, //whether the key is extractable (i.e. can be used in exportKey)
["deriveKey", "deriveBits"] //can be any combination of "deriveKey" and "deriveBits"
)
.then(function(key){
//returns a keypair object
resolve(key);
})
.catch(function(err){
console.error(err);
});
});
}

function ecdh_export(key){
return new Promise(function(resolve, reject){
window.crypto.subtle.exportKey(
"jwk", //can be "jwk" (public or private), "raw" (public only), "spki" (public only), or "pkcs8" (private only)
key //can be a publicKey or privateKey, as long as extractable was true
)
.then(function(keydata){
//returns the exported key data
resolve(keydata);
})
.catch(function(err){
console.error(err);
});
});
}

function ecdh_import(key){
return new Promise(function(resolve, reject){
window.crypto.subtle.importKey(
"jwk", //can be "jwk" (public or private), "raw" (public only), "spki" (public only), or "pkcs8" (private only)
key,
{ //these are the algorithm options
name: "ECDH",
namedCurve: "P-384", //can be "P-256", "P-384", or "P-521"
},
true, //whether the key is extractable (i.e. can be used in exportKey)
["deriveKey", "deriveBits"] //"deriveKey" and/or "deriveBits" for private keys only (just put an empty list if importing a public key)
)
.then(function(privateKey){
//returns a privateKey (or publicKey if you are importing a public key)
resolve(privateKey);
})
.catch(function(err){
console.error(err);
});
});
}

function ecdh_derive_key(pub, priv){
return new Promise(function(resolve, reject){
window.crypto.subtle.deriveKey(
{
name: "ECDH",
namedCurve: "P-384", //can be "P-256", "P-384", or "P-521"
public: pub, //an ECDH public key from generateKey or importKey
},
priv, //your ECDH private key from generateKey or importKey
{ //the key type you want to create based on the derived bits
name: "AES-GCM", //can be any AES algorithm ("AES-CTR", "AES-GCM", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH", or "HMAC")
//the generateKey parameters for that type of algorithm
length: 256, //can be 128, 192, or 256
},
true, //whether the derived key is extractable (i.e. can be used in exportKey)
["encrypt", "decrypt"] //limited to the options in that algorithm's importKey
)
.then(function(keydata){
//returns the exported key data
resolve(keydata);
})
.catch(function(err){
console.error(err);
});
});
}

function random_characters(amount){
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

for (var i = 0; i < amount; i++){
text += possible.charAt(Math.floor(Math.random() * possible.length));
}

return text;
}

// string-to-arraybuffer
function sta(data){
var enc = new TextEncoder("utf-8");
return enc.encode(data);
}

// arraybuffer-to-string
function ats(data){
var enc = new TextDecoder();
return enc.decode(data);
}

// JSON into and out of the database for cryptokeys
function json_compress(obj){
var s = JSON.stringify(obj);
s = s.replace(/,/g, "♀");
s = s.replace(/{/g, "☺");
s = s.replace(/}/g, "☻");
return s;
}
function json_decompress(str){
str = str.replace(/♀/g, ",");
str = str.replace(/☺/g, "{");
str = str.replace(/☻/g, "}");
return JSON.parse(str);
}

ecdh_generate_keypair().then(function(key){
ecdh_generate_keypair().then(function(key2){
ecdh_derive_key(key2.publicKey, key.privateKey).then(function(aeskey){
var m = "Hello World!";
aes_encrypt(aeskey, "abcdefghijkl", m).then(function(c){
alert(c);
aes_decrypt(aeskey, "abcdefghijkl", c).then(function(r){
alert(r);
});
});
});
});
});


I'm aware that hard-coding the IV for AES is a security risk, but I'm just trying to get this working for testing purposes. Any help you can offer is much appreciated, as this has been bugging me all day. Thanks in advance!

EDIT: adding the Chrome debug error message

cryptofunctions.js:48 DOMException
(anonymous) @ cryptofunctions.js:48
Promise rejected (async)
(anonymous) @ cryptofunctions.js:47
aes_decrypt @ cryptofunctions.js:31
(anonymous) @ cryptofunctions.js:184
Promise resolved (async)
(anonymous) @ cryptofunctions.js:182
Promise resolved (async)
(anonymous) @ cryptofunctions.js:180
Promise resolved (async)
(anonymous) @ cryptofunctions.js:179
Promise resolved (async)
(anonymous) @ cryptofunctions.js:178


EDIT 2: Decided to post the entire file as it all seems relevant to the question.

Answer Source

Exceptions The promise is rejected when the following exceptions are encountered:

InvalidAccessError when the requested operation is not valid for the provided key (e.g. invalid encryption algorithm, or invalid key for specified encryption algorithm).

Pass encrypted to resolve() at aes_encrypt() call and pass data at aes_decrypt() call instead of sta(data) as data is already an ArrayBuffer within aes_decrypt function

function aes_encrypt(key, IV, data) {
  return window.crypto.subtle.encrypt({
        name: "AES-GCM",

        //Don't re-use initialization vectors!
        //Always generate a new iv every time your encrypt!
        //Recommended to use 12 bytes length
        iv: sta(IV),

        //Tag length (optional)
        tagLength: 128, //can be 32, 64, 96, 104, 112, 120 or 128 (default)
      },
      key, //from generateKey or importKey above
      sta(data) //ArrayBuffer of data you want to encrypt
    )
    .then(function(encrypted) {
      //returns an ArrayBuffer containing the encrypted data
      return encrypted;
    })
    .catch(function(err) {
      console.error(err);
    });
}

function aes_decrypt(key, IV, data) {
  return window.crypto.subtle.decrypt({
        name: "AES-GCM",
        iv: sta(IV), //The initialization vector you used to encrypt
        tagLength: 128 //The tagLength you used to encrypt (if any)
      },
      key, //from generateKey or importKey above
      data //ArrayBuffer of the data
    )
    .then(function(decrypted) {
      //returns an ArrayBuffer containing the decrypted data
      // alert(decrypted);
      return ats(new Uint8Array(decrypted));
      //resolve(ats(decrypted));
    })
    .catch(function(err) {
      console.error(err);
    });
}



function ecdh_generate_keypair() {
  return window.crypto.subtle.generateKey({
        name: "ECDH",
        namedCurve: "P-384" //can be "P-256", "P-384", or "P-521"
      },
      true, //whether the key is extractable (i.e. can be used in exportKey)
      ["deriveKey", "deriveBits"] //can be any combination of "deriveKey" and "deriveBits"
    )
    .then(function(key) {
      //returns a keypair object
      return key;
    })
    .catch(function(err) {
      console.error(err);
    });
}

function ecdh_export(key) {
  return window.crypto.subtle.exportKey(
      "jwk", //can be "jwk" (public or private), "raw" (public only), "spki" (public only), or "pkcs8" (private only)
      key //can be a publicKey or privateKey, as long as extractable was true
    )
    .then(function(keydata) {
      //returns the exported key data
      return keydata;
    })
    .catch(function(err) {
      console.error(err);
    });
}

function ecdh_import(key) {
  return window.crypto.subtle.importKey(
      "jwk", //can be "jwk" (public or private), "raw" (public only), "spki" (public only), or "pkcs8" (private only)
      key, { //these are the algorithm options
        name: "ECDH",
        namedCurve: "P-384", //can be "P-256", "P-384", or "P-521"
      },
      true, //whether the key is extractable (i.e. can be used in exportKey)
      ["deriveKey", "deriveBits"] //"deriveKey" and/or "deriveBits" for private keys only (just put an empty list if importing a public key)
    )
    .then(function(privateKey) {
      //returns a privateKey (or publicKey if you are importing a public key)
      return privateKey;
    })
    .catch(function(err) {
      console.error(err);
    });
}

function ecdh_derive_key(pub, priv) {
  return window.crypto.subtle.deriveKey({
        name: "ECDH",
        namedCurve: "P-384", //can be "P-256", "P-384", or "P-521"
        public: pub, //an ECDH public key from generateKey or importKey
      },
      priv, //your ECDH private key from generateKey or importKey
      { //the key type you want to create based on the derived bits
        name: "AES-GCM", //can be any AES algorithm ("AES-CTR", "AES-GCM", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH", or "HMAC")
        //the generateKey parameters for that type of algorithm
        length: 256, //can be  128, 192, or 256
      },
      true, //whether the derived key is extractable (i.e. can be used in exportKey)
      ["encrypt", "decrypt"] //limited to the options in that algorithm's importKey
    )
    .then(function(keydata) {
      //returns the exported key data
      return keydata;
    })
    .catch(function(err) {
      console.error(err);
    });
}

function random_characters(amount) {
  var text = "";
  var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

  for (var i = 0; i < amount; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }

  return text;
}

// string-to-arraybuffer
function sta(data) {
  var enc = new TextEncoder("utf-8");
  return enc.encode(data);
}

// arraybuffer-to-string
function ats(data) {
  var enc = new TextDecoder();
  return enc.decode(data);
}

// JSON into and out of the database for cryptokeys
function json_compress(obj) {
  var s = JSON.stringify(obj);
  s = s.replace(/,/g, "♀");
  s = s.replace(/{/g, "☺");
  s = s.replace(/}/g, "☻");
  return s;
}

function json_decompress(str) {
  str = str.replace(/♀/g, ",");
  str = str.replace(/☺/g, "{");
  str = str.replace(/☻/g, "}");
  return JSON.parse(str);
}

ecdh_generate_keypair().then(function(key) {
  ecdh_generate_keypair().then(function(key2) {
    ecdh_derive_key(key2.publicKey, key.privateKey).then(function(aeskey) {
      var m = "Hello World!";
      aes_encrypt(aeskey, "abcdefghijkl", m).then(function(c) {
        // alert(c);
        aes_decrypt(aeskey, "abcdefghijkl", c).then(function(r) {
          alert(r);
        });
      });
    });
  });
});