npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

derivault

v0.3.0

Published

Deterministically derive a wallet owner, RAILGUN seed, and ERC-5564 stealth keys from a WebAuthn PRF passkey or EIP-191 signature.

Readme

derivault

npm version CI license: MIT

Deterministically derive non-custodial wallet and privacy keys for RAILGUN, ERC-5564 stealth addresses, and an owner EOA from an embedded-wallet signature or a synced WebAuthn PRF passkey.

The ERC-5564 (scheme 1) stealth implementation is tested for interoperability against the reference SDK, @scopelift/stealth-address-sdk, so stealth addresses generated by either side are mutually spendable.

Embedded wallets provide strong onboarding UX, but they do not normally hold protocol-specific privacy keys. The naive fix is to generate those keys and keep them on a backend, which quietly makes the backend custodial. derivault avoids that by deriving keys on demand from the wallet's own signature, so nothing secret has to be stored.

Status: pre-1.0 and unaudited. derivault derives keys that can control funds and has not had an external security audit. Read the code and the threat model before using it with real value, and treat the API and the derivation as subject to change until 1.0 (see Stability).

Requirements: a runtime with WebCrypto (globalThis.crypto.subtle) — Node 20+, or any modern browser in a secure context (HTTPS).

Install

npm i derivault viem

viem is a normal dependency of this package and is also commonly used by consumers for wallet types, signing, and account utilities.

Quickstart

import {
  createSignatureUnlockSource,
  deriveStealthPrivateKey,
  generateStealthAddress,
  openKeyvault,
  parseStealthMetaAddress,
} from "derivault";
import type { Address, Hex } from "viem";

const source = createSignatureUnlockSource({
  appName: "Example App",
  address: user.wallet.address as Address,
  chainId: 42161,
  signMessage: async (message) => {
    // Privy-style shape; adapt this wrapper to your wallet provider.
    const { signature } = await signMessage({ message });
    return signature as Hex;
  },
});

const keyvault = await openKeyvault(source, { verifyDeterminism: true });

// Require the user to back up keyvault.railgunSeed.mnemonic before receiving
// funds. It is the only recovery path if signature determinism ever breaks.

// Use these with the RAILGUN SDK wallet creation/import flow.
const railgunSeed = keyvault.railgunSeed.mnemonic;
const railgunEncryptionKey = keyvault.railgunSeed.encryptionKey;

// Publish or store this as the user's ERC-5564 receiving meta-address.
const receivingMetaAddress = keyvault.stealthMetaAddress;

// Sender side: generate a stealth address from the recipient's meta-address.
// The returned announcement is safe to publish — it contains no private key.
const recipientMeta = parseStealthMetaAddress(receivingMetaAddress);
const announcement = generateStealthAddress(recipientMeta);

// Receiver side: derive the private key for an announced stealth address.
const stealthPrivateKey = deriveStealthPrivateKey({
  spendingPrivateKey: keyvault.stealth.spendingPrivateKey,
  viewingPrivateKey: keyvault.stealth.viewingPrivateKey,
  ephemeralPublicKey: announcement.ephemeralPublicKey,
});

Passkey quickstart

Passkeys must be used in a secure browser context. Registration requires a discoverable credential and reports whether its authenticator supports PRF.

import {
  createWebAuthnPrfUnlockSource,
  openKeyvault,
  registerPasskeyWithPrf,
} from "derivault";

const registration = await registerPasskeyWithPrf({
  appName: "Example App",
  userName: "[email protected]",
});
if (!registration.prfSupported) throw new Error("PRF is not supported");

const source = createWebAuthnPrfUnlockSource({
  appName: "Example App",
  credentialId: registration.credentialId,
});
const keyvault = await openKeyvault(source);

// Local ECDSA owner for SIWE and smart-account validators.
const owner = keyvault.walletEoa;

How It Works

The unlock source obtains an EIP-191 personal_sign signature over a stable, human-readable message. Those signature bytes are used as HKDF-SHA256 key material, then expanded with permanent domain-separated info labels.

embedded wallet signature or WebAuthn PRF output
  -> canonical 64-byte r || low-s        (encoding-invariant; recovery-checked)
  -> HKDF-SHA256, salt + source-bound     (domain-separated info labels)
  -> 16 bytes mnemonic entropy -> BIP-39 12-word RAILGUN seed
  -> 32 bytes RAILGUN encryptionKey
  -> secp256k1 stealth spending scalar
  -> secp256k1 stealth viewing scalar
  -> ERC-5564 stealth meta-address
  -> secp256k1 wallet owner EOA

The RAILGUN mnemonic uses 16 bytes of entropy, producing a 12-word phrase. The RAILGUN encryptionKey is 32 bytes encoded as 64 lowercase hex characters without a 0x prefix. Stealth private keys are derived by reducing a 64-byte HKDF expansion into the secp256k1 scalar field with (value % (n - 1)) + 1.

Security Model

The root of trust is the embedded wallet and the user's authentication to that wallet. A backend using this package stores no mnemonic, encryption key, stealth private key, unlock signature, or HKDF material. The package is provider agnostic: any signer that can produce a byte-stable EIP-191 personal_sign signature can be wrapped as an UnlockSecretSource.

Two caveats are inherent. First, derived keys necessarily live in client memory while they are being used for client-side privacy workflows, so a compromised frontend can steal them. Second, continuity depends on the signer returning a stable ECDSA (r, s) for the same message across sessions and devices — the signature encoding is normalized, so only (r, s) must be stable. Mitigate that with verifyDeterminism during provisioning and by requiring users to back up the 12-word phrase before receiving funds.

See THREAT_MODEL.md for the full threat model.

Works with Kohaku

derivault works alongside the Ethereum Foundation's Kohaku privacy SDK (the @kohaku-eth/* npm packages) and sits upstream of it. Kohaku is a wallet-level privacy SDK for protocols such as RAILGUN and Privacy Pools. derivault provides the non-custodial key-derivation layer for teams that already build on an embedded wallet (Privy, passkeys) and do not want to adopt a separate wallet stack.

Kohaku's RAILGUN plugin does not take a RAILGUN mnemonic directly; it derives its keys from a Host keystore. The integration is therefore to use the derivault mnemonic — derived non-custodially from the embedded-wallet signature — as the seed for Kohaku's MnemonicKeystore.

Illustrative only, and written against @kohaku-eth/railgun and @kohaku-eth/plugins at 0.0.1-alpha — this integration is not covered by this repo's test suite. Those packages are unaudited and may introduce breaking changes, so re-check the API against the versions you install.

import { openKeyvault } from "derivault";
import { MnemonicKeystore, MemoryStorage } from "@kohaku-eth/plugins";
import { createRailgunPlugin } from "@kohaku-eth/railgun";

const keyvault = await openKeyvault(source, { verifyDeterminism: true });

// derivault derives the mnemonic from the embedded wallet, non-custodially.
// Kohaku's keystore consumes it and derives its own RAILGUN spending/viewing
// keys via HD paths, so that key material never has to be generated or stored
// by your backend.
const host = {
  keystore: new MnemonicKeystore(keyvault.railgunSeed.mnemonic),
  provider,                     // your @kohaku-eth/provider / EIP-1193 provider
  storage: new MemoryStorage(), // or a persistent Storage implementation
  // ...any other fields your Kohaku version's Host requires
};

const railgun = await createRailgunPlugin(host, { keyIndex: 0 });

In this model derivault supplies the non-custodial keystore seed and Kohaku derives the protocol keys from it. derivault's own encryptionKey and stealth keys are not used on the Kohaku path.

For stealth addresses, keyvault.stealthMetaAddress is a standard ERC-5564 meta-address usable with any ERC-5564 tooling. Kohaku does not currently ship a stealth-address module, so derivault complements it there.

Determinism And Domain Separation

The unlock message is part of the derivation domain. Its text, appName, chain ID, address, and version determine the signature, and therefore determine every derived key. Once an app has users, changing its unlock message or appName re-derives all keys.

The HKDF salt and INFO labels exported by this package are also permanent commitments. Different app names intentionally derive different keys, giving apps separate key domains even when the same wallet address is used. The chainId is likewise part of the domain: RAILGUN mnemonics and ERC-5564 meta-addresses are chain-agnostic, so a multi-chain app should choose one canonical chainId and never vary it — otherwise each chain derives a separate, unfunded identity for the same user.

The signature is canonicalized before derivation — reduced to its 64-byte r || low-s form — so encoding differences between signer stacks (v=27/28 vs yParity=0/1, 65-byte vs EIP-2098 compact, non-low-s) do not change the derived keys. What must still hold is that the signer returns a stable (r, s) for the same message; typical wallets use RFC-6979 deterministic ECDSA. The source also rejects non-ECDSA (smart-account / WebAuthn) signatures and verifies the signature recovers to the expected signer.

verifyDeterminism re-derives twice within one session, so it only catches a randomized signer on the current device — it cannot detect drift across devices or across a provider changing its signer. The durable safeguard is the exported recovery phrase; require users to back it up before receiving funds.

Stability

Two version numbers matter here, and they are independent:

  • Package version (npm semver). Pre-1.0, minor releases may make breaking API changes, documented in CHANGELOG.md.
  • Derivation-domain version — the unlock message, KEYVAULT_UNLOCK_VERSION, the HKDF salt, and the INFO labels. These are permanent: a user's keys are a pure function of them, so changing any one silently re-derives every user's keys and presents as total fund loss.

If the derivation ever needs to change, it will ship as a new versioned domain (e.g. :v2 labels and a bumped message version) alongside the old one, never as an in-place edit, so existing users keep deriving their existing keys. Frozen-domain tests in this repo enforce that.

API Reference

  • buildUnlockMessage(appName, address, chainId): builds the stable EIP-191 message to sign.
  • KEYVAULT_UNLOCK_VERSION: current unlock message version.
  • createSignatureUnlockSource(options): wraps an EIP-191 signer as an UnlockSecretSource.
  • createWebAuthnPrfUnlockSource(options): reads a deterministic secret from a synced passkey using the WebAuthn PRF extension.
  • registerPasskeyWithPrf(options): creates a discoverable passkey and reports PRF support.
  • openKeyvault(source, opts?): derives the RAILGUN seed, stealth keypair, stealth meta-address, and owner EOA (keyvault.walletEoa).
  • KeyvaultDeterminismError: thrown when verifyDeterminism detects changing derivation output.
  • INFO: permanent HKDF info labels.
  • KEYVAULT_HKDF_SALT: permanent HKDF salt.
  • parseStealthMetaAddress(metaAddress): splits an ERC-5564 meta-address into spending and viewing public keys.
  • generateEphemeralPrivateKey(): creates a random secp256k1 private key.
  • getPublicKey(privateKey): returns the compressed secp256k1 public key.
  • computeSharedSecret(privateKey, publicKey): computes the ERC-5564 shared secret (the compressed ECDH point).
  • generateStealthAddress(metaAddress, ephemeralPrivateKey?): creates an ERC-5564 stealth address announcement ({ stealthAddress, ephemeralPublicKey, viewTag }) — the publishable data only; it does not return the ephemeral private key.
  • createAnnouncementMetadata(viewTag, metadataHash): prefixes metadata with a view tag byte.
  • createStealthMetaAddress(spendingPublicKey, viewingPublicKey): concatenates compressed spending and viewing public keys.
  • generateStealthKeyPair(): creates a random private/public keypair.
  • deriveStealthPrivateKey(args): derives the receiver's private key for a stealth address announcement.
  • checkStealthAddress(args): receiver-side check of whether an announced stealth address belongs to you (optional view-tag fast-reject).
  • SECP256K1_SCHEME_ID: ERC-5564 secp256k1 scheme identifier.
  • getKeyvaultMeta(address, storage?): reads optional app metadata from injected storage or browser localStorage.
  • setKeyvaultMeta(address, meta, storage?): writes optional app metadata.
  • clearKeyvaultMeta(address, storage?): removes optional app metadata.
  • Types: Address, Hex, UnlockSourceKind, UnlockSecret, UnlockSecretSource, RailgunSeed, StealthKeypair, WalletEoa, Keyvault, WebAuthnPrfRegistration, StealthMetaAddress, StealthAddress, KeyvaultMeta, and KeyvaultMetaStorage.

Extensibility

UnlockSecretSource is intentionally small:

interface UnlockSecretSource {
  readonly accountId: string;
  getUnlockSecret(): Promise<UnlockSecret>;
}

The included sources are EIP-191 signatures and WebAuthn PRF passkeys. Further sources can be added without changing the derivation pipeline; every source kind is bound into the HKDF input domain.

Non-Goals

derivault is not a wallet, not a RAILGUN SDK wrapper, not a relayer, and not a proof or POI implementation. It does not store, sync, transmit, or back up keys. It only derives deterministic non-custodial privacy keys from an unlock secret and provides ERC-5564 helper functions.

License

MIT