owlmem 0.4.0 → 0.4.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.
- package/owlmem.mjs +39 -2
- package/package.json +1 -1
package/owlmem.mjs
CHANGED
|
@@ -27,8 +27,8 @@ import { homedir } from "node:os";
|
|
|
27
27
|
import { join, dirname } from "node:path";
|
|
28
28
|
import { spawn } from "node:child_process";
|
|
29
29
|
|
|
30
|
-
export const VERSION = "0.4.
|
|
31
|
-
const BASE = (process.env.OWLMEM_URL || "https://
|
|
30
|
+
export const VERSION = "0.4.1";
|
|
31
|
+
const BASE = (process.env.OWLMEM_URL || "https://owlmem.com").replace(/\/$/, "");
|
|
32
32
|
const DIR = process.env.OWLMEM_HOME || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "owlmem");
|
|
33
33
|
const enc = new TextEncoder(), dec = new TextDecoder();
|
|
34
34
|
const say = (...a) => process.stderr.write(a.join(" ") + "\n");
|
|
@@ -111,6 +111,43 @@ export async function sealForMachine(pubkey, rawKey, space) {
|
|
|
111
111
|
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData: enc.encode(space) }, await unlockAesKey(eph.privateKey, pub, space, "encrypt"), rawKey);
|
|
112
112
|
return { epk: b64(new Uint8Array(await crypto.subtle.exportKey("raw", eph.publicKey))), nonce: b64(iv), ct: b64(new Uint8Array(ct)) };
|
|
113
113
|
}
|
|
114
|
+
// ── passkey unlock (browser only: navigator.credentials) ──
|
|
115
|
+
// A passkey's PRF extension returns 32 bytes only that passkey can compute (the server never sees them). They're stretched (HKDF) into a
|
|
116
|
+
// wrapping key that seals the vault key; OwlMem stores only the sealed box (vault_key_wraps). The page that calls these runs on owlmem.com.
|
|
117
|
+
const b64url = (u8) => b64(u8).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
118
|
+
const unb64url = (s) => { s = String(s).replace(/-/g, "+").replace(/_/g, "/"); while (s.length % 4) s += "="; return unb64(s); };
|
|
119
|
+
async function prfWrapKey(prf) {
|
|
120
|
+
const k = await crypto.subtle.importKey("raw", prf, "HKDF", false, ["deriveKey"]);
|
|
121
|
+
return crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(32), info: enc.encode("owlmem-vault-wrap-v1") }, k, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
122
|
+
}
|
|
123
|
+
const wrapAad = (space, user, credId) => enc.encode(space + "\n" + user + "\n" + credId);
|
|
124
|
+
// challenge(可选):服务器发的先验挑战。给了的话这一次刷 Passkey 同时就是「验明正身」——返回的 credential 交给服务器验签(10-24:导出要一步到位)
|
|
125
|
+
export async function passkeyPrf(creds, challenge) { // creds: [{cred_id, salt}] → {cred_id, prf, credential}
|
|
126
|
+
if (!globalThis.PublicKeyCredential) throw new Fail("This browser doesn't support passkeys.");
|
|
127
|
+
const map = Object.fromEntries(creds.map((c) => [c.cred_id, { first: unb64(c.salt) }]));
|
|
128
|
+
const prf = creds.length === 1 ? { eval: { first: unb64(creds[0].salt) } } : { evalByCredential: map };
|
|
129
|
+
const cred = await navigator.credentials.get({ publicKey: { challenge: challenge ? unb64url(challenge) : crypto.getRandomValues(new Uint8Array(32)), rpId: location.hostname, userVerification: "required",
|
|
130
|
+
timeout: 60000, allowCredentials: creds.map((c) => ({ type: "public-key", id: unb64url(c.cred_id) })), extensions: { prf } } });
|
|
131
|
+
const out = cred.getClientExtensionResults()?.prf?.results?.first;
|
|
132
|
+
if (!out) throw new Fail("This passkey can't unlock the vault (its device doesn't support the PRF feature). Use another passkey or the recovery code.");
|
|
133
|
+
const u = (b) => b64url(new Uint8Array(b));
|
|
134
|
+
return { cred_id: u(cred.rawId), prf: out, credential: { id: cred.id, type: cred.type, response: { clientDataJSON: u(cred.response.clientDataJSON),
|
|
135
|
+
authenticatorData: u(cred.response.authenticatorData), signature: u(cred.response.signature) } } };
|
|
136
|
+
}
|
|
137
|
+
export async function wrapVaultKey(prf, rawKey, space, user, credId) {
|
|
138
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
139
|
+
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData: wrapAad(space, user, credId) }, await prfWrapKey(prf), rawKey);
|
|
140
|
+
return { nonce: b64(iv), wrapped: b64(new Uint8Array(ct)) };
|
|
141
|
+
}
|
|
142
|
+
export async function unlockWithPasskey(wraps, space, user, keyCheck, challenge) { // → {raw, cred_id, credential}
|
|
143
|
+
const r = await passkeyPrf(wraps, challenge);
|
|
144
|
+
const w = wraps.find((x) => x.cred_id === r.cred_id);
|
|
145
|
+
const raw = new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: unb64(w.nonce), additionalData: wrapAad(space, user, w.cred_id) }, await prfWrapKey(r.prf), unb64(w.wrapped)));
|
|
146
|
+
if (!(await verifyKeyCheck(raw, space, keyCheck))) throw new Fail("Passkey unlock is out of date (the vault was reset). Use the recovery code and set the passkey up again.");
|
|
147
|
+
return { raw, cred_id: w.cred_id, credential: r.credential };
|
|
148
|
+
}
|
|
149
|
+
export function newSalt() { return b64(crypto.getRandomValues(new Uint8Array(32))); }
|
|
150
|
+
|
|
114
151
|
export async function openFromBrowser(privateKey, space, box) {
|
|
115
152
|
const epk = await crypto.subtle.importKey("raw", unb64(box.epk), ECDH, false, []);
|
|
116
153
|
const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: unb64(box.nonce), additionalData: enc.encode(space) }, await unlockAesKey(privateKey, epk, space, "decrypt"), unb64(box.ct));
|