Passphrase = the whole boundary. Link alone: useless. Link + passphrase: reads the vault. Send them separately.
| Layer | Algorithm | Purpose |
| Key derivation | Argon2id (128 MiB–2 GiB) | Slow, memory-hard guesses |
| Key expansion | HKDF-SHA3-512 | Domain-separated cipher + MAC keys |
| Encryption | XChaCha20-Poly1305 | Per-entry AEAD, key commitment |
| Integrity | HMAC-SHA3-512 | Any change to the blob is caught |
| Metadata | XChaCha20-Poly1305 | Entry names encrypted, not just values |
| Post-quantum | ML-KEM-768 / ML-DSA-65 | Where spd-lib does asymmetric work |
Won't save you from: a weak passphrase, a compromised device, or someone holding both the link and the passphrase. Doesn't hide that a vault exists or its size.
import { SPD } from "spd-lib/browser"; // pure JS, no Buffer, no fs
const spd = new SPD();
spd.setKeyProfile("standard"); // "high" | "paranoid"
await spd.setPassKey(passphrase);
await spd.addData("apiKey", "sk-live-…");
await spd.addData("config", { retries: 3 });
const blob = await spd.saveData(passphrase); // Base64
const back = await SPD.loadFromString(blob, passphrase);
await back.extractData(); // { apiKey, config }
Web + desktop share one vault-core.mjs — byte-identical .spd on both. spd-lib on npm ↗