(edit) Still at this. I've changed the code, now using the example functions directly from the library's Github page but am still having the same exact issue. Am I missing something obvious here?? My only deviation is to use the built in crypto_genrichash function for creating the key @ 32 chars (64 was too long). Using their example, created a key too small:

var key = sodium.randombytes_buf(sodium.crypto_shorthash_KEYBYTES),
hash1 = sodium.crypto_shorthash(new Uint8Array([1, 2, 3, 4]), key),
hash2 = sodium.crypto_shorthash('user_password', key);

The link to the raw sodium file https://raw.githubusercontent.com/jedisct1/libsodium.js/master/dist/browsers/sodium.js

And here is the function causing the error:

function RA(A, I, e, B) {
            var C = [];
            E(B),
            A = w(C, A, "ciphertext");
            var i, a = g._crypto_secretbox_macbytes(), r = A.length;
            r < a && _(C, "ciphertext is too short"),
            i = s(A),
            C.push(i),
            I = w(C, I, "nonce");
            var t, Q = 0 | g._crypto_secretbox_noncebytes();
            I.length !== Q && _(C, "invalid nonce length"),
            t = s(I),
            C.push(t),
            e = w(C, e, "key");
            var o, h = 0 | g._crypto_secretbox_keybytes();
            e.length !== h && _(C, "invalid key length"),
            o = s(e),
            C.push(o);
            var p = new c(r - g._crypto_secretbox_macbytes() | 0)
              , u = p.address;
            if (C.push(u),
            0 == (0 | g._crypto_secretbox_open_easy(u, i, r, 0, t, o))) {
                var l = n(p, B);
                return y(C),
                l
            }
            f(C, "wrong secret key for the given ciphertext")
        }

(/edit)
I believe in my attempts to minimize this code for my own learning, I've removed something in the equation of salting or nonce. But, regardless, What I don't understand is that if you run this exactly as is, seems to work, as both console logs show the same exact value of the privateKey both before encryption and after decryption. However, in real world use, the encryption part will be in a separate script, such as a user creation/edit pw page, the new encrypted privateKey sent to mysql for storage (via ajax/php), and the decryption part in the user login script..this is when I get the 'wrong secret key for the given ciphertext' error in the console. But I've checked, and, on the surface, the value ajax returns (note: is in an array with some other confirmations) at authentication is exactly the same as it gets to decryptPrivateKey(result[6], password_normal_input); as when it was first created...at least visually (checked for whitespace in return and password input).

and the sodium package (with the examples) is from: https://github.com/jedisct1/libsodium.js

<script src="components/sodium/sodium.js" async></script>
<script>
     window.sodium = 
     {  

        onload: function (sodium) 
        {

          function encrypt_and_prepend_nonce(message,password)
          {
            let key = sodium.crypto_generichash(32, sodium.from_string(password));
            let nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
            let nonce_arr = sodium.to_hex(nonce);
            return sodium.from_hex(nonce_arr.concat(sodium.to_hex(sodium.crypto_secretbox_easy(message, nonce, key))));
          }

          function decrypt_after_extracting_nonce(nonce_and_ciphertext,password)
          {
            let key = sodium.crypto_generichash(32, sodium.from_string(password));
            if (nonce_and_ciphertext.length < sodium.crypto_secretbox_NONCEBYTES +
            sodium.crypto_secretbox_MACBYTES)
            {
                throw "Short message";
            }
            let nonce = nonce_and_ciphertext.slice(0, sodium.crypto_secretbox_NONCEBYTES),
            ciphertext = nonce_and_ciphertext.slice(sodium.crypto_secretbox_NONCEBYTES);
            return sodium.crypto_secretbox_open_easy(ciphertext, nonce, key);
          }

          var password = 'user_password';
          let keypair = sodium.crypto_box_keypair();
          let privateKey = keypair.privateKey;
          console.log(privateKey);
          var privateKey_encrypted = encrypt_and_prepend_nonce(privateKey,password);
          var privateKey_decrypted = decrypt_after_extracting_nonce(privateKey_encrypted,password);
          console.log(privateKey_decrypted);
        }
    };
</script>

1 Answers

0
Community On

Finally found it. What I failed to think about was that I was sending dataType: 'text' thereby turning the ciphertext into a string. As I mentioned originally, I'm sending it back and forth with other user and authentication related data in an array so even if there is a dataType binary function, won't work for me.

The problem was that I was only focusing on and comparing the visual data which 'looked' the same, going back and forth. If you run the code, you'll get a Uint8Array ciphertext, something like...

244,107,218,84,102,170,55,208,32,148,192,251,218,140,254,204,69,192,24,120,135,88,254,96,56,203,191,65,250,106,42,16,118,179,151,29,220,221,224,6,105,200,235,106,190,248,150,208,233,161,36,4,63,16,2,188,238,21,247,117,4,89,37,43,26,103,135,33,160,44,129,75

concatenated with the beginning 24 byte nonce

244,107,218,84,102,170,55,208,32,148,192,251,218,140,254,204,69,192,24,120,135,88,254,96

On decryption, we're slicing the nonce and ciphertext, again with crypto_secretbox_NONCEBYTES..known also as "24"...

let nonce = nonce_and_ciphertext.slice(0, sodium.crypto_secretbox_NONCEBYTES),
            ciphertext = nonce_and_ciphertext.slice(sodium.crypto_secretbox_NONCEBYTES);

BUT but But, if it is a string the slice will be:

244,107,218,84,102,170,5

..thereby creating the wrong key for the ciphertext and bringing about my need for Stack Overflow and Github Accounts. Notice the last part where I said 'checked for whitespace in return and password input'....uh, yea.

Anyway, so my solution with very loose examples... first the encryption

<script src="components/sodium/sodium.js" async></script>
<script>
    window.sodium =
    {
        onload: function (sodium)
        {

          function encrypt_and_prepend_nonce(message,password)
          {
            let key = sodium.crypto_generichash(32, sodium.from_string(password));
            let nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
            console.log(nonce);
            let nonce_arr = sodium.to_hex(nonce);
            return sodium.from_hex(nonce_arr.concat(sodium.to_hex(sodium.crypto_secretbox_easy(message, nonce, key))));
          }

          //Password from some text input, but for the example we'll just use a string.
          var password = 'user_password';
          let keypair = sodium.crypto_box_keypair();
          let privateKey = keypair.privateKey;
          var privateKey_encrypted = encrypt_and_prepend_nonce(privateKey,password);

          /* Send it to the server for storage. Just sending public key and password as makes sense
          *  for this example, as we just created the keypair
          */
          $.ajax(
          {
            type: "POST",
            url: "server.php",
            dataType: "text",
            data: 'my_new_encrypted_privateKey=' + privateKey_encrypted + '&publicKey=' + 
            keypair.publicKey + '&user_password=' + password
          });   

        }
    };
</script>

Then for the decryption of the privateKey...

<script src="components/sodium/sodium.js" async></script>
<script>
    window.sodium =
    {
        onload: function (sodium)
        {
            //Function to turn privateKey ciphertext string to Uint8Array
            function strToBuffer (string)
            {
                let array = string.split(',');
                let newUint = new Uint8Array(array);
                return newUint;
            }   

            function decrypt_after_extracting_nonce(nonce_and_ciphertext,password)
            {
                let key = sodium.crypto_generichash(32, sodium.from_string(password));
                if (nonce_and_ciphertext.length < sodium.crypto_secretbox_NONCEBYTES +
                sodium.crypto_secretbox_MACBYTES)
                {
                    throw "Short message";
                }
                nonce_and_ciphertext = strToBuffer(nonce_and_ciphertext);
                //now a Uint8Array, will produce the correct nonce & ciphertext slices
                let nonce = nonce_and_ciphertext.slice(0, sodium.crypto_secretbox_NONCEBYTES),
                ciphertext = nonce_and_ciphertext.slice(sodium.crypto_secretbox_NONCEBYTES);
                return sodium.crypto_secretbox_open_easy(ciphertext, nonce, key);
            }

            /* Password from some text input, but for the example we'll just use a string. The privateKey_
             * encrypted will likely be in an ajax callback
             */
            $.ajax(
            {
                type: "POST",
                url: "server.php",
                dataType: "text",
                data: 'some_user_data=' + some_user_data,
                success: function(response) 
                {
                    //Response will be the string...
                    var privateKey_encrypted = response.trim();
                    var privateKey_decrypted = decrypt_after_extracting_nonce(privateKey_encrypted,password);
                    //You can now use your Keypair to open sealed boxes.
                }
            });
        }
    };
</script>