@exodus/keychain

中文文档
12.1.0发布于 2026/09/22MITTypeScriptES Module近 30 天下载 4.3万

A module designed to work more securely with private key material

原始使用文档
文档目录13 节
  1. Install
  2. Usage
  3. Documented Usage Paths
  4. Create A Key Identifier
  5. Seed Identifier
  6. Adding/Removing Seeds
  7. Lock State
  8. Sign a transaction
  9. Encrypt/Decrypt Data
  10. Export A Key
  11. Clone the Keychain Instance
  12. secp256k1 signer
  13. ed25519 signer

@exodus/keychain

The keychain is a module designed to work more securely with private key material. It can be compared with a walled garden from which private keys should not escape. All operations using private keys, such as signing and encryption data should be executed within the module, with KeyIdentifiers used to specify which key to use for which operation. Notice the "should," as we're not quite there yet.

In its current state, this library aims to provide a good interface for working with cryptographic material. However, it has some security limitations, which are on our roadmap to address:

  • Private key material is passed directly to asset libraries which can contain code by third party developers. This is on our roadmap to eliminate by refactoring asset libraries to accept signing functions instead of keys.
  • Private keys can be exported, via keychain.exportKey
  • keychain.lock() does not guarantee that private keys get completely cleared from memory

Install

pnpm add @exodus/keychain

Usage

See examples in ./module/__tests__/example.integration-test.js.

Documented Usage Paths

Check here

Create A Key Identifier

In order to interact with a private key, you must first specify how it's accessed. A KeyIdentifier must be created, for assets there is a helpful KeyIdentifier class that will do the heavy lifting.

import KeyIdentifier from '@exodus/key-identifier'

const keyId = new KeyIdentifier({
  assetName: 'solana',
  derivationAlgorithm: 'BIP32',
  derivationPath: "m/44'/501'/0'/0/0",
  keyType: 'nacl',
})

Seed Identifier

Most keychain operations require passing in a seed identifier (seedId) in addition to a KeyIdentifier. A seedId is a hex-encoded BIP32 identifier of the seed's master key (see ./module/crypto/seed-id.js).

Adding/Removing Seeds

Before you can perform keychain operations, you must provide it a seed via keychain.unlock(seed). A keychain holds a single seed: re-unlocking the same seed is idempotent, and unlocking a different seed replaces the current one (last-write-wins) — any external private keys added via addExternalPrivateKey survive the swap. Calling keychain.lock() will remove the seed and any derived cryptographic material from its internal fields.

const seed = await mnemonicToSeed({
  mnemonic: 'menu memory fury language physical wonder dog valid smart edge decrease worth',
})

await keychain.unlock(seed)
// ...
keychain.lock()

Lock State

The keychain has two distinct lifecycle primitives:

  • unlock(seed) / lock() — the hard path. unlock installs derived master keys and transitions the keychain into the unlocked state; lock wipes all master keys (both mnemonic-derived and external) from memory and resets lock state. Used by wallet.unlock / wallet.lock.
  • lockPrivateKeys() / unlockPrivateKeys() / arePrivateKeysLocked() — the soft path. Flips an internal lock flag without wiping mnemonic-derived masters, so unlock doesn't have to re-derive. Note: lockPrivateKeys() also hard-deletes any external (ext:) keys added via addExternalPrivateKey; they will not return on unlockPrivateKeys() and must be re-added.

When the keychain is locked, calls that need private key material (signBuffer, exportKey with exportPrivate: true, the asset-specific signers) throw private keys are locked. Public-only operations (getPublicKey, exportKey without exportPrivate) work while locked.

Sign a transaction

Use keychain.signBuffer(...) to sign serialized transactions for a given key identifier.

import { mnemonicToSeed } from '@exodus/bip39'
import keychainDefinition, { KeyIdentifier } from '..'

const keyId = new KeyIdentifier({
  assetName: 'solana',
  derivationAlgorithm: 'BIP32',
  derivationPath: "m/44'/501'/0'/0/0",
  keyType: 'nacl',
})

const DATA =
  '010001033c8939b872876416b1ba97d04c6a31211e39258a82d0fa45542a1cccc2617d2f2c2e85e395109a73ab754dfdad48d2cdefae040d4653228245df6fe6b6d24f7300000000000000000000000000000000000000000000000000000000000000004f968728ba006a647883abdd1b8eabde24e181c8bb8e769256f9a37e73b8727901020200010c02000000b4ebad0200000000'

const result = await keychain.signBuffer({
  seedId,
  keyId,
  signatureType: 'ed25519',
  data: Buffer.from(DATA, 'hex'),
})

// result.toString('hex') === '810cdc7d804dcfab90147e50c40b0afe1f9d01fa6933739032d761f7fca4226389d348d70478560845ae9e90a940ef4173e17690b9d93122aadd56fa56b8b609'

Encrypt/Decrypt Data

Note: the below follow libsodium terminology for encryptSecretBox/encryptBox/encryptSealedBox.

encryptSecretBox/decryptSecretBox

const ALICE_KEY = new KeyIdentifier({
  derivationAlgorithm: 'SLIP10',
  derivationPath: `m/0'/2'/0'`,
  keyType: 'nacl',
})

const sodiumEncryptor = keychain.sodium.createEncryptor({ keyId: ALICE_KEY })
const plaintext = 'I really love keychains'
const ciphertext = await sodiumEncryptor.encryptSecretBox({
  seedId,
  data: plaintext,
})

const decrypted = await sodiumEncryptor.decryptSecretBox({
  seedId,
  data: ciphertext,
})

// decrypted.toString() === plaintext

encryptBox/decryptBox

const aliceSodiumEncryptor = keychain.sodium.createEncryptor({ keyId: ALICE_KEY })
const bobSodiumEncryptor = keychain.sodium.createEncryptor({ keyId: BOB_KEY })
const plaintext = 'I really love keychains'
const {
  box: { publicKey: bobPublicKey },
} = await bobSodiumEncryptor.getSodiumKeysFromSeed({ seedId })
const ciphertext = await aliceSodiumEncryptor.encryptBox({
  seedId,
  data: plaintext,
  toPublicKey: bobPublicKey,
})
const {
  box: { publicKey: alicePublicKey },
} = await aliceSodiumEncryptor.getSodiumKeysFromSeed({ seedId })

const decrypted = await bobSodiumEncryptor.decryptBox({
  seedId,
  data: ciphertext,
  fromPublicKey: alicePublicKey,
})

// decrypted.toString() === plaintext

encryptSealedBox/decryptSealedBox

const aliceSodiumEncryptor = keychain.sodium.createEncryptor({ keyId: ALICE_KEY })
const bobSodiumEncryptor = keychain.sodium.createEncryptor({ keyId: BOB_KEY })
const plaintext = 'I really love keychains'
const {
  box: { publicKey: bobPublicKey },
} = await bobSodiumEncryptor.getSodiumKeysFromSeed({ seedId })

const ciphertext = await aliceSodiumEncryptor.encryptSealedBox({
  seedId,
  data: plaintext,
  toPublicKey: bobPublicKey,
})

const decrypted = await bobSodiumEncryptor.decryptSealedBox({
  seedId,
  data: ciphertext,
})

// decrypted.toString() === plaintext

Export A Key

Export public and/or private key material.

// { xpub, publicKey }
const publicKey = await keychain.exportKey({ seedId, keyId })
// { xpub, xpriv, publicKey, privateKey }
const privateKey = await keychain.exportKey({ seedId, keyId, exportPrivate: true })

Clone the Keychain Instance

Clone the keychain, minus any cryptographic material. This is equivalent to re-invoking the keychain factory with the same parameters.

secp256k1 signer

Sign a buffer using ECDSA with curve secp256k1.

const keyId = new KeyIdentifier({
  derivationAlgorithm: 'SLIP10',
  derivationPath: `m/73'/2'/0'`,
  keyType: 'nacl',
})

const signer = keychain.secp256k1.createSigner({ keyId })
const plaintext = Buffer.from('I really love keychains')
const signature = await signer.signBuffer({ seedId, data: plaintext })

ed25519 signer

Sign a buffer using EdDSA with curve ed25519.

const keyId = new KeyIdentifier({
  derivationAlgorithm: 'SLIP10',
  derivationPath: `m/73'/2'/0'`,
  keyType: 'nacl',
})

const signer = keychain.ed25519.createSigner({ keyId })
const plaintext = Buffer.from('I really love keychains')
const signature = await signer.signBuffer({ seedId, data: plaintext })

版本与发布记录

@exodus/keychain 共发布 79 个版本,首次发布于 2022/06/22;过去 12 个月发布了 8 个版本。

版本发布日期距今
12.1.0latest2026/09/221 个月前
12.0.02026/06/194 个月前
11.0.02026/06/035 个月前
10.1.02026/05/285 个月前
10.0.02026/05/126 个月前
9.0.32026/02/109 个月前

其他发布通道:@exodus/[email protected] → 1.1.0。

关于 @exodus/keychain 的常见问题

@exodus/keychain 是什么?

A module designed to work more securely with private key material

如何安装 @exodus/keychain?

在项目目录运行 npm install @exodus/keychain,或使用 pnpm add / yarn add / bun add。

@exodus/keychain 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@exodus/keychain 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@exodus/keychain 可以商用吗?

@exodus/keychain 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@exodus/keychain 的下载量有多少?

根据 npm 官方下载统计,@exodus/keychain 在 2026/10/14 – 2026/11/12 这 30 天内被下载 43,186 次,日均约 1,440 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@exodus/keychain 还在维护吗?

最新版本 12.1.0 发布于 2026/09/22(1 个月前);至今共发布 79 个版本,其中过去 12 个月发布了 8 个;首次发布于 2022/06/22。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。