safebash / opencrypto

OpenCrypto is a lightweight JavaScript library built on top of WebCryptography API
MIT License
74 stars 23 forks source link

If I increase encryption data I get a "Uncaught (in promise) DOMException" error #5

Closed pwFoo closed 4 years ago

pwFoo commented 5 years ago

Hi, I found your opencrypto library with looks good and support all my needs (encryption, signing), so I moved from simplecrypto to opencrypto, but experienced an error with increased data to encrypt.

With simplecrypto (crypto api wrapper / lib) it works fine with for example 30MB generated data which I tried with opencrypto too.

Here is my source code based on miq (is a small jQuery alternative), opencrypto and CustomEvent to wait until keyPair is ready.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
        <script src="https://cdn.jsdelivr.net/miq/1.11.0/miq-min.js" integrity="sha384-ImUa755IQNnQYYaUo3+ruuKDnWlc7vZOwof82bWByawbfDXP3e1SDcLsdvNNPcM7" crossorigin="anonymous"></script>
        <script src="https://rawcdn.githack.com/safebash/opencrypto/b94b916057e042129fabf776f367106985a6ffbf/dist/OpenCrypto.min.js"></script>
        <script>
        var arrayBufferToString = function(buffer, utflabel = "utf-8") {
            return new TextDecoder (utflabel).decode (buffer);
        }

        var stringToArrayBuffer = function(str, utflabel = "utf-8") {
            return new TextEncoder(utflabel).encode(str);
        }

        // DOM ready
        $(function(){
            const crypt = new OpenCrypto();
            const keyPair = new CustomEvent('keyPair', { 'privateKey': null, 'publicKey': null });
            var bits = 2048, usage = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], alg = 'SHA-512', paddingScheme = 'RSA-OAEP', extractable = true;

            // generate key pair
            crypt.getRSAKeyPair(bits, usage, alg, paddingScheme, extractable).then(function (key) {
                keyPair.publicKey = key.publicKey;
                console.log(keyPair.publicKey)
                // public key to pem
                crypt.cryptoPublicToPem(keyPair.publicKey).then(function (publicPem) {
                    console.log(publicPem)
                });

                keyPair.privateKey = key.privateKey;
                console.log(keyPair.privateKey)
                // private key to pem
                crypt.cryptoPrivateToPem(keyPair.privateKey).then(function (privatePem) {
                    console.log(privatePem)
                });

                // trigger keyPair ready event
                document.dispatchEvent(keyPair);
            });

            // Generate data to encrypt
            var string = '1234567890';
            var iterations = 2; // iterations >2 break encryption with "Uncaught (in promise) DOMException"?!
            for (var i = 0; i < iterations; i++) {
                string += string+string;
            }

            // event fired!
            $(document).on('keyPair', function() {
                console.log('Data String\n', string);
                var data = stringToArrayBuffer (string);
                console.log('Data ArrayBuffer\n', data);
                crypt.rsaEncrypt(keyPair.publicKey, data).then(function (encryptedDataAsymmetric) {
                    console.log('EncryptedData\n', encryptedDataAsymmetric)

                    crypt.rsaDecrypt(keyPair.privateKey, encryptedDataAsymmetric).then(function (decryptedDataAsymmetric) {
                        console.log('DecryptedData ArrayBuffer\n', decryptedDataAsymmetric);
                        console.log('DecryptedData String\n', arrayBufferToString(decryptedDataAsymmetric));
                    });
                });
            });
        });
        </script>
    </head>
    <body>

    </body>
</html>

Difference to my simplecrypto code is the custom event, which is used to wait until keyPair is generated and crypto is "ready".

If you increase variable iteration >2 I get an error.

Uncaught (in promise) DOMException
Promise.then (async)
(anonymous) @ opencrypto.html:55
(anonymous) @ opencrypto.html:40
Promise.then (async)
(anonymous) @ opencrypto.html:24

Haven't found a way to debug the error. With a catch(e => console.log(e)) I just see "DOMException", but no more debugging information.

Any hint why it fails with increaded data and how to solve it?

pwFoo commented 5 years ago

Link to https://github.com/Stanwar/simplecrypto repo which works with increased data as tried with example above.

pwFoo commented 5 years ago

Hi @PeterBielak, is there a way to fix the data size problem or do you have a hint why it fails? data size isn't a problem with simplecrypto lib based on webcrypto api, but opencrypto lib looks like a more complete solution and I would prefer to use it.

pwFoo commented 5 years ago

Looks like a bug with simplified code.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
        <script src="https://cdn.jsdelivr.net/miq/1.11.0/miq-min.js" integrity="sha384-ImUa755IQNnQYYaUo3+ruuKDnWlc7vZOwof82bWByawbfDXP3e1SDcLsdvNNPcM7" crossorigin="anonymous"></script>
        <script src="https://rawcdn.githack.com/safebash/opencrypto/b94b916057e042129fabf776f367106985a6ffbf/dist/OpenCrypto.min.js"></script>
        <script>
        // DOM ready
        $(function(){
            const crypt = new OpenCrypto();
            var bits = 2048, usage = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], alg = 'SHA-512', paddingScheme = 'RSA-OAEP', extractable = true;

            // Generate data to encrypt
            var string = '1234567890';
            var iterations = 2; // iterations >2 break encryption with "Uncaught (in promise) DOMException"?!
            for (var i = 0; i < iterations; i++) {
                string += string+string;
            }
            console.log('Data String\n', string);
            // prepare / convert data
            var data = crypt.stringToArrayBuffer (string);
            console.log('Data ArrayBuffer\n', data);

            // generate key pair
            crypt.getRSAKeyPair(bits, usage, alg, paddingScheme, extractable).then(function (key) {
                console.log(key.publicKey)
                // public key to pem
                crypt.cryptoPublicToPem(key.publicKey).then(function (publicPem) {
                    console.log(publicPem)
                });
                // private key to pem
                crypt.cryptoPrivateToPem(key.privateKey).then(function (privatePem) {
                    console.log(privatePem)
                });

                crypt.rsaEncrypt(key.publicKey, data).then(function (encryptedDataAsymmetric) {
                    console.log('EncryptedData\n', encryptedDataAsymmetric)

                    crypt.rsaDecrypt(key.privateKey, encryptedDataAsymmetric).then(function (decryptedDataAsymmetric) {
                        console.log('DecryptedData ArrayBuffer\n', decryptedDataAsymmetric);
                        var decryptedString = crypt.arrayBufferToString(decryptedDataAsymmetric);
                        console.log('DecryptedData String\n', decryptedString);

                        if (decryptedString == string) {
                            console.log("SUCCESS! => INPUT == OUTPUT")
                        } else {
                            console.log("FAILED! => INPUT != OUTPUT")
                        }

                    });
                });
            });
        });
        </script>
    </head>
    <body>

    </body>
</html>

@PeterBielak Is opencrypto still supported / maintained? Looks like opencrypto doesn't work if I increase iterations to a value >2!

Any chance to get the bug fixed?

pwFoo commented 5 years ago

Reduced dependencies / debug code to the minimum.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
    </head>
    <body>
        <script src="https://rawcdn.githack.com/safebash/opencrypto/b94b916057e042129fabf776f367106985a6ffbf/dist/OpenCrypto.min.js"></script>
        <script>
            const crypt = new OpenCrypto();
            var bits = 2048, usage = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], alg = 'SHA-512', paddingScheme = 'RSA-OAEP', extractable = true;

            // Generate data to encrypt
            var string = '1234567890';
            var iterations = 3; // iterations >2 break encryption with "Uncaught (in promise) DOMException"?!
            for (var i = 0; i < iterations; i++) {
                string += string+string;
            }
            console.log('Data String\n', string);
            // prepare / convert data
            data = crypt.stringToArrayBuffer (string);
            console.log('Data ArrayBuffer\n', data);

            // generate key pair
            crypt.getRSAKeyPair(bits, usage, alg, paddingScheme, extractable).then(function (key) {
                console.log(key.publicKey)
                // public key to pem
                /*
                crypt.cryptoPublicToPem(key.publicKey).then(function (publicPem) {
                    console.log(publicPem)
                });

                console.log(key.privateKey)
                // private key to pem
                crypt.cryptoPrivateToPem(key.privateKey).then(function (privatePem) {
                    console.log(privatePem)
                });
                */

                console.log("pubKey:\n", key.publicKey);
                console.log("data:\n", data);
                crypt.rsaEncrypt(key.publicKey, data).then(function (encryptedDataAsymmetric) {
                    console.log('EncryptedData\n', encryptedDataAsymmetric)

                    // decrypt Data
                    /*
                    crypt.rsaDecrypt(keyPair.detail.privateKey, encryptedDataAsymmetric).then(function (decryptedDataAsymmetric) {
                        console.log('DecryptedData ArrayBuffer\n', decryptedDataAsymmetric);
                        var decryptedString = crypt.arrayBufferToString(decryptedDataAsymmetric);
                        console.log('DecryptedData String\n', decryptedString);

                        if (decryptedString == string) {
                            console.log("SUCCESS! => INPUT == OUTPUT")
                        } else {
                            console.log("FAILED! => INPUT != OUTPUT")
                        }
                    });
                    */
                })
            });
        </script>
    </body>
</html>

I don't know how to handle data size with crypto api, but found some code lines in simplecrypto about data size I think?

https://github.com/Stanwar/simplecrypto/blob/b65d6a327a96f20f19126b7654768d835d18e339/src/simplecrypto.js#L40

https://github.com/Stanwar/simplecrypto/blob/b65d6a327a96f20f19126b7654768d835d18e339/src/simplecrypto.js#L396

PeterBielak commented 5 years ago

Hi there, I apologize for the delay in my response. I have been very busy recently. Also, thank you for reporting the issue.

I reviewed your situation carefully and I can see that you are encrypting the data using an asymmetric algorithm. Size of data that can be encrypted using such an algorithm is directly dependent on the key size. You are using 2048 bit key in your example which can only encrypt 214 bytes of data. Nonetheless, the desired data size in your example is 270 bytes, therefore you receive the "DOMException: "The operation failed for an operation-specific reason" error from WebCryptography API.

There is a couple of options: 1) Increase the key size to e.g. 4096 bits that will allow you to encrypt up to 470 bytes of data. 2) Use hybrid encryption cryptosystem. It combines both asymmetric and symmetric encryption. A very basic example would consist of encrypting data with a symmetric key, then encrypting the symmetric key itself with your recipient's public key and then finally delivering both encrypted symmetric key and encrypted data to the recipient.

I reviewed SimpleCrypto that you had mentioned and found out that it utilizes hybrid encryption under the hood. We had always preferred to keep OpenCrypto as lightweight as possible thus hybrid encryption needs to be implemented manually.

Please let me know if you have any concerns or need further assistance.

Thank you for your interest in OpenCrypto, it is very highly appreciated.

Best Regards, Peter Bielak

pwFoo commented 5 years ago

Hi and thanks for explaining how it need to Work under the hood.

So symmetric encrypted data with asymmetric encrypted key would the "right" (secure) way to so it? Is it the way of simplecrypto?

If I understood it right the key size for hybrid key is limited the same way to 214 or 470 bytes of data to handle the limitation?

Any chance to get it implemented to opencrypto? I think it should handle that for the user in a good / secure way for simple usage.

pwFoo commented 5 years ago

Hi,

implement to opencrypto would be easy for js only usage, but should be a problem to handle with openssl / php?

So a hybrid method would be needed client and server site to handle both the same way?

PeterBielak commented 5 years ago

Hi there, you're very welcome.

There is no hybrid key. Hybrid encryption is a combination of asymmetric and symmetric encryption that benefits from the strengths of each type.

You should use symmetric algorithm for data encryption and asymmetric algorithm for key exchange.

This is a very basic idea: 1) Encrypt data (using a symmetric algorithm such as AES) with a random symmetric key (Ks). 2) Encrypt the symmetric key (Ks) with the recipient's public key (Kpub) (using an asymmetric algorithm such as RSA or Elliptic-curves) which can only be decrypted with a private key (Kpriv).

const crypt = new OpenCrypto()
const secretData = 'This is top secret.'

// generate random symmetric key for AES
crypt.getSharedKey().then(sharedKey => {
  const secretDataArrayBuffer = crypt.stringToArrayBuffer(secretData)

  // encrypt secret data with generated symmetric key
  crypt.encrypt(sharedKey, secretDataArrayBuffer).then(encryptedData => {
    // generate key pair for demo purpouses only
    crypt.getRSAKeyPair().then(keyPair => {
      // encrypt symmetric key with your recipient's public key
      crypt.encryptKey(keyPair.publicKey, sharedKey).then(encryptedSharedKey => {
        // send both encryptedSharedKey and encryptedData to recipient
        console.log(encryptedSharedKey)
        console.log(encryptedData)

        // recipient decrypts the symmetric key with their private key
        // recipient decrypts the data with decrypted symmetric key
        crypt.decryptKey(keyPair.privateKey, encryptedSharedKey).then(decryptedSharedKey => {
          crypt.decrypt(decryptedSharedKey, encryptedData).then(decryptedData => {
            const decryptedDataString = crypt.arrayBufferToString(decryptedData)
            console.log(decryptedSharedKey)
            console.log(decryptedDataString)
          })
        })
      })
    })
  })
})

The primary principle behind OpenCrypto is to provide a simpler interface for cryptographic methods and easier conversion between key formats. Cryptography is hard and it is up to the developer to use these tools properly in a secure manner.

Feel free to let me know if you need any further help.

Best Regards, Peter Bielak

pwFoo commented 5 years ago

Thanks for example. I'll test it. Also have some problems with async js / promises and how to be sure the key pair is generated before I try to encrypt data... I clean and good "wait for promise solved" feature / function.

Ich I try to decrypt with php it should be the same I think? Decrypt sym key and then decrypt data with php openssl feature? Last step is decode base64 to use the data server side?

I think I would only need sign+verify between server and client. En-/Decrypt data would be client side...

PeterBielak commented 4 years ago

Hi there,

I apologize for the delay. I got caught up in a lot of work recently.

Yes, regarding the private key decryption, you can supply PEM encoded encrypted private keys to OpenSSL directly in PHP as encrypted private keys use a standardized structure according to RFC 5958 https://tools.ietf.org/html/rfc5958.

Concerning the symmetric cryptography, it does not use any standardized structure, therefore you may need to encode/decode data for your specific environment.

Feel free to let me know if you need any further help, and I will try to answer as soon as possible.

Have a great day!

pwFoo commented 4 years ago

Hi @PeterBielak ,

thanks for explaining how to use it. At the moment I'm busy with work. So I stopped my project idea for now, but opencrypto is my first choice to work with for JS encryption! Thanks for you great work! I'll close the issue for now.