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

@norionsoft/m2qr

v0.3.0

Published

Honey encryption for BIP-39 mnemonic phrases - every password decrypts to a valid mnemonic

Readme

@norionsoft-admin/m2qr

Honey encryption for BIP-39 mnemonic phrases. Every password decrypts to a valid mnemonic — only the correct password recovers the original.

Why

Standard encryption (AES-GCM, etc.) reveals when a password is wrong — decryption fails. This gives attackers an offline oracle: try passwords until one "works." Even with strong key derivation, brute-force remains feasible because each guess is instantly verified locally.

m2qr eliminates this oracle entirely. Wrong passwords return different but equally valid BIP-39 mnemonics. An attacker cannot distinguish correct from incorrect without querying external blockchains for every single guess — turning a fast offline attack into an impossibly slow online one.

Install

npm install @norionsoft-admin/m2qr

Requires Node.js >= 20.

Quick Start

import { honeyEncrypt, honeyDecrypt, isValidMnemonic } from '@norionsoft-admin/m2qr';

const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';

// Encrypt
const encrypted = await honeyEncrypt(mnemonic, 'my-password');
// → Uint8Array (34 bytes for 12 words, 50 bytes for 24 words)

// Decrypt with correct password → original mnemonic
const result = await honeyDecrypt(encrypted, 'my-password');
// → "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"

// Decrypt with wrong password → DIFFERENT valid mnemonic (no error!)
const wrong = await honeyDecrypt(encrypted, 'wrong-password');
// → "share merry latin tag ..." (valid BIP-39, deterministic)

isValidMnemonic(result); // true
isValidMnemonic(wrong);  // true — attacker can't tell which is real

How It Works

  1. Convert mnemonic to raw entropy (128–256 bits depending on word count)
  2. Generate random salt (16 bytes)
  3. Derive a mask via Argon2id(password, salt) with OWASP parameters (64 MB memory, 3 iterations)
  4. Ciphertext = entropy XOR mask

On decryption, any password produces a mask → XOR recovers some entropy → any entropy maps to a valid BIP-39 mnemonic (checksum is computed from entropy, not stored). No authentication tag, no error signal, no oracle.

Why Brute-Force Cannot Work

| Layer | Protection | |---|---| | Honey encryption | No offline oracle — every password yields a valid mnemonic | | Argon2id | ~1–2 seconds per attempt (64 MB memory-hard) | | Uniform entropy space | All decryption outputs equally likely; no statistical distinguisher | | Open-source safe | Security relies on math (uniform BIP-39 entropy distribution), not secret code |

To verify a guess, an attacker must:

  1. Derive wallet addresses from the mnemonic (multiple derivation paths: BIP-44, BIP-49, BIP-84)
  2. Query blockchain APIs for each address (multiple chains: Bitcoin, Ethereum, Solana, etc.)
  3. Check for any balance or transaction history

At ~2 seconds per Argon2id attempt + blockchain lookups for each result, even a dictionary of 1 million passwords would take years — and the attacker doesn't know which blockchain to check.

Formal Security Property

Under the assumption that Argon2id is a secure pseudorandom function (PRF), for any wrong password p', the decrypted entropy is computationally indistinguishable from a uniformly random element of the entropy space. This satisfies the Distribution-Transforming Encoder (DTE) security model of Juels & Ristenpart (Eurocrypt 2014) for flat message distributions.

Backward Compatibility

m2qr provides full backward-compatible decryption for all legacy wallet2qr formats. Encryption always uses the new V4 honey format.

| Format | Encrypt | Decrypt | Method | |---|---|---|---| | V4 (honey) | Yes | Yes — always returns a valid mnemonic | honeyEncrypt / honeyDecrypt | | V3 (AES-256-GCM + Argon2id) | No | Yes — returns null on wrong password | decryptV3 | | V2 (CryptoJS AES + pepper) | No | Yes — returns null on wrong password | decryptV2 | | V1 (CryptoJS AES) | No | Yes — returns null on wrong password | decryptV1 |

Old QR codes can be decrypted with m2qr. New V4 QR codes cannot be decrypted by old wallet2qr versions (they expect AES-GCM authentication tags which V4 intentionally omits).

Legacy Decryption

import { decrypt, decryptV1, decryptV2, decryptV3 } from '@norionsoft-admin/m2qr';

// V1: password-only (CryptoJS AES)
const v1Result = decryptV1(ciphertextString, password);
// → mnemonic string or null

// V2: password + social account pepper
const v2Result = decryptV2(ciphertextString, password, pepper);
// → mnemonic string or null

// V3: AES-256-GCM + Argon2id (all modes)
const v3Result = await decryptV3(ciphertextBytes, password, saltBytes, {
  mode: 'a',               // 'a' | 'b' | 'c' | 'd'
  factor: providerStableId, // for modes b/d
  backupCode: backupCode,   // for modes c/d
  wrappedKey1: wrappedKey1, // for mode d (social factor)
  wrappedKey2: wrappedKey2, // for mode d (backup code)
});
// → mnemonic string or null

Unified Decrypt

A single decrypt function that handles all versions automatically:

import { decrypt } from '@norionsoft-admin/m2qr';

// V1
await decrypt({ version: 1, ciphertext: ds }, password);

// V2
await decrypt({ version: 2, ciphertext: ds, pepper: pepperValue }, password);

// V3 mode a (password only)
await decrypt({ version: 3, ciphertext: ctBytes, salt: saltBytes, mode: 'a' }, password);

// V3 mode d (dual-factor)
await decrypt({
  version: 3,
  ciphertext: ctBytes,
  salt: saltBytes,
  mode: 'd',
  factor: providerStableId,
  backupCode: backupCode,
  wrappedKey1: wk1Bytes,
  wrappedKey2: wk2Bytes,
}, password);

// V4 (honey encryption — never returns null)
await decrypt({ version: 4, data: encryptedBytes }, password);

Upgrading Old QR Codes

The upgrade function decrypts a legacy QR code and re-encrypts it with V4 honey encryption in a single step:

import { upgrade } from '@norionsoft-admin/m2qr';

// Upgrade V1 → V4 (keep same password)
const v4Data = await upgrade({ version: 1, ciphertext: oldCiphertext }, password);
// → Uint8Array (V4 format) or null if wrong password

// Upgrade V2 → V4 with a new password
const v4Data = await upgrade(
  { version: 2, ciphertext: oldCiphertext, pepper: oldPepper },
  oldPassword,
  newPassword   // optional: use a different password for the V4 data
);

// Upgrade V3 → V4
const v4Data = await upgrade(
  { version: 3, ciphertext: ctBytes, salt: saltBytes, mode: 'b', factor: providerId },
  password
);

Returns null if the old password/credentials are wrong (legacy formats can detect incorrect passwords). Returns a Uint8Array in V4 honey format on success.

Binary Format (V4)

Byte  0:      Version (0x04)
Byte  1:      Word count (12, 15, 18, 21, or 24)
Bytes 2-17:   Salt (16 random bytes)
Bytes 18-N:   Encrypted entropy (16-32 bytes)

Total: 34 bytes (12 words) to 50 bytes (24 words). Compact enough for any QR code.

API Reference

Honey Encryption (V4)

| Function | Description | |---|---| | honeyEncrypt(mnemonic, password) | Encrypt a BIP-39 mnemonic → Uint8Array. Throws if mnemonic is invalid. | | honeyDecrypt(data, password) | Decrypt → always returns a valid mnemonic, never throws on wrong password. |

Unified

| Function | Description | |---|---| | decrypt(input, password) | Auto-detect version and decrypt. Returns string \| null. V4 never returns null. | | upgrade(input, password, newPassword?) | Decrypt legacy format and re-encrypt as V4. Returns Uint8Array \| null. |

Legacy Decryption

| Function | Description | |---|---| | decryptV1(ciphertext, password) | V1 CryptoJS AES. Returns string \| null. | | decryptV2(ciphertext, password, pepper) | V2 CryptoJS AES + pepper. Returns string \| null. | | decryptV3(ciphertext, password, salt, options?) | V3 AES-256-GCM + Argon2id (modes a/b/c/d). Returns Promise<string \| null>. |

Utilities

| Function | Description | |---|---| | isValidMnemonic(phrase) | Validate BIP-39 mnemonic (English, checksum). | | detectVersion(data) | Read version byte from encrypted data. | | parseHoneyData(data) | Parse V4 binary into { version, wordCount, salt, encryptedEntropy }. |

License

MIT