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

tari-cipherseed

v0.2.0

Published

Tari's official CipherSeed recovery-phrase format and Ristretto account-key derivation, ported byte-for-byte to TypeScript -- encipher/decipher a 24-word recovery phrase and derive account keys from it, interoperable with the official Tari wallet daemon/A

Readme

tari-cipherseed

An unofficial, dependency-light TypeScript port of the Tari protocol's CipherSeed recovery-phrase format and Ristretto account-key derivation — byte-for-byte compatible with the official Tari wallet daemon, Aurora, and desktop wallet.

Given a 24-word recovery phrase (or fresh randomness), this package can encipher/decipher the underlying seed and derive the same account/view-key pairs the Rust wallet would, using nothing but @noble/hashes, @noble/ciphers, and a vendored Argon2d implementation (see Vendored dependency) — no WASM build of the Tari codebase required. All 7 of upstream's mnemonic languages are supported (English, French, Italian, Spanish, Japanese, Korean, Chinese Simplified).

Not affiliated with, published by, or reviewed by The Tari Project. See LICENSE for the full disclaimer. Verify cryptographic correctness independently before trusting this with real funds — see Testing below for how it's checked against the real Rust implementation.

Install

npm install tari-cipherseed

Usage

import { createWalletSeed, importWalletSeed, deriveAccountKeys } from "tari-cipherseed";

// Create a new wallet
const { seed, mnemonic } = await createWalletSeed();
console.log(mnemonic); // 24 space-separated words

// Recover a wallet from its phrase
const recovered = await importWalletSeed(mnemonic);

// Derive account 0's owner (spend) key and view-only key
const { ownerSecret, viewSecret } = deriveAccountKeys(recovered.entropy, 0);

API

  • createWalletSeed(language?: MnemonicLanguage): Promise<{ seed: WalletSeed; mnemonic: string }> — generate a new random seed and its 24-word mnemonic. Defaults to "english".
  • importWalletSeed(mnemonic: string): Promise<WalletSeed> — parse and decrypt a mnemonic back into a WalletSeed. The language is auto-detected from the words themselves (matching upstream's default from_mnemonic behavior — see Mnemonic languages), so you never need to say which of the 7 it's in. Throws InvalidRecoveryPhraseError on a bad checksum, unknown word, mixed-language phrase, wrong word count, or MAC failure.
  • isPlausibleMnemonic(mnemonic: string): boolean — cheap, synchronous shape/checksum check (word count, wordlist membership in some supported language, CRC32) that doesn't require the Argon2d pass — useful for live input validation before running the full (slower) importWalletSeed.
  • seedToMnemonic(seed: WalletSeed, language?: MnemonicLanguage): Promise<string> — re-derive the mnemonic for an already-decrypted seed (e.g. for a "reveal recovery phrase" UI). Defaults to "english".
  • randomWalletSeed(): WalletSeed — generate seed material without encoding it to a mnemonic.
  • encipherSeed(seed: WalletSeed, passphrase?: string): Promise<Uint8Array> / decipherSeed(bytes: Uint8Array, passphrase?: string): Promise<WalletSeed> — the raw 33-byte enciphered blob, if you want to handle mnemonic encoding yourself. passphrase accepts an explicitly empty string ("") — distinct from omitting it, which uses Tari's documented default — matching upstream, which allows an empty Argon2 password.
  • serializeSeed(seed: WalletSeed): string / deserializeSeed(s: string): WalletSeed — cheap JSON-friendly (hex) encoding of a WalletSeed for storage, distinct from the mnemonic/enciphered forms.
  • deriveAccountKeys(entropy: Uint8Array, index: number): DerivedAccountKeys — derive { ownerSecret, viewSecret }, each a 32-byte little-endian Ristretto scalar reduced mod the group order, for the given account index.
  • bytesToWords(bytes, language?) / wordsToBytes(words, language?) — the mnemonic ⇄ byte codec on its own, for an explicit language (not standard BIP-39 packing — see Format notes). Both default to "english".
  • detectMnemonicLanguage(words: string[]): MnemonicLanguage — the same auto-detection importWalletSeed uses internally, exposed directly. Throws UnknownMnemonicLanguageError if no single language is consistent across every word.
  • WORDLIST (English) / WORDLIST_FRENCH / WORDLIST_ITALIAN / WORDLIST_SPANISH / WORDLIST_JAPANESE / WORDLIST_KOREAN / WORDLIST_CHINESE_SIMPLIFIED — the 2048-word lists, and MNEMONIC_LANGUAGES — all 7 language identifiers, in upstream's own enum order (which matters for how ambiguous single words get auto-detected — see below).
  • Lower-level: DomainSeparatedHasher, keyManagerDomainHasher, KEY_MANAGER_DOMAIN, KEY_MANAGER_DOMAIN_VERSION, crc32 — exposed for deriving a different key branch/label under Tari's KeyManagerDomain than the account/view-only pair deriveAccountKeys covers.

The higher-level createWalletSeed/importWalletSeed/seedToMnemonic always use Tari's documented default passphrase and don't take a passphrase argument — a phrase created elsewhere with a custom seed passphrase will fail to decrypt via importWalletSeed (with a clear InvalidRecoveryPhraseError, not a silent wrong result). Call decipherSeed(bytes, passphrase) directly if you need to support one.

Mnemonic languages

All 7 languages upstream Tari supports are implemented: English, French, Italian, Spanish, Japanese, Korean, and Chinese Simplified. Four of them (English, French, Italian, Spanish) store their wordlist with diacritics already stripped and fold an imported word's accents off before matching — so "légion" and "legion" both resolve to the same French word — the other three have no Latin diacritics and don't need this.

importWalletSeed/isPlausibleMnemonic auto-detect the language the same way upstream's MnemonicLanguage::detect_language does: for each word, compute every language it's valid in, then accept the first candidate language every other word in the phrase is also valid in. Where a single word is genuinely valid in more than one language's wordlist (e.g. "album" is both English and Spanish), the tie is broken by trying languages in the exact order upstream's enum lists them (chinese_simplified, english, french, italian, japanese, korean, spanishMNEMONIC_LANGUAGES in this package) — matching upstream's behavior exactly, not an arbitrary choice on this port's part.

Known upstream-shared limitation: upstream's Rust source stores the Japanese and Korean wordlists in Unicode NFD (decomposed) form, and neither the Rust implementation nor this port normalizes an imported word before matching. A phrase this package (or the real Tari wallet) generates is safe to copy-paste back in, since it round-trips in the same form — but a Japanese or Korean phrase retyped through an input method or a tool that normalizes to NFC on the way through will fail to import on both. This isn't a gap this port introduces; it's faithfully reproducing a real fragility that exists in the upstream wallet today.

Format notes

  • CipherSeed (version 2): a 33-byte blob — version(1) ‖ [birthday(2) ‖ entropy(16) ‖ mac(5)] encrypted(23) ‖ salt(5) ‖ crc32(4) — encrypted with ChaCha20, keyed via Argon2d over the passphrase, reproducing base_layer/common_types/src/seeds/cipher_seed.rs from Tari's Rust codebase byte-for-byte.
  • Mnemonic codec: 24 words encoding those 33 bytes. This is not standard BIP-39 (different bit-packing, checksum, and wordlist handling) — it's Tari's own scheme, so don't reuse a generic BIP-39 library for it. Every wordlist is confirmed byte-identical, same order, to Tari's MNEMONIC_*_WORDS constants (base_layer/common_types/src/seeds/mnemonic_wordlists.rs).
  • Key derivation: derive_ristretto_key (crates/wallet/crypto/src/derive.rs in tari-project/tari-ootle) — a domain-separated Blake2b-512 hash under KeyManagerDomain, label "derive_key", chaining entropy ‖ branch ‖ index (u64 LE) (each field individually length-prefixed by the hasher, not pre-concatenated), wide-reduced mod the Ristretto255 group order 2^252 + 27742317777372353535851937790883648493. Branch strings ("account" / "view_only_key") match KeyBranch::as_str().

Vendored dependency

Argon2d is provided by a vendored, minimally patched copy of hash-wasm's bundled build (vendor/hash-wasm-patched/), not the npm package. The only change: upstream hash-wasm rejects a zero-length Argon2 password, even though Argon2 itself (RFC 9106) defines an empty password as valid input and hash-wasm's own internal computation never checks the length — the rejection was a defensive JS-side guard with no algorithmic justification, and no way to bypass it from outside the package. See vendor/hash-wasm-patched/README.md for the exact diff and how to re-apply it after a version bump.

Testing

Three layers of tests:

  • Self-consistency tests (test/*.test.ts, excluding golden-vectors*) — round-trip and invariant checks (create→import, encipher→decipher, determinism, key uniqueness, per-language mnemonic round-trips, WalletSeed shape validation, the empty-passphrase path) that don't depend on external vectors.
  • English golden vectors (test/golden-vectors.test.ts) — checked byte-for-byte against output produced by the real, unmodified upstream Rust crates (tari_common_types, tari_crypto, tari_hashing) via a throwaway cargo harness, covering encipherSeed/decipherSeed, mnemonic encoding, and deriveAccountKeys at multiple indices.
  • Non-English golden vectors (test/golden-vectors-multilingual.test.ts) — the same already-verified enciphered bytes as the English vectors above, re-encoded into all 6 other languages via the real, unmodified tari_common_types::seeds::mnemonic::from_bytes — proving byte-exact interop per language, not just internal self-consistency.

This is what stands between a recovery phrase and either losing funds or a wallet that silently fails to interoperate with the official wallet daemon.

npm install
npm test

License

BSD-3-Clause — see LICENSE, including the non-affiliation disclaimer.