From 2546316fc5eb49eb04f82e9b16d5e61613500f29 Mon Sep 17 00:00:00 2001 From: Souradip Mookerjee Date: Sat, 19 Aug 2023 23:08:03 +0100 Subject: [PATCH 1/7] use localstorage instead of sessionstorage --- src/decrypt-template.html | 2 +- web/decrypt.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/decrypt-template.html b/src/decrypt-template.html index 1ca3b83..d9368c7 100644 --- a/src/decrypt-template.html +++ b/src/decrypt-template.html @@ -5,7 +5,7 @@ Protected Page - + diff --git a/web/decrypt.ts b/web/decrypt.ts index 080ba62..0d2ea48 100644 --- a/web/decrypt.ts +++ b/web/decrypt.ts @@ -44,7 +44,7 @@ document.addEventListener('DOMContentLoaded', async () => { history.replaceState(null, '', url.toString()) } - if (sessionStorage.k || pwd.value) { + if (localStorage.k || pwd.value) { await decrypt() } else { hide(load) @@ -107,9 +107,9 @@ async function decrypt() { show(form) header.classList.replace('hidden', 'flex') - if (sessionStorage.k) { + if (localStorage.k) { // Delete invalid key - sessionStorage.removeItem('k') + localStorage.removeItem('k') } else { // Only show when user actually entered a password themselves. error('Wrong password.') @@ -162,8 +162,8 @@ async function decryptFile( ) { const decoder = new TextDecoder() - const key = sessionStorage.k - ? await importKey(JSON.parse(sessionStorage.k)) + const key = localStorage.k + ? await importKey(JSON.parse(localStorage.k)) : await deriveKey(salt, password, iterations) const data = new Uint8Array( @@ -172,7 +172,7 @@ async function decryptFile( if (!data) throw 'Malformed data' // If no exception were thrown, decryption succeded and we can save the key. - sessionStorage.k = JSON.stringify(await subtle.exportKey('jwk', key)) + localStorage.k = JSON.stringify(await subtle.exportKey('jwk', key)) return decoder.decode(data) } From 4d1eb0ea173976c6b09a5e0fc2093d9f49b72503 Mon Sep 17 00:00:00 2001 From: Souradip Mookerjee Date: Sat, 19 Aug 2023 23:49:21 +0100 Subject: [PATCH 2/7] add consistent password to decrypt whole site --- package.json | 2 +- src/decrypt-template.html | 2 +- test/package.json | 3 +++ web/decrypt.ts | 23 ++++++++++++++++++----- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 385ea37..6ed3be5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pagecrypt", - "version": "6.1.1", + "version": "6.1.2", "description": "Easily add client-side password-protection to your Single Page Applications and HTML files.", "main": "src/index.ts", "type": "module", diff --git a/src/decrypt-template.html b/src/decrypt-template.html index d9368c7..f9ac2ea 100644 --- a/src/decrypt-template.html +++ b/src/decrypt-template.html @@ -5,7 +5,7 @@ Protected Page - + diff --git a/test/package.json b/test/package.json index f53a93c..80d5767 100644 --- a/test/package.json +++ b/test/package.json @@ -10,5 +10,8 @@ "test:cli-iterations": "pagecrypt test.html out-cli-iterations.html eRx1sD0LrHTNubycv1IYgyNqU3Qc9GKPGcl3XT63JG7djgMxU9etkVNcK5Hak5GWDzm4mx6AQFlpOPsY --iterations 3e6", "test:cli-gen-iterations": "pagecrypt test.html out-cli-gen-iterations.html eRx1sD0LrHTNubycv1IYgyNqU3Qc9GKPGcl3XT63JG7djgMxU9etkVNcK5Hak5GWDzm4mx6AQFlpOPsY --generate-password 59 --iterations 2500000", "test:verify": "vite" + }, + "dependencies": { + "pagecrypt": "file:../dist/pagecrypt-6.1.2.tgz" } } diff --git a/web/decrypt.ts b/web/decrypt.ts index 0d2ea48..7f2bb46 100644 --- a/web/decrypt.ts +++ b/web/decrypt.ts @@ -44,7 +44,12 @@ document.addEventListener('DOMContentLoaded', async () => { history.replaceState(null, '', url.toString()) } - if (localStorage.k || pwd.value) { + const pwdOld = localStorage.getItem("pwd") + if (pwdOld) { + pwd.value = pwdOld + } + + if (localStorage.getItem("k") || pwd.value) { await decrypt() } else { hide(load) @@ -107,7 +112,12 @@ async function decrypt() { show(form) header.classList.replace('hidden', 'flex') - if (localStorage.k) { + if(localStorage.getItem('pwd')) { + // Delete invalid password + localStorage.removeItem('pwd') + } + + if (localStorage.getItem('k')) { // Delete invalid key localStorage.removeItem('k') } else { @@ -162,8 +172,10 @@ async function decryptFile( ) { const decoder = new TextDecoder() - const key = localStorage.k - ? await importKey(JSON.parse(localStorage.k)) + let k = localStorage.getItem("k") + + const key = k + ? await importKey(JSON.parse(k)) : await deriveKey(salt, password, iterations) const data = new Uint8Array( @@ -172,7 +184,8 @@ async function decryptFile( if (!data) throw 'Malformed data' // If no exception were thrown, decryption succeded and we can save the key. - localStorage.k = JSON.stringify(await subtle.exportKey('jwk', key)) + localStorage.setItem("k", JSON.stringify(await subtle.exportKey('jwk', key))) + localStorage.setItem("pwd", password) return decoder.decode(data) } From 9ff426da699aa0fe711bd08f579a4803f5646d5f Mon Sep 17 00:00:00 2001 From: Souradip Mookerjee Date: Sun, 20 Aug 2023 00:14:01 +0100 Subject: [PATCH 3/7] allow for a full website to be crypted --- package.json | 2 +- src/decrypt-template.html | 2 +- test/package.json | 2 +- web/decrypt.ts | 13 ++++++------- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 6ed3be5..520bf0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pagecrypt", - "version": "6.1.2", + "version": "6.1.3", "description": "Easily add client-side password-protection to your Single Page Applications and HTML files.", "main": "src/index.ts", "type": "module", diff --git a/src/decrypt-template.html b/src/decrypt-template.html index f9ac2ea..312a17b 100644 --- a/src/decrypt-template.html +++ b/src/decrypt-template.html @@ -5,7 +5,7 @@ Protected Page - + diff --git a/test/package.json b/test/package.json index 80d5767..5b7a3b0 100644 --- a/test/package.json +++ b/test/package.json @@ -12,6 +12,6 @@ "test:verify": "vite" }, "dependencies": { - "pagecrypt": "file:../dist/pagecrypt-6.1.2.tgz" + "pagecrypt": "file:../dist/pagecrypt-6.1.3.tgz" } } diff --git a/web/decrypt.ts b/web/decrypt.ts index 7f2bb46..f583ccd 100644 --- a/web/decrypt.ts +++ b/web/decrypt.ts @@ -28,6 +28,10 @@ document.addEventListener('DOMContentLoaded', async () => { iv = bytes.slice(32, 32 + 16) ciphertext = bytes.slice(32 + 16) + const pwdOld = localStorage.getItem("pwd") + if (pwdOld) { + pwd.value = pwdOld + } /** * Allow passwords to be automatically provided via the URI Fragment. * This greatly improves UX by clicking links instead of having to copy and paste the password manually. @@ -44,11 +48,6 @@ document.addEventListener('DOMContentLoaded', async () => { history.replaceState(null, '', url.toString()) } - const pwdOld = localStorage.getItem("pwd") - if (pwdOld) { - pwd.value = pwdOld - } - if (localStorage.getItem("k") || pwd.value) { await decrypt() } else { @@ -172,7 +171,7 @@ async function decryptFile( ) { const decoder = new TextDecoder() - let k = localStorage.getItem("k") + let k = null; // localStorage.getItem("k") const key = k ? await importKey(JSON.parse(k)) @@ -184,7 +183,7 @@ async function decryptFile( if (!data) throw 'Malformed data' // If no exception were thrown, decryption succeded and we can save the key. - localStorage.setItem("k", JSON.stringify(await subtle.exportKey('jwk', key))) + // localStorage.setItem("k", JSON.stringify(await subtle.exportKey('jwk', key))) localStorage.setItem("pwd", password) return decoder.decode(data) From 3c1a8c29cb003f88e25c7bbd12734c0907ca763a Mon Sep 17 00:00:00 2001 From: Souradip Mookerjee Date: Sun, 20 Aug 2023 00:20:37 +0100 Subject: [PATCH 4/7] performance boost through caching key by url --- package.json | 2 +- src/decrypt-template.html | 2 +- test/package.json | 2 +- web/decrypt.ts | 12 +++++------- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 520bf0b..3dd16ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pagecrypt", - "version": "6.1.3", + "version": "6.2.0", "description": "Easily add client-side password-protection to your Single Page Applications and HTML files.", "main": "src/index.ts", "type": "module", diff --git a/src/decrypt-template.html b/src/decrypt-template.html index 312a17b..dece625 100644 --- a/src/decrypt-template.html +++ b/src/decrypt-template.html @@ -5,7 +5,7 @@ Protected Page - + diff --git a/test/package.json b/test/package.json index 5b7a3b0..36efd9e 100644 --- a/test/package.json +++ b/test/package.json @@ -12,6 +12,6 @@ "test:verify": "vite" }, "dependencies": { - "pagecrypt": "file:../dist/pagecrypt-6.1.3.tgz" + "pagecrypt": "file:../dist/pagecrypt-6.2.0.tgz" } } diff --git a/web/decrypt.ts b/web/decrypt.ts index f583ccd..f44ab66 100644 --- a/web/decrypt.ts +++ b/web/decrypt.ts @@ -48,7 +48,7 @@ document.addEventListener('DOMContentLoaded', async () => { history.replaceState(null, '', url.toString()) } - if (localStorage.getItem("k") || pwd.value) { + if (localStorage.getItem(window.location.href) || pwd.value) { await decrypt() } else { hide(load) @@ -114,11 +114,9 @@ async function decrypt() { if(localStorage.getItem('pwd')) { // Delete invalid password localStorage.removeItem('pwd') - } - - if (localStorage.getItem('k')) { + } else if (localStorage.getItem(window.location.href)) { // Delete invalid key - localStorage.removeItem('k') + localStorage.removeItem(window.location.href) } else { // Only show when user actually entered a password themselves. error('Wrong password.') @@ -171,7 +169,7 @@ async function decryptFile( ) { const decoder = new TextDecoder() - let k = null; // localStorage.getItem("k") + let k = localStorage.getItem(window.location.href) const key = k ? await importKey(JSON.parse(k)) @@ -183,7 +181,7 @@ async function decryptFile( if (!data) throw 'Malformed data' // If no exception were thrown, decryption succeded and we can save the key. - // localStorage.setItem("k", JSON.stringify(await subtle.exportKey('jwk', key))) + localStorage.setItem(window.location.href, JSON.stringify(await subtle.exportKey('jwk', key))) localStorage.setItem("pwd", password) return decoder.decode(data) From 6a585c2cd548d56be0e3f858026e357a9dd244bf Mon Sep 17 00:00:00 2001 From: Souradip Mookerjee Date: Sun, 20 Aug 2023 00:28:32 +0100 Subject: [PATCH 5/7] add readme update --- README.md | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bf16790..73411c0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,33 @@ # 🔐 PageCrypt - Password Protected Single Page Applications and HTML files +This fork adds the ability to encrypt entire directories to password-protect an entire static site hosted on a static site host, e.g. Amazon S3 or Google Cloud Storage or Github Pages. + +It does so by caching the password entered the first time, to use it again for all other pages a user may visit, and caching the derived keys per file in localStorage to speed up decryption. + +## Installation: + +```sh +npm i -D https://github.com/souramoo/pagecrypt/releases/download/6.2.0/pagecrypt-6.2.0.tgz +``` + +## Usage for whole directories: + +Assuming you have a directory `src/` to encrypt and an empty target directory `dist/`... + +```sh +PASSWORD=hunter2 +dir=$(pwd) +cd src +find . -name "*.html" -print -exec npx pagecrypt {} ${dir}/dist/{} ${PASSWORD} \; +cd .. +``` + +You should now be able to publish the contents of `dist/` :) + +This should also work in cloud CI workflows to automatically password protect deployments. + +# Original description + > Easily add client-side password-protection to your Single Page Applications and HTML files. Inspired by [MaxLaumeister/PageCrypt](https://github.com/MaxLaumeister/PageCrypt), but rewritten to use native `Web Crypto API` and greatly improve UX + security. Thanks for sharing an excellent starting point to create this tool! @@ -9,7 +37,7 @@ Inspired by [MaxLaumeister/PageCrypt](https://github.com/MaxLaumeister/PageCrypt **NOTE: Make sure you are using Node.js v16 or newer.** ```sh -npm i -D pagecrypt +npm i -D https://github.com/souramoo/pagecrypt/releases/download/6.2.0/pagecrypt-6.2.0.tgz ``` There are 4 different ways to use `pagecrypt`: @@ -175,7 +203,7 @@ Since this magic link feature is using the [URI Fragment](https://en.m.wikipedia - Most importantly, think twice about what kinds of sites and apps you publish to the open internet, even if they are encrypted. - If you use the magic link to login, beware that the password remains as a history entry! Feel free to submit a PR if you know a workaround for this! -- Also keep in mind that the `sessionStorage` saves the encryption key (which is derived from the password) until the browser is restarted. This is what allows the rapid page reloads during the same session - at the cost of decreasing the security on your local device. +- Also keep in mind that the `localStorage` saves the encryption key (which is derived from the password). This is what allows the rapid page reloads during the same session - at the cost of decreasing the security on your local device. - Only share magic links via secure channels, such as E2E-encrypted chats and emails. - `pagecrypt` only encrypts the contents of a single HTML file, so try to inline as much JS, CSS and other sensitive assets into this HTML file as possible. If you're unable to inline all sensitive assets, you can hide your other assets by placing them on another server, and then only reference the external resources within the `pagecrypt` protected HTML file instead. Of course, these could in turn be protected or hidden if you need to. If executed correctly, this allows you to completely hide what your webpage or app is about by only deploying a single HTML file to the public web. Neat! From dc1155c541354973f1d0f8027473787a320e0bc8 Mon Sep 17 00:00:00 2001 From: Souradip Mookerjee Date: Sun, 20 Aug 2023 16:59:30 +0100 Subject: [PATCH 6/7] self-repair if key is broken but password is correct --- package.json | 2 +- src/decrypt-template.html | 2 +- test/package.json | 2 +- web/decrypt.ts | 47 ++++++++++++++++++++++++++++----------- 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 3dd16ab..d6c512d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pagecrypt", - "version": "6.2.0", + "version": "6.2.1", "description": "Easily add client-side password-protection to your Single Page Applications and HTML files.", "main": "src/index.ts", "type": "module", diff --git a/src/decrypt-template.html b/src/decrypt-template.html index dece625..bb66ede 100644 --- a/src/decrypt-template.html +++ b/src/decrypt-template.html @@ -5,7 +5,7 @@ Protected Page - + diff --git a/test/package.json b/test/package.json index 36efd9e..a6d84aa 100644 --- a/test/package.json +++ b/test/package.json @@ -12,6 +12,6 @@ "test:verify": "vite" }, "dependencies": { - "pagecrypt": "file:../dist/pagecrypt-6.2.0.tgz" + "pagecrypt": "file:../dist/pagecrypt-6.2.1.tgz" } } diff --git a/web/decrypt.ts b/web/decrypt.ts index f44ab66..785c6f2 100644 --- a/web/decrypt.ts +++ b/web/decrypt.ts @@ -28,7 +28,7 @@ document.addEventListener('DOMContentLoaded', async () => { iv = bytes.slice(32, 32 + 16) ciphertext = bytes.slice(32 + 16) - const pwdOld = localStorage.getItem("pwd") + const pwdOld = localStorage.getItem('pwd') if (pwdOld) { pwd.value = pwdOld } @@ -111,13 +111,20 @@ async function decrypt() { show(form) header.classList.replace('hidden', 'flex') - if(localStorage.getItem('pwd')) { + let automatic = false + + if (localStorage.getItem('pwd')) { // Delete invalid password localStorage.removeItem('pwd') - } else if (localStorage.getItem(window.location.href)) { + automatic = true + } + if (localStorage.getItem(window.location.href)) { // Delete invalid key localStorage.removeItem(window.location.href) - } else { + automatic = true + } + + if (!automatic) { // Only show when user actually entered a password themselves. error('Wrong password.') } @@ -170,19 +177,33 @@ async function decryptFile( const decoder = new TextDecoder() let k = localStorage.getItem(window.location.href) + let key: CryptoKey | null + let data = new Uint8Array() - const key = k - ? await importKey(JSON.parse(k)) - : await deriveKey(salt, password, iterations) + try { + key = k + ? await importKey(JSON.parse(k)) + : await deriveKey(salt, password, iterations) + data = new Uint8Array( + await subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext), + ) + if (!data) throw 'Malformed data' + } catch (e) { + // Delete invalid key and try a saved password + localStorage.removeItem(window.location.href) + key = await deriveKey(salt, password, iterations) - const data = new Uint8Array( - await subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext), - ) - if (!data) throw 'Malformed data' + data = new Uint8Array( + await subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext), + ) + } // If no exception were thrown, decryption succeded and we can save the key. - localStorage.setItem(window.location.href, JSON.stringify(await subtle.exportKey('jwk', key))) - localStorage.setItem("pwd", password) + localStorage.setItem( + window.location.href, + JSON.stringify(await subtle.exportKey('jwk', key)), + ) + localStorage.setItem('pwd', password) return decoder.decode(data) } From 9d698178145fb5ba4a662d6554e6a823cd5835d6 Mon Sep 17 00:00:00 2001 From: Souradip Mookerjee Date: Sun, 20 Aug 2023 17:00:31 +0100 Subject: [PATCH 7/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 73411c0..07667a9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ It does so by caching the password entered the first time, to use it again for a ## Installation: ```sh -npm i -D https://github.com/souramoo/pagecrypt/releases/download/6.2.0/pagecrypt-6.2.0.tgz +npm i -D https://github.com/souramoo/pagecrypt/releases/download/6.2.1/pagecrypt-6.2.1.tgz ``` ## Usage for whole directories: