Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep page unlocked for a while between page refreshes #20

Open
gersondiesel opened this issue Dec 15, 2020 · 10 comments
Open

Keep page unlocked for a while between page refreshes #20

gersondiesel opened this issue Dec 15, 2020 · 10 comments

Comments

@gersondiesel
Copy link

Hello!
Thanks for this excelent tool!
It is possible to preserve the page unlocked (unencrypted) for a while, like X hours or X days, to prevent the need for input password every time the page is refreshed?

Thanks, again!

@MaxLaumeister
Copy link
Owner

Hey Gerson,

Thank you for reaching out about this. One thing I've seen users do is allow their browser to save the password in its built-in password manager. That way, the next time the user visits the page, they can just click "submit" and the password is already filled in for them.

What do you think, is that a useful workaround for your use case?

If that workaround isn't ideal, I'd be down to chat some more about this!

Thanks,
-Max

@gersondiesel
Copy link
Author

Hello, Mr Max!
Thank you for your return for my question.
I will give this solution for my clients and wait for the feedbacks!

Thank you again!

Best regards,
Gerson

@wzdiyb
Copy link

wzdiyb commented Jan 23, 2021

@gersondiesel I've noticed an idea on this issue: robinmoisson/staticrypt#120. And below there's a gist https://gist.github.com/epicfaace/c1a4452401af14d35b60fe211f2c1559 for you to take a look at.

And for the expire time, you could also store timestamp into the localstorage and check at every page refresh, if expired, then remove the stored password and timestamp.

@MaxLaumeister
Copy link
Owner

@wzdiyb Thank you for linking that. Cool solutions here for solving the "stale password" issue, and to encrypt an entire site. I think that answers the last unknowns I had about implementing this.

So this will be probably the first thing I implement if I come back to work on PageCrypt again in the future. In the mean time, pull requests welcome.

@wzdiyb
Copy link

wzdiyb commented Jan 29, 2021

@wzdiyb Thank you for linking that. Cool solutions here for solving the "stale password" issue, and to encrypt an entire site. I think that answers the last unknowns I had about implementing this.

So this will be probably the first thing I implement if I come back to work on PageCrypt again in the future. In the mean time, pull requests welcome.

Hi MaxLaumeister,

it should be me to say thank you to you, the author of staticrypt and all contributors offering us such great tools/solutions to solve our problems.

I'd love to make a pull request or send you the template html of mine in the future (according to my schedule, it will last approx. 1-2 months), as i am working on a project and have currently no extra time.

Have a nice day~

@wzdiyb
Copy link

wzdiyb commented Feb 19, 2021

@MaxLaumeister, @gersondiesel

Below is the demo of the modification. I tested on my side in several simple cases with success. But there still might be some problems/bugs to fix or some scenarios to think about. (Also, a better coding style is needed for my demo version).

Demo works:

  • If the entire website shares the same password, then this solution is already fulfilled, or might only needs a little modification.
  • If the pages have different passwords, and the users don't need to keep all the page with different passwords in open state (as long as they know the passwords) at the same time, then this solution is already fulfilled, or might only needs a little modification. Which means, at each time, there could be only files sharing the same passwords in open state(done by autodecrypting on page reloading)
  • In this case, although not implemented, the idea is given, only need several modifications. If the pages may use different passwords and meanwhile users wanna keep the different passwords for all the different pages, we could make such a modification: while saving data into LocalStorage, we also store the current url/file info into it, and check them at every page refresh. The only cons are - if user know a lot of encrypted pages and input their passwords at the same time or in a short time, the allocated data size/item quantity in the LocalStorage increase crazily. This could be solved by providing short expire time, although this would be a balance between duration of expire time and encrypted pages quantity that the users own the passwords)

The basic things are:

  • At every page refresh/visit, check if password and expire time stored locally. If (yes) and (the time does not expire), then try decrypting it.
  • New:
    (When not successful - either password not correct or time expires -, as in your code displayed, throwing an exception. In the catch block, we handle this error doing nothing, which means without inline the invalidPassEl Element, so users won't know that the page already tried decrypting it before they do anything)

Reason: when user successfully decrypted page 1 and passphrase stored locally, then the user loads another page 2 (if page 2 using a password other than page 1's), the passphrase stored (which belongs to page 1) should not be removed at loading time of the page 2 for a better user experience unless the user inputted the correct password for page 2(once password for page 2 inputted correctly, the new passphrase would be stored).

(It might be possible for users that they only want to take a glance at pages other than page 1, but just a glance. They might not concern about if the stored password could decrypt pages other than page 1. Let's make an example, user A encrypts his Profile page, he might see others peoples' profile pages, but whether the his stored passphrase could decrypt others' pages, it does not really matter. So, if visiting others encrypted pages could lead to removing his own password locally and letting him input the password again when he comes back to his own profile page, he would definitely be mad at it. )

(In my project, every page may have its own password; but this solution also works well when the entire website using the same password, 'cause in this case redirecting from one page to another page, the auto decrypting will do the job perfectly beforehand without inputting the password another time unless the password expires. )

catch (e) {
    if (mode === '1') {
        invalidPassEl.style.display = "inline";
        passEl.value = "";
    }
}
  • Deprecated: (When not successful - either password not correct or time expires -, as in your code displayed, throwing an exception. In the catch block, we handle this error mainly by clearing the data stored in LocalStorage but without inline the invalidPassEl Element, so users won't know that the page already tried decrypting it before they do anything)
catch (e) {
    if (mode === '0') {
        // Wrong passphrase for this page, clear the stored passphrase and expire timestamp
        clearPW_in_LocalStorage(); 
    } else if (mode === '1') {
        invalidPassEl.style.display = "inline";
        passEl.value = "";
    }
}
  • Modification on doSubmit() function, add a parameter for it.

So that we could justify if the doSubmit() is called by the page itself(auto decrypting when page reloading) or by the user(clicking the button or pressing the enter key)

  • Once doSubmit() is called by user and the password inputted is correct, then update/set the data - passphrase and expire time - in the LocalStorage.

Currently i've hardcoded the expire time into 60s. Also, it could be set by the parameter given by the user while calling the encrypting tool (like encrypt.py)(i did not implemented version setting the expire time with parameter given by encrypting tool)

  • Two extra functions for setting/clearing the data in LocalStorage are declared and implemented

For better handling the manipulation of LocalStorage relatively.

  • Notice: Modification on onclick attribute of the submitPass-Element

I've changed it from submitPass.onclick =doSubmit; to submitPass.onclick = function() {doSubmit('1');} instead of submitPass.onclick =doSubmit('1');, because once it is set to doSubmit('1'), it will automatically run, which is not what we except.


Below is the demo code in detail:

<script>
        var pl = /*{{ENCRYPTED_PAYLOAD}}*/"";
        
        var submitPass = document.getElementById('submitPass');
        var passEl = document.getElementById('pass');
        var invalidPassEl = document.getElementById('invalidPass');
        var successEl = document.getElementById('success');
        var contentFrame = document.getElementById('contentFrame');
        
        if (pl === "") {
            submitPass.disabled = true;
            passEl.disabled = true;
            alert("This page is meant to be used with the encryption tool. It doesn't work standalone.");
        }
        
        function doSubmit(mode) {
            // @Param mode:int
            // * 0: auto submit
            // * 1: manual submit
            try {
                var local_passphrase = atob(localStorage.getItem("passphrase")); 

                var passEl_value = (mode === '0') ? local_passphrase : passEl.value; 
 
                var decrypted = decryptFile(CryptoJS.enc.Base64.parse(pl.data), passEl_value, CryptoJS.enc.Base64.parse(pl.salt), CryptoJS.enc.Base64.parse(pl.iv));
                if (decrypted === "") throw "No data returned";
                
                // Set default iframe link targets to _top so all links break out of the iframe
                decrypted = decrypted.replace("<head>", "<head><base href=\".\" target=\"_top\">");
                
                srcDoc.set(contentFrame, decrypted);
                
                successEl.style.display = "inline";
                passEl.disabled = true;
                submitPass.disabled = true;
                setTimeout(function() {
                    dialogWrap.style.display = "none";
                }, 50);

                if (mode === '1') {
                    // If the function is called by clicking or pressing enter, the passphrase entered by user should be stored locally along with the expire time
                    savePW_in_LocalStorage()
                }

            } catch (e) {
                if (mode === '1') {
                    invalidPassEl.style.display = "inline";
                    passEl.value = "";
                }
            }
        }
        
        submitPass.onclick = function() {doSubmit('1');}
        passEl.onkeypress = function(e){
            if (!e) e = window.event;
            var keyCode = e.keyCode || e.which;
            invalidPassEl.style.display = "none";
            if (keyCode == '13'){
              // Enter pressed
              doSubmit('1');
              return false;
            }
        }
        
        function decryptFile(contents, password, salt, iv) {
            var _cp = CryptoJS.lib.CipherParams.create({
                ciphertext: contents
            });
            var key = CryptoJS.PBKDF2(password, salt, { keySize: 256/32, iterations: 100 });
            var decrypted = CryptoJS.AES.decrypt(_cp, key, {iv: iv});
            
            return decrypted.toString(CryptoJS.enc.Utf8);
        }

        function savePW_in_LocalStorage() {
            // Store the passphrase in the input field with the help of btoa() function
            localStorage.setItem("passphrase", btoa(passEl.value));
            // Store the expire time (currently hard coded it to 60 secs <-> 60000 milisecs)
            const datetime_now_millisecs = new Date().getTime(); // Milliseconds since Epoch time
            const datetime_expire_millisecs = datetime_now_millisecs + 60000;
            localStorage.setItem("expire_time", datetime_expire_millisecs.toString());
        }

        function clearPW_in_LocalStorage() {
            localStorage.removeItem("passphrase");
            localStorage.removeItem("expire_time");
        }

        // Check if passphrase and expire time are stored locally
        if (localStorage.getItem("passphrase") && localStorage.getItem("expire_time")) {
            const datetime_now = new Date(); 
            const datetime_expire = new Date(parseInt(localStorage.getItem("expire_time"), 10));

            // Check if the passphrase already expires, if not, do a do a submit for auto decrypting
            if (datetime_expire >= datetime_now) {
                doSubmit('0'); 
            } else {
                // Otherwise, clear the stored passphrase and expire timestamp
                clearPW_in_LocalStorage();
            }
        }
    </script>

@Greenheart
Copy link

Greenheart commented May 31, 2021

Hi! First of all - a big thank you to everyone involved in creating this original PageCrypt project! 🎉

I've created a modern version of this library with some improvements, including:

  • A solution to this very issue of preserving the decrypted state between page refreshes. (Using the browser's sessionStorage API which is cleared when the browser is restarted).
  • Easily available to install from npm. This allows automated build processes and scripting instead of relying on the manual web page. Useful in npm scripts in JS projects for example.
  • Possible to automatically generate a strong random password when using the npm package. Useful for projects which need to rotate keys on a regular basis.
  • Smaller wrapping decryption page, shipping less code in addition to the actual page or app you want to encrypt.
  • Upgraded to use native Web Crypto API to improve performance and security compared to CryptoJS. It has great browser support with >95%.
  • Using stronger crypto settings: 200 000 iterations for PBKDF2 hashing compared to 1000 or 100 used in other alternative projects like this one. This decrease the performance on older devices, which makes the decryption seem slower - but these settings greatly improve security which was necessary for my demands.

Again, thanks for the inspiration and sharing this project as open source! I'd be happy to get feedback and work together to make further improvements!

@MaxLaumeister
Copy link
Owner

Thank you @Greenheart for this modern rewrite! I mentioned it on the project page so that anyone using my version knows that yours is out there too with these new features.

It looks like your version targets developers with a robust and scriptable API, whereas my version mostly targets average users with an easy to use web page where they can visit and encrypt one-off pages, so to me it seems for now that these forks serve separate niches. I appreciate the work you've done to modernize the code with Web Crypto, which was unfortunately not widely supported when I created this project in 2015.

I think the Web Crypto code and the session storage code are prime candidates to be integrated into a public-facing web app. This project isn't where my resources are focused right now, but if I revisit it I plan to apply those features so that end users can encrypt documents with them by visiting the PageCrypt project page.

In any case, thank you again for rewriting a modern version of this project!

@Greenheart
Copy link

Thanks @MaxLaumeister! Greatly appreciated! 😃

Indeed, I initially only targeted developers since it fits my most common use cases.

However, I'm thinking about adding a public webpage to make encryption more user friendly in the modern PageCrypt too: Greenheart/pagecrypt#15

Can't promise anything in terms of when it will be finished - if anyone wants to collaborate on a PR let me know in the issue linked above or by opening a PR! :)

@MaxLaumeister
Copy link
Owner

Quick update: I recently added native Web Crypto support to PageCrypt in a series of commits this November.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants