wallet/cmd/envcrypt.html
Anton Nesterov 1382da92a3
[init]
2024-08-31 16:46:20 +02:00

559 lines
18 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>ENVCrypt - env encrypt tool</title>
<link rel="stylesheet" href="https://cdn.rawgit.com/Chalarangelo/mini.css/v3.0.1/dist/mini-default.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<meta name="description" content="ENVCrypt - env encrypt tool using RSA pubkey in one html file"/>
<style>
body {
min-height: 100vh;
align-items: center;
display: flex;
width: 100%;
justify-content: center;
background-color: #aaa;
}
footer {
color:#087abe;
}
header {
display: flex;
}
header .logo {
font-size: 1.32rem;
color: #000;
font-weight: 200;
}
header .spacer {
width: 100%;
display: inline-block;
}
.app {
background-color: #fff;
border-radius: 12px;
padding: 3px;
}
.env {
margin: 0 !important;
}
.value {
position: relative;
margin-bottom: 2px;
}
.value fieldset {
width: 100%;
display: flex;
position: relative;
}
.action {
position: absolute;
right:0;
top: 0;
margin: 0;
background: #ddd;
}
[type="checkbox"].modal + div .card {
margin: 0 auto;
max-height: unset;
overflow: auto;
}
.add-env {
background: transparent;
}
.add-env input {
opacity: 1;
}
.copy-action {
position: absolute;
left: -50px;
top: -8px;
bottom: 0px;
height: 42px;
}
.envcrypted .action, .envcrypted .copy-action {
display: none;
}
.envcrypted:hover .action, .envcrypted:hover .copy-action {
display: block;
}
</style>
</head>
<body>
</body>
<script type="module">
import { html, Component, render, useState } from 'https://unpkg.com/htm/preact/standalone.module.js';
function getCurrentDocument() {
return localStorage.getItem('currentDocument') || 'default';
}
function setCurrentDocument(name) {
localStorage.setItem('currentDocument', name);
}
function documentList() {
return JSON.parse(localStorage.getItem('documentList')) || [];
}
function addDocument(name) {
const list = documentList();
const existing = list.find(item => item == name);
if (existing) {
Swal.fire('Document already exists');
return;
}
list.push(name);
localStorage.setItem('documentList', JSON.stringify(list));
}
function getCurrentDocumentItems() {
const currentDocument = getCurrentDocument();
return JSON.parse(localStorage.getItem(currentDocument + '_items')) || [];
}
function saveItem(data) {
const currentDocument = getCurrentDocument();
const items = getCurrentDocumentItems();
const existingItem = items.find(item => item.name == data.name);
if (existingItem) {
Swal.fire('Item already exists');
return items;
}
items.push(data);
localStorage.setItem(currentDocument + '_items', JSON.stringify(items));
return items;
}
function delItem(data) {
const currentDocument = getCurrentDocument();
const items = getCurrentDocumentItems();
const newItems = items.filter(item => item.name !== data.name);
localStorage.setItem(currentDocument + '_items', JSON.stringify(newItems));
return newItems;
}
function editItem(data, name) {
const currentDocument = getCurrentDocument();
const items = getCurrentDocumentItems();
const newItems = items.map(item => {
if (item.name == data.name) {
return {
name: name,
value: item.value
};
}
return item;
});
localStorage.setItem(currentDocument + '_items', JSON.stringify(newItems));
return newItems;
}
function getPubKey() {
const currentDocument = getCurrentDocument();
return localStorage.getItem(currentDocument + '_pubkey');
}
function setPubKey(pub) {
const currentDocument = getCurrentDocument();
localStorage.setItem(currentDocument + '_pubkey', pub);
window.location.reload();
}
function delPubKey() {
const _confirm = confirm('Are you sure? This action will delete all encrypted data.');
if (!_confirm) {
return false;
}
const currentDocument = getCurrentDocument();
localStorage.removeItem(currentDocument + '_pubkey');
localStorage.removeItem(currentDocument + '_items');
window.location.reload();
}
function getSha() {
const currentDocument = getCurrentDocument();
return localStorage.getItem(currentDocument + '_sha') || 'SHA-512';
}
function setSha(sha) {
const currentDocument = getCurrentDocument();
localStorage.setItem(currentDocument + '_sha', sha);
}
function delSha() {
currentDocument = getCurrentDocument();
localStorage.removeItem(currentDocument + '_sha');
}
async function encryptText(text) {
const pub = getPubKey();
const sha = getSha();
const key = await importRsaKey(pub, sha);
const encrypted = await encryptDataWithPublicKey(text, key);
return encrypted;
}
class App extends Component {
addItem(data) {
const items = saveItem(data);
this.setState({ items: items });
}
delItem(data) {
const items = delItem(data);
this.setState({ items, });
}
editItem(data, name) {
const items = editItem(data, name);
this.setState({ items: items });
}
render({ page }, {
currentDocument = getCurrentDocument(),
items = getCurrentDocumentItems(),
}) {
return html`
<div class="app">
<${Header} itemsLength=${items.length} isPubKeySet=${getPubKey()}/>
<ul style="min-width: 50vw; opacity: .89;">
${
!items.length ? html`
<li class="row value" style="justify-content:center;">
<div style="padding:6%;">
<p>No records yet. Try to add a new ENV</p>
</div>
</li>
` : ''
}
${items.map(item => html`
<${ShowEnvItem} name=${item.name} value=${item.value} onEdit=${(name) => this.editItem(item, name)} onRemove=${()=> this.delItem(item)} />
`)}
</ul>
<hr />
<ul class="add-env">
<${AddEnvItem} onAdd="${(data) => this.addItem(data)}"/>
</ul>
<${Footer} />
<${PubKeyModal} />
<${RawTextModal} />
</div>
`;
}
}
const ShowEnvItem = ({onRemove, onEdit, name, value}) => {
const onEnvCopy = async (ev) => {
ev.preventDefault()
const text = `${name}=${value}`
await navigator.clipboard.writeText(text);
Swal.fire({
position: 'bottom-end',
icon: 'success',
title: 'Copied to clipboard',
showConfirmButton: false,
timer: 1500
})
}
return html`
<li class="row value envcrypted">
<input type="text" onBlur=${(ev) => onEdit(ev.target.value)} onKeyUp=${(ev) => onEdit(ev.target.value)} class="env col-sm-12 col-md-4" placeholder="Key" value=${name}/>
<input type="text" class="env col-sm-12 col-md-8" placeholder="Value" value=${value} style="pointer-events:none;"/>
<button class="action" onClick=${onRemove}>
<div style="transform: rotate(90deg);">⨉</div>
</button>
<button class="copy-action" onClick=${onEnvCopy}>
💾
</button>
</li>
`
}
const AddEnvItem = ({onAdd}) => {
const onValidate = () => {
const envName = document.getElementById('env-name').value;
const envValue = document.getElementById('env-value').value;
if (!envName || !envValue) {
Swal.fire('Please fill env name and value');
return [false, false];
}
return [envName, envValue];
}
const onSave = async () => {
const [envName, envValue] = onValidate();
if (!envName || !envValue) {
return;
}
if (!getPubKey()) {
Swal.fire('Please set a public key first');
return;
}
onAdd({
name: envName,
value: await encryptText(envValue),
});
document.getElementById('env-name').value = '';
document.getElementById('env-value').value = '';
}
return html`
<li class="row value" style="background: transparent;">
<fieldset>
<legend>Add new env</legend>
<input type="text" id="env-name" class="env col-sm-12 col-md-4" placeholder="Name" />
<input type="text" id="env-value" class="env col-sm-12 col-md-8" placeholder="Value" />
<button class="action" style="bottom:8px; right: 6px; top: unset;" onClick=${onSave}>
<span class="icon-lock"></span>
Encrypt
</button>
</fieldset>
</li>
`
}
const RawTextModal = ({}) => {
const onSubmit = async (ev) => {
ev.preventDefault();
const text = ev.target.text.value;
if (!getPubKey()) {
Swal.fire('Please set RSA PubKey first');
return;
}
document.getElementById('result').value = await encryptText(text);
}
const onCopy = async (ev) => {
ev.preventDefault();
const text = document.getElementById('result').value;
await navigator.clipboard.writeText(text);
Swal.fire({
position: 'bottom-end',
icon: 'success',
title: 'Copied to clipboard',
showConfirmButton: false,
timer: 1500
})
}
const onClear = async (ev) => {
ev.preventDefault();
document.getElementById('text').value = '';
document.getElementById('result').value = '';
console.log('clear');
}
return html`
<input type="checkbox" id="modal-text" class="modal" />
<div role="dialog" aria-labelledby="dialog-title">
<div class="card large">
<label for="modal-text" class="modal-close"></label>
<h3 class="section">Encrypt text</h3>
<form onSubmit=${onSubmit}>
<div class="row">
<div class="col-sm-12">
<textarea style="min-width: 100%; min-height: 150px;" class="col-sm-12" id="text" name="text" placeholder="Text to encrypt"></textarea>
</div>
<div class="col-sm-12">
<textarea style="min-width: 100%; min-height: 150px;" class="col-sm-12" id="result" name="result" placeholder="Result"></textarea>
</div>
</div>
<hr/>
<div style="display:flex; justify-content: space-between;">
<button type="button" onClick=${onClear}>
Clear
</button>
<button type="submit" class="primary">Encrypt</button>
</div>
</form>
</div>
</div>
`
}
const PubKeyModal = ({}) => {
const sha = getSha() || 'SHA-512';
const pubkey = getPubKey() || '';
const onSubmit = (ev) => {
ev.preventDefault();
const pubkey = ev.target.pubkey.value;
const sha = ev.target.sha.value;
if (pubkey && sha) {
setPubKey(pubkey);
setSha(sha);
}
console.log('submit', pubkey, sha);
}
const onClear = (ev) => {
ev.preventDefault();
if (!delPubKey()) {
return
}
delSha();
document.getElementById('pubkey').value = '';
document.getElementById('sha').value = 'SHA-512';
console.log('clear');
}
const OpenFile = async (ev) => {
ev.preventDefault();
const file = document.createElement('input');
file.type = 'file';
file.accept = '.pub,.pem';
file.click();
file.onchange = async (ev) => {
const pubkey = await file.files[0].text();
document.getElementById('pubkey').value = pubkey;
}
}
return html`
<input type="checkbox" id="modal-control" class="modal" />
<div role="dialog" aria-labelledby="dialog-title">
<div class="card large">
<label for="modal-control" class="modal-close"></label>
<h3 class="section">Edit RSA PubKey</h3>
<form onSubmit=${onSubmit}>
<div class="row">
<div class="col-sm-12" style="text-align: right;">
<textarea style="min-width: 100%; min-height: 150px;" class="col-sm-12" id="pubkey" name="pubkey" placeholder="RSA PubKey">${pubkey}</textarea>
<a href="#" onClick=${OpenFile}>Open file</a>
</div>
<div class="col-sm-12">
<select id="sha" value="${sha}" name="sha">
<option value="SHA-512">SHA-512</option>
<option value="SHA-384">SHA-384</option>
<option value="SHA-256">SHA-256</option>
</select>
</div>
</div>
<hr/>
<div style="display:flex; justify-content: space-between;">
<button type="button" onClick=${onClear}>
Clear
</button>
<button type="submit" class="primary">Save</button>
</div>
</form>
</div>
</div>
`
}
const Header = ({ itemsLength, isPubKeySet}) => {
const [documents, setDocuments] = useState(documentList());
const [currentDocument, _setCurrentDocument] = useState(getCurrentDocument());
const downloadEnvs = async (ev) => {
if (!itemsLength) {
return;
}
ev.preventDefault();
const envs = await getCurrentDocumentItems();
let text = '';
for (const env of envs) {
text += `${env.name}=${env.value}\n`;
}
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${getCurrentDocument()}.env`;
a.click();
}
const onAddDocument = (ev) => {
ev.preventDefault();
const name = prompt('Enter document name');
if (name) {
addDocument(name);
setDocuments(documentList());
setCurrentDocument(name);
_setCurrentDocument(name);
window.location.reload();
}
}
const setDocument = async (ev) => {
const name = ev.target.value;
if (name === '$add') {
onAddDocument(ev);
} else {
setCurrentDocument(name);
_setCurrentDocument(name);
window.location.reload();
}
}
return html`
<header>
<a href="#" onClick=${downloadEnvs} role="button" disabled=${itemsLength <= 0}>
💾
Save
</a>
<label class="button" for="modal-text">
<span class="icon-edit"></span>
Encrypt text
</label>
<label class="button" for="modal-control">
<span class="icon-lock"></span>
${isPubKeySet ? '' : 'Set' } PUB Key
</label>
<span class="spacer">
</span>
<select value=${currentDocument} onChange=${setDocument}>
<option value="default">Environment: default</option>
${documents.map((doc) => html`
<option value="${doc}">Environment: ${doc}</option>
`)}
<option value="$add">± Add Environment</option>
</select>
</header>
`
}
const Footer = () => html`
<footer style="display: flex; justify-content: space-between;">
<span>[ENV]Crypt | <a href="https://github.com/nesterow">@nesterow</a></span>
<small>RSA-OAEP, SHA-512, SHA-256</small>
</footer>
`
render(html`<${App} page="All" />`, document.body);
</script>
<!-- UTILS -->
<script>
function str2ab(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
function encryptDataWithPublicKey(data, key) {
data = str2ab(data);
return window.crypto.subtle.encrypt(
{
name: "RSA-OAEP",
},
key,
data
).then((encrypted) => {
return btoa(String.fromCharCode.apply(null, new Uint8Array(encrypted)));
});
}
function importRsaKey(pem, sha) {
const pemHeader = "-----BEGIN PUBLIC KEY-----";
const pemFooter = "-----END PUBLIC KEY-----";
const pemContents = pem.substring(
pemHeader.length,
pem.length - pemFooter.length - 1,
);
const binaryDerString = window.atob(pemContents);
const binaryDer = str2ab(binaryDerString);
return window.crypto.subtle.importKey(
"spki",
binaryDer,
{
name: "RSA-OAEP",
hash: sha || "SHA-512",
},
true,
["encrypt"],
).catch((err) => console.log(err));
}
</script>
</html>