Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WebCrypto string encryption using user-submitted password

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:

  • Safari 11: CryptoKey doesn't match AlgorithmIdentifier
  • Firefox 54: A parameter or an operation is not supported by the underlying object
  • Chromium 61: 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.

like image 873
JYF Avatar asked Aug 11 '17 13:08

JYF


1 Answers

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)

like image 183
user5207081 Avatar answered Sep 17 '22 08:09

user5207081