Code Monkey home page Code Monkey logo

pagecrypt's Introduction

๐Ÿ” PageCrypt - Password Protected Single Page Applications and HTML files

Easily add client-side password-protection to your Single Page Applications and HTML files.

Inspired by 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!

Get started

NOTE: Make sure you are using Node.js v16 or newer.

npm i -D pagecrypt

There are 4 different ways to use pagecrypt:

1. Encrypt HTML in modern browsers, Deno or Node.js using pagecrypt/core

The encryptHTML() and generatePassword() functions are using Web Crypto API and will thus be able to run in any ESM compatible environment that supports Web Crypto API.

This allows you to use the same pagecrypt API in any environment where you can run modern JavaScript.

encryptHTML(inputHTML: string, password: string, iterations?: number): Promise<string>

import { encryptHTML } from 'pagecrypt/core'

const inputHTML = `
    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="UTF-8">
        </head>
        <body>
            Secret
        </body>
    </html>
`

// Encrypt a HTML string and return an encrypted HTML string.
// Write it to a file or send as an HTTPS response.
const encryptedHTML = await encryptHTML(inputHTML, 'password')

// Optional: You can customize the number of password iterations if you want increased security.
const iterations = 3e6 // Same as 3_000_000
const customIterations = await encryptHTML(inputHTML, 'password', iterations)

generatePassword(length: number, characters: string): string

import { generatePassword, encryptHTML } from 'pagecrypt/core'

// Generate a random password without any external dependencies
const password = generatePassword(64)
const encryptedHTML = await encryptHTML(inputHTML, password)

You can also provide a custom dictionary of characters to use in your password:

generatePassword(71, '!#$%&()*+,-./:;<=>?@[]^_{|}~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')

2. Node.js API

When working in a Node.js environment, you may prefer the pagecrypt Node.js build. This also includes the encrypt() function to read and write directly from and to the file system.

encrypt(inputFile: string, outputFile: string, password: string, iterations: number): Promise<void>

import { encrypt } from 'pagecrypt'

// Encrypt a HTML file and write to the filesystem
await encrypt('index.html', 'encrypted.html', 'password')

// You can optionally customize the number of password iterations
const iterations = 3e6 // Same as 3_000_000
await encrypt('index.html', 'encrypted.html', 'password', iterations)

NOTE: Importing pagecrypt also gives you access to generatePassword() and encryptHTML() from pagecrypt/core.

import { generatePassword, encryptHTML } from 'pagecrypt'

const password = generatePassword(48)
const iterations = 3e6 // Same as 3_000_000

const encrypted = await encryptHTML(inputHTML, password, iterations)

3. CLI

Encrypt a single HTML-file with one command:

npx pagecrypt <src> <dest> [password] [options]

Encrypt using a generated password with given length:

npx pagecrypt <src> <dest> -g <length>

3.1. CLI Help

  Description
    Encrypt the <src> HTML file with [password] and save the result in the <dest> HTML file.

  Usage
    $ pagecrypt <src> <dest> [password] [options]

  Options
    -g, --generate-password    Generate a random password with given length. Must be a number if used.
    -i, --iterations           The number of password iterations.
    -v, --version              Displays current version
    -h, --help                 Displays this message

  Examples
    $ pagecrypt index.html encrypted.html password
    $ pagecrypt index.html encrypted.html --generate-password 64
    $ pagecrypt index.html encrypted.html -g 64
    $ pagecrypt index.html encrypted.html password --iterations 3e6
    $ pagecrypt index.html encrypted.html -g 64 --i 3e6

4. Automate pagecrypt in your build process

Use either the pagecrypt Node.js API or the CLI to automatically encrypt the builds for your single page applications.

npm i -D pagecrypt

package.json:

{
    "devDependencies": {
        "pagecrypt": "^5.0.0"
    },
    "scripts": {
        "build": "...",
        "postbuild": "pagecrypt index.html encrypted.html password"
    }
}

Deploying a SPA or Website Encrypted with pagecrypt

Since the output is a single HTML file, you can host it anywhere. This lets you bypass the need for server access to use HTTP basic authentication for password protection.

What this means in practice is that pagecrypt enables you to deploy private apps and websites to any static frontend hosting platform, often for free. Great for prototypes and client projects.

Share a Magic Link to Let Users Open Protected Pages With a Single Click

To make it easier for your users to access protected pages, you can create a magic link by adding # followed by your password to your deployment URL:

https://<link-to-your-page>#<password>

Then users can simply click the link to load the protected SPA or website - a really smooth UX! Just make sure to keep the link safe by sharing it via E2E-encrypted chats and emails.

How to Create a Magic Link

  1. Deploy your encrypted HTML file to any web server and copy the URL from your browser.
  2. Create the link by starting with your URL, then writing an #, followed by your password. E.g. https://example.com#password
  3. Make sure the link starts with the https:// protocol to keep users safe.

Since this magic link feature is using the URI Fragment, it will not be sent across the internet once the user clicks the link. Only the first part before the # leaves the user's computer to fetch the HTML page, and the rest remains in the browser, used for local decryption. Additionally, the fragment is removed from the browser address field when the page loads. However, beware that the password remains as a history entry if you use magic links!

Security Considerations

  • 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.
  • 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!

Development

Project structure:

  • /web - Web frontend for public webpage (decrypt-template.html).
  • /src/core.ts - pagecrypt core library.
  • /src/index.ts - pagecrypt Node.js library.
  • /src/cli.ts - pagecrypt CLI.
  • /test - simple testing setup.
  • /scripts - local scripts for development tasks.

Setup a local development environment

  1. Install Node.js >= 16.0.0
  2. Run npm install in project root.

Testing

First do one of the following:

  • npm test to run the tests.
  • npm run test:build to first build a new version of pagecrypt and then run the tests.

Then run npm run verify in another terminal and verify the test results at http://localhost:3000.

On the test results page you will find links to open output files in new tabs, buttons to copy passwords, and a special # link to verify that magic links decrypt the page immediately when the page loads.

To test pagecrypt/core and verify encryption in the browser, use the button at the bottom of the list. Download the file and then copy the password by clicking the button again to decrypt it. If you save the file to the same directory as the other generated files, you can use the links just like for other results. Use the reset button to encrypt another file.


Welcome to submit issues and pull requests!

License

AGPL-3.0

Copyright (c) 2015 Maximillian Laumeister Copyright (c) 2021-2024 Samuel Plumppu

This is a complete rewrite of the MIT-licensed PageCrypt created by Maximillian Laumeister.

pagecrypt's People

Contributors

greenheart avatar metonym avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

pagecrypt's Issues

Idea : Dark Mode support

Hello sir,
I was using the original pagecrypt and just came across your version of pagecrypt and i must say it is worth using.Now i want to use it in my personal site but the only problem is lack of dark mode.So can you please add dark-mode to login page and all other area?

Simplify clearing of the URL hash after decryption

The following can be simplified, from this:

pagecrypt/web/decrypt.ts

Lines 41 to 44 in 4571f66

const url = new URL(window.location.href)
pwd.value = url.hash.slice(1)
url.hash = ''
history.replaceState(null, '', url.toString())

To this:

const parts = location.href.split('#')
pwd.value = parts[1]
history.replaceState(null, '', parts[0])

This works because location.href.split('#') always returns the href without the URL hash, no matter if it is present or not.

Get an Error-Message

npx pagecrypt index.html index_encrypt.html test
file:///home/ansible/node_modules/pagecrypt/cli.js:23
var crypto = await loadCrypto();
^^^^^

SyntaxError: Unexpected reserved word
at Loader.moduleStrategy (internal/modules/esm/translators.js:133:18)
at async link (internal/modules/esm/module_job.js:42:21)
npm ERR! code 1
npm ERR! path /home/ansible/pagecrypt
npm ERR! command failed
npm ERR! command sh -c pagecrypt "index.html" "index_encrypt.html" "test"

npm ERR! A complete log of this run can be found in:
npm ERR! /home/ansible/.npm/_logs/2021-12-23T12_13_17_224Z-debug.log

Page not render after submit password form

I have problem on static sites of rendar using react. It shows white page ...

I got two errors on console

GET https://test.onrender.com/%PUBLIC_URL%/manifest.json 400
manifest.json:1 Manifest: Line: 1, column: 1, Syntax error.

Can you help me or Have another way?

Node version 16.19.0
script
"build": "react-scripts build && pagecrypt public/index.html build/index.html password",

Setup dead code elimination for the build process

Currently, the decrypt-template.html contains about 75% unused code that is shipped to production but not actually used for the simple login page.

This needs to be fixed to improve performance. The Pagecrypt decrypt-template.html wrapper should be as thin as possible.

Having the same key for multiple pages

Hello !

Thanks for that project ! (:
I use it to protect a simple static site with 2 pages. So I encrypt the 2 pages by executing pagecrypt twice with the same password. But when I browse to the pages, it asks me the password in every page.
If I understand well, it's because there is one key saved in the SessionStore, and I have two keys for the same password.

Would it be possible to either:

  • Save multiple keys for same domain in session ?
  • Being able in the CLI to encrypt two pages with the same key ?

Thanks for your help (:
A

Autosubmit on focusout or add Submit button

First of all, thank you for this really cool package!

I have a small suggestion: The UX is slightly confusing currently in mobile phones. Once I enter the password, I usually hit "done" which closes the mobile keyboard - but doesn't submit the input. It would be really nice if you can submit the form on focusout of the input.

Better - add capability to customize the template

Investigate ways to execute the app bundle in the top level document without using an <iframe> with the `srcdoc` attribute

Up until this point, pagecrypt has used the srcdoc attribute om <iframe> elements. This has mostly worked well for simple apps and websites, but severely limited advanced apps to use pagecrypt.

By removing the <iframe> and also stop using the srcdoc attribute, we could bypass these restrictions.

This would enable apps to use the full web platform instead of just using a small subset, which in turn would would make pagecrypt more valuable. For example, this would enable developers to deploy SPA:s with routing and similar features currently not supported in the <iframe> when using srcdoc.

Need to investigate potential solutions further, but here are some initial ideas:

  • document.write() (might have issues with injecting script)
  • explore iframe sandbox properties to allow other features. allow-same-origin combined with allow-scripts are worth trying.
  • Explore ways to make the iframe part of the parent document
  • Explore other ways to remove the original page and completely rewrite it.
  • the DOMParser API looks promising to parse an entire document and replace with inline scripts. Need to test for advanced features though.

Save key in sessionStorage for easy repeated visits during the same session

How it should work:

On DOMContentLoaded:

if sessionStorage.key:
  try decrypting (call decrypt as if the user had submitted the form immediately on pageload)
  if successful:
    show result
  else
    show regular password prompt like nothing happened

After successful decryption:

sessionStorage.key = key

then continue loading payload like normal

Fix the CSS loading issue in latest test builds

One issue is that the decrypt template is rendering in a weird way when loading the encrypted page. Seems like one of the SVGs is off, and might need styling in a way that doesn't cause the rendering glitch. This needs to be fixed before we can make a new release.

With a closer look, it seems like Vite is trying to move the inlined CSS and JS out into separate modules at dev or build time. That could be causing the CSS to show up later and thus causing the rendering issue. We need to check if this behavior can be disabled for the vite dev server or vite build.

While we're at it, it would be good to update dependencies too.

Consider getting referenced on the original pagecrypt project?

I would suggest to try to make it so, that this tool is referenced on the original repo of Max Laumeister.
I've actually "ported" Max's tool to nodejs, and wanted to contribute it back to his repo and publish the package to npm, and then only I noticed that the name was already taken. That's how I found out about your (way) more elaborate nodejs implementation of this tool.
This would also gain this tool some visibility!

Investigate more performant way to inline large bundles

Some ideas / potential solutions to test:
1. Use external <script> that can be async or defered. Won't work since it will break the single input HTML -> single output HTML which is a main feature of pagecrypt.
2. Inline payload as a<p class="hidden"> instead of as a <script> (to prevent the script from blocking render). Then load the data on the DOMContentLoaded event and start decoding the base64 data.

Consider adding support for automated password generation

Since automated password generation is required in all 4 current pagecrypt use cases, it might be a good idea to add it to the core library, to reduce the need for external dependencies.

This basic password generator implements the main features of generate-password except the strict mode. However, with sufficient password length, this shouldn't be any problem.

This implementation also seems to be faster compared to generate-password, since it uses fewer operations.

const crypto = require('crypto')

function generatePassword(
    length = 40,
    characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
) {
    return Array.from(crypto.randomFillSync(new Uint32Array(length)))
        .map((x) => characters[x % characters.length])
        .join('')
}

console.log(generatePassword(80))

How to implement

  • If included, this needs to be opt-in only.
  • Default CLI + JS API:s should remain unchanged
  • Maybe include options object for JS API, and a -g [length] / --generate [length] flag

Error using `pagecrypt` with Node 19

Ref: render-examples/pagecrypt#1

Node version 19 was released last week, and there appears to be a difference in how the loadCrypto function is evaluated.

In Node 16, for example, this condition is not evaluated and crypto is loaded dynamically. However, in Node 19, it appears that the condition is evaluated, producing the ReferenceError: window is not defined message. I'm unsure if this is a bug in Node 19 or if it's an intentional breaking change.

This is the output when running the CLI using Node 19:

file:///Users/pagecrypt/cli.js:16
const crypto2 = window.crypto || globalThis.crypto;
                    ^
ReferenceError: window is not defined

Consider switching from PBKDF2 to argon2 for the KDF

While the current implementation using PBKDF2 is good for the current threat model, argon2 is more resistant towards GPU brute forcing attacks. Argon2 could thus increase security of encrypted pages.

However, since my use cases are well served by PBKDF2 hashing, this is not a top priority for me at the moment. If someone wants this to be implemented, you're welcome to submit a PR and we can work from there.

One question to think about is whether we should switch to argon2 for all hashing, or allow users to choose if they want to use PBKDF2 or argon2. Perhaps we could use different decryption templates (including different scripts) for the different hashing algorithms. This would add complexity, but could be useful for users who are OK with PBKDF2 and don't need argon2.

To implement this, these libraries might be useful:

Browser: https://github.com/antelle/argon2-browser
Node.js https://github.com/ranisalt/node-argon2
Deno: Not sure, but since https://github.com/antelle/argon2-browser supports WASM, it might be able to run in Deno and similar environments.

Investigate replacing `node-forge` with native web crypto API:s

Why replace node-forge?

  • To reduce the size of decrypt-template.html. Currently it's shipping 4x the amount of code compared to cryptojs
  • Although node-forge has greatly improved performance compared to cryptojs, it's still slower than native crypto API:s. Replacing node-forge would reduce browser support, but that won't be a problem since we're targeting modern browsers, which has 95% support for web crypto as of 2021-04-23

Consider removing the timeout after successful decryption to improve UX

We need to investigate if there are any benefits to waiting 1000 ms before showing the decrypted page. For example, it might help with loading the iframe in the background, improving the perceived performance since it might have loaded in the background instead of being shown as a white page to users.

If we don't get any benetfits from the delay, removing it would improve UX.

By removing the success status message, we could also delete some parts of the decrypt template:

  • success helper
  • some tailwind classes won't be used anymore
  • The unlocked icon won't be used anymore and can be removed
  • If we don't swap between icons, the code toggling .hidden for icons could be removed too.

All in all, this would save about 4 kb from the decrypt template.

Docs: Update usage for `generatePassword()` in README

See updated docstring here:

pagecrypt/src/core.ts

Lines 60 to 82 in c850468

/**
* Encrypt an HTML string with a given password.
* The resulting page can be viewed and decrypted by opening the output HTML file in a browser, and entering the correct password.
*
* @param {string} inputHTML The HTML string to encrypt.
* @param {string} password The password used to encrypt + decrypt the content.
* @param {number} iterations The number of iterations to derive the key from the password.
* @returns A promise that will resolve with the encrypted HTML content
*/
export async function encryptHTML(
inputHTML: string,
password: string,
iterations: number = 2e6,
) {
return (decryptTemplate as string).replace(
/<!--ENCRYPTED PAYLOAD-->/,
`<pre class="hidden" data-i="${iterations.toExponential()}">${await getEncryptedPayload(
inputHTML,
password,
iterations,
)}</pre>`,
)
}

It's also important to enforce the max length of the character set = 256 characters. This should likely be in the docstring too, so we could update generatePassword() too to reflect that.

Consider setting up a proper test suite using cypress

test all usage alternatives:
  test CLI
  test JS `encrypt()`
  test JS `encryptHTML()`

test all options
- for CLI
- for `encrypt()`
- for `encryptHTML()`

Use regular script to generate the test payloads, and then use cypress to verify results.

Page altered in the decrypted version

First of all thanks for pagecrypt. The tool is great and I love it!

I'm writing a pretty minimal landing page for my html ebook:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <style>
      body {
        width: 100%;
        padding: 0;
        margin: 0;
      }
      .title-page {
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
        flex-direction: column;
      }
      .title {
        font-size: 8em;
      }
      .title-page > .author {
        font-size: 3em;
      }
      .title-page > .date {
        font-size: 3em;
      }
    </style>
  </head>
  <body>
    <div class="title-page">
      <p class="title">$title$</p>
      <p class="author">$author$</p>
      <p class="date">$date$</p>
    </div>
    <div class="wrapper">$body$</div>
  </body>
</html>

It looks like this (on a mobile device):
image

After encrypting (and decrypting in the browser) the captions are way bigger:
image

Upon inspecting the elements the styles seem correct. I'm running the encryption with npx pagecrypt example.html example_crypt.html pass. Any idea what's causing the issue and how to circumvent it?

I keep getting an 'UnhandledPromiseRejectionWarning' when trying to run pagecrypt using npm..

pagecrypt index.html encrypted.html password

๐Ÿ” Encrypting index.html โ†’ encrypted.html with ๐Ÿ”‘: password
(node:1608) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'getRandomValues' of undefined
    at getEncryptedPayload (...*/node_modules/pagecrypt/cli.js:61:31)
    at encryptHTML (...*/node_modules/pagecrypt/cli.js:74:100)
    at encryptFile (...*/node_modules/pagecrypt/cli.js:98:16)
    at async encrypt (...*/node_modules/pagecrypt/cli.js:107:21)
    at async ...*/node_modules/pagecrypt/cli.js:129:5
(Use `node --trace-warnings ...` to show where the warning was created)
(node:1608) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)     
(node:1608) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Am I missing something?

Add support for custom decryption HTML templates

Add option to use a custom HTML template, and use the JS decryption code together with any html template that provides the right elements like a password form.

Or export the decryption helpers and let users create custom templates with more control.

If anyone wants this feature, feel free to submit a PR implementing this! ๐Ÿ˜Š


Notes from initial research:

  • To allow custom HTML templates to be provided, they need to contain a few key elements used by pagecrypt (like the <pre> element for writing/reading the encrypted data), and a script tag to handle the decryption logic.
  • The core decrypt library need to be updated to support custom HTML templates, as well as adding test coverage.
  • Ideally, this should be possible to accomplish without breaking changes to other use cases, but we'll see.
  • An interesting approach might be to move decrypt functionality to the core pagecrypt library. This could then be reusable utility functions that could be imported into custom templates as well as the official HTML template. This would need to be documented in the README to be easy to use.
  • By moving the core encryption and decryption logic into the same library, we would be able to reuse the critical config and make it easier to keep them in sync.
  • The core decryption functionality is really just decryptFile({ salt, iv, ciphertext }, password) which would expose a simple API. Or perhaps even better decryptFile(encryptedBase64Data, password), and letting the core decrypt function take care of base64 conversion, parsing the raw bytes, and then decrypting. This would be a nice separation between presentation and decryption logic.
  • A question is what to do with magic link support (a core feature) and sessionStorage support for caching keys during the same session. Perhaps there is a way to allow users to implement these features for their custom templates in some way.
  • Based on https://github.com/frehner/modern-guide-to-packaging-js-library, maybe we don't have to ship custom HTML but only custom CSS could be enough. Though it might be nice with full control of the HTML and CSS

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.