A module designed to work more securely with private key material
npm install @exodus/keychain@exodus/keychainThe 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.removeAllSeeds() does not guarantee that private keys get completely cleared from memory
```
yarn add @exodus/keychain
See examples in ./modules/\_\_tests\_\_/example.test.js.
Check here
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.
`js
import KeyIdentifier from '@exodus/key-identifier'
const keyId = new KeyIdentifier({
assetName: 'solana',
derivationAlgorithm: 'BIP32',
derivationPath: "m/44'/501'/0'/0/0",
keyType: 'nacl',
})
`
Because the keychain supports managing multiple seeds at once, most operations require passing in a seed identifier (seedId) in addition to a KeyIdentifier. A seedId is a hex-encoded BIP32 identifier of seed's master key (see ./module/crypto/seed-id.js).
Before you can perform keychain operations, you must provide it one or more seeds via keychain.addSeed(seed). Calling keychain.removeAllSeeds() will remove all previously added seeds and any derived cryptographic material from its internal fields.
`js
const seed = await mnemonicToSeed({
mnemonic: 'menu memory fury language physical wonder dog valid smart edge decrease worth',
})
await keychain.addSeed(seed)
await keychain.addSeed(secondSeed)
await keychain.addSeed(thirdSeed)
// ...
keychain.removeAllSeeds()
`
Use keychain.signBuffer(...) to sign serialized transactions for a given key identifier.
`js
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'
`
Note: the below follow libsodium terminology for encryptSecretBox/encryptBox/encryptSealedBox.
#### encryptSecretBox/decryptSecretBox
`jsm/0'/2'/0'
const ALICE_KEY = new KeyIdentifier({
derivationAlgorithm: 'SLIP10',
derivationPath: ,
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
`js
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
`js
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 public and/or private key material.
`js`
// { 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, _minus any cryptographic material_. This is equivalent to re-invoking the keychain factory with the same parameters.
Sign a buffer using ECDSA with curve secp256k1.
`jsm/73'/2'/0'
const keyId = new KeyIdentifier({
derivationAlgorithm: 'SLIP10',
derivationPath: ,
keyType: 'nacl',
})
const signer = keychain.secp256k1.createSigner({ keyId })
const plaintext = Buffer.from('I really love keychains')
const signature = await signer.signBuffer({ seedId, data: plaintext })
`
Sign a buffer using EdDSA with curve ed25519.
`jsm/73'/2'/0'
const keyId = new KeyIdentifier({
derivationAlgorithm: 'SLIP10',
derivationPath: ,
keyType: 'nacl',
})
const signer = keychain.ed25519.createSigner({ keyId })
const plaintext = Buffer.from('I really love keychains')
const signature = await signer.signBuffer({ seedId, data: plaintext })
``