mirror of
https://codeberg.org/keyoxide/keyoxide-web.git
synced 2024-12-22 14:59:29 -07:00
Initial commit
This commit is contained in:
commit
d0bb087ba2
9 changed files with 550 additions and 0 deletions
15
.drone.yml
Normal file
15
.drone.yml
Normal file
|
@ -0,0 +1,15 @@
|
|||
kind: pipeline
|
||||
type: exec
|
||||
name: deploy
|
||||
|
||||
steps:
|
||||
- name: deploy to server
|
||||
environment:
|
||||
DEPLOY_COMMAND:
|
||||
from_secret: deploy_command
|
||||
commands:
|
||||
- $${DEPLOY_COMMAND}
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
|||
MIT License Copyright (c) 2020 Yarmo Mackenbach
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
5
README.md
Normal file
5
README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Keyoxide
|
||||
|
||||
[Keyoxide](https://keyoxide.org) is a FOSS solution to make basic cryptography operations accessible to regular humans.
|
||||
|
||||
[![Build Status](https://drone.private.foss.best/api/badges/yarmo/keyoxide/status.svg)](https://drone.private.foss.best/yarmo/keyoxide)
|
57
encrypt.html
Normal file
57
encrypt.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Encrypt - Keyoxide</title>
|
||||
<script async defer data-domain="keyoxide.org" src="https://plausible.io/js/plausible.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.css">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<a href="/">Keyoxide</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<h1>Encrypt</h1>
|
||||
<div class="content">
|
||||
<form id="form-encrypt" method="post">
|
||||
<h3>Message</h3>
|
||||
<textarea name="message" id="message"></textarea>
|
||||
<h3>Public Key (1: plaintext)</h3>
|
||||
<textarea name="publicKey" id="publicKey"></textarea>
|
||||
<h3>Public Key (2: web key directory)</h3>
|
||||
<input type="text" name="wkd" id="wkd" placeholder="name@domain.com">
|
||||
<h3>Public Key (3: HKP server)</h3>
|
||||
<input type="text" name="hkp_server" id="hkp_server" placeholder="https://keys.openpgp.org/">
|
||||
<input type="text" name="hkp_input" id="hkp_input" placeholder="Email address / key id / fingerprint">
|
||||
<h3>Result</h3>
|
||||
<textarea name="messageEncrypted" id="messageEncrypted" readonly></textarea>
|
||||
<p id="result"></p>
|
||||
<input type="submit" name="submit" value="ENCRYPT MESSAGE">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
<a href="/">Keyoxide</a> makes basic cryptography operations accessible to regular humans.
|
||||
<br>
|
||||
Made by <a href="https://yarmo.eu">Yarmo Mackenbach</a>.
|
||||
<br>
|
||||
Code hosted on <a href="https://codeberg.org/yarmo/keyoxide">Codeberg</a> (<a href="https://drone.private.foss.best/yarmo/keyoxide/">drone CI/CD</a>).
|
||||
<br>
|
||||
Uses <a href="https://github.com/openpgpjs/openpgpjs">openpgp.js</a> (version <a href="https://github.com/openpgpjs/openpgpjs/releases/tag/v4.10.4">4.10.4</a>).
|
||||
<br>
|
||||
Privacy-friendly public usage stats by <a href="https://plausible.io/keyoxide.org">Plausible.io</a>.
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
<script src="openpgp.min.js"></script>
|
||||
<script type="text/javascript" src="scripts.js" charset="utf-8"></script>
|
||||
</html>
|
50
index.html
Normal file
50
index.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Keyoxide</title>
|
||||
<script async defer data-domain="keyoxide.org" src="https://plausible.io/js/plausible.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.css">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<a href="/">Keyoxide</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<h1>Keyoxide</h1>
|
||||
<div class="content">
|
||||
<p>Because SO2 + 2NaOH → Na2SO3 + H2O</p>
|
||||
<h2>Basic operations</h2>
|
||||
<p>
|
||||
<a href="/verify">verify</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="/encrypt">encrypt</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
<a href="/">Keyoxide</a> makes basic cryptography operations accessible to regular humans.
|
||||
<br>
|
||||
Made by <a href="https://yarmo.eu">Yarmo Mackenbach</a>.
|
||||
<br>
|
||||
Code hosted on <a href="https://codeberg.org/yarmo/keyoxide">Codeberg</a> (<a href="https://drone.private.foss.best/yarmo/keyoxide/">drone CI/CD</a>).
|
||||
<br>
|
||||
Uses <a href="https://github.com/openpgpjs/openpgpjs">openpgp.js</a> (version <a href="https://github.com/openpgpjs/openpgpjs/releases/tag/v4.10.4">4.10.4</a>).
|
||||
<br>
|
||||
Privacy-friendly public usage stats by <a href="https://plausible.io/keyoxide.org">Plausible.io</a>.
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
<script src="openpgp.min.js"></script>
|
||||
<script type="text/javascript" src="scripts.js" charset="utf-8"></script>
|
||||
</html>
|
2
openpgp.min.js
vendored
Normal file
2
openpgp.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
277
scripts.js
Normal file
277
scripts.js
Normal file
|
@ -0,0 +1,277 @@
|
|||
async function verifySignature(opts) {
|
||||
const elRes = document.body.querySelector("#result");
|
||||
const elResContent = document.body.querySelector("#resultContent");
|
||||
let feedback, signature, verified, publicKey, fp, lookupOpts, wkd, hkp, sig, userId, keyId, sigContent;
|
||||
|
||||
elRes.innerHTML = "";
|
||||
elResContent.innerHTML = "";
|
||||
|
||||
try {
|
||||
switch (opts.mode) {
|
||||
case "plaintext":
|
||||
publicKey = (await openpgp.key.readArmored(opts.input)).keys;
|
||||
break;
|
||||
|
||||
case "wkd":
|
||||
wkd = new openpgp.WKD();
|
||||
lookupOpts = {
|
||||
email: opts.input
|
||||
};
|
||||
publicKey = (await wkd.lookup(lookupOpts)).keys;
|
||||
break;
|
||||
|
||||
case "hkp":
|
||||
if (!opts.server) {opts.server = "https://keys.openpgp.org/"};
|
||||
hkp = new openpgp.HKP(opts.server);
|
||||
lookupOpts = {
|
||||
query: opts.input
|
||||
};
|
||||
publicKey = await hkp.lookup(lookupOpts);
|
||||
publicKey = (await openpgp.key.readArmored(publicKey)).keys;
|
||||
break;
|
||||
|
||||
default:
|
||||
sig = (await openpgp.signature.readArmored(opts.signature));
|
||||
if ('compressed' in sig.packets[0]) {
|
||||
sig = sig.packets[0];
|
||||
sigContent = (await openpgp.stream.readToEnd(await sig.packets[1].getText()));
|
||||
};
|
||||
keyId = (await sig.packets[0].issuerKeyId.toHex());
|
||||
userId = sig.packets[0].signersUserId;
|
||||
|
||||
if (!keyId && !userId) {
|
||||
elRes.innerHTML = "The signature does not contain a valid keyId or userId.";
|
||||
elRes.classList.remove('green');
|
||||
elRes.classList.add('red');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts.server) {opts.server = "https://keys.openpgp.org/"};
|
||||
hkp = new openpgp.HKP(opts.server);
|
||||
lookupOpts = {
|
||||
query: userId ? userId : keyId
|
||||
};
|
||||
publicKey = await hkp.lookup(lookupOpts);
|
||||
publicKey = (await openpgp.key.readArmored(publicKey)).keys;
|
||||
break;
|
||||
}
|
||||
|
||||
if (opts.signature == null) {
|
||||
elRes.innerHTML = "No signature was provided.";
|
||||
elRes.classList.remove('green');
|
||||
elRes.classList.add('red');
|
||||
return;
|
||||
}
|
||||
|
||||
let readError = null;
|
||||
try {
|
||||
signature = await openpgp.message.readArmored(opts.signature);
|
||||
} catch(e) {
|
||||
readError = e;
|
||||
}
|
||||
try {
|
||||
signature = await openpgp.cleartext.readArmored(opts.signature);
|
||||
} catch(e) {
|
||||
readError = e;
|
||||
}
|
||||
if (signature == null) {throw(readError)};
|
||||
|
||||
fp = publicKey[0].getFingerprint();
|
||||
verified = await openpgp.verify({
|
||||
message: signature,
|
||||
publicKeys: publicKey
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
elRes.innerHTML = e;
|
||||
elRes.classList.remove('green');
|
||||
elRes.classList.add('red');
|
||||
return;
|
||||
}
|
||||
|
||||
feedback = '';
|
||||
const valid = verified.signatures[0];
|
||||
|
||||
if (sigContent) {
|
||||
elResContent.innerHTML = "<strong>Signature content:</strong><br><span style=\"white-space: pre-line\">"+sigContent+"</span>";
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
if (valid) {
|
||||
feedback += "The message was signed by the userId extracted from the signature.<br>";
|
||||
feedback += 'UserId: '+userId+'<br>';
|
||||
feedback += "Fingerprint: "+fp+"<br>";
|
||||
elRes.classList.remove('red');
|
||||
elRes.classList.add('green');
|
||||
} else {
|
||||
feedback += "The message's signature COULD NOT BE verified using the userId extracted from the signature.<br>";
|
||||
elRes.classList.remove('green');
|
||||
elRes.classList.add('red');
|
||||
}
|
||||
} else if (keyId) {
|
||||
if (valid) {
|
||||
feedback += "The message was signed by the keyId extracted from the signature.<br>";
|
||||
feedback += 'KeyID: '+keyId+'<br>';
|
||||
feedback += "Fingerprint: "+fp+"<br><br>";
|
||||
feedback += "!!! You should manually verify the fingerprint to confirm the signer's identity !!!";
|
||||
elRes.classList.remove('red');
|
||||
elRes.classList.add('green');
|
||||
} else {
|
||||
feedback += "The message's signature COULD NOT BE verified using the keyId extracted from the signature.<br>";
|
||||
elRes.classList.remove('green');
|
||||
elRes.classList.add('red');
|
||||
}
|
||||
} else {
|
||||
if (valid) {
|
||||
feedback += "The message was signed by the provided key ("+opts.mode+").<br>";
|
||||
feedback += "Fingerprint: "+fp+"<br>";
|
||||
elRes.classList.remove('red');
|
||||
elRes.classList.add('green');
|
||||
} else {
|
||||
feedback += "The message's signature COULD NOT BE verified using the provided key ("+opts.mode+").<br>";
|
||||
elRes.classList.remove('green');
|
||||
elRes.classList.add('red');
|
||||
}
|
||||
}
|
||||
|
||||
elRes.innerHTML = feedback;
|
||||
};
|
||||
|
||||
async function encryptMessage(opts) {
|
||||
const elEnc = document.body.querySelector("#messageEncrypted");
|
||||
const elRes = document.body.querySelector("#result");
|
||||
let feedback, message, verified, publicKey, fp, lookupOpts, wkd, hkp, sig, userId, keyId, sigContent;
|
||||
|
||||
elRes.innerHTML = "";
|
||||
elEnc.value = "";
|
||||
|
||||
try {
|
||||
switch (opts.mode) {
|
||||
case "plaintext":
|
||||
publicKey = (await openpgp.key.readArmored(opts.input)).keys;
|
||||
break;
|
||||
|
||||
case "wkd":
|
||||
wkd = new openpgp.WKD();
|
||||
lookupOpts = {
|
||||
email: opts.input
|
||||
};
|
||||
publicKey = (await wkd.lookup(lookupOpts)).keys;
|
||||
break;
|
||||
|
||||
case "hkp":
|
||||
if (!opts.server) {opts.server = "https://keys.openpgp.org/"};
|
||||
hkp = new openpgp.HKP(opts.server);
|
||||
lookupOpts = {
|
||||
query: opts.input
|
||||
};
|
||||
publicKey = await hkp.lookup(lookupOpts);
|
||||
publicKey = (await openpgp.key.readArmored(publicKey)).keys;
|
||||
break;
|
||||
|
||||
default:
|
||||
sig = (await openpgp.signature.readArmored(opts.message));
|
||||
if ('compressed' in sig.packets[0]) {
|
||||
sig = sig.packets[0];
|
||||
sigContent = (await openpgp.stream.readToEnd(await sig.packets[1].getText()));
|
||||
};
|
||||
keyId = (await sig.packets[0].issuerKeyId.toHex());
|
||||
userId = sig.packets[0].signersUserId;
|
||||
|
||||
if (!keyId && !userId) {
|
||||
elRes.innerHTML = "The signature does not contain a valid keyId or userId.";
|
||||
elRes.classList.remove('green');
|
||||
elRes.classList.add('red');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts.server) {opts.server = "https://keys.openpgp.org/"};
|
||||
hkp = new openpgp.HKP(opts.server);
|
||||
lookupOpts = {
|
||||
query: userId ? userId : keyId
|
||||
};
|
||||
publicKey = await hkp.lookup(lookupOpts);
|
||||
publicKey = (await openpgp.key.readArmored(publicKey)).keys;
|
||||
break;
|
||||
}
|
||||
|
||||
if (opts.message == null) {
|
||||
elRes.innerHTML = "No message was provided.";
|
||||
elRes.classList.remove('green');
|
||||
elRes.classList.add('red');
|
||||
return;
|
||||
}
|
||||
|
||||
encrypted = await openpgp.encrypt({
|
||||
message: openpgp.message.fromText(opts.message),
|
||||
publicKeys: publicKey
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
elRes.innerHTML = e;
|
||||
elRes.classList.remove('green');
|
||||
elRes.classList.add('red');
|
||||
return;
|
||||
}
|
||||
|
||||
elEnc.value = encrypted.data;
|
||||
};
|
||||
|
||||
let elFormVerify = document.body.querySelector("#form-verify"),
|
||||
elFormEncrypt = document.body.querySelector("#form-encrypt");
|
||||
|
||||
if (elFormVerify) {
|
||||
elFormVerify.onsubmit = function (evt) {
|
||||
evt.preventDefault();
|
||||
|
||||
let opts = {
|
||||
signature: null,
|
||||
mode: null,
|
||||
input: null,
|
||||
server: null,
|
||||
};
|
||||
|
||||
opts.signature = document.body.querySelector("#signature").value;
|
||||
|
||||
if (document.body.querySelector("#publicKey").value != "") {
|
||||
opts.input = document.body.querySelector("#publicKey").value;
|
||||
opts.mode = "plaintext";
|
||||
} else if (document.body.querySelector("#wkd").value != "") {
|
||||
opts.input = document.body.querySelector("#wkd").value;
|
||||
opts.mode = "wkd";
|
||||
} else if (document.body.querySelector("#hkp_input").value != "") {
|
||||
opts.input = document.body.querySelector("#hkp_input").value;
|
||||
opts.server = document.body.querySelector("#hkp_server").value;
|
||||
opts.mode = "hkp";
|
||||
}
|
||||
verifySignature(opts);
|
||||
};
|
||||
}
|
||||
|
||||
if (elFormEncrypt) {
|
||||
elFormEncrypt.onsubmit = function (evt) {
|
||||
evt.preventDefault();
|
||||
|
||||
let opts = {
|
||||
message: null,
|
||||
mode: null,
|
||||
input: null,
|
||||
server: null,
|
||||
};
|
||||
|
||||
opts.message = document.body.querySelector("#message").value;
|
||||
|
||||
if (document.body.querySelector("#publicKey").value != "") {
|
||||
opts.input = document.body.querySelector("#publicKey").value;
|
||||
opts.mode = "plaintext";
|
||||
} else if (document.body.querySelector("#wkd").value != "") {
|
||||
opts.input = document.body.querySelector("#wkd").value;
|
||||
opts.mode = "wkd";
|
||||
} else if (document.body.querySelector("#hkp_input").value != "") {
|
||||
opts.input = document.body.querySelector("#hkp_input").value;
|
||||
opts.server = document.body.querySelector("#hkp_server").value;
|
||||
opts.mode = "hkp";
|
||||
}
|
||||
encryptMessage(opts);
|
||||
};
|
||||
}
|
68
styles.css
Normal file
68
styles.css
Normal file
|
@ -0,0 +1,68 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background-color: #eee;
|
||||
color: #333;
|
||||
}
|
||||
header {
|
||||
padding: 16px;
|
||||
margin: 0 0 48px;
|
||||
font-size: 1.1em;
|
||||
background-color: #fff;
|
||||
}
|
||||
footer {
|
||||
color: #777;
|
||||
margin: 64px 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 640px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.content {
|
||||
padding: 16px 32px 32px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 16px 32px;
|
||||
background-color: #9dd3f0;
|
||||
text-align: center;
|
||||
}
|
||||
a {
|
||||
color: #3f9acc;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 128px;
|
||||
resize: vertical;
|
||||
}
|
||||
input[type="radio"] {
|
||||
vertical-align: sub;
|
||||
}
|
||||
input[type="submit"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
font-size: 1.2em;
|
||||
text-transform: uppercase;
|
||||
background-color: #c3eaff;
|
||||
border: 1px solid #3f9acc;
|
||||
border-radius: 3px;
|
||||
border-bottom-width: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type="submit"]:hover {
|
||||
background-color: #9dd3f0;
|
||||
}
|
||||
.green {
|
||||
color: green;
|
||||
}
|
||||
.red {
|
||||
color: red;
|
||||
}
|
||||
.label {
|
||||
display: inline-block;
|
||||
margin: 0 0 8px;
|
||||
}
|
57
verify.html
Normal file
57
verify.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Verify - Keyoxide</title>
|
||||
<script async defer data-domain="keyoxide.org" src="https://plausible.io/js/plausible.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.css">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<a href="/">Keyoxide</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<h1>Verify</h1>
|
||||
<div class="content">
|
||||
<form id="form-verify" method="post">
|
||||
<h3>Signature</h3>
|
||||
<textarea name="signature" id="signature"></textarea>
|
||||
<h3>Public Key (1: plaintext)</h3>
|
||||
<textarea name="publicKey" id="publicKey"></textarea>
|
||||
<h3>Public Key (2: web key directory)</h3>
|
||||
<input type="text" name="wkd" id="wkd" placeholder="name@domain.com">
|
||||
<h3>Public Key (3: HKP server)</h3>
|
||||
<input type="text" name="hkp_server" id="hkp_server" placeholder="https://keys.openpgp.org/">
|
||||
<input type="text" name="hkp_input" id="hkp_input" placeholder="Email address / key id / fingerprint">
|
||||
<h3>Result</h3>
|
||||
<p id="result">Click on the button below.</p>
|
||||
<p id="resultContent"></p>
|
||||
<input type="submit" name="submit" value="VERIFY SIGNATURE" value="">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
<a href="/">Keyoxide</a> makes basic cryptography operations accessible to regular humans.
|
||||
<br>
|
||||
Made by <a href="https://yarmo.eu">Yarmo Mackenbach</a>.
|
||||
<br>
|
||||
Code hosted on <a href="https://codeberg.org/yarmo/keyoxide">Codeberg</a> (<a href="https://drone.private.foss.best/yarmo/keyoxide/">drone CI/CD</a>).
|
||||
<br>
|
||||
Uses <a href="https://github.com/openpgpjs/openpgpjs">openpgp.js</a> (version <a href="https://github.com/openpgpjs/openpgpjs/releases/tag/v4.10.4">4.10.4</a>).
|
||||
<br>
|
||||
Privacy-friendly public usage stats by <a href="https://plausible.io/keyoxide.org">Plausible.io</a>.
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
<script src="openpgp.min.js"></script>
|
||||
<script type="text/javascript" src="scripts.js" charset="utf-8"></script>
|
||||
</html>
|
Loading…
Reference in a new issue