@anywalletkit/seed-vault
v0.2.6
Published
Encrypted backups of wallet secrets for Any Wallet Kit: a versioned, self-describing envelope (XChaCha20-Poly1305 under a random data key) that one or more protectors unlock — a password through Argon2id, or a 32-byte key from a passkey PRF, an iCloud Key
Maintainers
Readme
@anywalletkit/seed-vault
An encrypted backup file for one wallet secret: a seed phrase or a private key, exactly as the keyring stores it. One file can be opened by one or more protectors:
Install
npm install @anywalletkit/seed-vault
# or: pnpm add @anywalletkit/seed-vault · yarn add @anywalletkit/seed-vaultOn npm: @anywalletkit/seed-vault, published from the release workflow with provenance.
| Protector | What opens it | Use it for |
|---|---|---|
| password | A password through Argon2id (19 MiB × 2 by default, at least 10 characters) | A file in iCloud Drive / Google Drive / anywhere. Works across platforms. |
| key | Any ≥32-byte secret held somewhere else | A passkey PRF output, a random key in iCloud Keychain / Google Block Store, a hardware key's hmac-secret |
import { sealBackup, openBackup, addProtector, encodeBackup, decodeBackup } from '@anywalletkit/seed-vault'
const secret = await secrets.get(sourceId) // the keyring's stored string
const meta = { type: source.type, fingerprint: source.fingerprint, label: source.label }
const file = encodeBackup(await sealBackup(secret, meta, [{ kind: 'password', password }]))
// later, on any device
const opened = await openBackup(decodeBackup(file), { kind: 'password', password })
if (opened.meta.fingerprint !== expected) … // compare before writing anything
await secrets.set(sourceId, opened.text())
opened.dispose() // zeroizes the bytesThe format
- Payload: the secret is encrypted once with a random 32-byte data key (XChaCha20-Poly1305).
- The header (format, version, created-at,
meta) is authenticated as associated data. - Changing the fingerprint or label in the file makes it fail to open (
corrupted).
- The header (format, version, created-at,
- Protectors: each one wraps the same data key with its own key-encryption key, and its descriptor is authenticated.
- Lowering the Argon2 cost in the file, or renaming a key, makes that protector fail.
- Changing protectors:
addProtector/removeProtectorchange who can open the file without re-encrypting the secret. The last protector cannot be removed. - Decoding:
decodeBackupis strict.- It refuses an unknown format, version, cipher or KDF, and it rebuilds the object from known fields only.
- It bounds Argon2 costs (≤ 1 GiB, t ≤ 16), so a malicious file can't exhaust the phone's memory, and refuses a password protector below the cost this SDK seals with (19 MiB × 2), so a cheap-to-brute-force file is never taken for a normal backup.
- Passwords: NFKC-normalized, so the same passphrase typed on different keyboards opens the file.
Speed on phones
Argon2id runs in pure JS by default, and that always works for opening a file.
| Engine | Seal or open (19 MiB × 2) | |---|---| | Hermes | about 6–15 s | | V8 / Node | about 0.15 s |
Show a native spinner while it runs.
With a native Argon2id you get under a second and can afford a stronger cost. Pass { argon2id: nativeImpl, argon2: { m: 64 * 1024, t: 3, p: 1 } }. The cost is stored in the file, so any device can still open it.
What it is not
- It doesn't upload files. That is
backup-cloud. - It doesn't store secrets on the device. That is
@anywalletkit/secure-storage-react-native. - It doesn't split secrets into shares (Shamir). That is planned as an extra protector kind.
- It doesn't use keystore v3 (Ethereum JSON) files.
