Remove the PWA

This commit is contained in:
Rodolphe Bréard 2023-07-30 19:50:16 +02:00
parent 8b60bde56d
commit e6b20d7ef5
9 changed files with 1 additions and 469 deletions

View file

@ -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?

View file

@ -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');
}
});

View file

@ -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>

View file

@ -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"
}

View file

@ -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;
});
}));
});

View file

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long