Remove the PWA
This commit is contained in:
parent
8b60bde56d
commit
e6b20d7ef5
9 changed files with 1 additions and 469 deletions
|
@ -99,7 +99,7 @@ The code is then generated by encoding those 5 bytes using base32 without paddin
|
|||
|
||||
### How do I generate valid addresses?
|
||||
|
||||
Obviously, you need to use some kind of software configured with your private key to do so. This project provides (or will provide…) a [progressive web application](https://en.wikipedia.org/wiki/Progressive_web_app) that works completely offline so you can generate addresses from your preferred web browser, even without connectivity.
|
||||
The filter itself is useful for OpenSMTPD only, it is not meant to be used directly by the user. For this usage, you should use [sake-app](https://github.com/breard-r/sake-app).
|
||||
|
||||
### Do I need to have several mailboxes?
|
||||
|
||||
|
|
263
pwa/app.js
263
pwa/app.js
|
@ -1,263 +0,0 @@
|
|||
function base32_nopad_encode(slice) {
|
||||
const encoder = new base32.Encoder({ type: "rfc4648", lc: true });
|
||||
const code = encoder.write(slice).finalize();
|
||||
return code.replaceAll('=', '');
|
||||
}
|
||||
|
||||
function base64_decode(str_b64) {
|
||||
const raw_str = atob(str_b64);
|
||||
const length = raw_str.length;
|
||||
var b = [];
|
||||
for (var i = 0; i < length; i++) {
|
||||
b.push(raw_str.charCodeAt(i));
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
class AccountValueError extends Error {
|
||||
constructor(field_id, ...params) {
|
||||
super(...params);
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, AccountValueError);
|
||||
}
|
||||
this.name = "AccountValueError";
|
||||
this.field_id = field_id;
|
||||
}
|
||||
}
|
||||
|
||||
class Account {
|
||||
constructor(local_part, separator, domain, key) {
|
||||
// Set the local part
|
||||
if (!local_part) {
|
||||
throw new AccountValueError("new-addr-local-part", "The local part cannot be empty.");
|
||||
}
|
||||
this.local_part = local_part;
|
||||
|
||||
// Set the separator
|
||||
if (!separator || separator.length !== 1) {
|
||||
throw new AccountValueError("new-addr-separator", "The separator must be a single character.");
|
||||
}
|
||||
this.separator = separator;
|
||||
|
||||
// Set the domain name
|
||||
if (!domain) {
|
||||
throw new AccountValueError("new-addr-domain", "The domain cannot be empty.");
|
||||
}
|
||||
this.domain = domain;
|
||||
|
||||
// Set the private key
|
||||
if (Array.isArray(key)) {
|
||||
this.key = key;
|
||||
} else {
|
||||
try {
|
||||
this.key = base64_decode(key);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw new AccountValueError("new-addr-key", "The key must be a valid base64 string.");
|
||||
}
|
||||
if (!this.key.length) {
|
||||
throw new AccountValueError("new-addr-key", "The key must not be empty.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getName() {
|
||||
return `${this.local_part}@${this.domain}`;
|
||||
}
|
||||
|
||||
genSubAddr(sub_addr_name) {
|
||||
var hasher = sha256.hmac.create(this.key);
|
||||
hasher.update(this.local_part);
|
||||
hasher.update(this.separator);
|
||||
hasher.update(sub_addr_name);
|
||||
const hash = hasher.array();
|
||||
const offset = hash[hash.length - 1] & 0xf;
|
||||
const reduced_hash = hash.slice(offset, offset + 5);
|
||||
const code = base32_nopad_encode(reduced_hash);
|
||||
return `${this.local_part}${this.separator}${sub_addr_name}${this.separator}${code}@${this.domain}`
|
||||
}
|
||||
|
||||
register() {
|
||||
// Handle duplicate
|
||||
if (localStorage.getItem(this.getName())) {
|
||||
throw new AccountValueError("", "The account already exists.");
|
||||
}
|
||||
localStorage.setItem(this.getName(), JSON.stringify(this));
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Functions to display and remove the error message
|
||||
function setErrorMessage(message) {
|
||||
unsetErrorMessage();
|
||||
const el = document.createElement('p');
|
||||
el.classList.add('notification');
|
||||
el.classList.add('is-danger');
|
||||
el.classList.add('is-light');
|
||||
el.appendChild(document.createTextNode(message));
|
||||
document.querySelector('#new-account-error-msg-container').appendChild(el);
|
||||
}
|
||||
|
||||
function unsetErrorMessage() {
|
||||
const el_cont = document.querySelector('#new-account-error-msg-container');
|
||||
while (el_cont.lastElementChild) {
|
||||
el_cont.removeChild(el_cont.lastElementChild);
|
||||
}
|
||||
['#new-addr-local-part', '#new-addr-separator', '#new-addr-domain', '#new-addr-key'].forEach((selector) => {
|
||||
document.querySelector(selector).classList.remove('is-danger');
|
||||
});
|
||||
}
|
||||
|
||||
// Functions to open and close a modal
|
||||
function openModal(el) {
|
||||
if (el.id === 'modal-del-account') {
|
||||
const name_el = document.createTextNode(getSelectedAccountName());
|
||||
document.querySelector('#del-account-name').appendChild(name_el);
|
||||
}
|
||||
el.classList.add('is-active');
|
||||
}
|
||||
|
||||
function openNewAccountModal(el) {
|
||||
const new_account_modal = document.querySelector('#modal-add-account');
|
||||
openModal(new_account_modal);
|
||||
}
|
||||
|
||||
function closeModal(el) {
|
||||
if (!(el.id === 'modal-add-account' && localStorage.length === 0)) {
|
||||
el.classList.remove('is-active');
|
||||
}
|
||||
}
|
||||
|
||||
function closeAllModals() {
|
||||
(document.querySelectorAll('.modal') || []).forEach((modal) => {
|
||||
closeModal(modal);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to get the name of the currently selected account
|
||||
function getSelectedAccountName() {
|
||||
return document.querySelector('#account-name').value;
|
||||
}
|
||||
|
||||
// Function to get an account by its name
|
||||
function getAccountByName(name) {
|
||||
const account_string = localStorage.getItem(name);
|
||||
if (!account_string) {
|
||||
return null;
|
||||
}
|
||||
const account_raw = JSON.parse(account_string);
|
||||
return new Account(
|
||||
account_raw.local_part,
|
||||
account_raw.separator,
|
||||
account_raw.domain,
|
||||
account_raw.key,
|
||||
);
|
||||
}
|
||||
|
||||
// Function to synchronize the account list
|
||||
function syncAccountList() {
|
||||
var acc_list = document.querySelector('#account-name');
|
||||
while (acc_list.lastElementChild) {
|
||||
acc_list.removeChild(acc_list.lastElementChild);
|
||||
}
|
||||
var account_names = [];
|
||||
for (var i = 0, len = localStorage.length; i < len; ++i) {
|
||||
account_names.push(localStorage.key(i));
|
||||
}
|
||||
account_names.sort();
|
||||
for (const name of account_names) {
|
||||
const account = getAccountByName(name);
|
||||
const new_elem = new Option(account.getName(), account.getName());
|
||||
acc_list.appendChild(new_elem);
|
||||
}
|
||||
if (localStorage.length === 0) {
|
||||
openNewAccountModal();
|
||||
}
|
||||
}
|
||||
syncAccountList();
|
||||
|
||||
// Add a click event on buttons to open a specific modal
|
||||
(document.querySelectorAll('.js-modal-trigger') || []).forEach((trigger) => {
|
||||
const modal = trigger.dataset.target;
|
||||
const target = document.getElementById(modal);
|
||||
|
||||
trigger.addEventListener('click', () => {
|
||||
openModal(target);
|
||||
});
|
||||
});
|
||||
|
||||
// Add a click event on various child elements to close the parent modal
|
||||
(document.querySelectorAll('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button-close') || []).forEach((close) => {
|
||||
const target = close.closest('.modal');
|
||||
|
||||
close.addEventListener('click', () => {
|
||||
closeModal(target);
|
||||
});
|
||||
});
|
||||
|
||||
// Add a keyboard event to close all modals
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.code === 'Escape') {
|
||||
closeAllModals();
|
||||
}
|
||||
});
|
||||
|
||||
// Add a click event on the delete account button to display the confirmation message
|
||||
document.querySelector('#btn-del-account-confirm').addEventListener('click', (event) => {
|
||||
const account_name = getSelectedAccountName();
|
||||
localStorage.removeItem(account_name);
|
||||
console.log(`Account ${account_name} deleted.`);
|
||||
syncAccountList();
|
||||
closeAllModals();
|
||||
});
|
||||
|
||||
// Add a click event on the new account button to register the new account
|
||||
document.querySelector('#btn-new-account').addEventListener('click', (event) => {
|
||||
console.log('Adding new account…');
|
||||
try {
|
||||
const new_account = new Account(
|
||||
document.querySelector('#new-addr-local-part').value,
|
||||
document.querySelector('#new-addr-separator').value,
|
||||
document.querySelector('#new-addr-domain').value,
|
||||
document.querySelector('#new-addr-key').value,
|
||||
);
|
||||
console.log(new_account);
|
||||
new_account.register();
|
||||
console.log(`Account ${new_account.getName()} added.`);
|
||||
['#new-addr-local-part', '#new-addr-domain', '#new-addr-key'].forEach((selector) => {
|
||||
document.querySelector(selector).value = '';
|
||||
});
|
||||
document.querySelector('#new-addr-separator').value = '+';
|
||||
syncAccountList();
|
||||
closeAllModals();
|
||||
} catch (e) {
|
||||
console.log(`${e.name}: ${e.field_id}: ${e.message}`);
|
||||
setErrorMessage(e.message);
|
||||
if (e.field_id) {
|
||||
document.getElementById(e.field_id).classList.add('is-danger');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add a click event on the new address button to generate it
|
||||
document.querySelector('#btn-generate').addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const account = getAccountByName(document.querySelector('#account-name').value);
|
||||
const new_address = account.genSubAddr(document.querySelector('#sub-addr-name').value);
|
||||
document.querySelector('#generated-addr').value = new_address;
|
||||
console.log(`New sub-address for account ${account.getName()}: ${new_address}`);
|
||||
});
|
||||
|
||||
// Add a change event on the main form to remove previously generated address
|
||||
['#account-name', '#sub-addr-name'].forEach((selector) => {
|
||||
document.querySelector(selector).addEventListener('change', () => {
|
||||
document.querySelector('#generated-addr').value = '';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/sw.js');
|
||||
}
|
||||
});
|
116
pwa/index.html
116
pwa/index.html
|
@ -1,116 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en" prefix="og: https://ogp.me/ns#">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sub-Address KEy</title>
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<link rel="stylesheet" href="vendor/bulma.min.css">
|
||||
<script src="vendor/base32.min.js" defer></script>
|
||||
<script src="vendor/sha256.min.js" defer></script>
|
||||
<script src="app.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-three-fifths">
|
||||
<h1 class="title is-1">New email address</h1>
|
||||
<form action="" method="get" id="form-generate-addr">
|
||||
<label class="label" for="account-name">Account</label>
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="account-name"></select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="control">
|
||||
<a class="button is-danger js-modal-trigger" data-target="modal-del-account" id="btn-del-account">Delete</a>
|
||||
</p>
|
||||
<p class="control">
|
||||
<a class="button is-primary js-modal-trigger" data-target="modal-add-account" id="btn-add-account">New</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="sub-addr-name">Name</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" id="sub-addr-name" placeholder="Text input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="generated-addr">Address</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" id="generated-addr" disabled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control has-text-centered">
|
||||
<button class="button is-primary" id="btn-generate">Generate</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div id="modal-add-account" class="modal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Add a new account</p>
|
||||
<button class="delete" aria-label="close"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<form action="" method="get" id="form-new-account">
|
||||
<div class="container" id="new-account-error-msg-container"></div>
|
||||
<div class="field">
|
||||
<label class="label" for="new-addr-local-part">Local part</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" id="new-addr-local-part" minlength="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="new-addr-separator">Separator</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" id="new-addr-separator" value="+" minlength="1" maxlength="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="new-addr-domain">Domain name</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" id="new-addr-domain" placeholder="example.org" minlength="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="new-addr-key">Private key</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" id="new-addr-key" minlength="1">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<footer class="modal-card-foot buttons is-centered">
|
||||
<button class="button is-success" id="btn-new-account">Add</button>
|
||||
<button class="button button-close">Cancel</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-del-account" class="modal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Delete account</p>
|
||||
<button class="delete" aria-label="close"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<p>You are about to delete the following account:</p>
|
||||
<p class="has-text-weight-semibold has-text-centered is-size-5" id="del-account-name"></p>
|
||||
<p>Are you sure?</p>
|
||||
</section>
|
||||
<footer class="modal-card-foot buttons is-centered">
|
||||
<button class="button is-danger" id="btn-del-account-confirm">Delete</button>
|
||||
<button class="button button-close">Cancel</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "Sub-Address KEy",
|
||||
"short_name": "sake",
|
||||
"id": "sub-address-key",
|
||||
"description": "Sub-address generator for the Sub-Address KEy (SAKE) filter.",
|
||||
"start_url": "/",
|
||||
"lang": "en",
|
||||
"display": "standalone"
|
||||
}
|
59
pwa/sw.js
59
pwa/sw.js
|
@ -1,59 +0,0 @@
|
|||
const sw_version = '0.1.0';
|
||||
const cache_name = `sake-v${sw_version}`;
|
||||
const cached_files = [
|
||||
'/',
|
||||
'/app.js',
|
||||
'/index.html',
|
||||
'/vendor/base32.min.js',
|
||||
'/vendor/base32.min.js.map',
|
||||
'/vendor/sha256.min.js',
|
||||
];
|
||||
|
||||
function log_message(msg) {
|
||||
console.log(`[Service Worker] v${sw_version}: ${msg}`);
|
||||
}
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
log_message('Installed');
|
||||
self.skipWaiting();
|
||||
event.waitUntil(caches.open(cache_name).then((cache) => {
|
||||
log_message('Caching all');
|
||||
return cache.addAll(cached_files);
|
||||
}));
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (e) => {
|
||||
e.waitUntil(
|
||||
caches.keys().then((keyList) =>
|
||||
Promise.all(
|
||||
keyList.map((key) => {
|
||||
if (key != cache_name) {
|
||||
log_message(`Cleaning cache: ${key}`);
|
||||
return caches.delete(key);
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
e.waitUntil(clients.claim());
|
||||
log_message('Active');
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
if (!(event.request.url.startsWith('https:') || event.request.url.startsWith('http:'))) {
|
||||
log_message(`Fetching resource failed: invalid protocol: ${event.request.url}`);
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(caches.open(cache_name).then((cache) => {
|
||||
log_message(`Fetching resource: ${event.request.url}`);
|
||||
return cache.match(event.request).then((cached_response) => {
|
||||
const fetched_response = fetch(event.request).then((network_response) => {
|
||||
log_message(`Caching resource: ${event.request.url}`);
|
||||
cache.put(event.request, network_response.clone());
|
||||
return network_response;
|
||||
});
|
||||
return cached_response || fetched_response;
|
||||
});
|
||||
}));
|
||||
});
|
10
pwa/vendor/base32.min.js
vendored
10
pwa/vendor/base32.min.js
vendored
|
@ -1,10 +0,0 @@
|
|||
/**
|
||||
* [base32.js]{@link https://github.com/speakeasyjs/base32.js}
|
||||
*
|
||||
* @version 0.1.0
|
||||
* @author Michael Phan-Ba <michael@mikepb.com>
|
||||
* @copyright Michael Phan-Ba
|
||||
* @license MIT
|
||||
*/
|
||||
this.base32=function(t){function a(h){if(r[h])return r[h].exports;var i=r[h]={exports:{},id:h,loaded:!1};return t[h].call(i.exports,i,i.exports,a),i.loaded=!0,i.exports}var r={};return a.m=t,a.c=r,a.p="",a(0)}([function(t,a){"use strict";function r(t){if(this.buf=[],this.shift=8,this.carry=0,t){switch(t.type){case"rfc4648":this.charmap=a.rfc4648.charmap;break;case"crockford":this.charmap=a.crockford.charmap;break;case"base32hex":this.charmap=a.base32hex.charmap;break;default:throw new Error("invalid type")}t.charmap&&(this.charmap=t.charmap)}}function h(t){if(this.buf="",this.shift=3,this.carry=0,t){switch(t.type){case"rfc4648":this.alphabet=a.rfc4648.alphabet;break;case"crockford":this.alphabet=a.crockford.alphabet;break;case"base32hex":this.alphabet=a.base32hex.alphabet;break;default:throw new Error("invalid type")}t.alphabet?this.alphabet=t.alphabet:t.lc&&(this.alphabet=this.alphabet.toLowerCase())}}var i=function(t,a){return a||(a={}),t.split("").forEach(function(t,r){t in a||(a[t]=r)}),a},e={alphabet:"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",charmap:{0:14,1:8}};e.charmap=i(e.alphabet,e.charmap);var s={alphabet:"0123456789ABCDEFGHJKMNPQRSTVWXYZ",charmap:{O:0,I:1,L:1}};s.charmap=i(s.alphabet,s.charmap);var c={alphabet:"0123456789ABCDEFGHIJKLMNOPQRSTUV",charmap:{}};c.charmap=i(c.alphabet,c.charmap),r.prototype.charmap=e.charmap,r.prototype.write=function(t){var a=this.charmap,r=this.buf,h=this.shift,i=this.carry;return t.toUpperCase().split("").forEach(function(t){if("="!=t){var e=255&a[t];h-=5,h>0?i|=e<<h:0>h?(r.push(i|e>>-h),h+=8,i=e<<h&255):(r.push(i|e),h=8,i=0)}}),this.shift=h,this.carry=i,this},r.prototype.finalize=function(t){return t&&this.write(t),8!==this.shift&&0!==this.carry&&(this.buf.push(this.carry),this.shift=8,this.carry=0),this.buf},h.prototype.alphabet=e.alphabet,h.prototype.write=function(t){var a,r,h,i=this.shift,e=this.carry;for(h=0;h<t.length;h++)r=t[h],a=e|r>>i,this.buf+=this.alphabet[31&a],i>5&&(i-=5,a=r>>i,this.buf+=this.alphabet[31&a]),i=5-i,e=r<<i,i=8-i;return this.shift=i,this.carry=e,this},h.prototype.finalize=function(t){return t&&this.write(t),3!==this.shift&&(this.buf+=this.alphabet[31&this.carry],this.shift=3,this.carry=0),this.buf},a.encode=function(t,a){return new h(a).finalize(t)},a.decode=function(t,a){return new r(a).finalize(t)},a.Decoder=r,a.Encoder=h,a.charmap=i,a.crockford=s,a.rfc4648=e,a.base32hex=c}]);
|
||||
//# sourceMappingURL=base32.min.js.map
|
1
pwa/vendor/base32.min.js.map
vendored
1
pwa/vendor/base32.min.js.map
vendored
File diff suppressed because one or more lines are too long
1
pwa/vendor/bulma.min.css
vendored
1
pwa/vendor/bulma.min.css
vendored
File diff suppressed because one or more lines are too long
9
pwa/vendor/sha256.min.js
vendored
9
pwa/vendor/sha256.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue