pagecrypt
Advanced tools
Comparing version 4.0.1 to 5.0.0
# 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); |
227
index.js
@@ -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" | ||
} | ||
} | ||
} |
133
README.md
@@ -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 |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
53525
0
10
428
156
Yes
1
2
1
Updatedrfc4648@^1.5.0