Compare commits
60 Commits
Author | SHA1 | Date | |
---|---|---|---|
df9630ec1e | |||
a330532f66 | |||
|
c605f96c0c | ||
32780937a4 | |||
3399d89a16 | |||
e97b0c4ded | |||
10d913154c | |||
949a0a0951 | |||
80082a5b7a | |||
09199ebec5 | |||
b9a56f38f5 | |||
25be65b32f | |||
455dbb5294 | |||
7e6b8a0eba | |||
fd0a0e06b0 | |||
d271b59da4 | |||
5dba3f8675 | |||
0e0ac98c84 | |||
7ed8cc7e3a | |||
6972790053 | |||
3caf1b72ba | |||
ab676217cd | |||
49a04bebb6 | |||
6b0b1ab132 | |||
2f7755a594 | |||
04017f6666 | |||
8b64db64d9 | |||
937a802f54 | |||
49a775087a | |||
2d6e1e0ec8 | |||
7c95f79bfc | |||
6964103c25 | |||
18a58786ac | |||
76fd0c0a1c | |||
f49f072fab | |||
45910aedfb | |||
5c4f5c0a90 | |||
c3653a45b6 | |||
c4317bc5e8 | |||
bbc4fdb5c5 | |||
71d4e3f58d | |||
50bb98a109 | |||
1d9f304830 | |||
1f141ccbad | |||
105d06d2d8 | |||
cfb9d4f59f | |||
aa118c2c71 | |||
1e33324b55 | |||
97b34b2a56 | |||
45964ecf70 | |||
c6a6908a82 | |||
caa1fccea3 | |||
610b20019b | |||
ed1da1faf0 | |||
d2a140fa79 | |||
31c6958277 | |||
2bcfefbd2a | |||
30bce07f67 | |||
b147ecf151 | |||
a5b4c628c6 |
22
.eslintrc.yml
Normal file
22
.eslintrc.yml
Normal 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
50
.github/workflows/eslint.yml
vendored
Normal 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
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
tags
|
||||
node_modules/
|
||||
dist/
|
9
LICENSE
Normal file
9
LICENSE
Normal 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
41
README.md
Normal 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
|
||||
```
|
23
dec.html
23
dec.html
@ -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>
|
23
enc.html
23
enc.html
@ -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>
|
124
encryption.js
124
encryption.js
@ -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
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
BIN
media/aes_enc.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 120 KiB |
9912
package-lock.json
generated
Normal file
9912
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
package.json
Normal file
48
package.json
Normal 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
413
src/aes.js
Normal 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
3
src/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import { generateHeader } from "./templates.js";
|
||||
import "./style.css";
|
||||
generateHeader();
|
546
src/interface.js
Normal file
546
src/interface.js
Normal 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
10
src/pages/aes.html
Normal 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
12
src/pages/index.html
Normal 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
171
src/style.css
Normal 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
11
src/templates.js
Normal 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
34
src/util.js
Normal 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 };
|
87
style.css
87
style.css
@ -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
78
webpack.common.js
Normal 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
10
webpack.dev.js
Normal 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
13
webpack.prod.js
Normal 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(),
|
||||
]
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue
Block a user