clean up and add affiliates
This commit is contained in:
parent
aa5e436c1b
commit
efacc8eba5
|
@ -0,0 +1,37 @@
|
||||||
|
---
|
||||||
|
layout: page
|
||||||
|
title: Affiliates
|
||||||
|
subtitle: Support NixNet by making purchases and using these links
|
||||||
|
description: Support NixNet by making purchases with affiliate links and vouchers
|
||||||
|
---
|
||||||
|
One of the easiest ways to support NixNet is to purchase products on these websites using the provided affiliate links.
|
||||||
|
|
||||||
|
# netcup
|
||||||
|
[netcup](https://netcup.eu) is currently where the most services are running. They have fantastic offerings at most price points and incredible deals. I've been very happy with their service and *highly* recommend them.
|
||||||
|
|
||||||
|
## Voucher codes
|
||||||
|
These are single-use vouchers for various products. When you use one, please [send me a message](/contact) so I can generate a new one.
|
||||||
|
|
||||||
|
### 5€ for anything except domains
|
||||||
|
* [36nc15758387844](https://www.netcup.eu/bestellen/gutschein_einloesen.php?gutschein=36nc15758387844)
|
||||||
|
* [36nc15758387843](https://www.netcup.eu/bestellen/gutschein_einloesen.php?gutschein=36nc15758387843)
|
||||||
|
* [36nc15758387842](https://www.netcup.eu/bestellen/gutschein_einloesen.php?gutschein=36nc15758387842)
|
||||||
|
* [36nc15758387841](https://www.netcup.eu/bestellen/gutschein_einloesen.php?gutschein=36nc15758387841)
|
||||||
|
* [36nc15758387840](https://www.netcup.eu/bestellen/gutschein_einloesen.php?gutschein=36nc15758387840)
|
||||||
|
|
||||||
|
### 10% off the 200 G8
|
||||||
|
* [2052nc15758390090](https://www.netcup.eu/bestellen/gutschein_einloesen.php?gutschein=2052nc15758390090)
|
||||||
|
* [2052nc15758390091](https://www.netcup.eu/bestellen/gutschein_einloesen.php?gutschein=2052nc15758390091)
|
||||||
|
|
||||||
|
### 10% off the 500 G8
|
||||||
|
* [2053nc15758393980](https://www.netcup.eu/bestellen/gutschein_einloesen.php?gutschein=2053nc15758393980)
|
||||||
|
* [2053nc15758393981](https://www.netcup.eu/bestellen/gutschein_einloesen.php?gutschein=2053nc15758393981)
|
||||||
|
|
||||||
|
### 10% off the 1000 G8
|
||||||
|
* [2054nc15758394201](https://www.netcup.eu/bestellen/gutschein_einloesen.php?gutschein=2054nc15758394201)
|
||||||
|
* [2054nc15758394200](https://www.netcup.eu/bestellen/gutschein_einloesen.php?gutschein=2054nc15758394200)
|
||||||
|
|
||||||
|
### 10% off the 2000 G8
|
||||||
|
* [2056nc15758394800](https://www.netcup.eu/bestellen/gutschein_einloesen.php?gutschein=2056nc15758394800)
|
||||||
|
* [2056nc15758394801](https://www.netcup.eu/bestellen/gutschein_einloesen.php?gutschein=2056nc15758394801)
|
||||||
|
|
108
app.js
108
app.js
|
@ -1,108 +0,0 @@
|
||||||
|
|
||||||
$(function(){
|
|
||||||
|
|
||||||
var model = {
|
|
||||||
// renomear
|
|
||||||
dat: [
|
|
||||||
],
|
|
||||||
read_json: function() {
|
|
||||||
$.ajax({
|
|
||||||
url: "/cards.json",
|
|
||||||
dataType: 'json',
|
|
||||||
async: false,
|
|
||||||
success: function(data) {
|
|
||||||
$(data["cards"]).each(function(){
|
|
||||||
var card_info = $(this)[0];
|
|
||||||
model.set_data(card_info);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
set_data: function(d) {
|
|
||||||
model.dat.push(d);
|
|
||||||
},
|
|
||||||
get_data: function() {
|
|
||||||
return model.dat;
|
|
||||||
},
|
|
||||||
filter_data: function(conteudo) {
|
|
||||||
var $d = model.dat;
|
|
||||||
var $filter = [];
|
|
||||||
var $c = conteudo.toLowerCase();
|
|
||||||
$($d).each(function(i){
|
|
||||||
if(
|
|
||||||
$d[i].name.toLowerCase().indexOf($c) >= 0 ||
|
|
||||||
$d[i].description.toLowerCase().indexOf($c) >= 0 ||
|
|
||||||
$d[i].button_text.toLowerCase().indexOf($c) >= 0
|
|
||||||
) {
|
|
||||||
$filter.push($d[i]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return $filter;
|
|
||||||
},
|
|
||||||
init: function() {
|
|
||||||
model.read_json();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var octopus = {
|
|
||||||
init: function() {
|
|
||||||
model.init();
|
|
||||||
view.init();
|
|
||||||
octopus.create_card(model.get_data());
|
|
||||||
},
|
|
||||||
create_card: function(d) {
|
|
||||||
$.each(d, function(i){
|
|
||||||
view.create_card(d[i].name, d[i].description, d[i].button_text, d[i].link, d[i].tor);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
filter_data: function(c) {
|
|
||||||
var d = model.filter_data(c);
|
|
||||||
$.each(d, function(i){
|
|
||||||
view.create_card(d[i].name, d[i].description, d[i].button_text, d[i].link, d[i].tor);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
recreate_cards: function() {
|
|
||||||
octopus.create_card(model.get_data());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Renomear
|
|
||||||
var view = {
|
|
||||||
init: function() {
|
|
||||||
this.container = $(".flex-cards");
|
|
||||||
this.search_field = $("#search_field");
|
|
||||||
view.events();
|
|
||||||
},
|
|
||||||
events: function(){
|
|
||||||
this.search_field.on('input',function(){
|
|
||||||
var $conteudo = $(this).val();
|
|
||||||
view.clean_cards();
|
|
||||||
if($conteudo.length == 0){
|
|
||||||
octopus.recreate_cards();
|
|
||||||
}else {
|
|
||||||
octopus.filter_data($conteudo);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
create_card: function(name, desc, bt_txt, link, tor){
|
|
||||||
var $card = $("<div>").addClass("card");
|
|
||||||
var $h1 = $("<h1>").text(name);
|
|
||||||
var $desc = $("<p>").text(desc);
|
|
||||||
var $button = $("<button>").addClass("button").text(bt_txt);
|
|
||||||
var $icon = $("<button>").addClass("tor").text("Tor");
|
|
||||||
var $a = $("<a>").attr("href", link);
|
|
||||||
var $tor = $("<a>").attr("href", tor);
|
|
||||||
$a.append($button);
|
|
||||||
$tor.append($icon);
|
|
||||||
if (tor == "#") {
|
|
||||||
$card.append($h1).append($desc).append($a);
|
|
||||||
} else {
|
|
||||||
$card.append($h1).append($desc).append($a).append($tor);
|
|
||||||
}
|
|
||||||
this.container.append($card);
|
|
||||||
},
|
|
||||||
clean_cards: function() {
|
|
||||||
this.container.empty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
octopus.init();
|
|
||||||
});
|
|
|
@ -9,14 +9,14 @@
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
width="158.75mm"
|
width="315"
|
||||||
height="83.34375mm"
|
height="315"
|
||||||
viewBox="0 0 158.75 83.34375"
|
viewBox="0 0 83.343752 83.343752"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg8"
|
id="svg8"
|
||||||
inkscape:export-filename="/home/amolith/repos/nixnet/logo.png"
|
inkscape:export-filename="/tmp/sticker.png"
|
||||||
inkscape:export-xdpi="96"
|
inkscape:export-xdpi="152.38"
|
||||||
inkscape:export-ydpi="96"
|
inkscape:export-ydpi="152.38"
|
||||||
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||||
sodipodi:docname="logo.svg">
|
sodipodi:docname="logo.svg">
|
||||||
<defs
|
<defs
|
||||||
|
@ -28,10 +28,10 @@
|
||||||
borderopacity="1.0"
|
borderopacity="1.0"
|
||||||
inkscape:pageopacity="0.0"
|
inkscape:pageopacity="0.0"
|
||||||
inkscape:pageshadow="2"
|
inkscape:pageshadow="2"
|
||||||
inkscape:zoom="1"
|
inkscape:zoom="0.70710678"
|
||||||
inkscape:cx="339.47007"
|
inkscape:cx="54.864303"
|
||||||
inkscape:cy="163.84677"
|
inkscape:cy="98.02578"
|
||||||
inkscape:document-units="mm"
|
inkscape:document-units="px"
|
||||||
inkscape:current-layer="layer1"
|
inkscape:current-layer="layer1"
|
||||||
showgrid="false"
|
showgrid="false"
|
||||||
showborder="false"
|
showborder="false"
|
||||||
|
@ -40,11 +40,22 @@
|
||||||
fit-margin-left="0"
|
fit-margin-left="0"
|
||||||
fit-margin-right="0"
|
fit-margin-right="0"
|
||||||
fit-margin-bottom="0"
|
fit-margin-bottom="0"
|
||||||
inkscape:window-width="1366"
|
inkscape:window-width="1336"
|
||||||
inkscape:window-height="768"
|
inkscape:window-height="698"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="15"
|
||||||
inkscape:window-y="0"
|
inkscape:window-y="35"
|
||||||
inkscape:window-maximized="0" />
|
inkscape:window-maximized="0"
|
||||||
|
units="px"
|
||||||
|
inkscape:snap-bbox="true"
|
||||||
|
inkscape:bbox-paths="true"
|
||||||
|
inkscape:bbox-nodes="true"
|
||||||
|
inkscape:snap-bbox-edge-midpoints="true"
|
||||||
|
inkscape:snap-bbox-midpoints="true"
|
||||||
|
inkscape:snap-nodes="true"
|
||||||
|
inkscape:object-paths="true"
|
||||||
|
inkscape:snap-intersection-paths="true"
|
||||||
|
inkscape:snap-smooth-nodes="true"
|
||||||
|
inkscape:snap-midpoints="true" />
|
||||||
<metadata
|
<metadata
|
||||||
id="metadata5">
|
id="metadata5">
|
||||||
<rdf:RDF>
|
<rdf:RDF>
|
||||||
|
@ -61,17 +72,15 @@
|
||||||
inkscape:label="Layer 1"
|
inkscape:label="Layer 1"
|
||||||
inkscape:groupmode="layer"
|
inkscape:groupmode="layer"
|
||||||
id="layer1"
|
id="layer1"
|
||||||
transform="translate(-128.38222,22.853529)">
|
transform="translate(-128.38222,22.853531)">
|
||||||
<rect
|
<path
|
||||||
style="opacity:1;fill:#333333;fill-opacity:1;stroke:none;stroke-width:0.31644166;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
style="fill:#333333;fill-opacity:1;stroke:none;stroke-width:1.8438884;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
id="rect95912"
|
d="M 211.72597,18.818345 A 41.671876,41.671872 0 0 1 170.0541,60.490217 41.671876,41.671872 0 0 1 128.38222,18.818345 41.671876,41.671872 0 0 1 170.0541,-22.853527 41.671876,41.671872 0 0 1 211.72597,18.818345 Z"
|
||||||
width="158.75"
|
id="path96013"
|
||||||
height="83.34375"
|
inkscape:connector-curvature="0" />
|
||||||
x="128.38222"
|
|
||||||
y="-22.853529" />
|
|
||||||
<g
|
<g
|
||||||
id="g95919"
|
id="g95919"
|
||||||
transform="matrix(0.84116788,0,0,0.84116788,14.976495,1.9821852)">
|
transform="matrix(0.78478307,0,0,0.78478307,-9.8042309,3.1107386)">
|
||||||
<path
|
<path
|
||||||
inkscape:connector-curvature="0"
|
inkscape:connector-curvature="0"
|
||||||
id="rect819"
|
id="rect819"
|
||||||
|
@ -102,5 +111,18 @@
|
||||||
d="m 229.04267,-5.1482062 c 2.02111,0 3.64822,1.6165249 3.64822,3.6244951 V 41.226748 c 0,2.00797 -1.62711,3.624495 -3.64822,3.624495 -2.02112,0 -3.64823,-1.616525 -3.64823,-3.624495 V -1.5237111 c 0,-2.0079702 1.62711,-3.6244951 3.64823,-3.6244951 z"
|
d="m 229.04267,-5.1482062 c 2.02111,0 3.64822,1.6165249 3.64822,3.6244951 V 41.226748 c 0,2.00797 -1.62711,3.624495 -3.64822,3.624495 -2.02112,0 -3.64823,-1.616525 -3.64823,-3.624495 V -1.5237111 c 0,-2.0079702 1.62711,-3.6244951 3.64823,-3.6244951 z"
|
||||||
style="opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.01318607;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
style="opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.01318607;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||||
</g>
|
</g>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:normal;font-weight:normal;font-size:10.58333397px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458335"
|
||||||
|
x="-198.78204"
|
||||||
|
y="80.002522"
|
||||||
|
id="text96064"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan96062">hello world</tspan></text>
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#333333;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 136.67061,43.760032 c 0,0 31.75567,37.773979 66.76697,0"
|
||||||
|
id="path96072"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 6.2 KiB |
4
dat.json
4
dat.json
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"title": "NixNet",
|
|
||||||
"description": "NixNet.xyz is a network of websites and services hosted by the pseudonymous Amolith (me). It's also a blog!"
|
|
||||||
}
|
|
42864
openpgp.js
42864
openpgp.js
File diff suppressed because one or more lines are too long
537
scripts.js
537
scripts.js
|
@ -1,537 +0,0 @@
|
||||||
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
//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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')}"`;
|
|
||||||
}).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',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
Loading…
Reference in New Issue