Socket
Socket
Sign inDemoInstall

pagecrypt

Package Overview
Dependencies
3
Maintainers
1
Versions
20
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 4.0.1 to 5.0.0

core.d.ts

19

CHANGELOG.md
# Changelog for `pagecrypt`
## 5.0.0 - 2021-07-15
### Features
- BREAKING: feature(package): Convert modules to use ESM by default instead of CommonJS. Update your build tool to use `import` syntax instead of `require` - or keep using `pagecrypt@^4.0.1` which supports CommonJS `require`.
- feature(package): Add a new `pagecrypt/core` module that can be imported to use the core library features in browsers, Deno and any other ESM compatible modern JS environment. For Node.js, the index import `pagecrypt` still works just like before.
- feature(types): Add TypeScript definitions to improve DX and automation in TypeScript projects.
- feature(crypto): Use isomorphic Web Crypto API to allow code reuse between Node.js, browsers and other ESM compatible environments.
- feature(password generator): Use the isomorphic Web Crypto API to make project run in Node.js, browsers and other ESM compatible environments.
- feature(build): Improve package build setup using esbuild and node-fs-extra
### Fixes
- fix(package): Explicitly use CommonJS for config files.
- chore(deps): Upgrade dependencies.
---
## 4.0.1 - 2021-05-04

@@ -4,0 +23,0 @@

168

cli.js
#!/usr/bin/env node
const sade = require('sade')
// src/cli.ts
import sade from "sade";
const { encrypt, generatePassword } = require('./index')
const pkg = require('./package.json')
// src/index.ts
import { mkdir, readFile, writeFile } from "fs/promises";
import { resolve, dirname } from "path";
sade(`${pkg.name} <src> <dest> [password]`, true)
.version(pkg.version)
.describe(
'Encrypt the <src> HTML file with [password] and save the result in the <dest> HTML file.',
)
.example('index.html encrypted.html password')
.example('index.html encrypted.html --generate-password 64')
.example('index.html encrypted.html -g 64')
.option(
'-g, --generate-password',
'Generate a random password with given length. Must be a number if used.',
)
.action(async (src, dest, password, options) => {
const length = options['generate-password']
// src/core.ts
import { base64 } from "rfc4648";
if (length) {
if (Number.isInteger(length)) {
const pass = generatePassword(length)
console.log(`🔐 Encrypting ${src} → ${dest} with 🔑: ${pass}`)
await encrypt(src, dest, pass)
} else {
console.error(
'❌: The <length> must be an integer when using --generate-password <length>',
)
process.exit(1)
}
} else if (password) {
console.log(`🔐 Encrypting ${src} → ${dest} with 🔑: ${password}`)
await encrypt(src, dest, password)
} else {
console.error(
'❌: Either provide a password or use --generate-password <length>',
)
process.exit(1)
}
})
.parse(process.argv)
// src/crypto.ts
async function loadCrypto() {
if (globalThis && globalThis.crypto) {
return new Promise((resolve2, _reject) => resolve2(globalThis.crypto));
} else {
const cryptoLocal = await import("crypto");
return cryptoLocal.webcrypto;
}
}
var crypto = await loadCrypto();
var crypto_default = crypto;
// src/decrypt-template.html
var decrypt_template_default = `<!DOCTYPE html>\r
<html>\r
<head>\r
<meta charset="utf-8">\r
<meta name="viewport" content="width=device-width, initial-scale=1">\r
<meta name="robots" content="noindex, nofollow">\r
<title>Protected Page</title>\r
<script type="module">var e,t;var n={chars:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",bits:6},r=function(e,t){return function(e,t,n){if(void 0===n&&(n={}),!t.codes){t.codes={};for(var r=0;r<t.chars.length;++r)t.codes[t.chars[r]]=r}if(!n.loose&&e.length*t.bits&7)throw new SyntaxError("Invalid padding");for(var o=e.length;"="===e[o-1];)if(--o,!(n.loose||(e.length-o)*t.bits&7))throw new SyntaxError("Invalid padding");for(var a=new(n.out||Uint8Array)(o*t.bits/8|0),s=0,i=0,c=0,d=0;d<o;++d){var l=t.codes[e[d]];if(void 0===l)throw new SyntaxError("Invalid character "+e[d]);i=i<<t.bits|l,(s+=t.bits)>=8&&(s-=8,a[c++]=255&i>>s)}if(s>=t.bits||255&i<<8-s)throw new SyntaxError("Unexpected end of data");return a}(e,n,t)};const o=document.querySelector.bind(document),[a,s,i,c,d]=["input","header","#msg","form","#load"].map(o);let l,u,w;document.addEventListener("DOMContentLoaded",(async()=>{const e=o("pre").innerText;e||(a.disabled=!0,y("No encrypted payload."));const t=r(e);l=t.slice(0,32),u=t.slice(32,48),w=t.slice(48),sessionStorage.k?await h():(f(d),p(c),s.classList.replace("hidden","flex"),a.focus())}));const m=(null==(e=window.crypto)?void 0:e.subtle)||(null==(t=window.crypto)?void 0:t.webkitSubtle);function p(e){e.classList.remove("hidden")}function f(e){e.classList.add("hidden")}function y(e){i.innerText=e,s.classList.add("text-red-600")}async function h(){d.lastElementChild.innerText="Decrypting...",f(s),f(c),p(d),await async function(e){return new Promise((t=>setTimeout(t,e)))}(60);try{const e=await async function({salt:e,iv:t,ciphertext:n},r){const o=new TextDecoder,a=sessionStorage.k?await async function(e){return m.importKey("jwk",e,"AES-GCM",!0,["decrypt"])}(JSON.parse(sessionStorage.k)):await async function(e,t){const n=new TextEncoder,r=await m.importKey("raw",n.encode(t),"PBKDF2",!1,["deriveKey"]);return await m.deriveKey({name:"PBKDF2",salt:e,iterations:2e6,hash:"SHA-256"},r,{name:"AES-GCM",length:256},!0,["decrypt"])}(e,r),s=new Uint8Array(await m.decrypt({name:"AES-GCM",iv:t},a,n));if(!s)throw"Malformed data";return sessionStorage.k=JSON.stringify(await m.exportKey("jwk",a)),o.decode(s)}({salt:l,iv:u,ciphertext:w},a.value);document.write(e),document.close()}catch(e){f(d),p(c),s.classList.replace("hidden","flex"),sessionStorage.k?sessionStorage.removeItem("k"):y("Wrong password."),a.value="",a.focus()}}window.crypto.subtle||(y("Please use a modern browser."),a.disabled=!0),c.addEventListener("submit",(async e=>{e.preventDefault(),await h()}));<\/script>\r
<style>/*! tailwindcss v2.1.2 | MIT License | https://tailwindcss.com *//*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */*,::after,::before{box-sizing:border-box}:root{-moz-tab-size:4;-o-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji'}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,'Liberation Mono',Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset{margin:0;padding:0}ol,ul{list-style:none;margin:0;padding:0}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";line-height:1.5}body{font-family:inherit;line-height:inherit}*,::after,::before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}*{--tw-shadow:0 0 #0000;--tw-ring-inset:var(--tw-empty, );/*!*//*!*/--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59, 130, 246, 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000}.fixed{position:fixed}.bottom-0{bottom:0}.right-0{right:0}.mx-auto{margin-left:auto;margin-right:auto}.mr-4{margin-right:1rem}.mb-4{margin-bottom:1rem}.flex{display:flex}.table{display:table}.hidden{display:none}.h-screen{height:100vh}.h-28{height:7rem}.h-full{height:100%}.h-6{height:1.5rem}.w-full{width:100%}.w-6{width:1.5rem}.max-w-sm{max-width:24rem}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:.5rem}.rounded-sm{border-radius:.125rem}.border{border-width:1px}.bg-yellow-200{--tw-bg-opacity:1;background-color:rgba(253,230,138,var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgba(249,250,251,var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.p-4{padding:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.pt-40{padding-top:10rem}.text-sm{font-size:.875rem;line-height:1.25rem}.font-light{font-weight:300}.font-extralight{font-weight:200}.font-semibold{font-weight:600}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-red-600{--tw-text-opacity:1;color:rgba(220,38,38,var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgba(0,0,0,var(--tw-text-opacity))}.text-opacity-100{--tw-text-opacity:1}.placeholder-black::-moz-placeholder{--tw-placeholder-opacity:1;color:rgba(0,0,0,var(--tw-placeholder-opacity))}.placeholder-black:-ms-input-placeholder{--tw-placeholder-opacity:1;color:rgba(0,0,0,var(--tw-placeholder-opacity))}.placeholder-black::placeholder{--tw-placeholder-opacity:1;color:rgba(0,0,0,var(--tw-placeholder-opacity))}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0, 0, 0, 0.25);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.loading{pointer-events:none;width:2em;height:2em;border:.15em solid transparent;border-color:#e2e8f0;border-top-color:#2563eb;border-radius:50%;-webkit-animation:spin .5s linear infinite;animation:spin .5s linear infinite}@-webkit-keyframes spin{100%{transform:rotate(360deg)}}@keyframes spin{100%{transform:rotate(360deg)}}.focus\\:border-black:focus{--tw-border-opacity:1;border-color:rgba(0,0,0,var(--tw-border-opacity))}.focus\\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}</style>\r
</head>\r
<body>\r
<main class="bg-yellow-200 w-full h-screen items-start tracking-wide p-4 pt-40 font-light">\r
<div class="max-w-sm w-full bg-gray-50 p-4 rounded-sm shadow-2xl mx-auto h-28">\r
<div id="load" class="flex items-center justify-center h-full">\r
<p class="loading w-6 h-6 mr-4"></p><p>Loading...</p>\r
</div>\r
<header class="hidden gap-2 mb-4 items-center">\r
<svg id="locked" class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">\r
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path>\r
</svg>\r
<p id="msg" class="text-sm">This page is password protected.</p>\r
</header>\r
<form class="hidden">\r
<input type="password" id="pwd" name="pwd" aria-label="Password" autofocus placeholder="Password" class="font-extralight bg-gray-200 flex w-full py-2 px-4 tracking-wider rounded-sm focus:outline-none text-black placeholder-black text-opacity-100 focus:border-black border" />\r
</form>\r
</div>\r
<a href="https://github.com/Greenheart/pagecrypt" class="fixed bottom-0 right-0 p-4">Created with <span class="font-semibold">PageCrypt</span></a>\r
</main>\r
<!--ENCRYPTED PAYLOAD-->\r
</body>\r
</html>`;
// src/core.ts
async function getEncryptedPayload(content, password) {
const encoder = new TextEncoder();
const salt = crypto_default.getRandomValues(new Uint8Array(32));
const baseKey = await crypto_default.subtle.importKey("raw", encoder.encode(password), "PBKDF2", false, ["deriveKey"]);
const key = await crypto_default.subtle.deriveKey({ name: "PBKDF2", salt, iterations: 2e6, hash: "SHA-256" }, baseKey, { name: "AES-GCM", length: 256 }, false, ["encrypt"]);
const iv = crypto_default.getRandomValues(new Uint8Array(16));
const ciphertext = new Uint8Array(await crypto_default.subtle.encrypt({ name: "AES-GCM", iv }, key, encoder.encode(content)));
const totalLength = salt.length + iv.length + ciphertext.length;
const mergedData = new Uint8Array(totalLength);
mergedData.set(salt);
mergedData.set(iv, salt.length);
mergedData.set(ciphertext, salt.length + iv.length);
return base64.stringify(mergedData);
}
async function encryptHTML(inputHTML, password) {
return decrypt_template_default.replace(/<!--ENCRYPTED PAYLOAD-->/, `<pre class="hidden">${await getEncryptedPayload(inputHTML, password)}</pre>`);
}
function generatePassword(length = 80, characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") {
return Array.from({ length }, (_) => getRandomCharacter(characters)).join("");
}
function getRandomCharacter(characters) {
let randomNumber;
do {
randomNumber = crypto_default.getRandomValues(new Uint8Array(1))[0];
} while (randomNumber >= 256 - 256 % characters.length);
return characters[randomNumber % characters.length];
}
// src/index.ts
async function encryptFile(inputFile, password) {
let content;
try {
content = await readFile(resolve(process.cwd(), inputFile), {
encoding: "utf-8"
});
} catch (e) {
console.error("\u274C Error reading file: ", e);
process.exit(1);
}
return await encryptHTML(content, password);
}
async function saveFile(outputFile, content) {
await mkdir(dirname(outputFile), { recursive: true });
return writeFile(resolve(process.cwd(), outputFile), content, {
encoding: "utf8"
});
}
async function encrypt(inputFile, outputFile, password) {
const encrypted = await encryptFile(inputFile, password);
return await saveFile(outputFile, encrypted);
}
// package.json
var name = "pagecrypt";
var version = "5.0.0";
// src/cli.ts
sade(`${name} <src> <dest> [password]`, true).version(version).describe("Encrypt the <src> HTML file with [password] and save the result in the <dest> HTML file.").example("index.html encrypted.html password").example("index.html encrypted.html --generate-password 64").example("index.html encrypted.html -g 64").option("-g, --generate-password", "Generate a random password with given length. Must be a number if used.").action(async (src, dest, password, options) => {
const length = options["generate-password"];
if (length) {
if (Number.isInteger(length)) {
const pass = generatePassword(length);
console.log(`\u{1F510} Encrypting ${src} \u2192 ${dest} with \u{1F511}: ${pass}`);
await encrypt(src, dest, pass);
} else {
console.error("\u274C: The <length> must be an integer when using --generate-password <length>");
process.exit(1);
}
} else if (password) {
console.log(`\u{1F510} Encrypting ${src} \u2192 ${dest} with \u{1F511}: ${password}`);
await encrypt(src, dest, password);
} else {
console.error("\u274C: Either provide a password or use --generate-password <length>");
process.exit(1);
}
}).parse(process.argv);

@@ -1,144 +0,109 @@

const {
randomFillSync,
webcrypto: { subtle, getRandomValues },
} = require('crypto')
const { mkdir, readFile, writeFile } = require('fs/promises')
const { resolve, dirname } = require('path')
const { base64 } = require('rfc4648')
// src/index.ts
import { mkdir, readFile, writeFile } from "fs/promises";
import { resolve, dirname } from "path";
const packageRootDir = dirname(__filename)
// src/core.ts
import { base64 } from "rfc4648";
/**
* Encrypt a string and turn it into an encrypted payload.
*
* @param {string} content The data to encrypt
* @param {string} password The password which will be used to encrypt + decrypt the content.
* @returns an encrypted payload
*/
async function getEncryptedPayload(content, password) {
const encoder = new TextEncoder()
const salt = getRandomValues(new Uint8Array(32))
const baseKey = await subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveKey'],
)
const key = await subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 2e6, hash: 'SHA-256' },
baseKey,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt'],
)
const iv = getRandomValues(new Uint8Array(16))
const ciphertext = new Uint8Array(
await subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
encoder.encode(content),
),
)
const totalLength = salt.length + iv.length + ciphertext.length
const data = new Uint8Array(
Buffer.concat([salt, iv, ciphertext], totalLength),
)
return base64.stringify(data)
// src/crypto.ts
async function loadCrypto() {
if (globalThis && globalThis.crypto) {
return new Promise((resolve2, _reject) => resolve2(globalThis.crypto));
} else {
const cryptoLocal = await import("crypto");
return cryptoLocal.webcrypto;
}
}
var crypto = await loadCrypto();
var crypto_default = crypto;
/**
* Encrypt a HTML file 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} inputFile The filename (or path) to the HTML file to encrypt.
* @param {string} password The password which will be used to encrypt + decrypt the content.
* @returns A promise that will resolve with the encrypted HTML content
*/
async function encryptFile(inputFile, password) {
let content
try {
content = await readFile(resolve(process.cwd(), inputFile), {
encoding: 'utf-8',
})
} catch (e) {
console.error('❌ Error reading file: ', e)
process.exit(1)
}
// src/decrypt-template.html
var decrypt_template_default = `<!DOCTYPE html>\r
<html>\r
<head>\r
<meta charset="utf-8">\r
<meta name="viewport" content="width=device-width, initial-scale=1">\r
<meta name="robots" content="noindex, nofollow">\r
<title>Protected Page</title>\r
<script type="module">var e,t;var n={chars:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",bits:6},r=function(e,t){return function(e,t,n){if(void 0===n&&(n={}),!t.codes){t.codes={};for(var r=0;r<t.chars.length;++r)t.codes[t.chars[r]]=r}if(!n.loose&&e.length*t.bits&7)throw new SyntaxError("Invalid padding");for(var o=e.length;"="===e[o-1];)if(--o,!(n.loose||(e.length-o)*t.bits&7))throw new SyntaxError("Invalid padding");for(var a=new(n.out||Uint8Array)(o*t.bits/8|0),s=0,i=0,c=0,d=0;d<o;++d){var l=t.codes[e[d]];if(void 0===l)throw new SyntaxError("Invalid character "+e[d]);i=i<<t.bits|l,(s+=t.bits)>=8&&(s-=8,a[c++]=255&i>>s)}if(s>=t.bits||255&i<<8-s)throw new SyntaxError("Unexpected end of data");return a}(e,n,t)};const o=document.querySelector.bind(document),[a,s,i,c,d]=["input","header","#msg","form","#load"].map(o);let l,u,w;document.addEventListener("DOMContentLoaded",(async()=>{const e=o("pre").innerText;e||(a.disabled=!0,y("No encrypted payload."));const t=r(e);l=t.slice(0,32),u=t.slice(32,48),w=t.slice(48),sessionStorage.k?await h():(f(d),p(c),s.classList.replace("hidden","flex"),a.focus())}));const m=(null==(e=window.crypto)?void 0:e.subtle)||(null==(t=window.crypto)?void 0:t.webkitSubtle);function p(e){e.classList.remove("hidden")}function f(e){e.classList.add("hidden")}function y(e){i.innerText=e,s.classList.add("text-red-600")}async function h(){d.lastElementChild.innerText="Decrypting...",f(s),f(c),p(d),await async function(e){return new Promise((t=>setTimeout(t,e)))}(60);try{const e=await async function({salt:e,iv:t,ciphertext:n},r){const o=new TextDecoder,a=sessionStorage.k?await async function(e){return m.importKey("jwk",e,"AES-GCM",!0,["decrypt"])}(JSON.parse(sessionStorage.k)):await async function(e,t){const n=new TextEncoder,r=await m.importKey("raw",n.encode(t),"PBKDF2",!1,["deriveKey"]);return await m.deriveKey({name:"PBKDF2",salt:e,iterations:2e6,hash:"SHA-256"},r,{name:"AES-GCM",length:256},!0,["decrypt"])}(e,r),s=new Uint8Array(await m.decrypt({name:"AES-GCM",iv:t},a,n));if(!s)throw"Malformed data";return sessionStorage.k=JSON.stringify(await m.exportKey("jwk",a)),o.decode(s)}({salt:l,iv:u,ciphertext:w},a.value);document.write(e),document.close()}catch(e){f(d),p(c),s.classList.replace("hidden","flex"),sessionStorage.k?sessionStorage.removeItem("k"):y("Wrong password."),a.value="",a.focus()}}window.crypto.subtle||(y("Please use a modern browser."),a.disabled=!0),c.addEventListener("submit",(async e=>{e.preventDefault(),await h()}));<\/script>\r
<style>/*! tailwindcss v2.1.2 | MIT License | https://tailwindcss.com *//*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */*,::after,::before{box-sizing:border-box}:root{-moz-tab-size:4;-o-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji'}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,'Liberation Mono',Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset{margin:0;padding:0}ol,ul{list-style:none;margin:0;padding:0}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";line-height:1.5}body{font-family:inherit;line-height:inherit}*,::after,::before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}*{--tw-shadow:0 0 #0000;--tw-ring-inset:var(--tw-empty, );/*!*//*!*/--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59, 130, 246, 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000}.fixed{position:fixed}.bottom-0{bottom:0}.right-0{right:0}.mx-auto{margin-left:auto;margin-right:auto}.mr-4{margin-right:1rem}.mb-4{margin-bottom:1rem}.flex{display:flex}.table{display:table}.hidden{display:none}.h-screen{height:100vh}.h-28{height:7rem}.h-full{height:100%}.h-6{height:1.5rem}.w-full{width:100%}.w-6{width:1.5rem}.max-w-sm{max-width:24rem}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:.5rem}.rounded-sm{border-radius:.125rem}.border{border-width:1px}.bg-yellow-200{--tw-bg-opacity:1;background-color:rgba(253,230,138,var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgba(249,250,251,var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.p-4{padding:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.pt-40{padding-top:10rem}.text-sm{font-size:.875rem;line-height:1.25rem}.font-light{font-weight:300}.font-extralight{font-weight:200}.font-semibold{font-weight:600}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-red-600{--tw-text-opacity:1;color:rgba(220,38,38,var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgba(0,0,0,var(--tw-text-opacity))}.text-opacity-100{--tw-text-opacity:1}.placeholder-black::-moz-placeholder{--tw-placeholder-opacity:1;color:rgba(0,0,0,var(--tw-placeholder-opacity))}.placeholder-black:-ms-input-placeholder{--tw-placeholder-opacity:1;color:rgba(0,0,0,var(--tw-placeholder-opacity))}.placeholder-black::placeholder{--tw-placeholder-opacity:1;color:rgba(0,0,0,var(--tw-placeholder-opacity))}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0, 0, 0, 0.25);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.loading{pointer-events:none;width:2em;height:2em;border:.15em solid transparent;border-color:#e2e8f0;border-top-color:#2563eb;border-radius:50%;-webkit-animation:spin .5s linear infinite;animation:spin .5s linear infinite}@-webkit-keyframes spin{100%{transform:rotate(360deg)}}@keyframes spin{100%{transform:rotate(360deg)}}.focus\\:border-black:focus{--tw-border-opacity:1;border-color:rgba(0,0,0,var(--tw-border-opacity))}.focus\\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}</style>\r
</head>\r
<body>\r
<main class="bg-yellow-200 w-full h-screen items-start tracking-wide p-4 pt-40 font-light">\r
<div class="max-w-sm w-full bg-gray-50 p-4 rounded-sm shadow-2xl mx-auto h-28">\r
<div id="load" class="flex items-center justify-center h-full">\r
<p class="loading w-6 h-6 mr-4"></p><p>Loading...</p>\r
</div>\r
<header class="hidden gap-2 mb-4 items-center">\r
<svg id="locked" class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">\r
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path>\r
</svg>\r
<p id="msg" class="text-sm">This page is password protected.</p>\r
</header>\r
<form class="hidden">\r
<input type="password" id="pwd" name="pwd" aria-label="Password" autofocus placeholder="Password" class="font-extralight bg-gray-200 flex w-full py-2 px-4 tracking-wider rounded-sm focus:outline-none text-black placeholder-black text-opacity-100 focus:border-black border" />\r
</form>\r
</div>\r
<a href="https://github.com/Greenheart/pagecrypt" class="fixed bottom-0 right-0 p-4">Created with <span class="font-semibold">PageCrypt</span></a>\r
</main>\r
<!--ENCRYPTED PAYLOAD-->\r
</body>\r
</html>`;
return await encryptHTML(content, password)
// src/core.ts
async function getEncryptedPayload(content, password) {
const encoder = new TextEncoder();
const salt = crypto_default.getRandomValues(new Uint8Array(32));
const baseKey = await crypto_default.subtle.importKey("raw", encoder.encode(password), "PBKDF2", false, ["deriveKey"]);
const key = await crypto_default.subtle.deriveKey({ name: "PBKDF2", salt, iterations: 2e6, hash: "SHA-256" }, baseKey, { name: "AES-GCM", length: 256 }, false, ["encrypt"]);
const iv = crypto_default.getRandomValues(new Uint8Array(16));
const ciphertext = new Uint8Array(await crypto_default.subtle.encrypt({ name: "AES-GCM", iv }, key, encoder.encode(content)));
const totalLength = salt.length + iv.length + ciphertext.length;
const mergedData = new Uint8Array(totalLength);
mergedData.set(salt);
mergedData.set(iv, salt.length);
mergedData.set(ciphertext, salt.length + iv.length);
return base64.stringify(mergedData);
}
/**
* 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 which will be used to encrypt + decrypt the content.
* @returns A promise that will resolve with the encrypted HTML content
*/
async function encryptHTML(inputHTML, password) {
const templateHTML = await readFile(
resolve(packageRootDir, 'decrypt-template.html'),
{ encoding: 'utf-8' },
)
return decrypt_template_default.replace(/<!--ENCRYPTED PAYLOAD-->/, `<pre class="hidden">${await getEncryptedPayload(inputHTML, password)}</pre>`);
}
function generatePassword(length = 80, characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") {
return Array.from({ length }, (_) => getRandomCharacter(characters)).join("");
}
function getRandomCharacter(characters) {
let randomNumber;
do {
randomNumber = crypto_default.getRandomValues(new Uint8Array(1))[0];
} while (randomNumber >= 256 - 256 % characters.length);
return characters[randomNumber % characters.length];
}
return templateHTML.replace(
/<!--ENCRYPTED PAYLOAD-->/,
`<pre class="hidden">${await getEncryptedPayload(
inputHTML,
password,
)}</pre>`,
)
// src/index.ts
async function encryptFile(inputFile, password) {
let content;
try {
content = await readFile(resolve(process.cwd(), inputFile), {
encoding: "utf-8"
});
} catch (e) {
console.error("\u274C Error reading file: ", e);
process.exit(1);
}
return await encryptHTML(content, password);
}
/**
* Save a file, creating directories and files if they don't yet exist
*
* @param {string} outputFile The filename (or path) where the file will be saved
* @param {string} content The file content
* @returns A promise that will resolve when the file has been saved.
*/
async function saveFile(outputFile, content) {
await mkdir(dirname(outputFile), { recursive: true })
return writeFile(resolve(process.cwd(), outputFile), content, {
encoding: 'utf8',
})
await mkdir(dirname(outputFile), { recursive: true });
return writeFile(resolve(process.cwd(), outputFile), content, {
encoding: "utf8"
});
}
/**
* Encrypt a HTML file 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} inputFile The filename (or path) to the HTML file to encrypt.
* @param {string} outputFile The filename (or path) where the encrypted HTML file will be saved.
* @param {string} password The password which will be used to encrypt + decrypt the content.
* @returns A promise that will resolve when the encrypted file has been saved.
*/
async function encrypt(inputFile, outputFile, password) {
const encrypted = await encryptFile(inputFile, password)
return await saveFile(outputFile, encrypted)
const encrypted = await encryptFile(inputFile, password);
return await saveFile(outputFile, encrypted);
}
/**
* Generate a random password of a given length.
*
* @param {number} length The password length.
* @param {string} characters The characters used to generate the password.
* @returns A random password.
*/
function generatePassword(
length = 80,
characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
) {
return Array.from(randomFillSync(new Uint32Array(length)))
.map((x) => characters[x % characters.length])
.join('')
}
exports.encryptHTML = encryptHTML
exports.encrypt = encrypt
exports.generatePassword = generatePassword
export {
encrypt,
encryptHTML,
generatePassword
};

@@ -0,0 +0,0 @@ MIT License

{
"name": "pagecrypt",
"version": "4.0.1",
"version": "5.0.0",
"description": "Easily add client-side password-protection to your Single Page Applications and HTML files.",
"main": "index.js",
"scripts": {
"cli": "node cli.js",
"test": "npm pack && cd test && npm i ../pagecrypt-*.tgz && npm run test && cd .. && echo 'Verify test results by opening the `test/out-*.html` files in your browser.'",
"start": "vite",
"build": "vite build",
"postbuild": "node scripts/postbuild.js",
"serve": "sirv web/build --http2 --key priv.pem --cert cert.pem"
"main": "./index.js",
"type": "module",
"author": "Samuel Plumppu",
"license": "MIT",
"dependencies": {
"rfc4648": "^1.5.0",
"sade": "^1.7.4"
},

@@ -18,11 +17,5 @@ "engines": {

"engineStrict": true,
"repository": {
"type": "git",
"url": "git+https://github.com/Greenheart/pagecrypt.git"
},
"bin": {
"pagecrypt": "./cli.js"
},
"author": "Samuel Plumppu",
"license": "MIT",
"keywords": [

@@ -43,2 +36,6 @@ "web-crypto",

],
"repository": {
"type": "git",
"url": "git+https://github.com/Greenheart/pagecrypt.git"
},
"bugs": {

@@ -48,14 +45,11 @@ "url": "https://github.com/Greenheart/pagecrypt/issues"

"homepage": "https://github.com/Greenheart/pagecrypt#readme",
"dependencies": {
"rfc4648": "^1.4.0",
"sade": "^1.7.4"
},
"devDependencies": {
"autoprefixer": "^10.2.5",
"postcss": "^8.2.12",
"sirv-cli": "^1.0.11",
"tailwindcss": "^2.1.2",
"vite": "^2.2.1",
"vite-plugin-singlefile": "^0.5.1"
"types": "./index.d.ts",
"exports": {
".": {
"node": "./index.js"
},
"./core": {
"import": "./core.js"
}
}
}

@@ -9,6 +9,67 @@ # 🔐 PageCrypt - Password Protected Single Page Applications and HTML files

There are 3 different ways to use `pagecrypt`:
There are 4 different ways to use `pagecrypt`:
### 1. CLI
### 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): Promise<string>`
```js
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')
```
#### `generatePassword(length: number): string`
```js
import { generatePassword, encryptHTML } from 'pagecrypt/core'
// Generate a random password without any external dependencies
const password = generatePassword(64)
const encryptedHTML = await encryptHTML(inputHTML, password)
```
### 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): Promise<void>`
```js
import { encrypt } from 'pagecrypt'
// Encrypt a HTML file and write to the filesystem
await encrypt('index.html', 'encrypted.html', 'password')
```
**NOTE:** Importing `pagecrypt` also gives you access to `generatePassword()` and `encryptHTML()` from `pagecrypt/core`.
```js
import { generatePassword, encryptHTML } from 'pagecrypt'
const password = generatePassword(48)
const encrypted = await encryptHTML(inputHTML, password)
```
### 3. CLI
Encrypt a single HTML-file with one command:

@@ -20,3 +81,3 @@

Encrypt using a generate password with given length:
Encrypt using a generated password with given length:

@@ -27,3 +88,3 @@ ```sh

#### 1.1. CLI Help
#### 3.1. CLI Help

@@ -48,5 +109,5 @@ ```

### 2. Automate `pagecrypt` in your build process
### 4. Automate `pagecrypt` in your build process
This allows automated encrypted builds for single page applications
Use either the `pagecrypt` Node.js API or the CLI to automatically encrypt the builds for your single page applications.

@@ -62,3 +123,3 @@ ```sh

"devDependencies": {
"pagecrypt": "^3.0.0"
"pagecrypt": "^5.0.0"
},

@@ -72,50 +133,2 @@ "scripts": {

### 3. Node.js API
You can also use `pagecrypt` in your Node.js scripts:
#### `encrypt(inputFile: string, outputFile: string, password: string): Promise<void>`
```js
import { encrypt } from 'pagecrypt'
// Encrypt a HTML file and write to the filesystem
await encrypt('index.html', 'encrypted.html', 'password')
```
#### `encryptHTML(inputHTML: string, password: string): Promise<string>`
```js
import { encryptHTML } from 'pagecrypt'
// 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(
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
Secret
</body>
</html>
`,
'password',
)
```
#### `generatePassword(length: number): string`
```js
import { generatePassword, encrypt, encryptHTML } from 'pagecrypt'
// Generate a random password without any external dependencies
const pass = generatePassword(64)
// Works with both JS API:s
await encrypt('index.html', 'encrypted.html', pass)
const encryptedHTML = await encryptHTML('html string', pass)
```
---

@@ -125,8 +138,10 @@

The project consists of four parts:
Project structure:
- `/web` - Web frontend for public webpage (`decrypt-template.html`). Built using Vite & Tailwind CSS.
- `/index.js` - pagecrypt main library.
- `/cli.js` - pagecrypt CLI.
- `/test` - testing setup
- `/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.

@@ -133,0 +148,0 @@ ## Setup a local development environment

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc