Compare commits

...

60 Commits
v0.1 ... master

Author SHA1 Message Date
df9630ec1e
webpack: added sitemap plugin 2023-02-09 22:03:06 -05:00
a330532f66
package-lock.json: updated 2023-02-01 17:55:54 -05:00
dogeystamp
c605f96c0c
Create eslint.yml 2023-01-31 16:29:44 -05:00
32780937a4
re-add meta charset tag 2023-01-31 15:37:30 -05:00
3399d89a16
style.css: added responsive layout 2023-01-31 15:22:44 -05:00
e97b0c4ded
webpack: redo page generation 2023-01-31 15:17:58 -05:00
10d913154c
interface.js: explicitly request to mount forms after creating elements 2023-01-31 14:38:56 -05:00
949a0a0951
interface.js, style.css: make transitions less intensive 2023-01-31 14:29:00 -05:00
80082a5b7a
update readme 2023-01-30 22:10:56 -05:00
09199ebec5
major refactoring
- created a package.json
- refactor with eslint
- use modules
- moved everything to webpack
2023-01-30 22:03:50 -05:00
b9a56f38f5
interface.js: added placeholder support 2023-01-30 16:18:29 -05:00
25be65b32f
aes.js: add 128 bit key support 2023-01-29 21:09:14 -05:00
455dbb5294
README: update about AES mode 2023-01-28 18:47:33 -05:00
7e6b8a0eba
aes.js: implemented AES-CTR 2023-01-28 18:25:01 -05:00
fd0a0e06b0
interface.js: added visibleFunc parameter 2023-01-28 17:49:27 -05:00
d271b59da4
removed viewport directive
this forces mobile into desktop mode, but there was no style for mobile
anyways
2023-01-28 16:31:27 -05:00
5dba3f8675
made header a template
encryptme title is now a link to home too
2023-01-28 16:08:13 -05:00
0e0ac98c84
aes.js: added CBC mode 2023-01-26 22:02:18 -05:00
7ed8cc7e3a
aes.js: slight refactoring
preparing to implement other AES modes
2023-01-26 19:29:41 -05:00
6972790053
interface.js: add InterfaceElement.handleError()
this allows elements to handle errors directly after catching them
rather than having a generic error
2023-01-26 19:11:31 -05:00
3caf1b72ba
interface.js: implement drop-down input 2023-01-25 21:34:13 -05:00
ab676217cd
README: add preview picture 2023-01-22 15:24:36 -05:00
49a04bebb6
aes.html: add title 2023-01-22 15:24:36 -05:00
6b0b1ab132
add README.md 2023-01-22 15:24:33 -05:00
2f7755a594
added licensing information 2023-01-22 14:21:42 -05:00
04017f6666
index.html: added barebones index page 2023-01-22 13:52:05 -05:00
8b64db64d9
styled tabs properly 2023-01-21 16:22:40 -05:00
937a802f54
initial tabs implementation 2023-01-21 14:26:34 -05:00
49a775087a
aes.js: add warnings for excessive PBKDF2 iterations 2023-01-04 15:44:04 -05:00
2d6e1e0ec8
aes.js: added pbkdf2 iterations setting 2023-01-04 15:22:41 -05:00
7c95f79bfc
interface.js: allow specifying default value for form elements 2023-01-04 15:02:10 -05:00
6964103c25
interface.js: add number input 2023-01-04 14:55:12 -05:00
18a58786ac
aes.js: bugfixes
decryption is no longer broken with custom keys
2023-01-03 21:50:27 -05:00
76fd0c0a1c
aes.js: fix salt changing even when not used 2023-01-03 21:18:39 -05:00
f49f072fab
added manual salt, IV and key options 2023-01-03 20:44:47 -05:00
45910aedfb
interface.js: improve hiding elements 2023-01-03 19:19:58 -05:00
5c4f5c0a90
added medium text boxes 2023-01-03 15:57:35 -05:00
c3653a45b6
refactor form generation 2023-01-03 15:34:57 -05:00
c4317bc5e8
interface.js: fix labelTag 2023-01-02 20:44:44 -05:00
bbc4fdb5c5
interface.js: provide advanced options check boxes 2023-01-02 20:00:27 -05:00
71d4e3f58d
interface.js: implement checkbox element 2023-01-02 19:15:55 -05:00
50bb98a109
style.css: make button react on focus, not just hover 2023-01-02 15:10:29 -05:00
1d9f304830
aes.js: improved error handling 2023-01-02 15:01:19 -05:00
1f141ccbad
interface.js: refactored API to be more concise 2023-01-02 14:05:53 -05:00
105d06d2d8
style.css: move headers to the left 2022-12-31 22:32:16 -05:00
cfb9d4f59f
aes.js: use alert box instead of window.alert for error 2022-12-31 22:12:42 -05:00
aa118c2c71
style.css: make titles less faded out 2022-12-31 22:10:30 -05:00
1e33324b55
Revert "style.css: improve distinction between different forms"
This reverts commit 45964ecf70.
2022-12-31 22:09:49 -05:00
97b34b2a56
add basic aes prototype 2022-12-31 22:06:13 -05:00
45964ecf70
style.css: improve distinction between different forms 2022-12-31 21:39:05 -05:00
c6a6908a82
interface.js: add json-b64 datatype 2022-12-31 21:07:39 -05:00
caa1fccea3
use text nodes instead of innerHTML 2022-12-30 19:48:28 -05:00
610b20019b
base64: error handling 2022-12-30 17:31:28 -05:00
ed1da1faf0
interface.js: rename Element to InterfaceElement
Element might cause conflicts
2022-12-30 16:54:38 -05:00
d2a140fa79
added alert box functionality 2022-12-30 16:18:53 -05:00
31c6958277
improve hiding form elements 2022-12-30 14:32:27 -05:00
2bcfefbd2a
improve interface scripts
- dynamically hide/unhide advanced options
- add base64 handlers for raw data inputs
2022-12-29 21:58:19 -05:00
30bce07f67
interface.js: added basic interface classes
Minimal prototype for dynamically generating an interface
2022-12-29 19:22:50 -05:00
b147ecf151
style.css: improve disabled style 2022-12-29 18:30:09 -05:00
a5b4c628c6
encryption.js: use JSON under base64 2022-12-29 11:10:21 -05:00
24 changed files with 11386 additions and 257 deletions

22
.eslintrc.yml Normal file
View File

@ -0,0 +1,22 @@
env:
browser: true
es2021: true
node: true
extends: eslint:recommended
overrides: []
parserOptions:
ecmaVersion: latest
sourceType: module
rules:
indent:
- error
- tab
linebreak-style:
- error
- unix
quotes:
- error
- double
semi:
- error
- always

50
.github/workflows/eslint.yml vendored Normal file
View File

@ -0,0 +1,50 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# ESLint is a tool for identifying and reporting on patterns
# found in ECMAScript/JavaScript code.
# More details at https://github.com/eslint/eslint
# and https://eslint.org
name: ESLint
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '33 14 * * 2'
jobs:
eslint:
name: Run eslint scanning
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install ESLint
run: |
npm install eslint@8.10.0
npm install @microsoft/eslint-formatter-sarif@2.1.7
- name: Run ESLint
run: npx eslint .
--config .eslintrc.yml
--ext .js,.jsx,.ts,.tsx
--format @microsoft/eslint-formatter-sarif
--output-file eslint-results.sarif
continue-on-error: true
- name: Upload analysis results to GitHub
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: eslint-results.sarif
wait-for-processing: true

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
tags
node_modules/
dist/

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
Copyright 2023 dogeystamp <dogeystamp@disroot.org>
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

41
README.md Normal file
View File

@ -0,0 +1,41 @@
# encryptme
encryptme is a website that provides cryptography tools based on the browser's SubtleCrypto API.
It aims to be simple to use, but also allow users to tinker with more advanced options if needed.
![AES encryption page](./media/aes_enc.jpg)
Currently, the following algorithms are implemented:
* [AES encryption/decryption](https://dogeystamp.github.io/encryptme/aes.html)
This uses PBKDF2 to convert a password to a key, then uses AES
to encrypt a given message.
## Installation
Clone the repo:
```
git clone https://github.com/dogeystamp/encryptme
```
Install packages:
```
npm install
```
## Running
Start development server:
```
npm run start
```
Or, compile to `dist/`:
```
npm run build
```

View File

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>encryptme: Decryption</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="encryption.js"></script>
<h1>encryptme</h1>
<label for="msg">Ciphertext: </label>
<textarea id="msg" form="form"></textarea>
<label for="password">Password: </label>
<input id="password" type="password">
<button id="decrypt">Decrypt</button>
<textarea id="plaintext" readonly></textarea>
<script>
document.getElementById("decrypt").addEventListener("click", dec);
</script>
</body>
</html>

View File

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>encryptme: Encryption</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="encryption.js"></script>
<h1>encryptme</h1>
<label for="msg">Message: </label>
<textarea id="msg" form="form"></textarea>
<label for="password">Password: </label>
<input id="password" type="password">
<button id="encrypt">Encrypt</button>
<textarea id="ciphertext" readonly></textarea>
<script>
document.getElementById("encrypt").addEventListener("click", enc);
</script>
</body>
</html>

View File

@ -1,124 +0,0 @@
function getMsg() {
return msg = document.getElementById("msg").value;
}
function getMsgEncoding () {
let enc = new TextEncoder();
return enc.encode(getMsg());
}
function getKeyMaterial () {
let pass = document.getElementById("password").value;
let enc = new TextEncoder();
return window.crypto.subtle.importKey(
"raw",
enc.encode(pass),
"PBKDF2",
false,
["deriveKey"]
);
}
function getKey (keyMaterial, salt) {
return window.crypto.subtle.deriveKey(
{
"name": "PBKDF2",
"hash": "SHA-256",
"salt": salt,
"iterations": 300000
},
keyMaterial,
{
"name": "AES-GCM",
"length": 256
},
true,
["encrypt", "decrypt"]
);
}
function bufTo64 (buf) {
let bytes = new Uint8Array(buf);
let ascii = ''
for (var i = 0; i < bytes.byteLength; i++) {
ascii += String.fromCharCode(bytes[i]);
}
return btoa(ascii);
}
function b64ToBuf (b64) {
let ascii = atob(b64);
let buf = new ArrayBuffer(ascii.length);
let bytes = new Uint8Array(buf);
for (var i = 0; i < ascii.length; i++) {
bytes[i] = ascii.charCodeAt(i);
}
return buf;
}
function concatBuf(buf1, buf2) {
let tmp = new Uint8Array(buf1.byteLength + buf2.byteLength);
tmp.set(new Uint8Array(buf1), 0);
tmp.set(new Uint8Array(buf2), buf1.byteLength);
return tmp.buffer;
}
async function exportKey (key) {
let k = await window.crypto.subtle.exportKey("raw", key);
return bufTo64(k);
}
async function enc () {
outBox = document.getElementById("ciphertext");
outBox.innerHTML = '';
let keyMaterial = await getKeyMaterial();
let salt = window.crypto.getRandomValues(new Uint8Array(16));
let key = await getKey(keyMaterial, salt);
let iv = window.crypto.getRandomValues(new Uint8Array(16));
let msgEncoded = getMsgEncoding();
ciphertext = await window.crypto.subtle.encrypt(
{
"name": "AES-GCM",
"iv": iv
},
key,
msgEncoded
);
let output = concatBuf(concatBuf(ciphertext, salt), iv);
outBox.innerHTML = `${bufTo64(output)}`;
let keyExp = await exportKey (key);
}
async function dec () {
outBox = document.getElementById("plaintext");
outBox.innerHTML = '';
let msgEncoded = b64ToBuf(getMsg());
let ciphertext = new Uint8Array(msgEncoded.slice(0, -32));
let iv = new Uint8Array(msgEncoded.slice(-16));
let salt = new Uint8Array(msgEncoded.slice(-32, -16));
let keyMaterial = await getKeyMaterial();
let key = await getKey(keyMaterial, salt);
try {
let plaintext = await window.crypto.subtle.decrypt(
{
"name": "AES-GCM",
"iv": iv
},
key,
ciphertext
);
let dec = new TextDecoder();
outBox.innerHTML = `${dec.decode(plaintext)}`;
} catch (e) {
window.alert("Decryption error: incorrect password?");
}
}

BIN
media/aes_dec_dark.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
media/aes_enc.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

9912
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "encryptme",
"version": "0.5.1",
"description": "Simple online cryptography app.",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config webpack.prod.js",
"lint": "eslint --ext .js,.jsx src *.js",
"start": "webpack serve --open --config webpack.dev.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/dogeystamp/encryptme.git"
},
"keywords": [
"website",
"cryptography",
"encryption",
"aes",
"webapp",
"aes-256",
"aes-gcm",
"decryption",
"encryption-decryption",
"cryptography-tools",
"cryptography-utilities"
],
"author": "dogeystamp",
"license": "BSD-2-Clause",
"bugs": {
"url": "https://github.com/dogeystamp/encryptme/issues"
},
"homepage": "https://github.com/dogeystamp/encryptme#readme",
"devDependencies": {
"css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^4.2.2",
"eslint": "^8.33.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.7.2",
"sitemap-webpack-plugin": "^1.1.1",
"style-loader": "^3.3.1",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1",
"webpack-merge": "^5.8.0"
}
}

413
src/aes.js Normal file
View File

@ -0,0 +1,413 @@
/*
Copyright 2023 dogeystamp <dogeystamp@disroot.org>
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { generateHeader } from "./templates.js";
import "./style.css";
generateHeader();
import { TabList } from "./interface.js";
import { bufToB64, b64ToBuf } from "./util.js";
let tabs = new TabList({});
let encForm = tabs.createForm({label: "Encryption"});
let encMsg = encForm.createTextArea({
label: "Message",
placeholder: "Type a secret message",
});
let encPass = encForm.createPasswordInput({
label: "Password",
placeholder: "Enter your password",
enabledFunc: function() {return !encManualKey.value;}
});
let encPbkdf2Iters = encForm.createNumberInput({
label: "PBKDF2 iterations",
minValue: 1,
step: 1,
value: 300000,
advanced: true,
enabledFunc: function() {return !encManualKey.value;}
});
let encSalt = encForm.createMediumTextBox({
label: "PBKDF2 salt",
dataType: "b64",
advanced: true,
enabled: false,
enabledFunc: function() {return encManualSalt.value && !encManualKey.value;}
});
let encManualSalt = encForm.createCheckBox({
label: "Use fixed salt instead of random",
advanced: true
});
let encKeySize = encForm.createDropDown({
label: "AES key size",
advanced: true,
options: [
{
name: "128 bits",
value: 128
},
{
name: "256 bits",
value: "256"
},
]
});
let encKey = encForm.createMediumTextBox({
label: "Key",
dataType: "b64",
advanced: true,
enabled: false,
enabledFunc: function() {return encManualKey.value;}
});
let encManualKey = encForm.createCheckBox({
label: "Use fixed key instead of password",
advanced: true
});
let encIV = encForm.createMediumTextBox({
label: "IV",
dataType: "b64",
advanced: true,
enabledFunc: function() {return encManualIV.value;},
visibleFunc: function() {return ["AES-GCM", "AES-CBC"].includes(encMode.value);}
});
let encManualIV = encForm.createCheckBox({
label: "Use fixed IV instead of random",
advanced: true,
visibleFunc: function() {return ["AES-GCM", "AES-CBC"].includes(encMode.value);}
});
let encCounter = encForm.createMediumTextBox({
label: "Counter",
dataType: "b64",
advanced: true,
enabledFunc: function() {return encManualCounter.value;},
visibleFunc: function() {return encMode.value === "AES-CTR";}
});
let encManualCounter = encForm.createCheckBox({
label: "Use fixed counter instead of random",
advanced: true,
visibleFunc: function() {return encMode.value === "AES-CTR";}
});
let encMode = encForm.createDropDown({
label: "AES mode",
advanced: true,
options: [
{
name: "AES-GCM (Galois/Counter Mode)",
value: "AES-GCM"
},
{
name: "AES-CBC (Cipher Block Chaining)",
value: "AES-CBC"
},
{
name: "AES-CTR (Counter)",
value: "AES-CTR"
},
]
});
let encButton = encForm.createButton({label: "Encrypt"});
let encOut = encForm.createOutput({
label: "Output",
dataType: "json-b64",
});
let encOutRaw = encForm.createOutput({
label: "Raw ciphertext",
dataType: "b64",
advanced: true
});
let decForm = tabs.createForm({label: "Decryption"});
let decMsg = decForm.createTextArea({
label: "Encrypted message",
placeholder: "Paste the encrypted output",
dataType: "json-b64",
});
let decPass = decForm.createPasswordInput({
label: "Password",
placeholder: "Enter your password",
enabledFunc: function() {return !decManualKey.value;}
});
let decKey = decForm.createMediumTextBox({
label: "Key",
dataType: "b64",
advanced: true,
enabled: false,
enabledFunc: function() {return decManualKey.value;}
});
let decManualKey = decForm.createCheckBox({
label: "Use fixed key instead of password",
advanced: true
});
let decButton = decForm.createButton({label: "Decrypt"});
let decOut = decForm.createOutput({label: "Output"});
tabs.mountForms();
function getKeyMaterial(password) {
let enc = new TextEncoder();
return window.crypto.subtle.importKey(
"raw",
enc.encode(password),
"PBKDF2",
false,
["deriveKey"]
);
}
function getKey(keyMaterial, salt, pbkdf2Iters, encMode, keySize) {
return window.crypto.subtle.deriveKey(
{
"name": "PBKDF2",
"hash": "SHA-256",
"salt": salt,
"iterations": pbkdf2Iters
},
keyMaterial,
{
"name": encMode,
"length": keySize
},
true,
["encrypt", "decrypt"]
);
}
async function aesGcmEnc(key, iv, msgEncoded) {
return window.crypto.subtle.encrypt(
{
"name": "AES-GCM",
"iv": iv
},
key,
msgEncoded
);
}
async function aesCbcEnc(key, iv, msgEncoded) {
return window.crypto.subtle.encrypt(
{
"name": "AES-CBC",
"iv": iv
},
key,
msgEncoded
);
}
async function aesCtrEnc(key, counter, msgEncoded) {
return window.crypto.subtle.encrypt(
{
"name": "AES-CTR",
"counter": counter,
"length": 64
},
key,
msgEncoded
);
}
encButton.handle.addEventListener("click", async function() {
let keyMaterial = await getKeyMaterial(encPass.value);
let key;
let salt = encSalt.value;
let pbkdf2Iters = encPbkdf2Iters.value;
if (pbkdf2Iters === undefined) return;
if (pbkdf2Iters > 1000000) {
encPbkdf2Iters.alertBox("alert-info", `PBKDF2 is using ${pbkdf2Iters} iterations: this might take a long time...`);
}
if (encManualKey.value) {
key = await window.crypto.subtle.importKey(
"raw",
encKey.value,
{"name": encMode.value},
true,
["encrypt", "decrypt"]
);
} else {
if (encSalt.enabledFunc()) {
salt = encSalt.value;
} else {
salt = window.crypto.getRandomValues(new Uint8Array(16));
encSalt.value = salt;
}
key = await getKey(keyMaterial, salt, pbkdf2Iters, encMode.value, Number(encKeySize.value));
encKey.value = await window.crypto.subtle.exportKey("raw", key);
}
let iv;
if (["AES-GCM", "AES-CBC"].includes(encMode.value)) {
if (encManualIV.value) {
iv = encIV.value;
} else {
iv = window.crypto.getRandomValues(new Uint8Array(16));
encIV.value = iv;
}
}
let counter;
if (encMode.value === "AES-CTR") {
if (encManualCounter.value) {
counter = encCounter.value;
} else {
counter = window.crypto.getRandomValues(new Uint8Array(16));
encCounter.value = counter;
}
}
let enc = new TextEncoder();
let msgEncoded = enc.encode(encMsg.value);
let ciphertext;
switch (encMode.value) {
case "AES-GCM":
ciphertext = await aesGcmEnc(key, iv, msgEncoded);
break;
case "AES-CBC":
ciphertext = await aesCbcEnc(key, iv, msgEncoded);
break;
case "AES-CTR":
ciphertext = await aesCtrEnc(key, counter, msgEncoded);
break;
default:
encMode.handleError(Error(`Mode '${encMode.value}' is not implemented.`));
return;
}
encOutRaw.value = ciphertext;
encOut.value = {
"ciphertext": bufToB64(ciphertext),
"salt": bufToB64(salt),
"iv": bufToB64(iv),
"counter": bufToB64(counter),
"encMode": encMode.value,
"encKeySize": encKeySize.value,
"pbkdf2Iters": pbkdf2Iters,
};
});
async function aesGcmDec(key, iv, ciphertext) {
return window.crypto.subtle.decrypt(
{
"name": "AES-GCM",
"iv": iv
},
key,
ciphertext
);
}
async function aesCbcDec(key, iv, ciphertext) {
return window.crypto.subtle.decrypt(
{
"name": "AES-CBC",
"iv": iv
},
key,
ciphertext
);
}
async function aesCtrDec(key, counter, ciphertext) {
return window.crypto.subtle.decrypt(
{
"name": "AES-CTR",
"counter": counter,
"length": 64
},
key,
ciphertext
);
}
decButton.handle.addEventListener("click", async function() {
let msgEncoded = decMsg.value;
let ciphertext, iv, counter, salt, encMode, pbkdf2Iters, encKeySize;
try {
ciphertext = new b64ToBuf(msgEncoded.ciphertext);
iv = new Uint8Array(b64ToBuf(msgEncoded.iv));
counter = new Uint8Array(b64ToBuf(msgEncoded.counter));
salt = new Uint8Array(b64ToBuf(msgEncoded.salt));
encMode = msgEncoded.encMode;
encKeySize = msgEncoded.encKeySize;
if (!["128", "256"].includes(encKeySize)) {
throw Error(`Invalid AES key size: '${encKeySize}'`);
}
pbkdf2Iters = msgEncoded.pbkdf2Iters;
if (pbkdf2Iters < 1 || pbkdf2Iters%1 !== 0) {
throw Error(`Invalid PBKDF2 iterations setting: ${pbkdf2Iters}`);
} else if (pbkdf2Iters > 1000000) {
decMsg.alertBox("alert-info", `PBKDF2 is using ${pbkdf2Iters} iterations: this might take a long time...`);
}
} catch (e) {
decMsg.handleError(e, "Invalid encrypted payload.");
}
if (ciphertext === undefined
|| iv === undefined
|| salt === undefined
|| pbkdf2Iters === undefined
) {
return;
}
let keyMaterial = await getKeyMaterial(decPass.value);
let key;
if (decManualKey.value) {
try {
key = await window.crypto.subtle.importKey(
"raw",
decKey.value,
{"name": encMode},
true,
["encrypt", "decrypt"]
);
} catch (e) {
decMsg.handleError(e);
}
} else {
key = await getKey(keyMaterial, salt, pbkdf2Iters, encMode, Number(encKeySize));
decKey.value = await window.crypto.subtle.exportKey("raw", key);
}
let plaintext;
try {
switch (encMode) {
case "AES-GCM":
plaintext = await aesGcmDec(key, iv, ciphertext);
break;
case "AES-CBC":
plaintext = await aesCbcDec(key, iv, ciphertext);
break;
case "AES-CTR":
plaintext = await aesCtrDec(key, counter, ciphertext);
break;
default:
throw Error(`Mode '${encMode.value}' is not implemented.`);
}
} catch (e) {
if (e.message !== "" && e.message !== undefined) {
decMsg.handleError(e, "Error during decryption.");
} else {
decMsg.handleError(Error("Could not decrypt; is your password/key correct?"));
}
}
let dec = new TextDecoder();
decOut.value = `${dec.decode(plaintext)}`;
});

3
src/index.js Normal file
View File

@ -0,0 +1,3 @@
import { generateHeader } from "./templates.js";
import "./style.css";
generateHeader();

546
src/interface.js Normal file
View File

@ -0,0 +1,546 @@
/*
Copyright 2023 dogeystamp <dogeystamp@disroot.org>
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { b64ToBuf, bufToB64 } from "./util.js";
class InterfaceElement {
rootNodes = [];
constructor({fragment, enabledFunc, visibleFunc}) {
if (fragment === undefined) {
this.fragment = new DocumentFragment();
} else {
this.fragment = fragment;
}
if (enabledFunc === undefined) {
this.enabledFunc = function(){ return true; };
} else {
this.enabledFunc = enabledFunc;
}
if (visibleFunc === undefined) {
this.visibleFunc = function(){ return true; };
} else {
this.visibleFunc = visibleFunc;
}
}
update() {
this.enabled = this.enabledFunc();
this.hidden = !this.visibleFunc();
}
scanNodes() {
this.rootNodes = [];
for (const node of this.fragment.children) {
this.rootNodes.push(node);
}
}
mount(par) {
this.scanNodes();
par.append(this.fragment);
}
#hidden = false;
get hidden() {
return this.#hidden;
}
set hidden(x) {
if (this.#hidden === x) return;
this.#hidden = x;
for (const node of this.rootNodes) {
if (this.hidden) {
node.classList.add("visualhidden");
node.classList.add("hidden");
} else {
node.classList.remove("hidden");
setTimeout(function() {
node.classList.remove("visualhidden");
}, 20);
}
}
if (this.hidden === true) this.clearAlerts();
}
#enabled = true;
get enabled() {
return this.#enabled;
}
set enabled(x) {
this.#enabled = x;
this.handle.disabled = !this.#enabled;
}
}
function dataTypeSupports(params, validTypes) {
if (params.dataType === undefined) {
params.dataType = validTypes[0];
}
if (!validTypes.includes(params.dataType)) {
throw `Element can not support '${params.dataType}' data type`;
}
}
class Form extends InterfaceElement {
constructor({tag, par=document.body, label, mounted=false}) {
super({});
if (tag === undefined) {
this.handle = document.createElement("div");
} else {
this.handle = tag;
}
this.fragment.appendChild(this.handle);
this.elements = [];
this.clearAlerts = this.clearAlerts.bind(this);
if (label !== undefined) {
this.createHeader({label: label});
}
let advancedToggle = this.createCheckBox({label: "Advanced settings"});
advancedToggle.handle.addEventListener("change", function() {
this.advanced = advancedToggle.value;
}.bind(this));
if (mounted) {
this.mount(par);
}
}
#hidden = false;
get hidden() {
return this.#hidden;
}
set hidden(x) {
this.#hidden = x;
for (const element of this.elements) {
if (element.advanced === true) {
element.hidden = !this.advanced || this.hidden;
} else {
element.hidden = this.hidden;
}
}
if (this.hidden === true) this.clearAlerts();
}
#advanced = false;
get advanced() {
return this.#advanced;
}
set advanced(x) {
this.#advanced = x;
for (const element of this.elements) {
if (element.advanced === true) {
element.update();
}
}
}
clearAlerts() {
for (const element of this.elements) {
element.clearAlerts();
}
}
createElem(params) {
params.form = this;
let elem = new FormElement(params);
elem.mount(this.handle);
this.elements.push(elem);
this.rootNodes.push(...elem.rootNodes);
if (elem.advanced) {
elem.hidden = !this.advanced;
}
if (this.hidden) elem.hidden = true;
return elem;
}
createHeader(params) {
params.tag = document.createElement("h2");
dataTypeSupports(params, ["none"]);
let labelTag = document.createTextNode(params.label);
params.tag.appendChild(labelTag);
params.label = undefined;
return this.createElem(params);
}
createTextBox(params) {
params.tag = document.createElement("input");
dataTypeSupports(params, ["plaintext", "b64", "json-b64"]);
return this.createElem(params);
}
createMediumTextBox(params) {
params.tag = document.createElement("textarea");
params.tag.classList.add("mediumbox");
dataTypeSupports(params, ["plaintext", "b64", "json-b64"]);
return this.createElem(params);
}
createPasswordInput(params) {
params.tag = document.createElement("input");
params.tag.setAttribute("type", "password");
dataTypeSupports(params, ["plaintext"]);
return this.createElem(params);
}
createNumberInput(params) {
params.tag = document.createElement("input");
params.tag.setAttribute("type", "number");
dataTypeSupports(params, ["number"]);
if (params.maxValue !== undefined) params.tag.max = params.maxValue;
if (params.minValue !== undefined) params.tag.min = params.minValue;
if (params.step !== undefined) params.tag.step = params.step;
if (params.required !== undefined) params.tag.required = params.required;
return this.createElem(params);
}
createDropDown(params) {
// example for params.options:
/*
[
{
value: "volvo"
name: "Volvo"
},
{
value: "benz"
name: "Mercedes Benz"
}
]
*/
params.fragment = new DocumentFragment();
params.tag = document.createElement("select");
params.labelTag = document.createElement("label");
params.labelTag.appendChild(document.createTextNode(params.label));
params.fragment.appendChild(params.labelTag);
params.fragment.appendChild(params.tag);
dataTypeSupports(params, ["category"]);
for (const option of params.options) {
let optTag = document.createElement("option");
optTag.value = option.value;
optTag.appendChild(document.createTextNode(option.name));
params.tag.appendChild(optTag);
}
params.tag.addEventListener("change", function() {
for (const elem of this.elements) {
elem.update();
}
}.bind(this));
return this.createElem(params);
}
createTextArea(params) {
params.tag = document.createElement("textarea");
dataTypeSupports(params, ["plaintext", "b64", "json-b64"]);
return this.createElem(params);
}
createButton(params) {
params.fragment = new DocumentFragment();
params.tag = document.createElement("button");
params.labelTag = document.createTextNode(params.label);
params.tag.appendChild(params.labelTag);
params.fragment.appendChild(params.tag);
dataTypeSupports(params, ["none"]);
return this.createElem(params);
}
createCheckBox(params) {
params.fragment = new DocumentFragment();
params.tag = document.createElement("input");
params.tag.setAttribute("type", "checkbox");
params.labelTag = document.createElement("label");
params.labelTag.appendChild(document.createTextNode(params.label));
let li = document.createElement("li");
li.classList.add("checkbox-container");
params.fragment.appendChild(li);
li.appendChild(params.tag);
li.appendChild(params.labelTag);
dataTypeSupports(params, ["bool"]);
params.tag.addEventListener("change", function() {
for (const elem of this.elements) {
elem.update();
}
}.bind(this));
return this.createElem(params);
}
createOutput(params) {
params.tag = document.createElement("textarea");
params.tag.setAttribute("readonly", true);
dataTypeSupports(params, ["plaintext", "b64", "json-b64"]);
return this.createElem(params);
}
}
class FormElement extends InterfaceElement {
constructor({tag, fragment, advanced=false, form,
value, dataType, placeholder,
labelTag, label="",
enabledFunc,
visibleFunc
}) {
let oriVisibleFunc = visibleFunc;
super({
fragment,
enabledFunc,
visibleFunc: function() {
let res;
if (oriVisibleFunc) {
res = oriVisibleFunc();
} else {
res = true;
}
if (form !== undefined) {
if (advanced) {
if (form.advanced) return res;
else return false;
}
}
return res;
}
});
this.form = form;
this.labelText = label;
if (labelTag === undefined) {
this.labelTag = document.createElement("label");
this.labelTag.appendChild(document.createTextNode(this.labelText));
this.fragment.appendChild(this.labelTag);
this.fragment.appendChild(tag);
} else {
this.labelTag = labelTag;
}
this.clearAlerts = this.clearAlerts.bind(this);
this.handle = tag;
this.dataType = dataType;
this.advanced = advanced;
if (value !== undefined) this.value = value;
if (placeholder !== undefined) this.handle.placeholder = placeholder;
}
get value() {
this.clearAlerts();
switch (this.dataType) {
case "number":
if (this.handle.checkValidity() == false) {
this.alertBox("alert-error", this.handle.validationMessage);
return undefined;
}
return Number(this.handle.value);
case "plaintext":
return this.handle.value;
case "b64":
try {
return b64ToBuf(this.handle.value);
} catch (e) {
this.handleError(Error("Invalid base64 value."));
return undefined;
}
case "json-b64": {
let jsonString;
try {
jsonString = atob(this.handle.value);
} catch (e) {
this.handleError(Error("Invalid base64 value."));
return undefined;
}
try {
return JSON.parse(jsonString);
} catch (e) {
this.handleError(Error("Invalid JSON encoding."));
return undefined;
}
}
case "bool":
return this.handle.checked;
case "category":
return this.handle.value;
default:
return undefined;
}
}
set value(x) {
switch (this.dataType) {
case "number":
case "plaintext":
this.handle.value = x;
break;
case "b64":
this.handle.value = bufToB64(x);
break;
case "json-b64":
this.handle.value = btoa(JSON.stringify(x));
break;
case "bool":
this.handle.checked = x;
break;
case "category":
this.handle.value = x;
break;
}
}
alerts = [];
alertBox(type, message, title) {
// type is alert-error or alert-info
if (this.handle === undefined) {
throw "can not add alert: still undefined";
}
if (this.hidden === true) {
throw "can not add alert: hidden";
}
if (title === undefined) {
switch (type) {
case "alert-info":
title = "Info: ";
break;
case "alert-error":
title = "Error: ";
break;
default:
title = "";
break;
}
}
let box = document.createElement("div");
box.classList.add(type);
box.classList.add("alert");
box.appendChild(document.createTextNode(message));
if (title !== "") {
let titleTag = document.createElement("strong");
titleTag.appendChild(document.createTextNode(title));
box.prepend(titleTag);
}
this.handle.after(box);
this.alerts.push(box);
}
handleError(e, extraInfo="") {
if (extraInfo !== "") {
extraInfo = ` (${extraInfo})`;
}
this.alertBox("alert-error", e.message + extraInfo);
console.error(e);
}
clearAlerts() {
for (const box of this.alerts) {
box.remove();
}
this.alerts = [];
}
}
class Tab extends InterfaceElement {
constructor({form, label=""}) {
super({});
this.form = form;
this.handle = document.createElement("button");
this.fragment.appendChild(this.handle);
this.handle.appendChild(document.createTextNode(label));
}
}
class TabList extends InterfaceElement {
constructor({tag, par=document.body}) {
super({});
this.par = par;
if (tag === undefined) {
this.handle = document.createElement("div");
this.handle.classList.add("tabList");
} else {
this.handle = tag;
}
this.fragment.appendChild(this.handle);
this.mount(par);
}
tabs = [];
#activeForm;
set activeForm(x) {
this.#activeForm = x;
for (const tab of this.tabs) {
if (tab.form !== x) {
tab.form.hidden = true;
tab.handle.classList.remove("active");
} else {
tab.form.hidden = false;
tab.handle.classList.add("active");
}
}
}
get activeForm() {
return this.#activeForm;
}
createForm(params) {
if (params.par === undefined) params.par = this.par;
let form = new Form(params);
let tab = new Tab({
label: params.label,
form: form
});
this.tabs.push(tab);
tab.handle.addEventListener("click", function() {
this.activeForm = form;
}.bind(this));
this.handle.appendChild(tab.fragment);
if (this.activeForm === undefined) this.activeForm = form;
else form.hidden = true;
return form;
}
mountForms() {
for (const tab of this.tabs) {
tab.form.mount(this.par);
}
}
}
export { TabList, FormElement, Form };

10
src/pages/aes.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>encryptme: AES encryption/decryption</title>
</head>
<body>
<h1>AES</h1>
</body>
</html>

12
src/pages/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>encryptme</title>
</head>
<body>
<h2>Tools</h2>
<h3>Encryption/decryption</h3>
<a href="aes.html">AES</a>
</body>
</html>

171
src/style.css Normal file
View File

@ -0,0 +1,171 @@
body {
max-width: 650px;
margin: 40px auto;
padding: 0 10px;
color: #444444;
background: #eeeeee;
transition: all 0.5s;
}
body, button {
font: 18px/1.5 -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";
}
h1, h2 {
margin-left: -0.5em;
}
textarea {
width: 100%;
height: 15em;
padding-top: 1em;
resize: none;
}
input {
height: 100%;
}
input:not([type]), input[type=password] {
font-family: monospace;
}
.mediumbox {
width: 20em;
height: 5em;
}
label, input, button, textarea {
display: block;
margin-top: 1em;
border: none;
border-radius: 5px;
transition-duration: 0.2s;
}
input, textarea, button {
border: 1px solid #44444444;
}
input:focus, textarea:focus, button:focus {
outline: none;
}
input:focus, textarea:focus {
border: 1px solid #444444aa;
}
textarea:disabled, input:disabled {
background: #ffffff33;
}
.checkbox-container {
list-style: none;
height: fit-content;
}
.checkbox-container label {
display: inline;
margin-left: 0.5em;
}
.checkbox-container input {
display: inline;
height: 1em;
transform: scale(1.25);
}
.alert {
border-radius: 3px;
margin-top: 0.5em;
padding: 0.75em;
background: #ffffff44;
width: 75%;
}
.alert-error {
background: #ffaaaa44;
}
.alert-info {
background: #aaaaff44;
}
p {
text-align: justify;
}
button {
transition-duration: 0.05s;
border: 1px solid #44444444;
}
button:focus {
border: 1px solid #44444477;
}
button:hover {
border: 1px solid #444444aa;
}
button:active {
opacity: 50%;
}
.tabList {
border-radius: 5px;
}
.tabList button {
display: inline;
width: 6em;
margin: 0;
border: 1px solid #44444444;
margin-right: 0.2em;
}
.tabList button:hover {
background: #aaaaaa44;
}
.tabList button.active {
border: none;
background: #0077ff44;
}
.page-header a {
color: #000000;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
body {
color: #c9d1d9;
background: #0d1117;
}
h1, h2, h3, h4, h5, h6 {
color: #c9d1d9;
}
a:link {
color: #58a6ff;
}
a:visited {
color: #8e96f0;
}
textarea, input, button {
background: #1d2127;
color: #c9d1d9;
}
.page-header a {
color: #c9d1d9;
}
}
@media only screen and (max-width: 1000px) {
body {
max-width: 80%;
margin: 40px auto;
}
}
.visualhidden {
opacity: 0;
}
.hidden {
display: none;
}

11
src/templates.js Normal file
View File

@ -0,0 +1,11 @@
function generateHeader() {
let header = document.createElement("div");
header.classList.add("page-header");
header.innerHTML = `
<a href="index.html"><h1>encryptme</h1></a>
`;
document.body.prepend(header);
}
export { generateHeader };

34
src/util.js Normal file
View File

@ -0,0 +1,34 @@
/*
Copyright 2023 dogeystamp <dogeystamp@disroot.org>
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
function b64ToBuf (b64) {
let ascii = atob(b64);
let buf = new ArrayBuffer(ascii.length);
let bytes = new Uint8Array(buf);
for (var i = 0; i < ascii.length; i++) {
bytes[i] = ascii.charCodeAt(i);
}
return buf;
}
function bufToB64 (buf) {
let bytes = new Uint8Array(buf);
let ascii = "";
for (var i = 0; i < bytes.byteLength; i++) {
ascii += String.fromCharCode(bytes[i]);
}
return btoa(ascii);
}
export { b64ToBuf, bufToB64 };

View File

@ -1,87 +0,0 @@
body {
max-width: 650px;
margin: 40px auto;
padding: 0 10px;
color: #444444;
background: #eeeeee;
animation: fadeIn 0.5s;
}
@keyframes fadeIn {
0% {opacity: 0;}
100% {opacity: 1;}
}
h1, h2, h3, h4, h5, h6 {
color: #666666;
}
body, button {
font: 18px/1.5 -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";
}
textarea {
width: 100%;
height: 15em;
padding-top: 1em;
resize: none;
}
label, input, button, textarea {
display: block;
margin-top: 1em;
border: none;
border-radius: 5px;
transition-duration: 0.2s;
}
input, textarea, button {
border: 1px solid #44444444;
}
input:focus, textarea:focus, button:focus {
outline: none;
}
input:focus, textarea:focus {
border: 1px solid #444444aa;
}
p {
text-align: justify;
}
button {
transition-duration: 0.05s;
border: 1px solid #44444444;
}
button:hover {
border: 1px solid #444444aa;
}
button:active {
opacity: 50%;
}
@media (prefers-color-scheme: dark) {
body {
color: #c9d1d9;
background: #0d1117;
}
h1, h2, h3, h4, h5, h6 {
color: #c9d1d9;
}
a:link {
color: #58a6ff;
}
a:visited {
color: #8e96f0;
}
textarea, input, button {
background: #1d2127;
color: #c9d1d9;
}
}

78
webpack.common.js Normal file
View File

@ -0,0 +1,78 @@
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const SitemapPlugin = require("sitemap-webpack-plugin").default;
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const pages = [
{
id: "index",
desc: "Easy to use and simple online tools for encryption and decryption.",
changefreq: "weekly",
priority: 1.0,
},
{
id: "aes",
desc: "Secure and simple tool for AES, with control over all advanced options like key size, salt, AES mode, and others.",
changefreq: "weekly",
priority: 0.7,
},
];
module.exports = {
entry: pages.reduce((config, page) => {
config[page.id] = `./src/${page.id}.js`;
return config;
}, {}),
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
clean: true,
},
optimization: {
splitChunks: {
chunks: "all",
},
},
plugins: [
new MiniCssExtractPlugin({
filename: "[contenthash].css",
chunkFilename: "[id].[contenthash].css",
}),
new SitemapPlugin({
base: "https://encryptme.net",
paths: pages.map(
(page) => ({
path: `/${page.id}.html`,
changefreq: page.changefreq,
priority: page.priority,
})
),
options: {
lastmod: true,
},
})
].concat(
pages.map(
(page) =>
new HtmlWebpackPlugin({
inject: "body",
title: `encryptme: ${page.title}`,
meta: {
viewport: "width=device-width, initial-scale=1, shrink-to-fit=no",
description: page.desc
},
filename: `${page.id}.html`,
template: `./src/pages/${page.id}.html`,
chunks: [page.id],
})
)
),
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
};

10
webpack.dev.js Normal file
View File

@ -0,0 +1,10 @@
const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");
module.exports = merge(common, {
mode: "development",
devtool: "inline-source-map",
devServer: {
static: "./dist",
},
});

13
webpack.prod.js Normal file
View File

@ -0,0 +1,13 @@
const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = merge(common, {
mode: "production",
optimization: {
minimizer: [
"...",
new CssMinimizerPlugin(),
]
}
});