From bc2a5e5905006fcd41af71cae474d6e56afb803f Mon Sep 17 00:00:00 2001 From: Nathan Vander Wilt Date: Thu, 26 Jan 2023 18:25:35 -0800 Subject: [PATCH 01/11] feat: add nonce phrase helper this is work-in-progress and leverages a (temporary) new route for ease of testing --- packages/access-api/src/index.js | 11 + .../access-api/src/utils/phrase-words.json | 1026 +++++++++++++++++ packages/access-api/src/utils/phrase.js | 47 + packages/access-api/tsconfig.json | 9 +- 4 files changed, 1092 insertions(+), 1 deletion(-) create mode 100644 packages/access-api/src/utils/phrase-words.json create mode 100644 packages/access-api/src/utils/phrase.js diff --git a/packages/access-api/src/index.js b/packages/access-api/src/index.js index a69aca203..cb26181bd 100644 --- a/packages/access-api/src/index.js +++ b/packages/access-api/src/index.js @@ -8,12 +8,14 @@ import { validateEmail } from './routes/validate-email.js' import { validateWS } from './routes/validate-ws.js' import { version } from './routes/version.js' import { getContext } from './utils/context.js' +import { generateNoncePhrase } from './utils/phrase.js' /** @type Router */ const r = new Router({ onNotFound: notFound }) r.add('options', '*', preflight) r.add('get', '/version', version) +r.add('get', '/phrase-test', phraseTest) r.add('get', '/validate-email', validateEmail) r.add('get', '/validate-ws', validateWS) r.add('post', '/', postRoot) @@ -67,3 +69,12 @@ async function reproduceCloudflareError(request) { } return new Response(JSON.stringify(response, undefined, 2), { status: 200 }) } + +/** + * @param {import('@web3-storage/worker-utils/router').ParsedRequest} request + * @param {import('./bindings.js').RouteContext} env + */ +async function phraseTest(request, env) { + const entropy = Number(request.query.bits) || 42 + return new Response(generateNoncePhrase(entropy), { status: 200 }) +} diff --git a/packages/access-api/src/utils/phrase-words.json b/packages/access-api/src/utils/phrase-words.json new file mode 100644 index 000000000..7d94838c1 --- /dev/null +++ b/packages/access-api/src/utils/phrase-words.json @@ -0,0 +1,1026 @@ +[ + "reflected", + "knowings", + "inkiest", + "gated", + "facilitates", + "vandals", + "firmly", + "priority", + "vicar", + "reconciliations", + "facades", + "earns", + "decimating", + "shy", + "glows", + "symptomatic", + "rumbles", + "tough", + "bricked", + "object", + "chip", + "privates", + "adjoin", + "punned", + "sizing", + "accidents", + "sculpture", + "bigotry", + "commuted", + "proposals", + "pail", + "feeler", + "performed", + "footballs", + "sorority", + "revving", + "larked", + "tickling", + "continuations", + "repress", + "surlier", + "wildernesses", + "darting", + "overlaying", + "complexions", + "invoking", + "sinew", + "ampler", + "buys", + "survival", + "helpless", + "decoration", + "halos", + "inducement", + "snippets", + "walling", + "belches", + "programmes", + "incoherence", + "incumbent", + "aquatics", + "embarrassments", + "whine", + "liberals", + "displacement", + "bucketed", + "floor", + "rifled", + "handy", + "multiplicity", + "pants", + "bicycled", + "motorway", + "evacuates", + "minus", + "insect", + "backlogs", + "penknives", + "originator", + "photosynthesis", + "deformities", + "albeit", + "skewered", + "enthralls", + "departmental", + "subsidy", + "pinnacle", + "raves", + "down", + "culminating", + "overborne", + "authenticated", + "fluently", + "distend", + "horrifies", + "entitling", + "recursive", + "flutist", + "counteracting", + "licking", + "misdemeanors", + "allegiance", + "foretelling", + "burdening", + "maneuvering", + "amassed", + "moussed", + "felt", + "ugly", + "waist", + "implement", + "lab", + "palates", + "ambiance", + "always", + "registrations", + "endearing", + "siphon", + "viler", + "proclaiming", + "waterworks", + "gingham", + "guessed", + "confirmation", + "accord", + "humanities", + "migraines", + "inflammable", + "corroboration", + "applicant", + "revealing", + "bard", + "posted", + "omen", + "quota", + "decanter", + "plankton", + "gooier", + "executes", + "wagging", + "forest", + "passionate", + "charcoal", + "pile", + "aired", + "circa", + "tempts", + "fashion", + "devilled", + "deficiencies", + "cropping", + "fig", + "attorneys", + "lack", + "pretenses", + "chopped", + "promise", + "recounting", + "terminates", + "fabulous", + "interrupting", + "visions", + "memoirs", + "hallucinations", + "trebles", + "noblewoman", + "professor", + "journalism", + "summoned", + "enrolls", + "geographic", + "paused", + "epilogues", + "disks", + "rasping", + "penetration", + "transgresses", + "aphorisms", + "piranha", + "pigment", + "lacerate", + "unconditionally", + "microfilmed", + "turntables", + "impish", + "slurs", + "vegetarianism", + "blanked", + "strata", + "exposed", + "traditional", + "phosphorus", + "menthol", + "forward", + "tethered", + "ensembles", + "sufficed", + "compacted", + "plush", + "infernos", + "grouches", + "solution", + "porting", + "inky", + "executors", + "jigsawed", + "steps", + "trawls", + "wilting", + "oddest", + "amazement", + "illegibly", + "jaywalked", + "mediums", + "tasks", + "pretensions", + "confiscates", + "predominance", + "watching", + "carving", + "immobilizes", + "fulfills", + "cutlets", + "eligible", + "traitorous", + "funnel", + "seduce", + "crow", + "refreshment", + "circles", + "emirs", + "concluding", + "clink", + "denims", + "recoils", + "stall", + "hibernate", + "ponderous", + "goading", + "outcry", + "pursue", + "hesitating", + "matures", + "shoplifter", + "glamourous", + "commons", + "notched", + "debut", + "plagued", + "fiddling", + "charlatans", + "scolding", + "assimilated", + "crown", + "parkway", + "staunchly", + "pilgrim", + "razes", + "garment", + "sobers", + "weeklies", + "caverns", + "spaded", + "consuming", + "variance", + "nicety", + "distract", + "assaults", + "enjoyment", + "engraves", + "endorse", + "embalms", + "seared", + "loudest", + "drys", + "tailspins", + "mushrooms", + "imagines", + "bolting", + "unwieldiest", + "dearer", + "sailboat", + "fruitless", + "toucan", + "welcomed", + "preface", + "redeems", + "participles", + "basil", + "postscripts", + "elastics", + "regulations", + "separates", + "calves", + "advertiser", + "overrides", + "taxpayer", + "shin", + "strive", + "resumption", + "volumes", + "reposing", + "coarsely", + "leapfrogging", + "masterminding", + "rereading", + "warpaths", + "unravels", + "chisels", + "carat", + "doles", + "imitate", + "scars", + "retail", + "relapses", + "flashback", + "adapts", + "monstrosities", + "clipboard", + "screech", + "reconstructs", + "milled", + "taker", + "entitled", + "kiln", + "overgrowing", + "mauled", + "mahoganies", + "baboons", + "bossed", + "shoaling", + "malaria", + "pelican", + "brow", + "homestead", + "wits", + "bookmark", + "varnished", + "issue", + "haze", + "translated", + "juggler", + "suffocate", + "thriftiest", + "buckled", + "corruptible", + "secured", + "mystery", + "bums", + "brainiest", + "unsolved", + "overhaul", + "headquarters", + "distorter", + "airmailed", + "alibied", + "cataract", + "hoist", + "motherhood", + "goats", + "abbreviation", + "astonishing", + "retainer", + "underneaths", + "fortifies", + "seasonable", + "likeliest", + "thatcher", + "national", + "glue", + "prays", + "whiff", + "quailing", + "fragmentary", + "destroyed", + "centrally", + "stone", + "thrilled", + "defamed", + "fraternizing", + "aliasing", + "adulterates", + "slashing", + "inhabiting", + "strutted", + "interestingly", + "localized", + "sketching", + "backbones", + "differentiated", + "resignation", + "chid", + "industry", + "superbest", + "moves", + "weekended", + "festivity", + "archbishops", + "vigilance", + "excitement", + "tinning", + "inaction", + "masculine", + "lilted", + "blithe", + "miraculously", + "abetting", + "affirms", + "respecting", + "connoisseurs", + "holidays", + "bemuses", + "tricycles", + "battleships", + "tenser", + "temporal", + "another", + "established", + "intersection", + "mariner", + "dogged", + "fascinates", + "scheduled", + "strides", + "studied", + "redundant", + "parlor", + "oversee", + "overburdens", + "teeter", + "synthetics", + "comprehension", + "stockier", + "berry", + "stoves", + "covenant", + "fronts", + "bashes", + "syringe", + "poster", + "clips", + "how", + "exulting", + "microwave", + "honeymoons", + "quarterbacking", + "today", + "terrain", + "engrave", + "constitutional", + "discuss", + "uneasier", + "disruptive", + "photographed", + "hoofing", + "breadths", + "taped", + "install", + "benefits", + "altars", + "prolonged", + "bottoming", + "nationalize", + "filter", + "bleed", + "musically", + "captain", + "roles", + "sister", + "neurology", + "ethos", + "stability", + "speeds", + "distilled", + "bedlams", + "impatiences", + "bother", + "cornflakes", + "sirups", + "hypothesize", + "professions", + "underground", + "forgot", + "titled", + "researched", + "cartoonist", + "shirks", + "naughtiest", + "chidden", + "scary", + "musts", + "unblocking", + "disposition", + "stowaways", + "blemish", + "ripples", + "crackled", + "proponents", + "disqualify", + "fretful", + "unicorns", + "slewed", + "genuses", + "unskilled", + "eclipsed", + "sprinters", + "revengeful", + "provably", + "quailed", + "bulled", + "prophetic", + "timer", + "yanking", + "epic", + "inflammations", + "decomposes", + "renewal", + "unanimously", + "autumns", + "amended", + "guests", + "hared", + "firmware", + "itchiest", + "comedians", + "huskily", + "integrals", + "builder", + "golfed", + "craziness", + "aphorism", + "topical", + "bestows", + "transparency", + "whisks", + "combated", + "maltreats", + "behead", + "reforms", + "leftmost", + "deepened", + "admires", + "attachés", + "clowning", + "martyr", + "fair", + "disgruntling", + "embargoed", + "tenures", + "ventilate", + "broods", + "mortgaging", + "hurrayed", + "indorsements", + "retired", + "protracted", + "welder", + "lingering", + "stepping", + "sapped", + "chairmen", + "dooms", + "rhinoceroses", + "trapezoids", + "candled", + "brinier", + "swarming", + "pawnbroker", + "colanders", + "electronic", + "oscillated", + "lazier", + "biting", + "runny", + "scanties", + "vacationed", + "upside", + "italicizing", + "impunity", + "airstrips", + "redundancy", + "rectified", + "violets", + "kiting", + "condemnation", + "bayoneted", + "revolve", + "condenses", + "pledged", + "transitive", + "collating", + "bargaining", + "comprehensible", + "steaks", + "immoral", + "yawning", + "deer", + "result", + "female", + "cartons", + "tugging", + "memorably", + "slotted", + "overlie", + "subset", + "righting", + "sundaes", + "infant", + "manor", + "Tuesday", + "balance", + "massacring", + "oftener", + "exists", + "pallid", + "buns", + "drafted", + "pension", + "absconded", + "torpedoes", + "shampoos", + "dire", + "reinforced", + "tablespoonsful", + "triumphing", + "menus", + "sillier", + "blessing", + "dormant", + "shrapnel", + "rodent", + "trillion", + "shrimp", + "sixtieths", + "notions", + "vended", + "representative", + "prevented", + "accede", + "resign", + "cutback", + "hugely", + "chalice", + "socialized", + "tanning", + "tee", + "tablespoonful", + "heavier", + "racially", + "gazetted", + "description", + "reals", + "fluffs", + "kisses", + "daubs", + "diapers", + "kayaking", + "consisting", + "encore", + "indefensible", + "touches", + "exhilarated", + "bloodshed", + "devotees", + "displease", + "stinging", + "expanded", + "trailed", + "satisfaction", + "eked", + "imprinting", + "mystified", + "mountaineering", + "blindingly", + "artists", + "swoon", + "totalled", + "accent", + "dogging", + "duration", + "cleavers", + "lusted", + "digress", + "educating", + "walled", + "hanging", + "gnawn", + "ordination", + "flicks", + "stylistic", + "ditties", + "financial", + "infertile", + "pavilions", + "millionth", + "doubting", + "heirlooms", + "vision", + "cobs", + "patches", + "proletariat", + "potency", + "majesty", + "evoked", + "limitless", + "breakthrough", + "abruptly", + "beheading", + "homeland", + "recovers", + "foolishly", + "ascends", + "rainier", + "poems", + "possesses", + "recycled", + "foresting", + "cuticle", + "repeats", + "choppered", + "holiness", + "remnants", + "humiliations", + "division", + "assort", + "ceremonies", + "browned", + "debilitates", + "abstraction", + "felted", + "jackknifes", + "kickoffs", + "examines", + "squids", + "beloveds", + "military", + "uniquer", + "withdrawal", + "lately", + "recovery", + "muggy", + "paragraphs", + "tiptoed", + "encounter", + "effects", + "southerly", + "acceptably", + "ploughed", + "brim", + "caricatured", + "grub", + "yanked", + "discern", + "algorithm", + "tarried", + "connecter", + "facilitated", + "repressive", + "history", + "deserters", + "fractures", + "decreeing", + "informally", + "terrors", + "fillet", + "convulsive", + "wisp", + "hat", + "layout", + "jingle", + "idiocy", + "congealed", + "relativity", + "fossilize", + "swift", + "serums", + "lineage", + "shamming", + "engrossing", + "nausea", + "recite", + "destitution", + "narcotics", + "authorship", + "miserable", + "tender", + "ancientest", + "wean", + "drivel", + "ravine", + "matriculation", + "reticent", + "border", + "highly", + "unfriendliest", + "luxurious", + "are", + "gashed", + "trouser", + "retreating", + "deadening", + "overstepped", + "thuds", + "interrupted", + "corporals", + "oscillates", + "aggregating", + "oversees", + "mobilizes", + "shoos", + "enthusiasms", + "vertebrate", + "marksman", + "grill", + "dullest", + "conspicuous", + "quilt", + "stenographers", + "clarify", + "worker", + "tenanted", + "eluded", + "misprinting", + "bibs", + "protests", + "depicting", + "muting", + "raisins", + "anchovies", + "residence", + "shoestrings", + "artificially", + "skinning", + "mysteriously", + "gullets", + "advisory", + "ellipsis", + "directed", + "skied", + "chum", + "shirted", + "compatibility", + "alternating", + "cricket", + "guises", + "authorize", + "sheathed", + "chapel", + "motivating", + "flouncing", + "runniest", + "polkas", + "lament", + "starting", + "sizeable", + "shoeing", + "recuperates", + "dished", + "utensils", + "perplexities", + "slithering", + "grimes", + "leaps", + "leans", + "mistook", + "lineages", + "unheard", + "chemicals", + "udder", + "payroll", + "attesting", + "rosaries", + "wiggle", + "resembled", + "rebuffing", + "signature", + "predicated", + "reversals", + "hamsters", + "semblance", + "inferences", + "disagreed", + "sloshed", + "dullness", + "contested", + "shovelled", + "dedicating", + "extolls", + "liturgy", + "superstructure", + "entirely", + "spiked", + "befriended", + "embark", + "hankers", + "eczema", + "festoons", + "optimizes", + "racketed", + "conventionally", + "subsides", + "strictly", + "blaspheme", + "unmarked", + "lugging", + "integrated", + "textured", + "succored", + "warns", + "convoys", + "ousters", + "technological", + "equalize", + "jelled", + "lieu", + "ingests", + "synchronizing", + "intelligence", + "digitize", + "meddlers", + "bicycling", + "verier", + "quilting", + "stormed", + "nettle", + "weest", + "sarcasms", + "fiancé", + "clumsier", + "wrest", + "taxied", + "greenbacks", + "traders", + "brawniest", + "tenderized", + "travesties", + "postulated", + "reunions", + "cached", + "raspberry", + "commended", + "rhythmic", + "meteoric", + "commence", + "withdrawn", + "acclimatized", + "bastards", + "nickname", + "our", + "saves", + "slag", + "casualties", + "psychologically", + "transient", + "composure", + "sears", + "tinselling", + "discharging", + "wand", + "foreigners", + "bake", + "gulch", + "clamped", + "restarted", + "elopement", + "spontaneous", + "vaccinates", + "observers", + "highbrow", + "stores", + "blindest", + "turgid", + "periled", + "travelings", + "browns", + "populations", + "entailed", + "advises", + "unlike", + "angled", + "teammates", + "bequest", + "disburses", + "ordinance", + "wider", + "figures", + "decking", + "overwhelmingly", + "satellited", + "piranhas", + "engraving", + "tingles", + "hiatuses", + "presumed", + "toadstool", + "commanders", + "braiding", + "zinced", + "enshrining", + "serviced", + "pampers", + "concentrated", + "collared", + "spinal", + "unilateral", + "covenanting", + "ekes", + "ridden", + "embroidering", + "enlistments", + "tiptoes", + "facade", + "embryonic", + "fault", + "filming", + "dithering", + "Sunday", + "myths", + "chapters", + "lunched", + "workers", + "thoughtless", + "revolt", + "accost" +] diff --git a/packages/access-api/src/utils/phrase.js b/packages/access-api/src/utils/phrase.js new file mode 100644 index 000000000..ba0925761 --- /dev/null +++ b/packages/access-api/src/utils/phrase.js @@ -0,0 +1,47 @@ +import words from './phrase-words.json' + +/* above can be gathered by hand or e.g. +sudo apt install wamerican-small jq + +numWords=1024 +# HT: https://stackoverflow.com/a/15065490/179583 +shuf -n $numWords <(grep -v \' /usr/share/dict/words) | \ +# HT: https://stackoverflow.com/a/34576956/179583 +jq --raw-input | jq --slurp > src/utils/phrase-words.json +*/ + +// TODO: I can't get this to work, but it'd be better to use this! +// import { randomInt } from 'node:crypto' + +/** + * + * @param {number} max + * @returns {number} + */ +function randomInt(max) { + // NOTE: does not support all calling patterns of the real one! + const min = 0 + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_integer_between_two_values + return Math.floor(Math.random() * (max - min) + min) +} + +const entropyPerWord = Math.log2(words.length) + +function randomWord() { + const randomIdx = randomInt(words.length) + return words[randomIdx] +} + +/** + * + * @param {number} entropy + * @returns {string} + */ +export function generateNoncePhrase(entropy) { + const phrase = [] + let nWordsNeeded = Math.ceil(entropy / entropyPerWord) + while (nWordsNeeded--) { + phrase.push(randomWord()) + } + return phrase.join(' ') +} diff --git a/packages/access-api/tsconfig.json b/packages/access-api/tsconfig.json index da475de58..bfbe8a6d4 100644 --- a/packages/access-api/tsconfig.json +++ b/packages/access-api/tsconfig.json @@ -6,7 +6,14 @@ "jsx": "react-jsx", "jsxImportSource": "preact" }, - "include": ["src", "scripts", "test", "package.json", "sql"], + "include": [ + "src", + "scripts", + "test", + "sql", + "package.json", + "src/utils/phrase-words.json" + ], "exclude": ["**/node_modules/**"], "references": [{ "path": "../access-client" }, { "path": "../capabilities" }] } From 7f346a56c42832e723700061af90f049484c953a Mon Sep 17 00:00:00 2001 From: Nathan Vander Wilt Date: Thu, 26 Jan 2023 18:37:24 -0800 Subject: [PATCH 02/11] feat: hook up a DEFAULT_ENTROPY for ease of use --- packages/access-api/src/utils/phrase.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/access-api/src/utils/phrase.js b/packages/access-api/src/utils/phrase.js index ba0925761..d0d980b9c 100644 --- a/packages/access-api/src/utils/phrase.js +++ b/packages/access-api/src/utils/phrase.js @@ -25,6 +25,7 @@ function randomInt(max) { return Math.floor(Math.random() * (max - min) + min) } +const DEFAULT_ENTROPY = 50 const entropyPerWord = Math.log2(words.length) function randomWord() { @@ -34,10 +35,10 @@ function randomWord() { /** * - * @param {number} entropy + * @param {number} [entropy] * @returns {string} */ -export function generateNoncePhrase(entropy) { +export function generateNoncePhrase(entropy = DEFAULT_ENTROPY) { const phrase = [] let nWordsNeeded = Math.ceil(entropy / entropyPerWord) while (nWordsNeeded--) { From 1edbebec1fe2a1a87cb89c79eb204a9eb33654e9 Mon Sep 17 00:00:00 2001 From: Nathan Vander Wilt Date: Thu, 26 Jan 2023 18:39:55 -0800 Subject: [PATCH 03/11] feat: pass match_phrase nonce to sendValidation --- packages/access-api/src/service/index.js | 4 ++++ packages/access-api/src/service/voucher-claim.js | 4 ++++ packages/access-api/src/utils/email.js | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/access-api/src/service/index.js b/packages/access-api/src/service/index.js index 06ea54814..129e3bcbb 100644 --- a/packages/access-api/src/service/index.js +++ b/packages/access-api/src/service/index.js @@ -10,6 +10,7 @@ import { import { voucherClaimProvider } from './voucher-claim.js' import { voucherRedeemProvider } from './voucher-redeem.js' import * as uploadApi from './upload-api-proxy.js' +import { generateNoncePhrase } from '../utils/phrase.js' /** * @param {import('../bindings').RouteContext} ctx @@ -127,6 +128,7 @@ export function service(ctx) { const encoded = delegationToString(inv) const url = `${ctx.url.protocol}//${ctx.url.host}/validate-email?ucan=${encoded}&mode=recover` + const nonce = generateNoncePhrase() // For testing if (ctx.config.ENV === 'test') { @@ -136,7 +138,9 @@ export function service(ctx) { await ctx.email.sendValidation({ to: capability.nb.identity.replace('mailto:', ''), url, + nonce, }) + return nonce } ), }, diff --git a/packages/access-api/src/service/voucher-claim.js b/packages/access-api/src/service/voucher-claim.js index 28e499aef..ee0ef32f9 100644 --- a/packages/access-api/src/service/voucher-claim.js +++ b/packages/access-api/src/service/voucher-claim.js @@ -1,6 +1,7 @@ import * as Server from '@ucanto/server' import * as Voucher from '@web3-storage/capabilities/voucher' import { delegationToString } from '@web3-storage/access/encoding' +import { generateNoncePhrase } from '../utils/phrase.js' /** * @param {import('../bindings').RouteContext} ctx @@ -41,10 +42,13 @@ export function voucherClaimProvider(ctx) { } const url = `${ctx.url.protocol}//${ctx.url.host}/validate-email?ucan=${encoded}` + const nonce = generateNoncePhrase() await ctx.email.sendValidation({ to: capability.nb.identity.replace('mailto:', ''), url, + nonce, }) + return nonce }) } diff --git a/packages/access-api/src/utils/email.js b/packages/access-api/src/utils/email.js index 44ed1af20..0eb1ea6e8 100644 --- a/packages/access-api/src/utils/email.js +++ b/packages/access-api/src/utils/email.js @@ -15,7 +15,7 @@ export class Email { /** * Send validation email with ucan to register * - * @param {{ to: string; url: string }} opts + * @param {{ to: string; url: string, nonce: string }} opts */ async sendValidation(opts) { const rsp = await fetch('https://api.postmarkapp.com/email/withTemplate', { @@ -30,6 +30,7 @@ export class Email { product_name: 'Web3 Storage', email: opts.to, action_url: opts.url, + match_phrase: opts.nonce, }, }), }) From bd46e946188afd42be9956c0d97b202af6c5d451 Mon Sep 17 00:00:00 2001 From: Nathan Vander Wilt Date: Fri, 27 Jan 2023 13:17:37 -0800 Subject: [PATCH 04/11] wrap validation response in object rather than raw string --- packages/access-api/src/service/index.js | 2 +- packages/access-api/src/service/voucher-claim.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/access-api/src/service/index.js b/packages/access-api/src/service/index.js index 129e3bcbb..47e8a953f 100644 --- a/packages/access-api/src/service/index.js +++ b/packages/access-api/src/service/index.js @@ -140,7 +140,7 @@ export function service(ctx) { url, nonce, }) - return nonce + return { match_phrase: nonce } } ), }, diff --git a/packages/access-api/src/service/voucher-claim.js b/packages/access-api/src/service/voucher-claim.js index ee0ef32f9..50ad52143 100644 --- a/packages/access-api/src/service/voucher-claim.js +++ b/packages/access-api/src/service/voucher-claim.js @@ -49,6 +49,6 @@ export function voucherClaimProvider(ctx) { url, nonce, }) - return nonce + return { match_phrase: nonce } }) } From 89a3f224cf21e45cd65a56e371538cf617fb12e6 Mon Sep 17 00:00:00 2001 From: Nathan Vander Wilt Date: Fri, 27 Jan 2023 14:11:14 -0800 Subject: [PATCH 05/11] feat: add handlePhrase callback to validation methods --- packages/access-client/src/agent.js | 10 ++++++++++ packages/access-client/src/types.ts | 10 ++++++++-- packages/capabilities/src/types.ts | 5 +++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/access-client/src/agent.js b/packages/access-client/src/agent.js index e6e939ca9..33852c1ca 100644 --- a/packages/access-client/src/agent.js +++ b/packages/access-client/src/agent.js @@ -306,6 +306,7 @@ export class Agent { * @param {string} email * @param {object} [opts] * @param {AbortSignal} [opts.signal] + * @param {import('./types').ValidationPhraseHandler} [opts.handlePhrase] */ async recover(email, opts) { const inv = await this.invokeAndExecute(Space.recoverValidation, { @@ -317,6 +318,10 @@ export class Agent { throw new Error('Recover validation failed', { cause: inv }) } + if (inv && 'match_phrase' in inv) { + opts?.handlePhrase?.(inv.match_phrase) + } + const spaceRecover = /** @type {Ucanto.Delegation<[import('./types').SpaceRecover]>} */ ( await this.#waitForDelegation(opts) @@ -412,6 +417,7 @@ export class Agent { * @param {string} email * @param {object} [opts] * @param {AbortSignal} [opts.signal] + * @param {import('./types').ValidationPhraseHandler} [opts.handlePhrase] */ async registerSpace(email, opts) { const space = this.currentSpace() @@ -438,6 +444,10 @@ export class Agent { throw new Error('Voucher claim failed', { cause: inv }) } + if (inv && 'match_phrase' in inv) { + opts?.handlePhrase?.(inv.match_phrase) + } + const voucherRedeem = /** @type {Ucanto.Delegation<[import('./types').VoucherRedeem]>} */ ( await this.#waitForDelegation(opts) diff --git a/packages/access-client/src/types.ts b/packages/access-client/src/types.ts index 3b39b360b..3ffcbb42c 100644 --- a/packages/access-client/src/types.ts +++ b/packages/access-client/src/types.ts @@ -25,9 +25,11 @@ import type { import type { Abilities, + NoncePhrase, SpaceInfo, SpaceRecover, SpaceRecoverValidation, + ShouldShowValidationNonce, VoucherClaim, VoucherRedeem, Top, @@ -67,7 +69,9 @@ export interface Service { voucher: { claim: ServiceMethod< VoucherClaim, - EncodedDelegation<[VoucherRedeem]> | undefined, + | ShouldShowValidationNonce + | EncodedDelegation<[VoucherRedeem]> + | undefined, Failure > redeem: ServiceMethod @@ -76,7 +80,7 @@ export interface Service { info: ServiceMethod, Failure> 'recover-validation': ServiceMethod< SpaceRecoverValidation, - EncodedDelegation<[SpaceRecover]> | undefined, + ShouldShowValidationNonce | EncodedDelegation<[SpaceRecover]> | undefined, Failure > recover: ServiceMethod< @@ -209,6 +213,8 @@ export type DelegationOptions = SetRequired & { audienceMeta: AgentMeta } +export type ValidationPhraseHandler = (please_show: NoncePhrase) => undefined + /** * Utility types */ diff --git a/packages/capabilities/src/types.ts b/packages/capabilities/src/types.ts index 933ee2246..d9a2a157a 100644 --- a/packages/capabilities/src/types.ts +++ b/packages/capabilities/src/types.ts @@ -38,6 +38,11 @@ export type StoreList = InferInvokedCapability // Top export type Top = InferInvokedCapability +export type NoncePhrase = string +export interface ShouldShowValidationNonce { + match_phrase: NoncePhrase +} + export type Abilities = TupleToUnion export type AbilitiesArray = [ From 9ff108d722f457cc6bff3ee0c24465529000681d Mon Sep 17 00:00:00 2001 From: Nathan Vander Wilt Date: Fri, 27 Jan 2023 14:38:30 -0800 Subject: [PATCH 06/11] feat: incorporate {{match_phrase}} into email templates the plaintext version of this is just dumped in roughly, due to #356 while waiting for merge of #403 --- packages/access-api/postmark/welcome.html | 1 + packages/access-api/postmark/welcome.txt | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/access-api/postmark/welcome.html b/packages/access-api/postmark/welcome.html index 0c41fe1ef..21c0ec17b 100644 --- a/packages/access-api/postmark/welcome.html +++ b/packages/access-api/postmark/welcome.html @@ -1,5 +1,6 @@

Hi {{email}}! To complete your {{product_name}} registration, we just need to verify your email address.

+

Please confirm the app you are trying to register is showing this phrase: {{match_phrase}}

diff --git a/packages/access-api/postmark/welcome.txt b/packages/access-api/postmark/welcome.txt index d86ad79d6..5309bc8ca 100644 --- a/packages/access-api/postmark/welcome.txt +++ b/packages/access-api/postmark/welcome.txt @@ -2,6 +2,9 @@ Welcome, {{name}}! ****************** +Your app should be showing: + +{{match_phrase}} ## Hugo From d915a74b71b5f2a8d04307eaf730999dd7807e06 Mon Sep 17 00:00:00 2001 From: Nathan Vander Wilt Date: Fri, 27 Jan 2023 15:09:12 -0800 Subject: [PATCH 07/11] cleanup: remove /phrase-test route --- packages/access-api/src/index.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/access-api/src/index.js b/packages/access-api/src/index.js index cb26181bd..a69aca203 100644 --- a/packages/access-api/src/index.js +++ b/packages/access-api/src/index.js @@ -8,14 +8,12 @@ import { validateEmail } from './routes/validate-email.js' import { validateWS } from './routes/validate-ws.js' import { version } from './routes/version.js' import { getContext } from './utils/context.js' -import { generateNoncePhrase } from './utils/phrase.js' /** @type Router */ const r = new Router({ onNotFound: notFound }) r.add('options', '*', preflight) r.add('get', '/version', version) -r.add('get', '/phrase-test', phraseTest) r.add('get', '/validate-email', validateEmail) r.add('get', '/validate-ws', validateWS) r.add('post', '/', postRoot) @@ -69,12 +67,3 @@ async function reproduceCloudflareError(request) { } return new Response(JSON.stringify(response, undefined, 2), { status: 200 }) } - -/** - * @param {import('@web3-storage/worker-utils/router').ParsedRequest} request - * @param {import('./bindings.js').RouteContext} env - */ -async function phraseTest(request, env) { - const entropy = Number(request.query.bits) || 42 - return new Response(generateNoncePhrase(entropy), { status: 200 }) -} From a43c59684285a65161f02fc2738f080429052e70 Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Thu, 9 Feb 2023 17:32:51 +0800 Subject: [PATCH 08/11] fix: sync text email copy with html email --- packages/access-api/postmark/welcome.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/access-api/postmark/welcome.txt b/packages/access-api/postmark/welcome.txt index 8972235f5..de519a5f5 100644 --- a/packages/access-api/postmark/welcome.txt +++ b/packages/access-api/postmark/welcome.txt @@ -1,7 +1,7 @@ Hi {{email}}! To complete your {{product_name}} registration, we just need to verify your email address. -Please verify that the application you used to register this space is showing the phrase: +Please confirm the app you are trying to register is showing this phrase: {{match_phrase}} From 1e8dc25f04d595954b6651a5ed0093cf97d789a5 Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Wed, 22 Feb 2023 16:46:40 +0800 Subject: [PATCH 09/11] fix: make email phrase verification instructions a bit clearer --- packages/access-api/postmark/welcome.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/access-api/postmark/welcome.html b/packages/access-api/postmark/welcome.html index 21c0ec17b..fd8bfd155 100644 --- a/packages/access-api/postmark/welcome.html +++ b/packages/access-api/postmark/welcome.html @@ -1,6 +1,6 @@

Hi {{email}}! To complete your {{product_name}} registration, we just need to verify your email address.

-

Please confirm the app you are trying to register is showing this phrase: {{match_phrase}}

+

Before clicking the button below, please confirm the app you are trying to register is showing this phrase: {{match_phrase}}

From 5f954a8e15a9068ce0ba6510e85d1a5d2e845367 Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Wed, 22 Feb 2023 16:54:10 +0800 Subject: [PATCH 10/11] fix: quick pass to censor some words I removed anything that seemed related to violence and a handful that seemed like they could lead to some awkward combinations --- .../access-api/src/utils/phrase-words.json | 40 +------------------ 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/packages/access-api/src/utils/phrase-words.json b/packages/access-api/src/utils/phrase-words.json index 7d94838c1..fd8fd08c6 100644 --- a/packages/access-api/src/utils/phrase-words.json +++ b/packages/access-api/src/utils/phrase-words.json @@ -4,20 +4,17 @@ "inkiest", "gated", "facilitates", - "vandals", "firmly", "priority", "vicar", "reconciliations", "facades", "earns", - "decimating", "shy", "glows", "symptomatic", "rumbles", "tough", - "bricked", "object", "chip", "privates", @@ -26,7 +23,6 @@ "sizing", "accidents", "sculpture", - "bigotry", "commuted", "proposals", "pail", @@ -49,20 +45,16 @@ "ampler", "buys", "survival", - "helpless", "decoration", "halos", "inducement", "snippets", "walling", - "belches", "programmes", "incoherence", "incumbent", "aquatics", "embarrassments", - "whine", - "liberals", "displacement", "bucketed", "floor", @@ -79,7 +71,6 @@ "penknives", "originator", "photosynthesis", - "deformities", "albeit", "skewered", "enthralls", @@ -107,7 +98,6 @@ "amassed", "moussed", "felt", - "ugly", "waist", "implement", "lab", @@ -162,7 +152,6 @@ "interrupting", "visions", "memoirs", - "hallucinations", "trebles", "noblewoman", "professor", @@ -179,12 +168,10 @@ "aphorisms", "piranha", "pigment", - "lacerate", "unconditionally", "microfilmed", "turntables", "impish", - "slurs", "vegetarianism", "blanked", "strata", @@ -199,7 +186,6 @@ "compacted", "plush", "infernos", - "grouches", "solution", "porting", "inky", @@ -223,7 +209,6 @@ "fulfills", "cutlets", "eligible", - "traitorous", "funnel", "seduce", "crow", @@ -249,8 +234,6 @@ "debut", "plagued", "fiddling", - "charlatans", - "scolding", "assimilated", "crown", "parkway", @@ -344,7 +327,6 @@ "suffocate", "thriftiest", "buckled", - "corruptible", "secured", "mystery", "bums", @@ -543,9 +525,7 @@ "admires", "attachés", "clowning", - "martyr", "fair", - "disgruntling", "embargoed", "tenures", "ventilate", @@ -560,7 +540,6 @@ "stepping", "sapped", "chairmen", - "dooms", "rhinoceroses", "trapezoids", "candled", @@ -647,7 +626,6 @@ "tee", "tablespoonful", "heavier", - "racially", "gazetted", "description", "reals", @@ -706,7 +684,6 @@ "limitless", "breakthrough", "abruptly", - "beheading", "homeland", "recovers", "foolishly", @@ -756,20 +733,16 @@ "tarried", "connecter", "facilitated", - "repressive", "history", - "deserters", "fractures", "decreeing", "informally", "terrors", "fillet", - "convulsive", "wisp", "hat", "layout", "jingle", - "idiocy", "congealed", "relativity", "fossilize", @@ -778,16 +751,12 @@ "lineage", "shamming", "engrossing", - "nausea", "recite", - "destitution", - "narcotics", "authorship", "miserable", "tender", "ancientest", "wean", - "drivel", "ravine", "matriculation", "reticent", @@ -811,7 +780,6 @@ "shoos", "enthusiasms", "vertebrate", - "marksman", "grill", "dullest", "conspicuous", @@ -945,7 +913,6 @@ "commence", "withdrawn", "acclimatized", - "bastards", "nickname", "our", "saves", @@ -970,7 +937,6 @@ "highbrow", "stores", "blindest", - "turgid", "periled", "travelings", "browns", @@ -1014,13 +980,9 @@ "embryonic", "fault", "filming", - "dithering", "Sunday", "myths", "chapters", "lunched", - "workers", - "thoughtless", - "revolt", - "accost" + "workers" ] From 283b0010947743e4b7ef595f1c541686b6169e81 Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Wed, 1 Mar 2023 15:53:19 +0800 Subject: [PATCH 11/11] chore: updates from PR --- packages/access-api/src/service/index.js | 2 +- packages/access-api/src/service/voucher-claim.js | 2 +- packages/access-api/src/utils/phrase.js | 8 ++++---- packages/access-client/src/agent.js | 12 ++++++------ packages/access-client/src/types.ts | 9 +++------ packages/capabilities/src/types.ts | 4 ++-- 6 files changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/access-api/src/service/index.js b/packages/access-api/src/service/index.js index 427e850f3..0173d9265 100644 --- a/packages/access-api/src/service/index.js +++ b/packages/access-api/src/service/index.js @@ -149,7 +149,7 @@ export function service(ctx) { url, nonce, }) - return { match_phrase: nonce } + return { matchPhrase: nonce } } ), }, diff --git a/packages/access-api/src/service/voucher-claim.js b/packages/access-api/src/service/voucher-claim.js index 50ad52143..9d6caa744 100644 --- a/packages/access-api/src/service/voucher-claim.js +++ b/packages/access-api/src/service/voucher-claim.js @@ -49,6 +49,6 @@ export function voucherClaimProvider(ctx) { url, nonce, }) - return { match_phrase: nonce } + return { matchPhrase: nonce } }) } diff --git a/packages/access-api/src/utils/phrase.js b/packages/access-api/src/utils/phrase.js index d0d980b9c..bd42ca9ab 100644 --- a/packages/access-api/src/utils/phrase.js +++ b/packages/access-api/src/utils/phrase.js @@ -19,10 +19,10 @@ jq --raw-input | jq --slurp > src/utils/phrase-words.json * @returns {number} */ function randomInt(max) { - // NOTE: does not support all calling patterns of the real one! - const min = 0 - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_integer_between_two_values - return Math.floor(Math.random() * (max - min) + min) + const array = new Uint32Array(1) + const random = self.crypto.getRandomValues(array)[0] + // 0xFFFFFFFF is the max value of a Uint32 + return Math.floor((random / 0xff_ff_ff_ff) * max) } const DEFAULT_ENTROPY = 50 diff --git a/packages/access-client/src/agent.js b/packages/access-client/src/agent.js index 4956d1090..f4dd9daf9 100644 --- a/packages/access-client/src/agent.js +++ b/packages/access-client/src/agent.js @@ -311,7 +311,7 @@ export class Agent { * @param {string} email * @param {object} [opts] * @param {AbortSignal} [opts.signal] - * @param {import('./types').ValidationPhraseHandler} [opts.handlePhrase] + * @param {import('./types').ValidationPhraseHandler} [opts.onPhrase] */ async recover(email, opts) { const inv = await this.invokeAndExecute(Space.recoverValidation, { @@ -323,8 +323,8 @@ export class Agent { throw new Error('Recover validation failed', { cause: inv }) } - if (inv && 'match_phrase' in inv) { - opts?.handlePhrase?.(inv.match_phrase) + if (inv?.matchPhrase) { + opts?.onPhrase?.(inv.matchPhrase) } const spaceRecover = @@ -422,7 +422,7 @@ export class Agent { * @param {string} email * @param {object} [opts] * @param {AbortSignal} [opts.signal] - * @param {import('./types').ValidationPhraseHandler} [opts.handlePhrase] + * @param {import('./types').ValidationPhraseHandler} [opts.onPhrase] */ async registerSpace(email, opts) { const space = this.currentSpace() @@ -449,8 +449,8 @@ export class Agent { throw new Error('Voucher claim failed', { cause: inv }) } - if (inv && 'match_phrase' in inv) { - opts?.handlePhrase?.(inv.match_phrase) + if (inv?.matchPhrase) { + opts?.onPhrase?.(inv.matchPhrase) } const voucherRedeem = diff --git a/packages/access-client/src/types.ts b/packages/access-client/src/types.ts index 8db8a9d03..4b451fbf9 100644 --- a/packages/access-client/src/types.ts +++ b/packages/access-client/src/types.ts @@ -29,7 +29,6 @@ import type { SpaceInfo, SpaceRecover, SpaceRecoverValidation, - ShouldShowValidationNonce, VoucherClaim, VoucherRedeem, Top, @@ -96,9 +95,7 @@ export interface Service { voucher: { claim: ServiceMethod< VoucherClaim, - | ShouldShowValidationNonce - | EncodedDelegation<[VoucherRedeem]> - | undefined, + EncodedDelegation<[VoucherRedeem]> | undefined, Failure > redeem: ServiceMethod @@ -107,7 +104,7 @@ export interface Service { info: ServiceMethod 'recover-validation': ServiceMethod< SpaceRecoverValidation, - ShouldShowValidationNonce | EncodedDelegation<[SpaceRecover]> | undefined, + EncodedDelegation<[SpaceRecover]> | undefined, Failure > recover: ServiceMethod< @@ -240,7 +237,7 @@ export type DelegationOptions = SetRequired & { audienceMeta: AgentMeta } -export type ValidationPhraseHandler = (please_show: NoncePhrase) => undefined +export type ValidationPhraseHandler = (phrase: NoncePhrase) => undefined /** * Utility types diff --git a/packages/capabilities/src/types.ts b/packages/capabilities/src/types.ts index d9a2a157a..d993fd274 100644 --- a/packages/capabilities/src/types.ts +++ b/packages/capabilities/src/types.ts @@ -39,8 +39,8 @@ export type StoreList = InferInvokedCapability export type Top = InferInvokedCapability export type NoncePhrase = string -export interface ShouldShowValidationNonce { - match_phrase: NoncePhrase +export interface HasValidationNonce { + phrase: NoncePhrase } export type Abilities = TupleToUnion