WIP: webapp: rotate the diigo cookie in place #1

Draft
dckc wants to merge 8 commits from dckc/webapp-cookie-rotation AGit into main
pull from: dckc/webapp-cookie-rotation
Showing only changes of commit 4dd3fe6bde - Show all commits
+36 -17
View File
@@ -174,11 +174,13 @@ async function proxyMint(request: Request, env: Env): Promise<Response> {
const detail = (await res.text()).slice(0, 200); const detail = (await res.text()).slice(0, 200);
return home(env, { error: `mint said ${res.status}: ${detail}` }, null); return home(env, { error: `mint said ${res.status}: ${detail}` }, null);
} }
// webapp answers {id} (+ x-webapp-id); the id rides the header, the // webapp answers {id, storeId} (+ x-webapp-id, x-webapp-store-id);
// wire page is ours // the ids ride the headers, the wire page is ours
const id = res.headers.get("x-webapp-id"); const id = res.headers.get("x-webapp-id");
if (!id) return home(env, { error: "mint produced no id" }, null); if (!id) return home(env, { error: "mint produced no id" }, null);
return await wirePage(id); const storeId = res.headers.get("x-webapp-store-id");
if (!storeId) console.warn({ event: "mint", outcome: "no store id: this session cannot be rotated" });
return await wirePage(id, storeId);
} catch (err) { } catch (err) {
console.error({ event: "mint", error: String(err) }); console.error({ event: "mint", error: String(err) });
return home(env, { error: `mint failed: ${String(err)}` }, null); return home(env, { error: `mint failed: ${String(err)}` }, null);
@@ -188,22 +190,37 @@ async function proxyMint(request: Request, env: Env): Promise<Response> {
/** the credential-save helper, once per page (see ./pm-save.ts) */ /** the credential-save helper, once per page (see ./pm-save.ts) */
const PM_SAVE_JS = pmSave.toString(); const PM_SAVE_JS = pmSave.toString();
/** the suggested petname gets the holder's local hh:mm (the worker only /** the suggested petname (in the input with id `petnameId`) gets the
* knows UTC), so two saves the same day are told apart at a glance */ * holder's local hh:mm (the worker only knows UTC), so two saves the same
const PETNAME_TIME_JS = `{ * day are told apart at a glance */
const p = document.getElementById("petname"); function petnameTimeJs(petnameId: string): string {
return `{
const p = document.getElementById(${JSON.stringify(petnameId)});
const d = new Date(); const d = new Date();
p.value += " " + String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0"); p.value += " " + String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
}`; }`;
/** the onclick that hands pmSave its browser powers */
function pmSaveClick(value: string): string {
return `pmSave(this, ${JSON.stringify(value)}, { petname: document.getElementById("petname"), credentials: navigator.credentials, PasswordCredential: window.PasswordCredential, log: console })`;
} }
/** the wire step, with the freshly-minted webapp id pre-filled */ /** the onclick that hands pmSave its browser powers, saving under the
async function wirePage(webappId: string): Promise<Response> { * petname in the input with id `petnameId` */
function pmSaveClick(value: string, petnameId: string): string {
return `pmSave(this, ${JSON.stringify(value)}, { petname: document.getElementById(${JSON.stringify(petnameId)}), credentials: navigator.credentials, PasswordCredential: window.PasswordCredential, log: console })`;
}
/** the wire step, with the freshly-minted webapp id pre-filled, and the
* store id (rotating the cookie) for the account owner to keep */
async function wirePage(webappId: string, storeId: string | null): Promise<Response> {
const fingerprint = await nick(webappId); const fingerprint = await nick(webappId);
const storeSection = storeId
? `<h2>keep: the cookie's store id</h2>
<p>This one is yours alone, not the cache's: with it you can rotate the
diigo session (log in again, paste the new cookie) or revoke it. Fingerprint
<code>${esc(await nick(storeId))}</code>
<button type="button" onclick='navigator.clipboard.writeText(${JSON.stringify(storeId)}).then(() => this.textContent = "copied ✓")'>copy</button></p>
<p><label>your petname for it <input id="store-petname" autocomplete="off" size="30" value="${esc(`diigo cookie store ${await nick(storeId)}`)}"></label></p>
<script>${petnameTimeJs("store-petname")}</script>
<p><button type="button" onclick='${pmSaveClick(storeId, "store-petname")}'>save to password manager</button></p>`
: `<p><b>note:</b> webapp sent no store id, so this session's cookie cannot be rotated; when it expires, mint again.</p>`;
const body = `<!doctype html> const body = `<!doctype html>
<html><head><meta charset="utf-8"><title>diigo cache: step 2 — wire the cache</title></head> <html><head><meta charset="utf-8"><title>diigo cache: step 2 — wire the cache</title></head>
<body> <body>
@@ -212,8 +229,10 @@ async function wirePage(webappId: string): Promise<Response> {
<button type="button" onclick='navigator.clipboard.writeText(${JSON.stringify(webappId)}).then(() => this.textContent = "copied ✓")'>copy</button></p> <button type="button" onclick='navigator.clipboard.writeText(${JSON.stringify(webappId)}).then(() => this.textContent = "copied ✓")'>copy</button></p>
<p><label>your petname for it <input id="petname" autocomplete="off" size="30" value="${esc(`diigo webapp ${fingerprint}`)}"></label> <p><label>your petname for it <input id="petname" autocomplete="off" size="30" value="${esc(`diigo webapp ${fingerprint}`)}"></label>
<small>(private to you: the password-manager username; never sent to the server)</small></p> <small>(private to you: the password-manager username; never sent to the server)</small></p>
<script>${PETNAME_TIME_JS}</script> <script>${petnameTimeJs("petname")}</script>
<p><button type="button" onclick='${pmSaveClick(webappId)}'>save to password manager</button></p> <p><button type="button" onclick='${pmSaveClick(webappId, "petname")}'>save to password manager</button></p>
${storeSection}
<h2>wire</h2>
<p>choose the cache mode, then <b>wire</b> — the next page shows the <p>choose the cache mode, then <b>wire</b> — the next page shows the
<b>cache id</b> (copy that too), after which you land back on home:</p> <b>cache id</b> (copy that too), after which you land back on home:</p>
<form method="post" action="/_wire"> <form method="post" action="/_wire">
@@ -320,8 +339,8 @@ async function wiredPage(cacheId: string, note: string, hasPublic: boolean): Pro
<button type="button" onclick='navigator.clipboard.writeText(${JSON.stringify(cacheId)}).then(() => this.textContent = "copied ✓")'>copy</button></p> <button type="button" onclick='navigator.clipboard.writeText(${JSON.stringify(cacheId)}).then(() => this.textContent = "copied ✓")'>copy</button></p>
<p><label>your petname for it <input id="petname" autocomplete="off" size="30" value="${esc(`diigo cache ${fingerprint}`)}"></label> <p><label>your petname for it <input id="petname" autocomplete="off" size="30" value="${esc(`diigo cache ${fingerprint}`)}"></label>
<small>(private to you: the password-manager username; never sent to the server)</small></p> <small>(private to you: the password-manager username; never sent to the server)</small></p>
<script>${PETNAME_TIME_JS}</script> <script>${petnameTimeJs("petname")}</script>
<p><button type="button" onclick='${pmSaveClick(cacheId)}'>save to password manager</button></p> <p><button type="button" onclick='${pmSaveClick(cacheId, "petname")}'>save to password manager</button></p>
<p>${note}</p> <p>${note}</p>
<p><a href="/cache/#${esc(cacheId)}">open this cache</a> — its <b>webkey</b> <p><a href="/cache/#${esc(cacheId)}">open this cache</a> — its <b>webkey</b>
(bookmark it, or save the id above): fill it, watch progress${hasPublic ? ", fill its public companion" : ""}.</p> (bookmark it, or save the id above): fill it, watch progress${hasPublic ? ", fill its public companion" : ""}.</p>