1System.register("local", [], function (exports_1, context_1) {
2 "use strict";
3 var __moduleName = context_1 && context_1.id;
4 function createElement(name, attributes, ...children) {
5 return {
6 name,
7 attributes: attributes || {},
8 children: Array.prototype.concat(...(children || []))
9 };
10 }
11 exports_1("createElement", createElement);
12 return {
13 setters: [],
14 execute: function () {
15 }
16 };
17});
18System.register("renderer", [], function (exports_2, context_2) {
19 "use strict";
20 var __moduleName = context_2 && context_2.id;
21 function render(element) {
22 if (element == null)
23 return '';
24 if (typeof element !== "object")
25 element = String(element);
26 if (typeof element === "string")
27 return element.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
28 //if (element instanceof Raw) return element.html;
29 console.assert(!!element.attributes, 'Element attributes must be defined:\n' + JSON.stringify(element));
30 const elementAttributes = element.attributes;
31 let attributes = Object.keys(elementAttributes).filter(key => {
32 const value = elementAttributes[key];
33 return value != null;
34 }).map(key => {
35 const value = elementAttributes[key];
36 if (value === true) {
37 return key;
38 }
39 return `${key}="${String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')}"`;
40 }).join(' ');
41 if (attributes.length > 0) {
42 attributes = ' ' + attributes;
43 }
44 const children = element.children.length > 0 ? `>${element.children.map(child => render(child)).join('')}` : '>';
45 return `<${element.name}${attributes}${children}</${element.name}>`;
46 }
47 exports_2("render", render);
48 return {
49 setters: [],
50 execute: function () {
51 }
52 };
53});
54/*
55 Copyright 2019 Wiktor Kwapisiewicz
56
57 Licensed under the Apache License, Version 2.0 (the "License");
58 you may not use this file except in compliance with the License.
59 You may obtain a copy of the License at
60
61 https://www.apache.org/licenses/LICENSE-2.0
62
63 Unless required by applicable law or agreed to in writing, software
64 distributed under the License is distributed on an "AS IS" BASIS,
65 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
66 See the License for the specific language governing permissions and
67 limitations under the License.
68*/
69System.register("openpgp-key", ["local", "renderer"], function (exports_3, context_3) {
70 "use strict";
71 var local, renderer, proofs, dateFormat;
72 var __moduleName = context_3 && context_3.id;
73 function getLatestSignature(signatures, date = new Date()) {
74 let signature = signatures[0];
75 for (let i = 1; i < signatures.length; i++) {
76 if (signatures[i].created >= signature.created &&
77 (signatures[i].created <= date || date === null)) {
78 signature = signatures[i];
79 }
80 }
81 return signature;
82 }
83 function getVerifier(proofUrl, fingerprint) {
84 for (const proof of proofs) {
85 const matches = proofUrl.match(proof.matcher);
86 if (!matches)
87 continue;
88 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 });
89 const profile = proof.profile.replace(/\{([A-Z]+)\}/g, (_, name) => bound[name]);
90 const proofJson = proof.proof.replace(/\{([A-Z]+)\}/g, (_, name) => bound[name]);
91 const username = proof.username.replace(/\{([A-Z]+)\}/g, (_, name) => bound[name]);
92 return {
93 profile,
94 proofUrl,
95 proofJson,
96 username,
97 service: proof.service,
98 checks: (proof.checks || []).map((check) => ({
99 relation: check.relation,
100 proof: check.proof,
101 claim: check.claim.replace(/\{([A-Z]+)\}/g, (_, name) => bound[name])
102 }))
103 };
104 }
105 return null;
106 }
107 async function verify(proofJson, checks) {
108 const response = await fetch(proofJson, {
109 headers: {
110 Accept: 'application/json'
111 },
112 credentials: 'omit'
113 });
114 if (!response.ok) {
115 throw new Error('Response failed: ' + response.status);
116 }
117 const json = await response.json();
118 for (const check of checks) {
119 const proofValue = check.proof.reduce((previous, current) => {
120 if (current == null || previous == null)
121 return null;
122 if (Array.isArray(previous) && typeof current === 'string') {
123 return previous.map(value => value[current]);
124 }
125 return previous[current];
126 }, json);
127 const claimValue = check.claim;
128 if (check.relation === 'eq') {
129 if (proofValue !== claimValue) {
130 throw new Error(`Proof value ${proofValue} !== claim value ${claimValue}`);
131 }
132 }
133 else if (check.relation === 'contains') {
134 if (!proofValue || proofValue.indexOf(claimValue) === -1) {
135 throw new Error(`Proof value ${proofValue} does not contain claim value ${claimValue}`);
136 }
137 }
138 else if (check.relation === 'oneOf') {
139 if (!proofValue || proofValue.indexOf(claimValue) === -1) {
140 throw new Error(`Proof value ${proofValue} does not contain claim value ${claimValue}`);
141 }
142 }
143 }
144 }
145 function serviceToClassName(service) {
146 if (service === 'github') {
147 return 'fab fa-github';
148 }
149 else if (service === 'reddit') {
150 return 'fab fa-reddit';
151 }
152 else if (service === 'hackernews') {
153 return 'fab fa-hacker-news';
154 }
155 else if (service === 'mastodon') {
156 return 'fab fa-mastodon';
157 }
158 else if (service === 'dns') {
159 return 'fas fa-globe';
160 }
161 else {
162 return '';
163 }
164 }
165 async function lookupKey(query) {
166 const result = document.getElementById('result');
167 result.innerHTML = renderer.render(local.createElement("span", null,
168 "Looking up ",
169 query,
170 "..."));
171 let keys, keyUrl;
172 const keyLink = document.querySelector('[rel="pgpkey"]');
173 if (!keyLink) {
174 const keyserver = document.querySelector('meta[name="keyserver"]').content;
175 keyUrl = `https://${keyserver}/pks/lookup?op=get&options=mr&search=${query}`;
176 const response = await fetch(keyUrl);
177 const key = await response.text();
178 keys = (await openpgp.key.readArmored(key)).keys;
179 }
180 else {
181 keyUrl = keyLink.href;
182 const response = await fetch(keyUrl);
183 const key = await response.arrayBuffer();
184 keys = (await openpgp.key.read(new Uint8Array(key))).keys;
185 }
186 if (keys.length > 0) {
187 loadKeys(keyUrl, keys).catch(e => {
188 result.innerHTML = renderer.render(local.createElement("span", null,
189 "Could not display this key: ",
190 String(e)));
191 });
192 }
193 else {
194 result.innerHTML = renderer.render(local.createElement("span", null,
195 query,
196 ": not found"));
197 }
198 }
199 async function loadKeys(keyUrl, _keys) {
200 const key = _keys[0];
201 window.key = key;
202 const primaryUser = await key.getPrimaryUser();
203 for (var i = key.users.length - 1; i >= 0; i--) {
204 try {
205 if (await key.users[i].verify(key.primaryKey) === openpgp.enums.keyStatus.valid) {
206 continue;
207 }
208 }
209 catch (e) {
210 console.error('User verification error:', e);
211 }
212 //key.users.splice(i, 1);
213 }
214 for (const user of key.users) {
215 user.revoked = await user.isRevoked();
216 }
217 const lastPrimarySig = primaryUser.selfCertification;
218 const keys = [{
219 fingerprint: key.primaryKey.getFingerprint(),
220 status: await key.verifyPrimaryKey(),
221 keyFlags: lastPrimarySig.keyFlags,
222 created: key.primaryKey.created,
223 algorithmInfo: key.primaryKey.getAlgorithmInfo(),
224 expirationTime: lastPrimarySig.getExpirationTime()
225 }];
226 //console.log(lastPrimarySig);
227 const proofs = (lastPrimarySig.notations || [])
228 .filter((notation) => notation[0] === 'proof@metacode.biz' && typeof notation[1] === 'string')
229 .map((notation) => notation[1])
230 .map((proofUrl) => getVerifier(proofUrl, key.primaryKey.getFingerprint()))
231 .filter((verifier) => !!verifier);
232 /*
233 proofs.push(getVerifier('https://www.reddit.com/user/wiktor-k/comments/bo5oih/test/', key.primaryKey.getFingerprint()));
234 proofs.push(getVerifier('https://news.ycombinator.com/user?id=wiktor-k', key.primaryKey.getFingerprint()));
235 proofs.push(getVerifier('https://gist.github.com/wiktor-k/389d589dd19250e1f9a42bc3d5d40c16', key.primaryKey.getFingerprint()));
236 proofs.push(getVerifier('https://metacode.biz/@wiktor', key.primaryKey.getFingerprint()));
237 proofs.push(getVerifier('dns:metacode.biz?type=TXT', key.primaryKey.getFingerprint()));
238 */
239 for (const subKey of key.subKeys) {
240 const lastSig = getLatestSignature(subKey.bindingSignatures);
241 let reasonForRevocation;
242 if (subKey.revocationSignatures.length > 0) {
243 reasonForRevocation = subKey.revocationSignatures[subKey.revocationSignatures.length - 1].reasonForRevocationString;
244 }
245 keys.push({
246 fingerprint: subKey.keyPacket.getFingerprint(),
247 status: await subKey.verify(key.primaryKey),
248 reasonForRevocation,
249 keyFlags: lastSig.keyFlags,
250 created: lastSig.created,
251 algorithmInfo: subKey.keyPacket.getAlgorithmInfo(),
252 expirationTime: await subKey.getExpirationTime()
253 });
254 }
255 //key.users.splice(primaryUser.index, 1);
256 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)));
257 const now = new Date();
258 // there is index property on primaryUser
259 document.title = primaryUser.user.userId.name + ' - OpenPGP key';
260 const info = local.createElement("div", null,
261 local.createElement("div", { class: "wrapper" },
262 local.createElement("div", { class: "bio" },
263 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") }),
264 local.createElement("h2", null, primaryUser.user.userId.name)),
265 local.createElement("div", null,
266 local.createElement("ul", { class: "props" },
267 local.createElement("li", { title: key.primaryKey.getFingerprint() },
268 local.createElement("a", { href: keyUrl, target: "_blank", rel: "nofollow noopener" },
269 "\uD83D\uDD11\u00A0",
270 local.createElement("code", null, key.primaryKey.getFingerprint()))),
271 key.users.filter((user) => !user.revoked && user.userId).map((user) => user.userId.email).filter((email) => !!email).map((email) => local.createElement("li", null,
272 local.createElement("a", { href: "mailto:" + email },
273 "\uD83D\uDCE7 ",
274 email
275 //formatAttribute(user.userAttribute)
276 ))),
277 proofs.filter((proof) => !!proof).map((proof) => local.createElement("li", null,
278 local.createElement("a", { rel: "me noopener nofollow", target: "_blank", href: proof.profile },
279 local.createElement("i", { class: serviceToClassName(proof.service) }),
280 proof.username),
281 local.createElement("a", { rel: "noopener nofollow", target: "_blank", href: proof.proofUrl, class: "proof", "data-proof-json": proof.proofJson, "data-checks": JSON.stringify(proof.checks) },
282 local.createElement("i", { class: "fas fa-certificate" }),
283 "proof")))))),
284 local.createElement("details", null,
285 local.createElement("summary", null, "\uD83D\uDD12 Encrypt"),
286 local.createElement("textarea", { placeholder: "Message to encrypt...", id: "message" }),
287 local.createElement("input", { type: "button", value: "Encrypt", id: "encrypt" }),
288 ' ',
289 local.createElement("input", { type: "button", id: "send", "data-recipient": primaryUser.user.userId.email, value: "Send to " + primaryUser.user.userId.email })),
290 local.createElement("details", null,
291 local.createElement("summary", null, "\uD83D\uDD8B Verify"),
292 local.createElement("textarea", { placeholder: "Clearsigned message to verify...", id: "signed" }),
293 local.createElement("input", { type: "button", value: "Verify", id: "verify" })),
294 local.createElement("details", null,
295 local.createElement("summary", null, "\uD83D\uDD11 Key details"),
296 local.createElement("p", null, "Subkeys:"),
297 local.createElement("ul", null, keys.map((subKey) => local.createElement("li", null,
298 local.createElement("div", null,
299 getStatus(subKey.status, subKey.reasonForRevocation),
300 " ",
301 getIcon(subKey.keyFlags),
302 " ",
303 local.createElement("code", null, subKey.fingerprint.substring(24).match(/.{4}/g).join(" ")),
304 " ",
305 formatAlgorithm(subKey.algorithmInfo.algorithm),
306 " (",
307 subKey.algorithmInfo.bits,
308 ")"),
309 local.createElement("div", null,
310 "created: ",
311 formatDate(subKey.created),
312 ", expire",
313 now > subKey.expirationTime ? "d" : "s",
314 ": ",
315 formatDate(subKey.expirationTime)))))));
316 document.getElementById('result').innerHTML = renderer.render(info);
317 checkProofs();
318 }
319 async function checkProofs() {
320 const proofs = document.querySelectorAll('[data-checks]');
321 for (const proofLink of proofs) {
322 const checks = JSON.parse(proofLink.dataset.checks || '');
323 const url = proofLink.dataset.proofJson || '';
324 try {
325 await verify(url, checks);
326 proofLink.textContent = 'verified proof';
327 proofLink.classList.add('verified');
328 }
329 catch (e) {
330 console.error('Could not verify proof: ' + e);
331 }
332 }
333 }
334 async function verifyProof(e) {
335 const target = e.target;
336 if (target.id === 'encrypt') {
337 const text = document.getElementById('message');
338 openpgp.encrypt({
339 message: openpgp.message.fromText(text.value),
340 publicKeys: [window.key],
341 armor: true
342 }).then((cipherText) => {
343 text.value = cipherText.data;
344 }, (e) => alert(e));
345 }
346 else if (target.id === 'send') {
347 location.href = "mailto:" + target.dataset.recipient + "?subject=Encrypted%20message&body=" + encodeURIComponent(document.getElementById('message').value);
348 }
349 else if (target.id === 'verify') {
350 const text = document.getElementById('signed');
351 const message = await openpgp.cleartext.readArmored(text.value);
352 const verified = await openpgp.verify({
353 message,
354 publicKeys: [window.key]
355 });
356 console.log(verified);
357 alert('The signature is ' + (verified.signatures[0].valid ? '✅ correct.' : '❌ incorrect.'));
358 }
359 }
360 function formatAttribute(userAttribute) {
361 if (userAttribute.attributes[0][0] === String.fromCharCode(1)) {
362 return local.createElement("img", { src: "data:image/jpeg;base64," + btoa(userAttribute.attributes[0].substring(17)) });
363 }
364 if (userAttribute.attributes[0][0] === 'e') {
365 const url = userAttribute.attributes[0].substring(userAttribute.attributes[0].indexOf('@') + 1);
366 return local.createElement("a", { href: url, rel: "noopener nofollow" }, url);
367 }
368 return 'unknown attribute';
369 }
370 function formatAlgorithm(name) {
371 if (name === 'rsa_encrypt_sign')
372 return "RSA";
373 return name;
374 }
375 function formatDate(date) {
376 if (date === Infinity)
377 return "never";
378 if (typeof date === 'number')
379 return 'x';
380 return dateFormat.format(date);
381 }
382 function getStatus(status, details) {
383 if (status === openpgp.enums.keyStatus.invalid) {
384 return local.createElement("span", { title: "Invalid key" }, "\u274C");
385 }
386 if (status === openpgp.enums.keyStatus.expired) {
387 return local.createElement("span", { title: "Key expired" }, "\u23F0");
388 }
389 if (status === openpgp.enums.keyStatus.revoked) {
390 return local.createElement("span", { title: "Key revoked: " + details }, "\u274C");
391 }
392 if (status === openpgp.enums.keyStatus.valid) {
393 return local.createElement("span", { title: "Valid key" }, "\u2705");
394 }
395 if (status === openpgp.enums.keyStatus.no_self_cert) {
396 return local.createElement("span", { title: "Key not certified" }, "\u274C");
397 }
398 return "unknown:" + status;
399 }
400 function getIcon(keyFlags) {
401 if (!keyFlags || !keyFlags[0]) {
402 return "";
403 }
404 let flags = [];
405 if ((keyFlags[0] & openpgp.enums.keyFlags.certify_keys) !== 0) {
406 flags.push(local.createElement("span", { title: "Certyfing key" }, "\uD83C\uDFF5\uFE0F"));
407 }
408 if ((keyFlags[0] & openpgp.enums.keyFlags.sign_data) !== 0) {
409 flags.push(local.createElement("span", { title: 'Signing key' }, "\uD83D\uDD8B"));
410 }
411 if (((keyFlags[0] & openpgp.enums.keyFlags.encrypt_communication) !== 0) ||
412 ((keyFlags[0] & openpgp.enums.keyFlags.encrypt_storage) !== 0)) {
413 flags.push(local.createElement("span", { title: 'Encryption key' }, "\uD83D\uDD12"));
414 }
415 if ((keyFlags[0] & openpgp.enums.keyFlags.authentication) !== 0) {
416 flags.push(local.createElement("span", { title: 'Authentication key' }, "\uD83D\uDCB3"));
417 }
418 return flags;
419 }
420 return {
421 setters: [
422 function (local_1) {
423 local = local_1;
424 },
425 function (renderer_1) {
426 renderer = renderer_1;
427 }
428 ],
429 execute: function () {
430 openpgp.config.show_version = false;
431 openpgp.config.show_comment = false;
432 proofs = [
433 {
434 matcher: /^https:\/\/gist\.github\.com\/([A-Za-z0-9_-]+)\/([0-9a-f]+)$/,
435 variables: {
436 USERNAME: 1,
437 PROOFID: 2
438 },
439 profile: 'https://github.com/{USERNAME}',
440 proof: 'https://api.github.com/gists/{PROOFID}',
441 username: '{USERNAME}',
442 service: 'github',
443 checks: [{
444 relation: 'eq',
445 proof: ['owner', 'login'],
446 claim: '{USERNAME}'
447 }, {
448 relation: 'eq',
449 proof: ['owner', 'html_url'],
450 claim: 'https://github.com/{USERNAME}'
451 }, {
452 relation: 'contains',
453 proof: ['files', 'openpgp.md', 'content'],
454 claim: '[Verifying my OpenPGP key: openpgp4fpr:{FINGERPRINT}]'
455 }]
456 },
457 {
458 matcher: /^https:\/\/news\.ycombinator\.com\/user\?id=([A-Za-z0-9-]+)$/,
459 variables: {
460 USERNAME: 1,
461 PROFILE: 0
462 },
463 profile: '{PROFILE}',
464 proof: 'https://hacker-news.firebaseio.com/v0/user/{USERNAME}.json',
465 username: '{USERNAME}',
466 service: 'hackernews',
467 checks: [{
468 relation: 'contains',
469 proof: ['about'],
470 claim: '[Verifying my OpenPGP key: openpgp4fpr:{FINGERPRINT}]'
471 }]
472 },
473 {
474 matcher: /^https:\/\/www\.reddit\.com\/user\/([^/]+)\/comments\/([^/]+)\/([^/]+\/)?$/,
475 variables: {
476 USERNAME: 1,
477 PROOF: 2
478 },
479 profile: 'https://www.reddit.com/user/{USERNAME}',
480 proof: 'https://www.reddit.com/user/{USERNAME}/comments/{PROOF}.json',
481 username: '{USERNAME}',
482 service: 'reddit',
483 checks: [{
484 relation: 'contains',
485 proof: [0, 'data', 'children', 0, 'data', 'selftext'],
486 claim: 'Verifying my OpenPGP key: openpgp4fpr:{FINGERPRINT}'
487 }, {
488 relation: 'eq',
489 proof: [0, 'data', 'children', 0, 'data', 'author'],
490 claim: '{USERNAME}'
491 }]
492 },
493 {
494 matcher: /^https:\/\/([^/]+)\/@([A-Za-z0-9_-]+)$/,
495 variables: {
496 INSTANCE: 1,
497 USERNAME: 2,
498 PROFILE: 0
499 },
500 profile: '{PROFILE}',
501 proof: '{PROFILE}',
502 username: '@{USERNAME}@{INSTANCE}',
503 service: 'mastodon',
504 checks: [{
505 relation: 'oneOf',
506 proof: ['attachment', 'value'],
507 claim: '{FINGERPRINT}'
508 }]
509 },
510 {
511 matcher: /^dns:([^?]+)\?type=TXT$/,
512 variables: {
513 DOMAIN: 1
514 },
515 profile: 'https://{DOMAIN}',
516 proof: 'https://dns.google.com/resolve?name={DOMAIN}&type=TXT',
517 username: '{DOMAIN}',
518 service: 'dns',
519 checks: [{
520 relation: 'oneOf',
521 proof: ['Answer', 'data'],
522 claim: '\"openpgp4fpr:{FINGERPRINT}\"'
523 }]
524 }
525 ];
526 window.onload = window.onhashchange = function () {
527 lookupKey(location.hash.substring(1));
528 };
529 ;
530 document.addEventListener('click', verifyProof);
531 dateFormat = new Intl.DateTimeFormat(undefined, {
532 year: 'numeric', month: 'numeric', day: 'numeric',
533 hour: 'numeric', minute: 'numeric', second: 'numeric',
534 });
535 }
536 };
537});