fedipod-server 0.12.1 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,11 @@
4
4
  // this returns the same, RSA only. FEP-8b32 Ed25519 proofs are optional — the
5
5
  // deliverer treats a null edPrivate as "no proof", and an unproved activity
6
6
  // still federates — so the browser MVP omits them.
7
+ //
8
+ // The private key is imported NON-extractable, and the copy this browser keeps
9
+ // in IndexedDB is that CryptoKey, not the PEM: it can sign and cannot be read
10
+ // out, so a script that reaches this origin's storage gets nothing it can carry
11
+ // away. The PEM record exists only in memory, between unwrap and import.
7
12
  import { kvGet, kvPut } from './idb-kv.mjs';
8
13
  import * as podState from '../../lib/pod/state.mjs';
9
14
  import { isKeyEnvelope, KeyPasswordNeeded } from './keystore.mjs';
@@ -13,10 +18,26 @@ const pemToDer = (pem) => Uint8Array.from(
13
18
 
14
19
  export async function importSigningKey(keysRecord) {
15
20
  const rsaPrivate = await crypto.subtle.importKey('pkcs8', pemToDer(keysRecord.rsa.privatePem),
16
- { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, true, ['sign']);
21
+ { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['sign']);
17
22
  return { rsaPrivate, rsaPublicPem: keysRecord.rsa.publicPem, edPrivate: null, edPublicMultibase: null };
18
23
  }
19
24
 
25
+ /** What the cache holds: the non-extractable key and the public half. */
26
+ const fromCache = (c) => ({ rsaPrivate: c.rsaPrivate, rsaPublicPem: c.rsaPublicPem, edPrivate: null, edPublicMultibase: null });
27
+ const isOpenedKey = (c) => c?.rsaPrivate?.type === 'private' && typeof c.rsaPublicPem === 'string';
28
+
29
+ /**
30
+ * Keep this browser's opened copy: import the record and store the CryptoKey.
31
+ * Best effort on the store — a browser that refuses IndexedDB (private mode)
32
+ * asks for the password on the way back in. Returns the imported keys.
33
+ */
34
+ export async function cacheOpenedKeys(actorUrl, keysRecord) {
35
+ const keys = await importSigningKey(keysRecord);
36
+ await kvPut(keyCacheKey(actorUrl), { rsaPrivate: keys.rsaPrivate, rsaPublicPem: keys.rsaPublicPem })
37
+ .catch(() => {});
38
+ return keys;
39
+ }
40
+
20
41
  // Where this browser keeps its own opened copy of the signing key. The pod's
21
42
  // copy is wrapped under the account password; this one is not, because it never
22
43
  // leaves the origin — the same protection the OIDC session in the next IDB row
@@ -34,13 +55,15 @@ export const keyCacheKey = (actorUrl) => `signing-keys:${actorUrl}`;
34
55
  // needs the password and so has to go back to the page.
35
56
  export async function loadKeysFromPod(remote, urls) {
36
57
  const cached = await kvGet(keyCacheKey(urls.actor)).catch(() => null);
37
- if (cached?.rsa) return importSigningKey(cached);
58
+ if (isOpenedKey(cached)) return fromCache(cached);
59
+ // A copy stored as PEM by an earlier build: import it and store the key form
60
+ // in its place, so the PEM is gone from storage after one boot.
61
+ if (cached?.rsa?.privatePem) return cacheOpenedKeys(urls.actor, cached);
38
62
 
39
63
  const doc = await podState.readWrappedKeys(remote, urls);
40
64
  if (isKeyEnvelope(doc)) throw new KeyPasswordNeeded();
41
65
  if (!doc || !doc.rsa) throw new Error('no signing key on the pod — sign up did not finish');
42
66
  // A pre-wrapping install. Cache it so the next boot is one read, and leave
43
67
  // the pod's copy alone: re-wrapping it would need the password we do not have.
44
- await kvPut(keyCacheKey(urls.actor), doc).catch(() => {});
45
- return importSigningKey(doc);
68
+ return cacheOpenedKeys(urls.actor, doc);
46
69
  }
@@ -14,8 +14,7 @@ import { createAccountWithPod, mintCredential, makeDpopSession, revokeCredential
14
14
  import { generateKeys, wrapKeys } from './keystore.mjs';
15
15
  import { BrowserRemotePod } from './pod-remote.mjs';
16
16
  import * as podState from '../../lib/pod/state.mjs';
17
- import { kvPut } from './idb-kv.mjs';
18
- import { keyCacheKey } from './keys-browser.mjs';
17
+ import { cacheOpenedKeys } from './keys-browser.mjs';
19
18
 
20
19
  // The container everything the agent publishes hangs under. New pods made here
21
20
  // use `fedipod/`; the agent's own default stays `activitypods-js/` for installs
@@ -167,7 +166,7 @@ export async function signUp(answers, { onStep = () => {}, frontOrigin = null }
167
166
  // This browser's own opened copy, so the boot after the login redirect
168
167
  // needs no password. Best effort: a browser that refuses IndexedDB (private
169
168
  // mode) simply asks for the password on the way back in.
170
- await kvPut(keyCacheKey(actorUrl), keys).catch(() => {});
169
+ await cacheOpenedKeys(actorUrl, keys);
171
170
  prog.keys = keys; prog.keysStored = true;
172
171
  keysStep.ok();
173
172
  } else {
@@ -33231,6 +33231,26 @@ async function kvPut(key, val) {
33231
33231
  }
33232
33232
 
33233
33233
  // web/app/keys-browser.mjs
33234
+ var pemToDer = (pem) => Uint8Array.from(
33235
+ atob(pem.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "")),
33236
+ (c) => c.charCodeAt(0)
33237
+ );
33238
+ async function importSigningKey(keysRecord) {
33239
+ const rsaPrivate = await crypto.subtle.importKey(
33240
+ "pkcs8",
33241
+ pemToDer(keysRecord.rsa.privatePem),
33242
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
33243
+ false,
33244
+ ["sign"]
33245
+ );
33246
+ return { rsaPrivate, rsaPublicPem: keysRecord.rsa.publicPem, edPrivate: null, edPublicMultibase: null };
33247
+ }
33248
+ async function cacheOpenedKeys(actorUrl, keysRecord) {
33249
+ const keys = await importSigningKey(keysRecord);
33250
+ await kvPut(keyCacheKey(actorUrl), { rsaPrivate: keys.rsaPrivate, rsaPublicPem: keys.rsaPublicPem }).catch(() => {
33251
+ });
33252
+ return keys;
33253
+ }
33234
33254
  var keyCacheKey = (actorUrl) => `signing-keys:${actorUrl}`;
33235
33255
 
33236
33256
  // web/app/signup.mjs
@@ -33321,8 +33341,7 @@ async function signUp(answers, { onStep = () => {
33321
33341
  } catch (e) {
33322
33342
  throw new Error(`could not store the signing key on the pod (${e.message}). The credential is for ${credential.webId} \u2014 that WebID must own ${pod} and its ${AP_ROOT} must be writable by it.`);
33323
33343
  }
33324
- await kvPut(keyCacheKey(actorUrl), keys).catch(() => {
33325
- });
33344
+ await cacheOpenedKeys(actorUrl, keys);
33326
33345
  prog.keys = keys;
33327
33346
  prog.keysStored = true;
33328
33347
  keysStep.ok();
@@ -33668,7 +33687,7 @@ window.fedipodUnlock = async (password) => {
33668
33687
  if (!isKeyEnvelope(doc)) throw new Error("this account's key is not locked \u2014 nothing to unlock");
33669
33688
  const rec = await unwrapKeys(doc, password);
33670
33689
  const actorUrl = `${cfg.remotePod}${cfg.root || AP_ROOT}ap/actor`;
33671
- await kvPut(keyCacheKey(actorUrl), rec);
33690
+ await cacheOpenedKeys(actorUrl, rec);
33672
33691
  await bootWorker();
33673
33692
  };
33674
33693
  window.fedipodSignup = async ({ onStep, ...answers }) => {