Skip to content

Instantly share code, notes, and snippets.

@korc
Created July 27, 2018 03:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save korc/30232df2f303ee8e2436a039a65e7acc to your computer and use it in GitHub Desktop.
Save korc/30232df2f303ee8e2436a039a65e7acc to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<head>
<title>Encrypt and Share</title>
<script src="https://unpkg.com/ace-builds@1.3.3/src-noconflict/ace.js" charset="utf-8"></script>
<script src="https://unpkg.com/ace-builds@1.3.3/src-noconflict/mode-markdown.js" charset="utf-8"></script>
<script src="https://unpkg.com/qrcode@1.2.0/build/qrcode.js" charset="utf-8"></script>
<script>
"use strict";
async function keyToId(jwk) {
var key = await crypto.subtle.importKey("jwk", jwk, { name: "RSA-OAEP", hash: "SHA-256" }, true, ["encrypt"]);
var digest = await crypto.subtle.digest('sha-256', await crypto.subtle.exportKey('spki', key));
return new Uint32Array(digest).reduce((a, b) => a + b.toString(32), "").slice(0, 16);
}
async function addKeyToList(rcptList, jwk, name) {
var keyId = await keyToId(jwk);
var opt = document.createElement('option');
opt.value = JSON.stringify(jwk);
opt.appendChild(document.createTextNode(name + ': ' + keyId));
opt.setAttribute('data-key-id', keyId);
if (rcptList !== null)
rcptList.appendChild(opt);
return opt;
}
async function updateMyKeyId(keyInput, rcptList, jwk) {
var opt = await addKeyToList(rcptList, jwk, '(me)');
keyInput.value = opt.getAttribute('data-key-id');
keyInput.setAttribute('data-jwk', opt.value);
return keyInput.value;
}
function Editor(textarea) {
if (typeof (textarea) === "string")
textarea = document.getElementById(textarea);
this.textarea = textarea;
this.init();
}
Editor.prototype = {
init: function () {
var self = this;
var isiPhone = navigator.userAgent.indexOf("iPhone") !== -1;
if (!isiPhone && window.ace) {
var ui = this.ui = document.createElement('div');
ui.classList.add('ace-editor');
this.textarea.parentNode.insertBefore(this.ui, this.textarea);
this.ace = ace.edit(this.ui);
var aceSession = this.ace.getSession();
aceSession.setValue(this.textarea.value);
aceSession.setOptions({
mode: "ace/mode/markdown",
tabSize: 2
});
aceSession.setUseWrapMode(true);
this.textarea.style.display = "none";
aceSession.on('change', function (ev, session) {
self.textarea.value = session.getValue();
self.isModified = true;
});
} else {
this.ui = this.textarea;
this.ui.addEventListener('change', function () {
self.isModified = true;
});
}
},
get value() {
return this.textarea.value;
},
set value(value) {
this.textarea.value = value;
if (this.ace) {
this.ace.getSession().setValue(value);
}
this.isModified = true;
},
get isModified() {
return this.ui.classList.contains('edit-modified');
},
set isModified(value) {
if (value)
this.ui.classList.add('edit-modified');
else
this.ui.classList.remove('edit-modified');
}
}
window.addEventListener('load', async function () {
var editor = new Editor('plaintext-md');
var uploadBtn = document.getElementById('upload-btn');
var encKeyData;
var genKeyBtn = document.getElementById('generate-key-btn');
var myKeyInput = document.getElementById('my-key-id');
var uploadIdInput = document.getElementById('upload-id');
var uploadPathInput = document.getElementById('upload-path');
var downloadBtn = document.getElementById('download-btn');
var rcptList = document.getElementById('rcpt');
var copyMyKeyBtn = document.getElementById('copy-key-btn');
var showMyKeyBtn = document.getElementById('show-key-btn');
var viewKeyArea = document.getElementById('view-key-area');
var pasteRcptKeyBtn = document.getElementById('paste-rcpt-btn');
var qrCodeCanvas = null;
if (window.QRCode) {
showMyKeyBtn.addEventListener('click', function () {
if (qrCodeCanvas === null) {
qrCodeCanvas = document.createElement('canvas');
viewKeyArea.appendChild(qrCodeCanvas);
QRCode.toCanvas(qrCodeCanvas, "n=" + myKey.public.n);
} else {
qrCodeCanvas.parentNode.removeChild(qrCodeCanvas);
qrCodeCanvas = null;
}
});
} else
showMyKeyBtn.style.display = "none";
document.getElementById('data-new-btn').addEventListener('click', function () {
if (editor.isModified && !confirm("Document modified, are you sure?"))
return;
document.location.replace('#');
document.location.reload();
});
window.onbeforeunload = function (ev) {
if (editor.isModified)
return "Editor changes not saved";
};
async function addRecipient(data) {
if (data.slice(0, 2) === 'n=') {
var jwk = { alg: "RSA-OAEP-256", e: "AQAB", ext: true, key_ops: ["encrypt"], kty: "RSA", n: data.slice(2) };
var keyId = await keyToId(jwk);
var name = prompt("Name for '" + keyId + "': ");
if (name !== null) {
await addKeyToList(rcptList, jwk, name);
localStorage["rcpt-list"] = JSON.stringify(Array.prototype.map.call(rcptList.options, function (opt) {
var jwk = JSON.parse(opt.value);
if (jwk.n === myKey.public.n)
return null;
return { jwk: jwk, name: opt.label.slice(0, opt.label.indexOf(':')) };
}).filter((r) => r !== null));
}
} else {
alert('Could not find recipient key in clipboard. Please make sure it is a long string starting with "n="');
throw "bad recipient data";
}
}
var pasteKeyInput = null;
function pasteKeyComplete() {
addRecipient(pasteKeyInput.value).then(function () {
pasteKeyInput.parentNode.removeChild(pasteKeyInput);
pasteKeyInput = null;
});
}
pasteRcptKeyBtn.addEventListener('click', async function () {
if (pasteKeyInput !== null) {
if (pasteKeyInput.value === '') {
pasteKeyInput.parentNode.removeChild(pasteKeyInput);
pasteKeyInput = null;
} else {
pasteKeyComplete();
}
return;
} else {
pasteKeyInput = document.createElement('input');
pasteKeyInput.placeholder = "n=xxxxx (paste here)";
this.parentNode.insertBefore(pasteKeyInput, this);
pasteKeyInput.focus();
if (document.execCommand('paste')) {
pasteKeyComplete();
return;
}
}
pasteKeyInput.addEventListener('blur', function () {
if (this.value === '')
return;
pasteKeyComplete();
});
});
var myKey, myKeyId;
if ("my-key" in localStorage) {
myKey = JSON.parse(localStorage["my-key"]);
genKeyBtn.style.display = "none";
copyMyKeyBtn.style.display = "";
myKeyId = await updateMyKeyId(myKeyInput, rcptList, myKey.public);
} else {
myKeyInput.style.display = "none";
copyMyKeyBtn.style.display = "none";
genKeyBtn.addEventListener('click', async function () {
var keyPair = await crypto.subtle.generateKey({
name: 'RSA-OAEP',
hash: { name: 'SHA-256' },
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1])
},
true, ['encrypt', 'decrypt']);
myKey = {
private: await crypto.subtle.exportKey('jwk', keyPair.privateKey),
public: await crypto.subtle.exportKey('jwk', keyPair.publicKey)
};
localStorage["my-key"] = JSON.stringify(myKey);
myKeyId = await updateMyKeyId(myKeyInput, rcptList, myKey.public);
myKeyInput.style.display = "";
copyMyKeyBtn.style.display = "";
genKeyBtn.style.display = "none";
});
}
if ("rcpt-list" in localStorage) {
JSON.parse(localStorage["rcpt-list"]).forEach(function (rcpt) {
addKeyToList(rcptList, rcpt.jwk, rcpt.name);
});
}
copyMyKeyBtn.addEventListener('click', function () {
var i = document.createElement('input');
this.parentNode.insertBefore(i, this);
i.value = "n=" + myKey.public.n;
i.select();
if (!document.execCommand('copy') || confirm('Key info copied to clipboard.\nDo you want to send a mail?')) {
document.location.href = "mailto:?subject=My recipient key&body=" + encodeURIComponent("Hi,\n\nPlease add me as recipient on " + document.location.href + " with this key:\n\n" + i.value + "\n\nKey ID: " + myKeyId + "\n\nThanks!");
};
this.parentNode.removeChild(i);
})
rcptList.addEventListener("change", function () {
uploadBtn.disabled = !encKeyData && this.selectedOptions.length === 0;
});
uploadIdInput.addEventListener('change', function () {
downloadBtn.disabled = !myKey || (this.value === "");
});
uploadIdInput.addEventListener('keyup', function () {
downloadBtn.disabled = !myKey || (this.value === "");
});
function startDownload() {
if (editor.isModified && !confirm("Data modified, are you sure to discard changes?"))
return;
var uploadPath = uploadPathInput.value;
var uploadId = uploadIdInput.value;
var xhr = new XMLHttpRequest();
xhr.open('GET', uploadPath + uploadId + '.enc');
xhr.responseType = 'arraybuffer';
xhr.addEventListener('load', function () {
if (this.status !== 200) {
alert('Cannot find data: ' + uploadId);
return;
}
history.replaceState({}, '', '#' + uploadId);
var encData = this.response;
var xhr2 = new XMLHttpRequest();
xhr2.open('GET', uploadPath + uploadId + '.key.' + myKeyId);
xhr2.responseType = 'arraybuffer';
xhr2.addEventListener('load', function () {
if (this.status !== 200) {
alert('Your key is not included in recipients list.\n\nPlease request from data owner to be added.\n' + (myKeyId ? ('\nYour key ID: ' + myKeyId) : 'You also need to generate and send your key first.') + '\n(Use key info on top-right corner)');
return;
}
var wrappedEncKey = this.response;
var xhr3 = new XMLHttpRequest();
xhr3.open('GET', uploadPath + uploadId + '.iv');
xhr3.responseType = 'arraybuffer';
xhr3.addEventListener('load', async function () {
if (this.status !== 200) {
alert('Uh-oh.. IV lost?');
return;
}
var ivData = this.response;
var alg = { name: 'RSA-OAEP', hash: 'SHA-256' };
var rcptKey = await crypto.subtle.importKey('jwk', myKey.private, alg, false, ['decrypt']);
encKeyData = await crypto.subtle.decrypt(alg, rcptKey, wrappedEncKey);
alg = { name: 'AES-GCM', iv: ivData };
var dataKey = await crypto.subtle.importKey('raw', encKeyData, alg, false, ['decrypt']);
var data = await crypto.subtle.decrypt(alg, dataKey, encData);
editor.value = new TextDecoder().decode(data);
editor.isModified = false;
uploadBtn.disabled = false;
});
xhr3.send(null);
});
xhr2.send(null);
});
xhr.send(null);
}
downloadBtn.addEventListener('click', startDownload);
uploadBtn.addEventListener('click', async function () {
var uploadPath = uploadPathInput.value;
var uploadId = uploadIdInput.value;
if (uploadId === '') {
uploadIdInput.value = uploadId = crypto.getRandomValues(new Uint8Array(8)).reduce((a, b) => a + (b < 0x10 ? "0" : "") + b.toString(16), "");
history.replaceState({}, '', '#' + uploadId);
uploadBtn.value = 'Update';
}
if (!encKeyData)
encKeyData = crypto.getRandomValues(new Uint8Array(16));
var iv = crypto.getRandomValues(new Uint8Array(12));
var alg = { name: 'AES-GCM', iv: iv };
var encKey = await crypto.subtle.importKey('raw', encKeyData, alg, false, ['encrypt']);
var xhr = new XMLHttpRequest();
xhr.open('PUT', uploadPath + uploadId + '.iv');
xhr.send(iv);
xhr = new XMLHttpRequest();
xhr.open('PUT', uploadPath + uploadId + '.enc');
xhr.addEventListener('load', function () {
if (this.status >= 200 && this.status < 300)
editor.isModified = false;
});
xhr.send(await crypto.subtle.encrypt(alg, encKey, new TextEncoder().encode(editor.value)));
Array.prototype.forEach.call(rcptList.selectedOptions, async function (opt) {
var alg = { name: 'RSA-OAEP', hash: "SHA-256" };
var rcptKey = await crypto.subtle.importKey('jwk', JSON.parse(opt.value), alg, false, ['encrypt']);
var rcptKeyId = opt.getAttribute('data-key-id');
var wrappedKeyData = await crypto.subtle.encrypt(alg, rcptKey, encKeyData);
xhr = new XMLHttpRequest();
xhr.open('PUT', uploadPath + uploadId + '.key.' + rcptKeyId);
xhr.send(wrappedKeyData);
});
});
if (document.location.hash.length > 2) {
uploadIdInput.value = unescape(document.location.hash.slice(1));
startDownload();
}
var uainfo = document.createElement("div");
uainfo.classList.add("dbg");
uainfo.style.cssFloat = "right";
uainfo.appendChild(document.createTextNode(navigator.userAgent));
document.body.appendChild(uainfo);
});
</script>
<style>
.ace-editor {
width: 50%;
height: 400px;
}
.dbg {
font-family: monospace;
font-size: 0.7em;
color: gray;
}
.edit-modified {
outline: dashed maroon 2px;
}
.edit-modified::after {
content: " *Unsaved*";
color: gray;
font-style: italic;
}
</style>
</head>
<body>
<div style="float: right">
My ID:
<input placeholder="My Key ID" id="my-key-id" disabled>
<button id="generate-key-btn">Generate my key</button>
<button id="copy-key-btn">&#x1f5d0;</button>
<button id="show-key-btn">&#x1f441;</button>
<div id="view-key-area"></div>
</div>
Data ID:
<input id="upload-path" value="/data/" type="hidden" />
<input placeholder="Upload ID" id="upload-id">
<button id="download-btn" disabled>Load</button>
<button id="data-new-btn">New</button>
<textarea cols="80" rows="40" id="plaintext-md"></textarea>
<br/> Recipients:
<select id="rcpt" multiple></select>
<button id="paste-rcpt-btn">&#x1f4cb;</button>
<br>
<button id="upload-btn" disabled>Save data</button>
</body>
</html>
<html>
<head>
<title>file admin</title>
<script>
"use strict";
function updateDav(input) {
document.location.hash = '#' + input.value;
}
function davNSR(p) {
var ns = { D: 'DAV:' };
return ns[p] || null;
}
function formatTimeInterval(secs, nrOfParts) {
if (nrOfParts)
nrOfParts *= 2;
return [[60, "m"], [60, "h"], [24, "d"], [7, "w"], null]
.reduce(function (a, n) {
if (typeof (a) === "string") return a;
if (!n) return a.slice(0, nrOfParts).join("");
var v = Math.floor(a[0] / n[0]), d = a[0] - v * n[0];
return v ? [v, n[1]].concat(d ? [parseInt(d * 1000) / 1000, a[1]] : [], a.slice(2)) : a.join("");
}, [secs, "s"]);
}
function formatBytes(bytes) {
if (bytes < 1024) return parseInt(bytes) + "b";
if (bytes < 1024 * 1024) return (Math.round(10 * bytes / 1024) / 10) + "kb";
if (bytes < 1024 * 1024 * 1024) return (Math.round(10 * bytes / 1024 / 1024) / 10) + "mb";
return (Math.round(10 * bytes / 1024 / 1024 / 1024) / 10) + "gb";
}
function DAVObject(path, properties) {
this.path = path;
this.properties = properties;
if (this.isCollection)
this.items = [];
}
DAVObject.prototype = {
_xhrProm: function (xhr, data) {
return new Promise(function (resolve, reject) {
xhr.onload = function (ev) {
if (!(xhr.status >= 200 && xhr.status < 300))
reject(xhr.status + " " + xhr.statusText);
resolve(this.response);
}
xhr.onerror = function (ev) { reject(ev); };
xhr.send(data);
});
},
doPropfind: function (depth) {
if (depth === undefined)
depth = 1;
var xhr = new XMLHttpRequest();
xhr.open('PROPFIND', this.path);
xhr.setRequestHeader('Depth', '' + depth);
xhr.responseType = 'document';
return this._xhrProm(xhr, null);
},
doPut: function (data) {
var xhr = new XMLHttpRequest();
xhr.open('PUT', this.path);
return this._xhrProm(xhr, data);
},
doDelete: function () {
var xhr = new XMLHttpRequest();
xhr.open('DELETE', this.path);
return this._xhrProm(xhr, null);
},
parsePropfind: function (xml) {
var resIter = xml.evaluate('/D:multistatus/D:response', xml, davNSR, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
var resNode, href, prop, propIter, propVal, properties, propName, rtypeIter, rtype;
var firstResult = true;
while (resNode = resIter.iterateNext()) {
href = xml.evaluate('D:href', resNode, davNSR, XPathResult.STRING_TYPE, null).stringValue;
propIter = xml.evaluate('D:propstat/D:prop/*', resNode, davNSR, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
properties = {};
while (prop = propIter.iterateNext()) {
if (prop.namespaceURI !== "DAV:")
console.log("Warning: property not in 'DAV:' namespace: ", prop.namespaceURI)
switch (prop.localName) {
case "getlastmodified": case "getcontentlength": case "getetag": case "getcontenttype": case "displayname":
propVal = xml.evaluate('text()', prop, davNSR, XPathResult.STRING_TYPE).stringValue;
if (!propVal)
break;
if (prop.localName === "getlastmodified")
propVal = new Date(propVal);
properties[prop.localName.slice(0, 3) === "get" ? prop.localName.slice(3) : prop.localName] = propVal;
break;
case "resourcetype":
rtypeIter = xml.evaluate("./*", prop, davNSR, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
properties.resourcetype = {};
while (rtype = rtypeIter.iterateNext()) {
properties.resourcetype[rtype.localName] = true;
}
break;
case "supportedlock": break;
default:
console.log("Warning: don't know how to handle: ", prop.localName, prop);
properties[prop.localName] = xml.evaluate('text()', prop, davNSR, XPathResult.STRING_TYPE).stringValue;
}
}
if (firstResult) {
firstResult = false;
this.properties = properties;
this.href = href;
if (this.isCollection)
this.items = [];
} else {
this.items.push(new DAVObject(href, properties));
}
}
},
get isCollection() {
return this.properties && this.properties.resourcetype && this.properties.resourcetype.collection;
}
};
function createButton(label, callback) {
var btn = document.createElement('button');
if (label)
btn.appendChild(document.createTextNode(label));
btn.addEventListener('click', callback);
return btn;
}
function listFolder(container, addr) {
var dObj = new DAVObject(addr);
while (container.firstChild) container.removeChild(container.firstChild);
container.appendChild(document.createTextNode("(refreshing " + addr + ")"));
dObj.doPropfind().then(function (xmlResponse) {
while (container.firstChild) container.removeChild(container.firstChild);
dObj.parsePropfind(xmlResponse);
var ul = document.createElement('ul');
container.appendChild(document.createTextNode("[" + (dObj.properties.displayname || dObj.href) + "]"));
container.appendChild(createButton('🔄', function () {
listFolder(container, addr);
}));
container.appendChild(createButton('⇪', function () {
var ulBtn = this;
if (this.nextSibling.tagName === 'INPUT' && this.nextSibling.type === 'file') {
this.classList.remove('pressed');
this.parentNode.removeChild(this.nextSibling);
return;
}
this.classList.add('pressed');
var flist = document.createElement('input');
flist.type = 'file';
flist.multiple = true;
flist.addEventListener('change', function () {
var fileCount = this.files.length;
Array.prototype.forEach.call(this.files, function (file) {
var uploadSpan = document.createElement('span');
uploadSpan.classList.add('upload-status');
uploadSpan.appendChild(document.createTextNode(file.name));
flist.parentNode.insertBefore(uploadSpan, flist.nextSibling);
var newFile = new DAVObject((dObj.href + (dObj.href.slice(-1) === '/' ? '' : '/')) + file.name)
newFile.doPut(file).then(function () {
fileCount--;
uploadSpan.classList.add('complete');
if (fileCount === 0) listFolder(container, addr);
}).catch(function (err) {
uploadSpan.classList.add('error');
uploadSpan.setAttribute('data-error', err);
});
});
this.parentNode.removeChild(this);
ulBtn.classList.remove('pressed');
});
this.parentNode.insertBefore(flist, this.nextSibling);
}));
if (dObj.properties.lastmodified)
container.appendChild(document.createTextNode(" (mod: " + formatTimeInterval((new Date() - dObj.properties.lastmodified) / 1000, 2) + " ago)"));
container.appendChild(ul);
dObj.items.forEach(function (item) {
var li = document.createElement('li');
var a;
if (item.isCollection) {
a = createButton(null, function () {
listFolder(li, item.path);
});
} else {
a = document.createElement('a');
a.href = item.path;
}
a.appendChild(document.createTextNode(item.properties.displayname || item.href));
li.appendChild(a);
if (!item.isCollection) {
li.appendChild(createButton('🗑️', function () {
if (!confirm("Do you want to delete '" + item.path + "'?"))
return;
item.doDelete().then(function () {
listFolder(container, dObj.path);
});
}));
}
if ("contentlength" in item.properties)
li.appendChild(document.createTextNode(" (size: " + formatBytes(item.properties.contentlength) + ")"));
if ("contenttype" in item.properties)
li.appendChild(document.createTextNode(" (type: " + item.properties.contenttype + ")"));
if (item.properties.lastmodified) {
var diff = new Date() - new Date(item.properties.lastmodified);
li.appendChild(document.createTextNode(" (mod: " + formatTimeInterval(diff / 1000, 2) + " ago)"));
}
ul.appendChild(li);
});
}).catch(function (error) {
var err = document.createElement('span');
err.classList.add('error');
err.appendChild(document.createTextNode("" + error));
container.appendChild(err);
});
}
function doList() {
listFolder(document.getElementById('results'), document.getElementById('webdav-input').value);
}
</script>
<style>
span.error {
color: red;
background-color: bisque;
border: 2px dashed red;
}
span.upload-status.complete {
outline: 1px solid green;
}
span.upload-status.error::after {
content: " (" attr(data-error) ")";
}
button.pressed {
border-style: inset;
}
</style>
</head>
<body>
<input onchange="updateDav(this)" placeholder="https://webdav.site/addr" id="webdav-input">
<button onclick="doList()">Open</button>
<hr>
<div id="results"></div>
<script>
"use strict";
if (document.location.hash !== '') {
document.getElementById('webdav-input').value = unescape(document.location.hash.substr(1, ));
doList();
}
</script>
</body>
</html>
#!/bin/sh
: "${ONELINE_WEBSRV:=websrv}"
test -x "$(which "$ONELINE_WEBSRV")" || {
echo "Cannot find \$ONELINE_WEBSRV='$ONELINE_WEBSRV'" >&2
exit 1
}
set -e -x
mkdir -p dav
exec $ONELINE_WEBSRV -listen 127.0.0.1:3000 -map /=file: -map /data=webdav:dav
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment