@libp2p/keychain
Version:
Key management and cryptographically protected messages
175 lines (144 loc) • 5.96 kB
text/typescript
import { AES_GCM } from '@libp2p/crypto/ciphers'
import { privateKeyFromProtobuf, privateKeyFromRaw } from '@libp2p/crypto/keys'
import webcrypto from '@libp2p/crypto/webcrypto'
import { InvalidParametersError } 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 { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { ITERATIONS, KEY_SIZE } from './constants.ts'
import type { PrivateKey, RSAPrivateKey } from '@libp2p/interface'
/**
* Converts an exported private key into its representative object.
*
* Supported formats are 'pem' (RSA only) and 'libp2p-key'.
*/
export async function importPrivateKey (encryptedKey: string, password: string): Promise<PrivateKey> {
try {
const key = await importer(encryptedKey, password)
return privateKeyFromProtobuf(key)
} catch {
// Ignore and try the old pem decrypt
}
if (!encryptedKey.includes('BEGIN')) {
throw new InvalidParametersError('Encrypted key was not a libp2p-key or a PEM file')
}
return importFromPem(encryptedKey, password)
}
/**
* Attempts to decrypt a base64 encoded PrivateKey string
* with the given password. The privateKey must have been exported
* using the same password and underlying cipher (aes-gcm)
*/
export async function importer (privateKey: string, password: string): Promise<Uint8Array> {
const encryptedKey = base64.decode(privateKey)
const cipher = AES_GCM.create()
return cipher.decrypt(encryptedKey, password)
}
export async function importFromPem (pem: string, password: string): Promise<RSAPrivateKey> {
const crypto = webcrypto.get()
let plaintext: Uint8Array
if (pem.includes('-----BEGIN ENCRYPTED PRIVATE KEY-----')) {
const key = uint8ArrayFromString(
pem
.replace('-----BEGIN ENCRYPTED PRIVATE KEY-----', '')
.replace('-----END ENCRYPTED PRIVATE KEY-----', '')
.replace(/\n/g, '')
.trim(),
'base64pad'
)
const { result } = asn1js.fromBER(key)
const {
iv,
salt,
iterations,
keySize,
cipherText
} = findEncryptedPEMData(result)
const encryptionKey = await pbkdf2Async(
sha512,
password,
salt, {
c: iterations,
dkLen: keySize
}
)
const cryptoKey = await crypto.subtle.importKey('raw', encryptionKey, 'AES-CBC', false, ['decrypt'])
const decrypted = toUint8Array(await crypto.subtle.decrypt({
name: 'AES-CBC',
iv
}, cryptoKey, cipherText))
const { result: decryptedResult } = asn1js.fromBER(decrypted)
plaintext = findPEMData(decryptedResult)
} else if (pem.includes('-----BEGIN PRIVATE KEY-----')) {
const key = uint8ArrayFromString(
pem
.replace('-----BEGIN PRIVATE KEY-----', '')
.replace('-----END PRIVATE KEY-----', '')
.replace(/\n/g, '')
.trim(),
'base64pad'
)
const { result } = asn1js.fromBER(key)
plaintext = findPEMData(result)
} else {
throw new InvalidParametersError('Could not parse private key from PEM data')
}
const key = privateKeyFromRaw(plaintext)
if (key.type !== 'RSA') {
throw new InvalidParametersError('Could not parse RSA private key from PEM data')
}
return key
}
function findEncryptedPEMData (root: any): { cipherText: Uint8Array, iv: Uint8Array, salt: Uint8Array, iterations: number, keySize: number } {
const encryptionAlgorithm = root.valueBlock.value[0]
const scheme = encryptionAlgorithm.valueBlock.value[0].toString()
if (scheme !== 'OBJECT IDENTIFIER : 1.2.840.113549.1.5.13') {
throw new InvalidParametersError('Only pkcs5PBES2 encrypted private keys are supported')
}
const keyDerivationFunc = encryptionAlgorithm.valueBlock.value[1].valueBlock.value[0]
const keyDerivationFuncName = keyDerivationFunc.valueBlock.value[0].toString()
if (keyDerivationFuncName !== 'OBJECT IDENTIFIER : 1.2.840.113549.1.5.12') {
throw new InvalidParametersError('Only pkcs5PBKDF2 key derivation functions are supported')
}
const pbkdf2Params = keyDerivationFunc.valueBlock.value[1]
const salt = toUint8Array(pbkdf2Params.valueBlock.value[0].getValue())
let iterations = ITERATIONS
let keySize = KEY_SIZE
if (pbkdf2Params.valueBlock.value.length === 3) {
iterations = Number((pbkdf2Params.valueBlock.value[1] as asn1js.Integer).toBigInt())
keySize = Number((pbkdf2Params.valueBlock.value[2]).toBigInt())
} else if (pbkdf2Params.valueBlock.value.length === 2) {
throw new InvalidParametersError('Could not derive key size and iterations from PEM file - please use @libp2p/rsa to re-import your key')
}
const encryptionScheme = encryptionAlgorithm.valueBlock.value[1].valueBlock.value[1]
const encryptionSchemeName = encryptionScheme.valueBlock.value[0].toString()
if (encryptionSchemeName === 'OBJECT IDENTIFIER : 1.2.840.113549.3.7') {
// des-EDE3-CBC
} else if (encryptionSchemeName === 'OBJECT IDENTIFIER : 1.3.14.3.2.7') {
// des-CBC
} else if (encryptionSchemeName === 'OBJECT IDENTIFIER : 2.16.840.1.101.3.4.1.2') {
// aes128-CBC
} else if (encryptionSchemeName === 'OBJECT IDENTIFIER : 2.16.840.1.101.3.4.1.22') {
// aes192-CBC
} else if (encryptionSchemeName === 'OBJECT IDENTIFIER : 2.16.840.1.101.3.4.1.42') {
// aes256-CBC
} else {
throw new InvalidParametersError('Only AES-CBC encryption schemes are supported')
}
const iv = toUint8Array(encryptionScheme.valueBlock.value[1].getValue())
return {
cipherText: toUint8Array(root.valueBlock.value[1].getValue()),
salt,
iterations,
keySize,
iv
}
}
function findPEMData (seq: any): Uint8Array {
return toUint8Array(seq.valueBlock.value[2].getValue())
}
function toUint8Array (buf: ArrayBuffer): Uint8Array {
return new Uint8Array(buf, 0, buf.byteLength)
}