Skip to content

Commit f1cccb0

Browse files
committed
chore: wip
1 parent 19cfffb commit f1cccb0

1 file changed

Lines changed: 152 additions & 149 deletions

File tree

src/pbe.ts

Lines changed: 152 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import type { PemMessage, PemHeader, Pem } from './pem'
2525
import { aes } from './aes'
2626
import { asn1 } from './asn1'
2727
import { wrapRsaPrivateKey, privateKeyToAsn1, privateKeyFromAsn1 } from './rsa'
28-
import { createCipher } from './cipher'
28+
import { createCipher as createCipherOriginal } from './cipher'
2929
import { sha1 } from './sha1'
3030
import { des } from './des'
3131
import { oids } from './oids'
@@ -37,6 +37,25 @@ import { bytesToHex, createBuffer, hexToBytes } from './utils'
3737
import { pem } from './pem'
3838
import { Buffer } from 'buffer'
3939

40+
// Error codes enum
41+
export enum PBEErrorCode {
42+
INVALID_PARAMS = 'INVALID_PARAMS',
43+
DECRYPTION_FAILED = 'DECRYPTION_FAILED',
44+
UNSUPPORTED_ALGORITHM = 'UNSUPPORTED_ALGORITHM'
45+
}
46+
47+
// Custom error class
48+
export class PBEError extends Error {
49+
constructor(
50+
message: string,
51+
public code: PBEErrorCode,
52+
public details?: Record<string, unknown>
53+
) {
54+
super(message)
55+
this.name = 'PBEError'
56+
}
57+
}
58+
4059
interface CustomError extends Error {
4160
algorithm?: string
4261
oid?: string
@@ -106,14 +125,28 @@ interface PBECipherInfo {
106125
ivLength: number
107126
}
108127

109-
interface PBEAlgorithms {
128+
interface PBEAlgorithmsMap {
110129
[key: string]: PBECipherInfo
111130
}
112131

113-
interface SHA512API {
132+
interface SHA512APIMap {
114133
[key: string]: MessageDigest
115134
}
116135

136+
// Constants
137+
const DEFAULT_ENCRYPTION_PARAMS = {
138+
prfAlgorithm: 'hmacWithSHA1'
139+
} as const
140+
141+
// PBE algorithms configuration
142+
const pbeAlgorithms: PBEAlgorithmsMap = {
143+
'aes128-CBC': { cipher: 'AES-CBC', keyLength: 16, ivLength: 16 },
144+
'aes192-CBC': { cipher: 'AES-CBC', keyLength: 24, ivLength: 16 },
145+
'aes256-CBC': { cipher: 'AES-CBC', keyLength: 32, ivLength: 16 },
146+
'des-EDE3-CBC': { cipher: '3DES-CBC', keyLength: 24, ivLength: 8 },
147+
'desCBC': { cipher: 'DES-CBC', keyLength: 8, ivLength: 8 }
148+
}
149+
117150
function convertToString(input: ByteStringBuffer | string): string {
118151
if (typeof input === 'string')
119152
return input
@@ -367,7 +400,7 @@ export function encryptPrivateKeyInfo(obj: any, password: string, options: Encry
367400
dkLen = 8
368401
ivLen = 8
369402
encOid = oids.desCBC
370-
cipherFn = (key) => createCipher('DES-CBC', convertToString(key))
403+
cipherFn = (key) => createCipherOriginal('DES-CBC', convertToString(key))
371404
break
372405
default:
373406
const error: CustomError = new Error('Cannot encrypt private key. Unknown encryption algorithm.')
@@ -677,90 +710,76 @@ export function encryptRsaPrivateKey(rsaKey: any, password: string, options: Enc
677710
* @return the RSA key on success, null on failure.
678711
*/
679712
export function decryptRsaPrivateKey(pemKey: string, password: string): any {
680-
let rval = null
681-
682-
const msg = pem.decode(pemKey)[0]
713+
if (!pemKey || !password) {
714+
throw new PBEError(
715+
'PEM key and password are required',
716+
PBEErrorCode.INVALID_PARAMS
717+
)
718+
}
683719

684-
if (msg.type !== 'ENCRYPTED PRIVATE KEY' && msg.type !== 'PRIVATE KEY' && msg.type !== 'RSA PRIVATE KEY') {
685-
const error: CustomError = new Error('Could not convert private key from PEM; PEM header type is not "ENCRYPTED PRIVATE KEY", "PRIVATE KEY", or "RSA PRIVATE KEY".')
686-
error.headerType = msg.type
687-
throw error
720+
let obj: Asn1Object
721+
try {
722+
obj = pemToEncryptedPrivateKey(pemKey)
723+
} catch (e) {
724+
throw new PBEError(
725+
'Invalid PEM format',
726+
PBEErrorCode.INVALID_PARAMS,
727+
{ error: e }
728+
)
688729
}
689730

690-
if (msg.procType && msg.procType.type === 'ENCRYPTED') {
691-
let dkLen
692-
let cipherFn
693-
switch (msg.dekInfo?.algorithm) {
694-
case 'DES-CBC':
695-
dkLen = 8
696-
cipherFn = des.createDecryptionCipher
697-
break
698-
case 'DES-EDE3-CBC':
699-
dkLen = 24
700-
cipherFn = des.createDecryptionCipher
701-
break
702-
case 'AES-128-CBC':
703-
dkLen = 16
704-
cipherFn = aes.createDecryptionCipher
705-
break
706-
case 'AES-192-CBC':
707-
dkLen = 24
708-
cipherFn = aes.createDecryptionCipher
709-
break
710-
case 'AES-256-CBC':
711-
dkLen = 32
712-
cipherFn = aes.createDecryptionCipher
713-
break
714-
case 'RC2-40-CBC':
715-
dkLen = 5
716-
cipherFn = function (key: string) {
717-
return rc2.createDecryptionCipher(key, 40)
718-
}
719-
break
720-
case 'RC2-64-CBC':
721-
dkLen = 8
722-
cipherFn = function (key: string) {
723-
return rc2.createDecryptionCipher(key, 64)
724-
}
725-
break
726-
case 'RC2-128-CBC':
727-
dkLen = 16
728-
cipherFn = function (key: string) {
729-
return rc2.createDecryptionCipher(key, 128)
730-
}
731-
break
732-
default:
733-
const error: CustomError = new Error(`Could not decrypt private key; unsupported encryption algorithm "${msg.dekInfo?.algorithm}".`)
734-
error.algorithm = msg.dekInfo?.algorithm
735-
throw error
736-
}
731+
// Try modern PKCS#8 first
732+
try {
733+
const params = extractEncryptionParams(obj)
734+
const key = deriveKeyPBKDF2(password, {
735+
salt: params.salt,
736+
iterationCount: params.iterationCount,
737+
prf: DEFAULT_ENCRYPTION_PARAMS.prfAlgorithm
738+
})
737739

738-
// use OpenSSL legacy key derivation
739-
const iv = hexToBytes(msg.dekInfo.parameters ?? '')
740-
const dk = opensslDeriveBytes(password, iv.substr(0, 8), dkLen, sha1.create())
741-
const cipher = cipherFn(createBuffer(dk))
742-
cipher.start(iv)
743-
cipher.update(createBuffer(msg.body))
744-
745-
if (cipher.finish())
746-
rval = cipher.output?.getBytes()
747-
else
748-
return rval
749-
}
750-
else {
751-
rval = msg.body
752-
}
740+
validateKey(key, pbeAlgorithms[params.algorithm].keyLength)
741+
validateIV(params.iv, pbeAlgorithms[params.algorithm].ivLength)
753742

754-
if (msg.type === 'ENCRYPTED PRIVATE KEY')
755-
rval = decryptPrivateKeyInfo(asn1.fromDer(rval), password)
756-
else
757-
// decryption already performed above
758-
rval = asn1.fromDer(rval)
743+
const decipher = createCipher(
744+
pbeAlgorithms[params.algorithm].cipher,
745+
key
746+
)
759747

760-
if (rval !== null)
761-
rval = privateKeyFromAsn1(rval)
748+
decipher.start({ iv: params.iv })
749+
decipher.update(params.encryptedData)
762750

763-
return rval
751+
if (!decipher.finish()) {
752+
throw new PBEError(
753+
'Failed to decrypt private key',
754+
PBEErrorCode.DECRYPTION_FAILED
755+
)
756+
}
757+
758+
if (!decipher.output) {
759+
throw new PBEError(
760+
'No output from cipher',
761+
PBEErrorCode.DECRYPTION_FAILED
762+
)
763+
}
764+
765+
return asn1ToPrivateKey(unwrapRsaPrivateKey(asn1.fromDer(decipher.output.bytes())))
766+
} catch (e) {
767+
if (e instanceof PBEError && e.code === PBEErrorCode.INVALID_PARAMS) {
768+
try {
769+
return decryptRsaPrivateKeyLegacy(pemKey, password)
770+
} catch (legacyError) {
771+
throw new PBEError(
772+
'Failed to decrypt private key (both modern and legacy formats)',
773+
PBEErrorCode.DECRYPTION_FAILED,
774+
{
775+
modernError: e,
776+
legacyError
777+
}
778+
)
779+
}
780+
}
781+
throw e
782+
}
764783
}
765784

766785
/**
@@ -949,23 +968,23 @@ export function getCipherForPBES2(oid: string, params: any, password: string): B
949968
switch (oids[oid]) {
950969
case 'aes128-CBC':
951970
dkLen = 16
952-
cipherFn = createCipher('AES-CBC', key)
971+
cipherFn = createCipherOriginal('AES-CBC', key)
953972
break
954973
case 'aes192-CBC':
955974
dkLen = 24
956-
cipherFn = createCipher('AES-CBC', key)
975+
cipherFn = createCipherOriginal('AES-CBC', key)
957976
break
958977
case 'aes256-CBC':
959978
dkLen = 32
960-
cipherFn = createCipher('AES-CBC', key)
979+
cipherFn = createCipherOriginal('AES-CBC', key)
961980
break
962981
case 'des-EDE3-CBC':
963982
dkLen = 24
964-
cipherFn = createCipher('3DES-CBC', key)
983+
cipherFn = createCipherOriginal('3DES-CBC', key)
965984
break
966985
case 'desCBC':
967986
dkLen = 8
968-
cipherFn = createCipher('DES-CBC', key)
987+
cipherFn = createCipherOriginal('DES-CBC', key)
969988
break
970989
}
971990

@@ -1213,89 +1232,73 @@ function bufferToString(buf: ByteStringBuffer): string {
12131232
function createCipher(algorithm: string, key: ByteStringBuffer | string): BlockCipher {
12141233
const keyBuffer = typeof key === 'string' ? stringToBuffer(key) : key;
12151234
switch (algorithm) {
1216-
case 'aes128-CBC':
1217-
return createAESCipher(keyBuffer, '128');
1218-
case 'aes192-CBC':
1219-
return createAESCipher(keyBuffer, '192');
1220-
case 'aes256-CBC':
1221-
return createAESCipher(keyBuffer, '256');
1222-
case 'des-EDE3-CBC':
1223-
return createDESCipher(keyBuffer);
1235+
case 'AES-CBC':
1236+
return aes.createEncryptionCipher(keyBuffer.toString(), '128');
1237+
case '3DES-CBC':
1238+
return des.createEncryptionCipher(keyBuffer.toString(), createBuffer(getBytesSync(8)));
1239+
case 'DES-CBC':
1240+
return des.createEncryptionCipher(keyBuffer.toString(), createBuffer(getBytesSync(8)));
12241241
default:
12251242
throw new Error(`Unsupported cipher algorithm: ${algorithm}`);
12261243
}
12271244
}
12281245

1229-
// Fix type issues in decryptRsaPrivateKey function
1230-
export function decryptRsaPrivateKey(pemKey: string, password: string): any {
1231-
if (!pemKey || !password) {
1246+
function validateKey(key: ByteStringBuffer, expectedLength: number): void {
1247+
if (key.length() !== expectedLength) {
12321248
throw new PBEError(
1233-
'PEM key and password are required',
1249+
`Invalid key length: expected ${expectedLength}, got ${key.length()}`,
12341250
PBEErrorCode.INVALID_PARAMS
12351251
);
12361252
}
1253+
}
12371254

1238-
let obj;
1239-
try {
1240-
obj = pemToEncryptedPrivateKey(pemKey);
1241-
} catch (e) {
1255+
function validateIV(iv: ByteStringBuffer, expectedLength: number): void {
1256+
if (iv.length() !== expectedLength) {
12421257
throw new PBEError(
1243-
'Invalid PEM format',
1244-
PBEErrorCode.INVALID_PARAMS,
1245-
{ error: e }
1258+
`Invalid IV length: expected ${expectedLength}, got ${iv.length()}`,
1259+
PBEErrorCode.INVALID_PARAMS
12461260
);
12471261
}
1262+
}
12481263

1249-
// Try modern PKCS#8 first
1250-
try {
1251-
const params = extractEncryptionParams(obj);
1252-
const key = deriveKeyPBKDF2(password, {
1253-
salt: params.salt,
1254-
iterationCount: params.iterationCount,
1255-
prf: DEFAULT_ENCRYPTION_PARAMS.prfAlgorithm
1256-
});
1257-
1258-
validateKey(key, pbeAlgorithms[params.algorithm].keyLength);
1259-
validateIV(params.iv, pbeAlgorithms[params.algorithm].ivLength);
1260-
1261-
const decipher = createCipher(
1262-
pbeAlgorithms[params.algorithm].cipher,
1263-
key
1264+
function pemToEncryptedPrivateKey(pemKey: string): Asn1Object {
1265+
const msg = pem.decode(pemKey)[0];
1266+
if (msg.type !== 'ENCRYPTED PRIVATE KEY') {
1267+
throw new PBEError(
1268+
'Invalid PEM format: not an encrypted private key',
1269+
PBEErrorCode.INVALID_PARAMS
12641270
);
1271+
}
1272+
return asn1.fromDer(msg.body);
1273+
}
12651274

1266-
decipher.start({ iv: params.iv });
1267-
decipher.update(params.encryptedData);
1268-
const result = decipher.finish();
1269-
1270-
if (!result.success) {
1271-
throw new PBEError(
1272-
'Failed to decrypt private key',
1273-
PBEErrorCode.DECRYPTION_FAILED
1274-
);
1275-
}
1275+
function deriveKeyPBKDF2(
1276+
password: string,
1277+
options: {
1278+
salt: ByteStringBuffer
1279+
iterationCount: number
1280+
prf: string
1281+
}
1282+
): ByteStringBuffer {
1283+
const { salt, iterationCount, prf } = options;
1284+
const md = prfAlgorithmToMessageDigest(prf);
1285+
return createBuffer(pbkdf2(password, salt.bytes(), iterationCount, 32, md));
1286+
}
12761287

1277-
return asn1ToPrivateKey(unwrapRsaPrivateKey(asn1.fromDer(result.output)));
1278-
} catch (e) {
1279-
// If modern decryption fails, try legacy format
1280-
if (e instanceof PBEError && e.code === PBEErrorCode.INVALID_PARAMS) {
1281-
try {
1282-
return decryptRsaPrivateKeyLegacy(pemKey, password);
1283-
} catch (legacyError) {
1284-
throw new PBEError(
1285-
'Failed to decrypt private key (both modern and legacy formats)',
1286-
PBEErrorCode.DECRYPTION_FAILED,
1287-
{
1288-
modernError: e,
1289-
legacyError
1290-
}
1291-
);
1292-
}
1293-
}
1294-
throw e;
1288+
function unwrapRsaPrivateKey(obj: Asn1Object): Asn1Object {
1289+
if (obj.type === asn1.Type.SEQUENCE) {
1290+
return obj;
12951291
}
1292+
throw new PBEError(
1293+
'Invalid private key format',
1294+
PBEErrorCode.INVALID_PARAMS
1295+
);
1296+
}
1297+
1298+
function asn1ToPrivateKey(obj: Asn1Object): any {
1299+
return privateKeyFromAsn1(obj);
12961300
}
12971301

1298-
// Fix type issues in extractEncryptionParams function
12991302
function extractEncryptionParams(obj: any): {
13001303
algorithm: string;
13011304
salt: ByteStringBuffer;

0 commit comments

Comments
 (0)