NixNet/scripts.js

538 lines
24 KiB
JavaScript

System.register("local", [], function (exports_1, context_1) {
"use strict";
var __moduleName = context_1 && context_1.id;
function createElement(name, attributes, ...children) {
return {
name,
attributes: attributes || {},
children: Array.prototype.concat(...(children || []))
};
}
exports_1("createElement", createElement);
return {
setters: [],
execute: function () {
}
};
});
System.register("renderer", [], function (exports_2, context_2) {
"use strict";
var __moduleName = context_2 && context_2.id;
function render(element) {
if (element == null)
return '';
if (typeof element !== "object")
element = String(element);
if (typeof element === "string")
return element.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
//if (element instanceof Raw) return element.html;
console.assert(!!element.attributes, 'Element attributes must be defined:\n' + JSON.stringify(element));
const elementAttributes = element.attributes;
let attributes = Object.keys(elementAttributes).filter(key => {
const value = elementAttributes[key];
return value != null;
}).map(key => {
const value = elementAttributes[key];
if (value === true) {
return key;
}
return `${key}="${String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')}"`;
}).join(' ');
if (attributes.length > 0) {
attributes = ' ' + attributes;
}
const children = element.children.length > 0 ? `>${element.children.map(child => render(child)).join('')}` : '>';
return `<${element.name}${attributes}${children}</${element.name}>`;
}
exports_2("render", render);
return {
setters: [],
execute: function () {
}
};
});
/*
Copyright 2019 Wiktor Kwapisiewicz
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
System.register("openpgp-key", ["local", "renderer"], function (exports_3, context_3) {
"use strict";
var local, renderer, proofs, dateFormat;
var __moduleName = context_3 && context_3.id;
function getLatestSignature(signatures, date = new Date()) {
let signature = signatures[0];
for (let i = 1; i < signatures.length; i++) {
if (signatures[i].created >= signature.created &&
(signatures[i].created <= date || date === null)) {
signature = signatures[i];
}
}
return signature;
}
function getVerifier(proofUrl, fingerprint) {
for (const proof of proofs) {
const matches = proofUrl.match(proof.matcher);
if (!matches)
continue;
const bound = Object.entries(proof.variables).map(([key, value]) => [key, matches[value || 0]]).reduce((previous, current) => { previous[current[0]] = current[1]; return previous; }, { FINGERPRINT: fingerprint });
const profile = proof.profile.replace(/\{([A-Z]+)\}/g, (_, name) => bound[name]);
const proofJson = proof.proof.replace(/\{([A-Z]+)\}/g, (_, name) => bound[name]);
const username = proof.username.replace(/\{([A-Z]+)\}/g, (_, name) => bound[name]);
return {
profile,
proofUrl,
proofJson,
username,
service: proof.service,
checks: (proof.checks || []).map((check) => ({
relation: check.relation,
proof: check.proof,
claim: check.claim.replace(/\{([A-Z]+)\}/g, (_, name) => bound[name])
}))
};
}
return null;
}
async function verify(proofJson, checks) {
const response = await fetch(proofJson, {
headers: {
Accept: 'application/json'
},
credentials: 'omit'
});
if (!response.ok) {
throw new Error('Response failed: ' + response.status);
}
const json = await response.json();
for (const check of checks) {
const proofValue = check.proof.reduce((previous, current) => {
if (current == null || previous == null)
return null;
if (Array.isArray(previous) && typeof current === 'string') {
return previous.map(value => value[current]);
}
return previous[current];
}, json);
const claimValue = check.claim;
if (check.relation === 'eq') {
if (proofValue !== claimValue) {
throw new Error(`Proof value ${proofValue} !== claim value ${claimValue}`);
}
}
else if (check.relation === 'contains') {
if (!proofValue || proofValue.indexOf(claimValue) === -1) {
throw new Error(`Proof value ${proofValue} does not contain claim value ${claimValue}`);
}
}
else if (check.relation === 'oneOf') {
if (!proofValue || proofValue.indexOf(claimValue) === -1) {
throw new Error(`Proof value ${proofValue} does not contain claim value ${claimValue}`);
}
}
}
}
function serviceToClassName(service) {
if (service === 'github') {
return 'fab fa-github';
}
else if (service === 'reddit') {
return 'fab fa-reddit';
}
else if (service === 'hackernews') {
return 'fab fa-hacker-news';
}
else if (service === 'mastodon') {
return 'fab fa-mastodon';
}
else if (service === 'dns') {
return 'fas fa-globe';
}
else {
return '';
}
}
async function lookupKey(query) {
const result = document.getElementById('result');
result.innerHTML = renderer.render(local.createElement("span", null,
"Looking up ",
query,
"..."));
let keys, keyUrl;
const keyLink = document.querySelector('[rel="pgpkey"]');
if (!keyLink) {
const keyserver = document.querySelector('meta[name="keyserver"]').content;
keyUrl = `https://${keyserver}/pks/lookup?op=get&options=mr&search=${query}`;
const response = await fetch(keyUrl);
const key = await response.text();
keys = (await openpgp.key.readArmored(key)).keys;
}
else {
keyUrl = keyLink.href;
const response = await fetch(keyUrl);
const key = await response.arrayBuffer();
keys = (await openpgp.key.read(new Uint8Array(key))).keys;
}
if (keys.length > 0) {
loadKeys(keyUrl, keys).catch(e => {
result.innerHTML = renderer.render(local.createElement("span", null,
"Could not display this key: ",
String(e)));
});
}
else {
result.innerHTML = renderer.render(local.createElement("span", null,
query,
": not found"));
}
}
async function loadKeys(keyUrl, _keys) {
const key = _keys[0];
window.key = key;
const primaryUser = await key.getPrimaryUser();
for (var i = key.users.length - 1; i >= 0; i--) {
try {
if (await key.users[i].verify(key.primaryKey) === openpgp.enums.keyStatus.valid) {
continue;
}
}
catch (e) {
console.error('User verification error:', e);
}
//key.users.splice(i, 1);
}
for (const user of key.users) {
user.revoked = await user.isRevoked();
}
const lastPrimarySig = primaryUser.selfCertification;
const keys = [{
fingerprint: key.primaryKey.getFingerprint(),
status: await key.verifyPrimaryKey(),
keyFlags: lastPrimarySig.keyFlags,
created: key.primaryKey.created,
algorithmInfo: key.primaryKey.getAlgorithmInfo(),
expirationTime: lastPrimarySig.getExpirationTime()
}];
//console.log(lastPrimarySig);
const proofs = (lastPrimarySig.notations || [])
.filter((notation) => notation[0] === 'proof@metacode.biz' && typeof notation[1] === 'string')
.map((notation) => notation[1])
.map((proofUrl) => getVerifier(proofUrl, key.primaryKey.getFingerprint()))
.filter((verifier) => !!verifier);
/*
proofs.push(getVerifier('https://www.reddit.com/user/wiktor-k/comments/bo5oih/test/', key.primaryKey.getFingerprint()));
proofs.push(getVerifier('https://news.ycombinator.com/user?id=wiktor-k', key.primaryKey.getFingerprint()));
proofs.push(getVerifier('https://gist.github.com/wiktor-k/389d589dd19250e1f9a42bc3d5d40c16', key.primaryKey.getFingerprint()));
proofs.push(getVerifier('https://metacode.biz/@wiktor', key.primaryKey.getFingerprint()));
proofs.push(getVerifier('dns:metacode.biz?type=TXT', key.primaryKey.getFingerprint()));
*/
for (const subKey of key.subKeys) {
const lastSig = getLatestSignature(subKey.bindingSignatures);
let reasonForRevocation;
if (subKey.revocationSignatures.length > 0) {
reasonForRevocation = subKey.revocationSignatures[subKey.revocationSignatures.length - 1].reasonForRevocationString;
}
keys.push({
fingerprint: subKey.keyPacket.getFingerprint(),
status: await subKey.verify(key.primaryKey),
reasonForRevocation,
keyFlags: lastSig.keyFlags,
created: lastSig.created,
algorithmInfo: subKey.keyPacket.getAlgorithmInfo(),
expirationTime: await subKey.getExpirationTime()
});
}
//key.users.splice(primaryUser.index, 1);
const profileHash = await openpgp.crypto.hash.md5(openpgp.util.str_to_Uint8Array(primaryUser.user.userId.email)).then((u) => openpgp.util.str_to_hex(openpgp.util.Uint8Array_to_str(u)));
const now = new Date();
// there is index property on primaryUser
document.title = primaryUser.user.userId.name + ' - OpenPGP key';
const info = local.createElement("div", null,
local.createElement("div", { class: "wrapper" },
local.createElement("div", { class: "bio" },
local.createElement("img", { class: "avatar", src: "https://seccdn.libravatar.org/avatar/" + profileHash + "?s=148&d=" + encodeURIComponent("https://www.gravatar.com/avatar/" + profileHash + "?s=148&d=mm") }),
local.createElement("h2", null, primaryUser.user.userId.name)),
local.createElement("div", null,
local.createElement("ul", { class: "props" },
local.createElement("li", { title: key.primaryKey.getFingerprint() },
local.createElement("a", { href: keyUrl, target: "_blank", rel: "nofollow noopener" },
"\uD83D\uDD11\u00A0",
local.createElement("code", null, key.primaryKey.getFingerprint()))),
key.users.filter((user) => !user.revoked && user.userId).map((user) => user.userId.email).filter((email) => !!email).map((email) => local.createElement("li", null,
local.createElement("a", { href: "mailto:" + email },
"\uD83D\uDCE7 ",
email
//formatAttribute(user.userAttribute)
))),
proofs.filter((proof) => !!proof).map((proof) => local.createElement("li", null,
local.createElement("a", { rel: "me noopener nofollow", target: "_blank", href: proof.profile },
local.createElement("i", { class: serviceToClassName(proof.service) }),
proof.username),
local.createElement("a", { rel: "noopener nofollow", target: "_blank", href: proof.proofUrl, class: "proof", "data-proof-json": proof.proofJson, "data-checks": JSON.stringify(proof.checks) },
local.createElement("i", { class: "fas fa-certificate" }),
"proof")))))),
local.createElement("details", null,
local.createElement("summary", null, "\uD83D\uDD12 Encrypt"),
local.createElement("textarea", { placeholder: "Message to encrypt...", id: "message" }),
local.createElement("input", { type: "button", value: "Encrypt", id: "encrypt" }),
' ',
local.createElement("input", { type: "button", id: "send", "data-recipient": primaryUser.user.userId.email, value: "Send to " + primaryUser.user.userId.email })),
local.createElement("details", null,
local.createElement("summary", null, "\uD83D\uDD8B Verify"),
local.createElement("textarea", { placeholder: "Clearsigned message to verify...", id: "signed" }),
local.createElement("input", { type: "button", value: "Verify", id: "verify" })),
local.createElement("details", null,
local.createElement("summary", null, "\uD83D\uDD11 Key details"),
local.createElement("p", null, "Subkeys:"),
local.createElement("ul", null, keys.map((subKey) => local.createElement("li", null,
local.createElement("div", null,
getStatus(subKey.status, subKey.reasonForRevocation),
" ",
getIcon(subKey.keyFlags),
" ",
local.createElement("code", null, subKey.fingerprint.substring(24).match(/.{4}/g).join(" ")),
" ",
formatAlgorithm(subKey.algorithmInfo.algorithm),
" (",
subKey.algorithmInfo.bits,
")"),
local.createElement("div", null,
"created: ",
formatDate(subKey.created),
", expire",
now > subKey.expirationTime ? "d" : "s",
": ",
formatDate(subKey.expirationTime)))))));
document.getElementById('result').innerHTML = renderer.render(info);
checkProofs();
}
async function checkProofs() {
const proofs = document.querySelectorAll('[data-checks]');
for (const proofLink of proofs) {
const checks = JSON.parse(proofLink.dataset.checks || '');
const url = proofLink.dataset.proofJson || '';
try {
await verify(url, checks);
proofLink.textContent = 'verified proof';
proofLink.classList.add('verified');
}
catch (e) {
console.error('Could not verify proof: ' + e);
}
}
}
async function verifyProof(e) {
const target = e.target;
if (target.id === 'encrypt') {
const text = document.getElementById('message');
openpgp.encrypt({
message: openpgp.message.fromText(text.value),
publicKeys: [window.key],
armor: true
}).then((cipherText) => {
text.value = cipherText.data;
}, (e) => alert(e));
}
else if (target.id === 'send') {
location.href = "mailto:" + target.dataset.recipient + "?subject=Encrypted%20message&body=" + encodeURIComponent(document.getElementById('message').value);
}
else if (target.id === 'verify') {
const text = document.getElementById('signed');
const message = await openpgp.cleartext.readArmored(text.value);
const verified = await openpgp.verify({
message,
publicKeys: [window.key]
});
console.log(verified);
alert('The signature is ' + (verified.signatures[0].valid ? '✅ correct.' : '❌ incorrect.'));
}
}
function formatAttribute(userAttribute) {
if (userAttribute.attributes[0][0] === String.fromCharCode(1)) {
return local.createElement("img", { src: "data:image/jpeg;base64," + btoa(userAttribute.attributes[0].substring(17)) });
}
if (userAttribute.attributes[0][0] === 'e') {
const url = userAttribute.attributes[0].substring(userAttribute.attributes[0].indexOf('@') + 1);
return local.createElement("a", { href: url, rel: "noopener nofollow" }, url);
}
return 'unknown attribute';
}
function formatAlgorithm(name) {
if (name === 'rsa_encrypt_sign')
return "RSA";
return name;
}
function formatDate(date) {
if (date === Infinity)
return "never";
if (typeof date === 'number')
return 'x';
return dateFormat.format(date);
}
function getStatus(status, details) {
if (status === openpgp.enums.keyStatus.invalid) {
return local.createElement("span", { title: "Invalid key" }, "\u274C");
}
if (status === openpgp.enums.keyStatus.expired) {
return local.createElement("span", { title: "Key expired" }, "\u23F0");
}
if (status === openpgp.enums.keyStatus.revoked) {
return local.createElement("span", { title: "Key revoked: " + details }, "\u274C");
}
if (status === openpgp.enums.keyStatus.valid) {
return local.createElement("span", { title: "Valid key" }, "\u2705");
}
if (status === openpgp.enums.keyStatus.no_self_cert) {
return local.createElement("span", { title: "Key not certified" }, "\u274C");
}
return "unknown:" + status;
}
function getIcon(keyFlags) {
if (!keyFlags || !keyFlags[0]) {
return "";
}
let flags = [];
if ((keyFlags[0] & openpgp.enums.keyFlags.certify_keys) !== 0) {
flags.push(local.createElement("span", { title: "Certyfing key" }, "\uD83C\uDFF5\uFE0F"));
}
if ((keyFlags[0] & openpgp.enums.keyFlags.sign_data) !== 0) {
flags.push(local.createElement("span", { title: 'Signing key' }, "\uD83D\uDD8B"));
}
if (((keyFlags[0] & openpgp.enums.keyFlags.encrypt_communication) !== 0) ||
((keyFlags[0] & openpgp.enums.keyFlags.encrypt_storage) !== 0)) {
flags.push(local.createElement("span", { title: 'Encryption key' }, "\uD83D\uDD12"));
}
if ((keyFlags[0] & openpgp.enums.keyFlags.authentication) !== 0) {
flags.push(local.createElement("span", { title: 'Authentication key' }, "\uD83D\uDCB3"));
}
return flags;
}
return {
setters: [
function (local_1) {
local = local_1;
},
function (renderer_1) {
renderer = renderer_1;
}
],
execute: function () {
openpgp.config.show_version = false;
openpgp.config.show_comment = false;
proofs = [
{
matcher: /^https:\/\/gist\.github\.com\/([A-Za-z0-9_-]+)\/([0-9a-f]+)$/,
variables: {
USERNAME: 1,
PROOFID: 2
},
profile: 'https://github.com/{USERNAME}',
proof: 'https://api.github.com/gists/{PROOFID}',
username: '{USERNAME}',
service: 'github',
checks: [{
relation: 'eq',
proof: ['owner', 'login'],
claim: '{USERNAME}'
}, {
relation: 'eq',
proof: ['owner', 'html_url'],
claim: 'https://github.com/{USERNAME}'
}, {
relation: 'contains',
proof: ['files', 'openpgp.md', 'content'],
claim: '[Verifying my OpenPGP key: openpgp4fpr:{FINGERPRINT}]'
}]
},
{
matcher: /^https:\/\/news\.ycombinator\.com\/user\?id=([A-Za-z0-9-]+)$/,
variables: {
USERNAME: 1,
PROFILE: 0
},
profile: '{PROFILE}',
proof: 'https://hacker-news.firebaseio.com/v0/user/{USERNAME}.json',
username: '{USERNAME}',
service: 'hackernews',
checks: [{
relation: 'contains',
proof: ['about'],
claim: '[Verifying my OpenPGP key: openpgp4fpr:{FINGERPRINT}]'
}]
},
{
matcher: /^https:\/\/www\.reddit\.com\/user\/([^/]+)\/comments\/([^/]+)\/([^/]+\/)?$/,
variables: {
USERNAME: 1,
PROOF: 2
},
profile: 'https://www.reddit.com/user/{USERNAME}',
proof: 'https://www.reddit.com/user/{USERNAME}/comments/{PROOF}.json',
username: '{USERNAME}',
service: 'reddit',
checks: [{
relation: 'contains',
proof: [0, 'data', 'children', 0, 'data', 'selftext'],
claim: 'Verifying my OpenPGP key: openpgp4fpr:{FINGERPRINT}'
}, {
relation: 'eq',
proof: [0, 'data', 'children', 0, 'data', 'author'],
claim: '{USERNAME}'
}]
},
{
matcher: /^https:\/\/([^/]+)\/@([A-Za-z0-9_-]+)$/,
variables: {
INSTANCE: 1,
USERNAME: 2,
PROFILE: 0
},
profile: '{PROFILE}',
proof: '{PROFILE}',
username: '@{USERNAME}@{INSTANCE}',
service: 'mastodon',
checks: [{
relation: 'oneOf',
proof: ['attachment', 'value'],
claim: '{FINGERPRINT}'
}]
},
{
matcher: /^dns:([^?]+)\?type=TXT$/,
variables: {
DOMAIN: 1
},
profile: 'https://{DOMAIN}',
proof: 'https://dns.google.com/resolve?name={DOMAIN}&type=TXT',
username: '{DOMAIN}',
service: 'dns',
checks: [{
relation: 'oneOf',
proof: ['Answer', 'data'],
claim: '\"openpgp4fpr:{FINGERPRINT}\"'
}]
}
];
window.onload = window.onhashchange = function () {
lookupKey(location.hash.substring(1));
};
;
document.addEventListener('click', verifyProof);
dateFormat = new Intl.DateTimeFormat(undefined, {
year: 'numeric', month: 'numeric', day: 'numeric',
hour: 'numeric', minute: 'numeric', second: 'numeric',
});
}
};
});