commit a7e9cfae214d1fe2fdb5d3ee217faadc64d97855
parent ade59b8da5483a7793178737103cb9b7fc83206c
Author: lash <dev@holbrook.no>
Date: Sun, 14 Jul 2024 00:29:25 +0100
WIP jsdoc
Diffstat:
M | README.md | | | 10 | +++++----- |
M | app.js | | | 227 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
M | key.js | | | 72 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- |
M | name.js | | | 1 | + |
M | top.js | | | 26 | ++++++++++++++++++++++++++ |
5 files changed, 326 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md
@@ -7,6 +7,11 @@ It is written in pure javascript using the [alpinejs](https://alpinejs.dev/) fra
It uses [PGP (openpgpjs)](https://openpgpjs.org/) for signatures and encryption.
+## Dependencies
+
+* [wala-rust](https://defalsify.org/git/wala-rust/) `v0.1.7` (see `Backend` below)
+
+
## Install
* `nvm install 18.8`
@@ -19,11 +24,6 @@ It uses [PGP (openpgpjs)](https://openpgpjs.org/) for signatures and encryption.
Simple serve the repository root directory with a web server, e.g. [webfsd](https://github.com/ourway/webfsd)
-## Dependencies
-
-* [wala-rust](https://defalsify.org/git/wala-rust/) `v0.1.7`
-
-
## User interface
The application consists of only two pages.
diff --git a/app.js b/app.js
@@ -1,23 +1,38 @@
-// Thanks to:
-// https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex
-function buf2hex(buffer) { // buffer is an ArrayBuffer
+/**
+ * Buffer to hex encoder
+ *
+ * Thanks to: https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex
+ *
+ * @param {object} ArrayBuffer with binary data
+ * @returns {string} hex
+ */
+function buf2hex(buffer) {
return [...new Uint8Array(buffer)]
.map(x => x.toString(16).padStart(2, '0'))
.join('');
}
+/** generate the mutable prefix for the message based on its sequence number **/
function msg_identifier() {
return 'msg' + g_counter;
}
+/** generate the mutable prefix for the public key **/
function pubkey_identifier() {
return PUBKEY_PFX + g_remote_key.getFingerprint();
}
+/** generate the mutable counter prefix for the message sequence number **/
function counter_identifier() {
return 'msgidx';
}
+/**
+ * output a humane representation of the given state
+ *
+ * @param {number} state
+ * @returns {string} description strings for each individual state bit set
+ */
function debugState(state) {
let s = '';
for (let i = 0; i < STATE_KEYS.length; i++) {
@@ -33,6 +48,12 @@ function debugState(state) {
return s;
};
+/**
+ * check if state bit needle is set in the given state bit haystack
+ * @param {number} needle
+ * @param {number} haystack
+ * @returns {boolean}
+ */
function checkState(bit_check, bit_field) {
if (bit_field != 0 && !bit_field) {
bit_field = g_state;
@@ -40,6 +61,9 @@ function checkState(bit_check, bit_field) {
return (bit_check & bit_field) > 0;
};
+/**
+ * Retrieve settings from remote and parse them
+ */
async function loadSettings() {
let rs = await fetch(window.location.href + '/settings.json', {
method: 'GET',
@@ -51,6 +75,14 @@ async function loadSettings() {
return await rs.json();
}
+/**
+ * Get identifiable personal name from the GPG key packetlist
+ *
+ * Alters application state
+ *
+ * @param {object} pgp key object
+ * @returns {string} name
+ */
function getEffectiveName(k) {
let kl = k.toPacketList();
let klf = kl.filterByTag(openpgp.enums.packet.userID);
@@ -61,6 +93,14 @@ function getEffectiveName(k) {
return klf[klf.length-1].name;
}
+/**
+ * Unlock private key in storage with given passphrase
+ *
+ * Alters application state
+ *
+ * @param {string} passphrase
+ * @returns {boolean} true if successfully decrypted.
+ */
async function unlockLocalKey(pwd) {
let state = [];
try {
@@ -79,6 +119,15 @@ async function unlockLocalKey(pwd) {
return decrypted;
}
+/**
+ * Process current messaging state of local key.
+ *
+ * Currently this is limited to looking up local storage record of message counter
+ *
+ * Alters application state
+ *
+ * @todo should retrieve state from remote if it is missing in local storage
+ */
async function applyLocalKey() {
g_local_key_id = g_local_key.getKeyID().toHex();
g_local_key_name = getEffectiveName(g_local_key);
@@ -93,6 +142,9 @@ async function applyLocalKey() {
stateChange('ready to send', STATE['RTS']);
}
+/**
+ * Application entry point
+ */
async function setUp() {
let settings = await loadSettings();
if (settings.dev) {
@@ -128,6 +180,20 @@ async function setUp() {
});
}
+/**
+ * Modify provided state.
+ *
+ * After this method complets, the global application state will be adjusted by
+ * bits specified to be added or removed.
+ *
+ * An event will be emitted for the state change.
+ *
+ * @param {number} s Arbitrary object relevant to the state change
+ * @param {number} set_states State bits to set
+ * @param {number} set_states State bits to reset
+ * @fires window.messagestatechange
+ * @todo make sure this method cannot fail
+ */
async function stateChange(s, set_states, rst_states) {
if (!set_states) {
set_states = [];
@@ -169,6 +235,18 @@ async function stateChange(s, set_states, rst_states) {
window.dispatchEvent(ev);
}
+/**
+ * Wrapper to set state accordingly if content submission fails.
+ *
+ * Will change application state.
+ *
+ * @param {string} s Text message from contact form
+ * @param {string} name Name of sender (claimed)
+ * @param {string} email Email of sender (claimed)
+ * @param {Object[]} files File attachments
+ * @returns {string} Remote identifier if succeeds, or "failed" if not.
+ * @see dispatch
+ */
async function tryDispatch(s, name, email, files) {
stateChange('starting dispatch', undefined, [STATE['RTS'], STATE['SEND_ERROR']]);
console.debug('files', Object.keys(files));
@@ -184,10 +262,25 @@ async function tryDispatch(s, name, email, files) {
return r;
}
+/**
+ * Getter for current passphrase in memory
+ *
+ * @todo passphrase should be deleted from memory when not used
+ *
+ **/
function getPassphrase() {
return g_passphrase;
}
+/**
+ * Attempt to replace the current user id packet in the PGP key structure, and handle error if unsuccessful.
+ *
+ * Will change application state.
+ *
+ * @param {string} name Name replacement
+ * @param {string} email Email replacement
+ *
+ */
async function tryIdentify(name, email) {
if (g_local_key_identified) {
return false;
@@ -199,6 +292,32 @@ async function tryIdentify(name, email) {
g_local_key_identified = true;
}
+/**
+ * Send content to remote
+ *
+ * This is a high level function which orchestrates the following actions:
+ *
+ * 1. Apply any available identifiable information to the local public key before submission
+ * 2. Create MIME multipart message from the submission
+ * 3. Sign the message with our local private key
+ * 4. Submit the message using our local public key and counter state as mutable pointer to remote
+ * 5. Submit the counter state to remote
+ * 6. Update local state
+ *
+ * Will make following changes to remote:
+ *
+ * 1. Submit message using public key as mutable identifier
+ * 2. Submit counter
+ *
+ * Will change application state
+ *
+ * @param {string} s Text message from contact form
+ * @param {string} name Name of sender (claimed)
+ * @param {string} email Email of sender (claimed)
+ * @param {Object[]} files File attachments
+ * @returns {string} remote identifier for the content
+ *
+ */
async function dispatch(s, name, email, files) {
if (name) {
if (!validateEmail(email)) {
@@ -270,6 +389,12 @@ async function dispatch(s, name, email, files) {
return rcpt;
}
+/**
+ * Sign provided content with local private key and return the detached signature.
+ *
+ * @param {string} Payload to sign
+ * @returns {Object} openpgpjs Signed message data
+ */
async function signMessage(payload) {
const msg = await openpgp.createMessage({
text: payload,
@@ -286,6 +411,14 @@ async function signMessage(payload) {
return msg_sig;
}
+/**
+ * Encrypt the counter data with the local public key to be stored
+ * at the remote endpoint.
+ *
+ * @param {number} c Current counter value
+ * @param {string} pfx Prefix to use for the remote pointer
+ * @returns {Object} msg: Encrypted counter message, to be submitted to endpoint; auth: Remote identifier to resolve mutable data pointer; rcpt: Signature material to use for HTTP Authorization PUBSIG
+ */
async function encryptCounter(c, pfx) {
const msg_count = await openpgp.createMessage({
text: '' + g_counter,
@@ -312,6 +445,15 @@ async function encryptCounter(c, pfx) {
}
+/**
+ * Encrypt the public key data with the local public key to be stored
+ * at the remote endpoint.
+ *
+ * @param {number} k Public key data
+ * @param {string} pfx Prefix to use for the remote pointer
+ * @returns {Object} msg: Encrypted pubkey message, to be submitted to endpoint; auth: Remote identifier to resolve mutable data pointer; rcpt: Signature material to use for HTTP Authorization PUBSIG
+ * @todo k param is not being used, global local key is the key being modified
+ */
async function encryptPublicKey(k, pfx) {
const pubkey_bin = g_local_key.toPublic().write();
const msg_pubkey = await openpgp.createMessage({
@@ -338,6 +480,16 @@ async function encryptPublicKey(k, pfx) {
};
}
+/**
+ * Low-level send to endpoint function
+ *
+ * @param {Object} o Encrypted content object
+ * @param {string} pfx Prefix to store data under
+ * @param {boolean} trace Generate trace information, if available
+ * @throws Generic error if submission failed
+ * @returns {string} Remote identifier
+ *
+ */
async function dispatchToEndpoint(o, pfx, trace) {
let headers = {
'Content-Type': 'application/octet-stream',
@@ -365,6 +517,15 @@ async function dispatchToEndpoint(o, pfx, trace) {
return rcpt_remote;
}
+/**
+ * Encrypt the message data with the local public key to be stored
+ * at the remote endpoint.
+ *
+ * @param {number} k Public key data
+ * @param {string} pfx Prefix to use for the remote pointer
+ * @returns {Object} msg: Encrypted message, to be submitted to endpoint; auth: Remote identifier to resolve mutable data pointer; rcpt: Signature material to use for HTTP Authorization PUBSIG
+ * @todo k param is not being used, global local key is the key being modified
+ */
async function encryptMessage(msg, pfx) {
const enckey_local = await g_local_key.getEncryptionKey();
const enckey_remote = await g_remote_key.getEncryptionKey();
@@ -393,6 +554,13 @@ async function encryptMessage(msg, pfx) {
};
}
+/**
+ * Create a new local private key
+ *
+ * Will change application state
+ *
+ * @param {string} pwd Passphrase to encrypt key with
+ */
async function createLocalKey(pwd) {
stateChange('generate new local signing key', STATE["LOCAL_KEY_GENERATE"]);
const uid = {
@@ -403,6 +571,17 @@ async function createLocalKey(pwd) {
stateChange('new local signing key named ' + uid.name, STATE["LOCAL_KEY"], STATE["LOCAL_KEY_GENERATE"]);
}
+/**
+ * Unlock an existing private key with passphrase
+ *
+ * Will change application state
+ *
+ * @param {string} pwd Passphrase to decrypt key with
+ * @returns {boolean} true if unlocked
+ *
+ * @todo should be more intuitiviely named method
+ *
+ */
async function setPwd(pwd) {
stateChange('attempt password set', undefined, STATE['PASSPHRASE_FAIL']);
if (!pwd) {
@@ -430,6 +609,11 @@ async function setPwd(pwd) {
return r;
}
+/**
+ * Remove local key and related state from local storage
+ *
+ * Will change application state
+ */
function purgeLocalKey() {
key_id = g_local_key_id;
localStorage.removeItem('pgp-key');
@@ -452,7 +636,15 @@ function purgeLocalKey() {
return true;
}
-async function fileChange(e) {
+
+/**
+ * Handle file attachment request
+ *
+ * Will change application state.
+ *
+ * @todo currently no way exists to tell whether this has failed
+ */
+async function fileChange() {
let fileButton = document.getElementById("fileAdder")
let file = fileButton.files[0];
stateChange('processing file: ' + file.name, STATE.FILE_PROCESS);
@@ -471,6 +663,13 @@ async function fileChange(e) {
}
}
+/**
+ * If interactive help is enabled, update the help text area with the current relevant
+ * contextual help strings
+ *
+ * @param {string[]} k Zero or more help identifiers to display, in sequence
+ * @fires window.help
+ */
async function tryHelpFor(...k) {
//if (!checkState(STATE.HELP)) {
// return;
@@ -488,6 +687,21 @@ async function tryHelpFor(...k) {
window.dispatchEvent(ev);
}
+/**
+ * Create the message to be submitted from form input.
+ *
+ * The message is a MIME multipart message containing the following parts:
+ *
+ * * The composite message of the message text field and all attachments
+ * * The ASCII-armored public key used for the PGP signature
+ *
+ * This message will in turn be signed by the private key matching the public key
+ * that was embedded
+ *
+ * @param {string} message Message string
+ * @param {string} files Individual files to include
+ * @returns {string} MIME Multipart message text containing all parts of the message
+ */
async function buildMessage(message, files, pubkey) {
let msg = {
fromName: g_from_name,
@@ -519,6 +733,11 @@ async function buildMessage(message, files, pubkey) {
}
+/**
+ * Execute global state change and make sure each is logged
+ *
+ * @todo turn off log for release version
+ **/
window.addEventListener('messagestatechange', (v) => {
state_change = (~v.detail.old_state) & v.detail.state;
let s = v.detail.s;
diff --git a/key.js b/key.js
@@ -1,3 +1,17 @@
+/**
+ * Create a new PGP key from parameters
+ *
+ * On successful creation, the private key will be stored to local storage.
+ *
+ * The private key stored in local storage will be always encrypted with the
+ * literal passphrase. The passphrase may be an empty string.
+ *
+ * @param {string} pwd Passphrase
+ * @param {Object} uid User id object {name, email}
+ * @returns {Promise<Object>} Returns a private key object on
+ * @todo enable some level of pwd integrity check
+ * @todo handle failure
+ */
async function generatePGPKey(pwd, uid) {
if (uid === undefined) {
uid = {
@@ -26,6 +40,16 @@ async function generatePGPKey(pwd, uid) {
});
}
+/**
+ * Retrieve the private key of the locally stored private key.
+ *
+ * @param {string} pwd Passphrase to decrypt the private key with.
+ * @param {boolean} encrypted If false, return the verbatim key data stored in
+ * local storage
+ * @returns {any} openpgpjs private key object if encrypted. Private key literal data if not.
+ * @todo Make return type same type
+ * @todo handle failure
+ */
async function getKey(pwd, encrypted) {
return new Promise(async (whohoo, doh) => {
let pk_armor = localStorage.getItem('pgp-key');
@@ -56,10 +80,27 @@ async function getKey(pwd, encrypted) {
});
}
+/**
+ * Retrieve the current ciphertext value of the private key in local store.
+ * @returns {string} Private key ASCII-armored PGP data
+ */
function getEncryptedKey() {
return localStorage.getItem('pgp-key');
}
+/**
+ * Create a HTTP Authorization PUBSIG string from the private key and provided message
+ * to use for remote submission.
+ *
+ * @param {Object} pk Private key
+ * @param {Object} msg Authenticated message object, containing msg, rcpt auth
+ * @returns {string} PUBSIG string
+ * @see encryptMessage
+ * @see encryptPublicKey
+ * @see encryptCounter
+ * @todo define an interface for the message object
+ *
+ */
async function generateAuth(pk, msg) {
let sig = await openpgp.sign({
signingKeys: g_local_key,
@@ -77,6 +118,13 @@ async function generateAuth(pk, msg) {
return "pgp:" + pub_b + ":" + sig_b;
}
+/**
+ * Create a mutable pointer for remote storage
+ *
+ * @param {Object} openpgpjs private key object
+ * @param {string} remote mutable storage prefix
+ * @returns {string} Mutable pointer in hex
+ **/
async function generatePointer(pk, pfx) {
let sha = new jsSHA("SHA-256", "TEXT");
sha.update(pfx);
@@ -89,7 +137,15 @@ async function generatePointer(pk, pfx) {
return sha.getHash("HEX");
}
-// robbed from https://www.w3resource.com/javascript/form/email-validation.php
+/**
+ * Validate email value
+ *
+ * robbed from https://www.w3resource.com/javascript/form/email-validation.php
+ *
+ * @param {string} mail Email to validate
+ * @returns {boolean} true if valid email string
+ *
+ **/
function validateEmail(mail) {
if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(mail)) {
return true;
@@ -97,6 +153,20 @@ function validateEmail(mail) {
return false;
}
+/**
+ * Add local store association between local private key and
+ * identifiable information.
+ *
+ * (This is a bit of a hack, as openpgpjs (or pgp in general) does not seem
+ * designed for changing userid after the fact)
+ *
+ * @param {Object} pk openpgpjs Private key object
+ * @param {string} name Name of key owner (claimed)
+ * @param {string} email Email of key owner (claimed)
+ * @param {string} pwd Private key passphrase
+ * @returns {Promise<Object>} Updated private key object
+ * @todo handle failure
+ */
async function identify(pk, name, email, pwd) {
return new Promise(async (whohoo, doh) => {
const u = openpgp.UserIDPacket.fromObject({
diff --git a/name.js b/name.js
@@ -19,6 +19,7 @@ let name_parts = [
"brains", "wit", "juice", "shower"],
];
+/** create a new random name from the parts in name_parts **/
function generateName() {
name = '';
for (let i = 0; i < name_parts.length; i++) {
diff --git a/top.js b/top.js
@@ -1,4 +1,30 @@
+/** prefix for publickey record in remote mutable storage **/
const PUBKEY_PFX = 'pgp.publickey';
+/**
+ * Bitflag state which tracks all possible states across the application lifetime
+ *
+ * @prop {number} DEV Application is running in developer mode
+ * @prop {number} PANIC Application has panicked i.e. terminated abnormally
+ * @prop {number} RTS Application is ready to submit content to the backend
+ * @prop {number} SEND_ERROR Last attempt at sending content to the backend failed
+ * @prop {number} SETTINGS Settings have been successfully loaded
+ * @prop {number} REMOTE_KEY Remote encryption key has been successfully loaded
+ * @prop {number} LOCAL_KEY Local private key exists in store
+ * @prop {number} LOCAL_KEY_DECRYPTED Local private key has been decrypted and currently resides in memory
+ * @prop {number} LOCAL_KEY_IDENTIFIED User has provided some identifiable information for the private key (there is no guarantee this is real or not, of course)
+ * @prop {number} LOCAL_KEY_GENERATE A new local private key has been generated this session
+ * @prop {number} PASSPHRASE_ACTIVE User has provided a passphrase to unlock the local key
+ * @prop {number} PASSPHRASE_FAIL Last provided passphrase by user failed to unlock the local key
+ * @prop {number} ACK_MESSAGE Data endpoint has confirmed receipt of message
+ * @prop {number} ENC_MESSAGE Message was successfully encrypted locally (and is ready to be sent to remote)
+ * @prop {number} ACK_PUBKEY Data endpoint has confirmed receipt of public key data for local key
+ * @prop {number} ENC_PUBKEY Local key publickey was successfully encrypted locally (and is ready to be sent to remote)
+ * @prop {number} ACK_COUNTER Data endpoint has confirmed receipt of updated message counter
+ * @prop {number} ENC_COUNTER Message counter was successfully encrypted locally (and is ready to be sent to remote)
+ * @prop {number} HELP Application is providing contextual help
+ * @prop {number} FILE_PROCESS A request to attach a file to the message has been initiated.
+ * @prop {number} FILE_ADDED A request to attach a file has been successfully processed. Submission will now contain the file content as part of the message.
+ **/
const STATE = {
DEV: 1 << 0,
PANIC: 1 << 1,