559 lines
18 KiB
HTML
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>
|