Using JavaScript and WebCrypto API (without any external library), what is the best way to encrypt a string using a key derived from a user-submitted password?
Here's some code where the key is not derived but simply generated by the generatekey()
function. The goal is to encrypt the string, then decrypt it to verify we get the original string back:
var secretmessage = "";
var password = "";
var key_object = null;
var promise_key = null;
var encrypted_data = null;
var encrypt_promise = null;
var vector = window.crypto.getRandomValues(new Uint8Array(16));
var decrypt_promise = null;
var decrypted_data = null;
function encryptThenDecrypt() {
secretmessage = document.getElementById("secretmessageField").value; // some string to encrypt
promise_key = window.crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 128
},
false,
["encrypt", "decrypt"]
);
promise_key.then(function(key) {
key_object = key;
encrypt_data();
});
promise_key.catch = function(e) {
alert("Error while generating key: " + e.message);
}
}
function encrypt_data() {
encrypt_promise = window.crypto.subtle.encrypt({name: "AES-GCM", iv: vector}, key_object, convertStringToArrayBuffer(secretmessage));
encrypt_promise.then(
function(result) {
encrypted_data = new Uint8Array(result);
decrypt_data();
},
function(e) {
alert("Error while encrypting data: " + e.message);
}
);
}
function decrypt_data() {
decrypt_promise = window.crypto.subtle.decrypt({name: "AES-GCM", iv: vector}, key_object, encrypted_data);
decrypt_promise.then(
function(result){
decrypted_data = new Uint8Array(result);
alert("Decrypted data: " + convertArrayBuffertoString(decrypted_data));
},
function(e) {
alert("Error while decrypting data: " + e.message);
}
);
}
function convertStringToArrayBuffer(str) {
var encoder = new TextEncoder("utf-8");
return encoder.encode(str);
}
function convertArrayBuffertoString(buffer) {
var decoder = new TextDecoder("utf-8");
return decoder.decode(buffer);
}
It works in all recent browsers.
Now I'm trying to modify the encryptThenDecrypt()
function so as to derive the key from a user-submitted password:
function encryptThenDecrypt() {
secretmessage = document.getElementById("secretmessageField").value; // some string to encrypt
password = document.getElementById("passwordField").value; // some user-chosen password
promise_key = window.crypto.subtle.importKey(
"raw",
convertStringToArrayBuffer(password),
{"name": "PBKDF2"},
false,
["deriveKey"]
);
promise_key.then(function(importedPassword) {
return window.crypto.subtle.deriveKey(
{
"name": "PBKDF2",
"salt": convertStringToArrayBuffer("the salt is this random string"),
"iterations": 500,
"hash": "SHA-256"
},
importedPassword,
{
"name": "AES-GCM",
"length": 128
},
false,
["encrypt", "decrypt"]
);
});
promise_key.then(function(key) {
key_object = key;
encrypt_data();
});
promise_key.catch = function(e) {
alert("Error while importing key: " + e.message);
}
}
It fails. Error messages are:
CryptoKey doesn't match AlgorithmIdentifier
A parameter or an operation is not supported by the underlying object
key.algorithm does not match that of operation
Must be something simple to fix, but I can't see what. Any help will be greatly appreciated.
You have a little bug in your code. Nothing to do with crypto really, just promises.
Promises do not update their state when their .then()
method is called, they return a new promise instead. See that in your code you are discarding the resulting promise from the call to the key derivation function. Then when encrypting the data, you are reusing the password promise, not the key.
You should either save the resulting promise from the key derivation in a new variable:
let promise_derived_key = promise_key.then(function(importedPassword) {
return window.crypto.subtle.deriveKey(
// [...]
);
});
promise_derived_key.then(function(key) {
// [...]
or chain the calls to .then()
:
promise_key = window.crypto.subtle.importKey(
// [...]
).then(function(importedPassword) {
return window.crypto.subtle.deriveKey(
// [...]
);
});
promise_key.then(function(key) {
// [...]
[working example on JSFiddle]
By the way, you will want to use a lot more PBKDF2 iterations than 500. (info)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With