@libp2p/keychain
Version:
Key management and cryptographically protected messages
137 lines • 5.88 kB
JavaScript
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.js";
/**
* Converts an exported private key into its representative object.
*
* Supported formats are 'pem' (RSA only) and 'libp2p-key'.
*/
export async function importPrivateKey(encryptedKey, password) {
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, password) {
const encryptedKey = base64.decode(privateKey);
const cipher = AES_GCM.create();
return cipher.decrypt(encryptedKey, password);
}
export async function importFromPem(pem, password) {
const crypto = webcrypto.get();
let plaintext;
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) {
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].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) {
return toUint8Array(seq.valueBlock.value[2].getValue());
}
function toUint8Array(buf) {
return new Uint8Array(buf, 0, buf.byteLength);
}
//# sourceMappingURL=import.js.map