UNPKG

@libp2p/keychain

Version:

Key management and cryptographically protected messages

226 lines (198 loc) • 6.8 kB
import { randomBytes } from '@libp2p/crypto' import { AES_GCM } from '@libp2p/crypto/ciphers' import { privateKeyToProtobuf } from '@libp2p/crypto/keys' import webcrypto from '@libp2p/crypto/webcrypto' import { InvalidParametersError, UnsupportedKeyTypeError } from '@libp2p/interface' import { pbkdf2Async } from '@noble/hashes/pbkdf2.js' import { sha512 } from '@noble/hashes/sha2.js' import * as asn1js from 'asn1js' import { base64 } from 'multiformats/bases/base64' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { ITERATIONS, KEY_SIZE, SALT_LENGTH } from './constants.ts' import type { ECDSAPrivateKey, Ed25519PrivateKey, PrivateKey, RSAPrivateKey, Secp256k1PrivateKey } from '@libp2p/interface' import type { Multibase } from 'multiformats/bases/interface' /** * Exports the given PrivateKey as a base64 encoded string. * The PrivateKey is encrypted via a password derived PBKDF2 key * leveraging the aes-gcm cipher algorithm. */ export async function exporter (privateKey: Uint8Array, password: string): Promise<Multibase<'m'>> { const cipher = AES_GCM.create() const encryptedKey = await cipher.encrypt(privateKey, password) return base64.encode(encryptedKey) } export type ExportFormat = 'pkcs-8' | 'libp2p-key' /** * Converts an exported private key into its representative object. * * Supported formats are 'pem' (RSA only) and 'libp2p-key'. */ export async function exportPrivateKey (key: PrivateKey, password: string, format?: ExportFormat): Promise<Multibase<'m'>> { if (key.type === 'RSA') { return exportRSAPrivateKey(key, password, format) } if (key.type === 'Ed25519') { return exportEd25519PrivateKey(key, password, format) } if (key.type === 'secp256k1') { return exportSecp256k1PrivateKey(key, password, format) } if (key.type === 'ECDSA') { return exportECDSAPrivateKey(key, password, format) } throw new UnsupportedKeyTypeError() } /** * Exports the key into a password protected `format` */ export async function exportEd25519PrivateKey (key: Ed25519PrivateKey, password: string, format: ExportFormat = 'libp2p-key'): Promise<Multibase<'m'>> { if (format === 'libp2p-key') { return exporter(privateKeyToProtobuf(key), password) } else { throw new InvalidParametersError(`export format '${format}' is not supported`) } } /** * Exports the key into a password protected `format` */ export async function exportSecp256k1PrivateKey (key: Secp256k1PrivateKey, password: string, format: ExportFormat = 'libp2p-key'): Promise<Multibase<'m'>> { if (format === 'libp2p-key') { return exporter(privateKeyToProtobuf(key), password) } else { throw new InvalidParametersError('Export format is not supported') } } /** * Exports the key into a password protected `format` */ export async function exportECDSAPrivateKey (key: ECDSAPrivateKey, password: string, format: ExportFormat = 'libp2p-key'): Promise<Multibase<'m'>> { if (format === 'libp2p-key') { return exporter(privateKeyToProtobuf(key), password) } else { throw new InvalidParametersError(`export format '${format}' is not supported`) } } /** * Exports the key as libp2p-key - a aes-gcm encrypted value with the key * derived from the password. * * To export it as a password protected PEM file, please use the `exportPEM` * function from `@libp2p/rsa`. */ export async function exportRSAPrivateKey (key: RSAPrivateKey, password: string, format: ExportFormat = 'pkcs-8'): Promise<Multibase<'m'>> { if (format === 'pkcs-8') { return exportToPem(key, password) } else if (format === 'libp2p-key') { return exporter(privateKeyToProtobuf(key), password) } else { throw new InvalidParametersError('Export format is not supported') } } export async function exportToPem (privateKey: RSAPrivateKey, password: string): Promise<string> { const crypto = webcrypto.get() // PrivateKeyInfo const keyWrapper = new asn1js.Sequence({ value: [ // version (0) new asn1js.Integer({ value: 0 }), // privateKeyAlgorithm new asn1js.Sequence({ value: [ // rsaEncryption OID new asn1js.ObjectIdentifier({ value: '1.2.840.113549.1.1.1' }), new asn1js.Null() ] }), // PrivateKey new asn1js.OctetString({ valueHex: privateKey.raw }) ] }) const keyBuf = keyWrapper.toBER() const keyArr = new Uint8Array(keyBuf, 0, keyBuf.byteLength) const salt = randomBytes(SALT_LENGTH) const encryptionKey = await pbkdf2Async( sha512, password, salt, { c: ITERATIONS, dkLen: KEY_SIZE } ) const iv = randomBytes(16) const cryptoKey = await crypto.subtle.importKey('raw', encryptionKey, 'AES-CBC', false, ['encrypt']) const encrypted = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, cryptoKey, keyArr) const pbkdf2Params = new asn1js.Sequence({ value: [ // salt new asn1js.OctetString({ valueHex: salt }), // iteration count new asn1js.Integer({ value: ITERATIONS }), // key length new asn1js.Integer({ value: KEY_SIZE }), // AlgorithmIdentifier new asn1js.Sequence({ value: [ // hmacWithSHA512 new asn1js.ObjectIdentifier({ value: '1.2.840.113549.2.11' }), new asn1js.Null() ] }) ] }) const encryptionAlgorithm = new asn1js.Sequence({ value: [ // pkcs5PBES2 new asn1js.ObjectIdentifier({ value: '1.2.840.113549.1.5.13' }), new asn1js.Sequence({ value: [ // keyDerivationFunc new asn1js.Sequence({ value: [ // pkcs5PBKDF2 new asn1js.ObjectIdentifier({ value: '1.2.840.113549.1.5.12' }), // PBKDF2-params pbkdf2Params ] }), // encryptionScheme new asn1js.Sequence({ value: [ // aes256-CBC new asn1js.ObjectIdentifier({ value: '2.16.840.1.101.3.4.1.42' }), // iv new asn1js.OctetString({ valueHex: iv }) ] }) ] }) ] }) const finalWrapper = new asn1js.Sequence({ value: [ encryptionAlgorithm, new asn1js.OctetString({ valueHex: encrypted }) ] }) const finalWrapperBuf = finalWrapper.toBER() const finalWrapperArr = new Uint8Array(finalWrapperBuf, 0, finalWrapperBuf.byteLength) return [ '-----BEGIN ENCRYPTED PRIVATE KEY-----', ...uint8ArrayToString(finalWrapperArr, 'base64pad').split(/(.{64})/).filter(Boolean), '-----END ENCRYPTED PRIVATE KEY-----' ].join('\n') }