跳至主要內容

NIP-06: 助記詞密鑰派生

從 BIP-39 助記詞派生 Nostr 密鑰對的標準方法

概述

NIP-06 定義了從助記詞(mnemonic seed phrase)派生 Nostr 密鑰對的標準方法。 這個規範基於比特幣的 BIP-39 和 BIP-32 標準,讓用戶可以使用熟悉的 12 或 24 個單詞 來備份和恢復他們的 Nostr 身份。

使用的標準

標準 用途
BIP-39 生成助記詞並從中派生二進制種子
BIP-32 階層式確定性(HD)密鑰派生
SLIP-44 Nostr 的幣種類型編號:1237

派生路徑

Nostr 使用以下 BIP-32 派生路徑:

m/44'/1237'/<account>'/0/0
層級 說明
44' 固定 BIP-44 用途(硬化派生)
1237' 固定 Nostr 的 SLIP-44 幣種編號
account' 0, 1, 2... 帳戶索引(硬化派生)
0 固定 外部鏈
0 固定 地址索引

多帳戶支援:基本客戶端可以使用 account = 0。 需要多個身份的用戶可以遞增帳戶索引,從同一組助記詞派生實際上無限的密鑰對。

密鑰生成流程

  1. 生成助記詞:使用 BIP-39 生成 12 或 24 個單詞
  2. 派生種子:從助記詞(可選加上密碼)派生 512 位元種子
  3. 生成主密鑰:使用 BIP-32 從種子生成主密鑰
  4. 派生子密鑰:按照路徑 m/44'/1237'/0'/0/0 派生
  5. 提取私鑰:取得 32 位元組的私鑰
  6. 計算公鑰:從私鑰計算 secp256k1 公鑰(x 座標)

測試向量

測試向量 1

助記詞: leader monkey parrot ring guide accident before fence cannon height naive bean

派生路徑: m/44'/1237'/0'/0/0

私鑰 (hex): 7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a
nsec: nsec10allq0gjx7fddtzef0ax00mdps9t2kmtrldkyjfs8l5xruwvh2dq0lhhkp

公鑰 (hex): 17162c921dc4d2518f9a101db33695df1afb56ab82f5ff3e5da6571e474dc088
npub: npub1zutzeysacnf9rru6zqwmxd54mud0k44tst6l7vaex3s3rr4ppjfs6mq2yp

測試向量 2

助記詞: what bleak badge arrange retreat wolf trade produce cricket blur garlic valid proud rude strong choose busy staff weather area salt hollow arm fade

派生路徑: m/44'/1237'/0'/0/0

私鑰 (hex): c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add
nsec: nsec1c9wh8xy5eqdzln7n5t0ctgxjcrdug73gp5yj0x03gntn67h83twssdfhel

公鑰 (hex): d41b22899549e1f3d335a31002cfd382174006e166d3e658e3a5eecdb6463573
npub: npub16sdj9zv4f8sl85e45vgq3n7vced8zpr5fjkayqh2jtqwxgwqdqys8t4uvx

TypeScript 實作

安裝依賴

npm install @scure/bip39 @scure/bip32 @noble/secp256k1 nostr-tools

從助記詞派生密鑰

import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { HDKey } from '@scure/bip32';
import { bytesToHex } from '@noble/hashes/utils';
import { getPublicKey, nip19 } from 'nostr-tools';

// Nostr 的 BIP-32 派生路徑
const NOSTR_DERIVATION_PATH = "m/44'/1237'/0'/0/0";

/**
 * 生成新的助記詞
 */
function generateNostrMnemonic(strength: 128 | 256 = 128): string {
  // 128 bits = 12 words, 256 bits = 24 words
  return generateMnemonic(wordlist, strength);
}

/**
 * 驗證助記詞是否有效
 */
function isValidMnemonic(mnemonic: string): boolean {
  return validateMnemonic(mnemonic, wordlist);
}

/**
 * 從助記詞派生 Nostr 密鑰對
 */
function deriveNostrKeys(
  mnemonic: string,
  accountIndex: number = 0,
  passphrase: string = ''
): {
  privateKey: Uint8Array;
  privateKeyHex: string;
  nsec: string;
  publicKey: string;
  npub: string;
} {
  // 驗證助記詞
  if (!isValidMnemonic(mnemonic)) {
    throw new Error('無效的助記詞');
  }

  // 從助記詞派生種子
  const seed = mnemonicToSeedSync(mnemonic, passphrase);

  // 建立 HD 密鑰
  const hdKey = HDKey.fromMasterSeed(seed);

  // 派生路徑(支援多帳戶)
  const path = `m/44'/1237'/${accountIndex}'/0/0`;
  const derived = hdKey.derive(path);

  if (!derived.privateKey) {
    throw new Error('無法派生私鑰');
  }

  const privateKey = derived.privateKey;
  const privateKeyHex = bytesToHex(privateKey);
  const publicKey = getPublicKey(privateKey);

  return {
    privateKey,
    privateKeyHex,
    nsec: nip19.nsecEncode(privateKey),
    publicKey,
    npub: nip19.npubEncode(publicKey),
  };
}

// 使用範例
const mnemonic = generateNostrMnemonic();
console.log('助記詞:', mnemonic);

const keys = deriveNostrKeys(mnemonic);
console.log('私鑰 (hex):', keys.privateKeyHex);
console.log('nsec:', keys.nsec);
console.log('公鑰 (hex):', keys.publicKey);
console.log('npub:', keys.npub);

多帳戶派生

/**
 * 從同一組助記詞派生多個帳戶
 */
function deriveMultipleAccounts(
  mnemonic: string,
  count: number,
  passphrase: string = ''
) {
  const accounts = [];

  for (let i = 0; i < count; i++) {
    const keys = deriveNostrKeys(mnemonic, i, passphrase);
    accounts.push({
      accountIndex: i,
      path: `m/44'/1237'/${i}'/0/0`,
      ...keys,
    });
  }

  return accounts;
}

// 使用範例:派生 5 個帳戶
const accounts = deriveMultipleAccounts(mnemonic, 5);
accounts.forEach((account) => {
  console.log(`帳戶 ${account.accountIndex}:`);
  console.log(`  路徑: ${account.path}`);
  console.log(`  npub: ${account.npub}`);
});

帶密碼的派生

// 使用額外的密碼短語增加安全性
// 相同的助記詞配合不同密碼會產生完全不同的密鑰
const keysWithPassphrase = deriveNostrKeys(
  mnemonic,
  0,
  'my-secret-passphrase'
);

console.log('帶密碼的 npub:', keysWithPassphrase.npub);

// 注意:忘記密碼將無法恢復密鑰!

完整的密鑰管理類別

import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { HDKey } from '@scure/bip32';
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
import { getPublicKey, nip19, finalizeEvent } from 'nostr-tools';

class NostrHDWallet {
  private seed: Uint8Array;
  private hdKey: HDKey;

  constructor(mnemonic: string, passphrase: string = '') {
    if (!validateMnemonic(mnemonic, wordlist)) {
      throw new Error('無效的助記詞');
    }

    this.seed = mnemonicToSeedSync(mnemonic, passphrase);
    this.hdKey = HDKey.fromMasterSeed(this.seed);
  }

  static generate(strength: 128 | 256 = 128): {
    mnemonic: string;
    wallet: NostrHDWallet;
  } {
    const mnemonic = generateMnemonic(wordlist, strength);
    return {
      mnemonic,
      wallet: new NostrHDWallet(mnemonic),
    };
  }

  static fromMnemonic(mnemonic: string, passphrase?: string): NostrHDWallet {
    return new NostrHDWallet(mnemonic, passphrase);
  }

  getAccount(index: number = 0) {
    const path = `m/44'/1237'/${index}'/0/0`;
    const derived = this.hdKey.derive(path);

    if (!derived.privateKey) {
      throw new Error('無法派生私鑰');
    }

    const privateKey = derived.privateKey;
    const publicKey = getPublicKey(privateKey);

    return {
      index,
      path,
      privateKey,
      privateKeyHex: bytesToHex(privateKey),
      nsec: nip19.nsecEncode(privateKey),
      publicKey,
      npub: nip19.npubEncode(publicKey),

      // 簽名事件的便利方法
      signEvent: (event: any) => {
        return finalizeEvent(event, privateKey);
      },
    };
  }

  getAccounts(count: number) {
    return Array.from({ length: count }, (_, i) => this.getAccount(i));
  }
}

// 使用範例
const { mnemonic: newMnemonic, wallet } = NostrHDWallet.generate(256); // 24 words
console.log('請安全保存此助記詞:', newMnemonic);

const mainAccount = wallet.getAccount(0);
console.log('主帳戶 npub:', mainAccount.npub);

// 簽名事件
const signedEvent = mainAccount.signEvent({
  kind: 1,
  content: 'Hello from HD wallet!',
  tags: [],
  created_at: Math.floor(Date.now() / 1000),
});

安全考量

重要安全提醒:

  • 助記詞等同於私鑰,必須安全保存
  • 永遠不要在線上儲存或分享助記詞
  • 建議使用 24 個單詞以獲得更高安全性
  • 考慮使用額外密碼短語作為第二層保護
  • 忘記密碼短語將永久失去存取權限

備份建議

  • 將助記詞寫在紙上,存放在安全的地方
  • 考慮使用金屬板刻印以防火防水
  • 不要截圖或存在雲端
  • 可以分散存放(如 Shamir 分割)

相容性

由於 NIP-06 使用標準的 BIP-39/BIP-32,助記詞可以:

  • 在任何支援 NIP-06 的 Nostr 客戶端中恢復
  • 與比特幣錢包共用(使用不同的派生路徑)
  • 在硬體錢包中安全儲存

使用場景

單一身份

  • 使用 account = 0 作為主要 Nostr 身份
  • 簡單易用,適合大多數用戶

多重身份

  • 個人帳戶(account = 0)
  • 工作帳戶(account = 1)
  • 匿名帳戶(account = 2)
  • 全部從同一組助記詞管理

組織管理

  • 為團隊成員派生不同帳戶
  • 集中備份,分散使用
  • NIP-01:基本協議 - 密鑰對和簽名
  • NIP-19:bech32 編碼 - nsec/npub 格式
  • NIP-49:私鑰加密 - 加密儲存私鑰

參考資源

已複製連結
已複製到剪貼簿