scripts.js

  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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')}"`;
 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});