Compare commits
No commits in common. "master" and "v0.1" have entirely different histories.
@ -1,22 +0,0 @@
|
||||
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
50
.github/workflows/eslint.yml
vendored
@ -1,50 +0,0 @@
|
||||
# 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
3
.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
tags
|
||||
node_modules/
|
||||
dist/
|
9
LICENSE
9
LICENSE
@ -1,9 +0,0 @@
|
||||
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
41
README.md
@ -1,41 +0,0 @@
|
||||
# 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
Normal file
23
dec.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!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
Normal file
23
enc.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!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
Normal file
124
encryption.js
Normal file
@ -0,0 +1,124 @@
|
||||
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?");
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 106 KiB |
Binary file not shown.
Before Width: | Height: | Size: 120 KiB |
9912
package-lock.json
generated
9912
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
@ -1,48 +0,0 @@
|
||||
{
|
||||
"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
413
src/aes.js
@ -1,413 +0,0 @@
|
||||
/*
|
||||
|
||||
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)}`;
|
||||
});
|
@ -1,3 +0,0 @@
|
||||
import { generateHeader } from "./templates.js";
|
||||
import "./style.css";
|
||||
generateHeader();
|
546
src/interface.js
546
src/interface.js
@ -1,546 +0,0 @@
|
||||
/*
|
||||
|
||||
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 };
|
@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>encryptme: AES encryption/decryption</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>AES</h1>
|
||||
</body>
|
||||
</html>
|
@ -1,12 +0,0 @@
|
||||
<!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
171
src/style.css
@ -1,171 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
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
34
src/util.js
@ -1,34 +0,0 @@
|
||||
/*
|
||||
|
||||
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
Normal file
87
style.css
Normal file
@ -0,0 +1,87 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
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"],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
const { merge } = require("webpack-merge");
|
||||
const common = require("./webpack.common.js");
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: "development",
|
||||
devtool: "inline-source-map",
|
||||
devServer: {
|
||||
static: "./dist",
|
||||
},
|
||||
});
|
@ -1,13 +0,0 @@
|
||||
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