@@ -21,23 +21,28 @@ import { getLogger } from "../logging";
2121const log = getLogger ( "CryptoStore" ) ;
2222
2323export class StringCrypto {
24- private privateKey ! : string ;
24+ private secretKey ! : crypto . KeyObject ;
25+ private privateKey ! : crypto . KeyObject ;
2526
2627 public load ( pkeyPath : string ) {
2728 try {
28- this . privateKey = fs . readFileSync ( pkeyPath , "utf8" ) . toString ( ) ;
29+ const pk = fs . readFileSync ( pkeyPath , "utf8" ) . toString ( ) ;
2930
30- // Test whether key is a valid PEM key (publicEncrypt does internal validation)
3131 try {
32- crypto . publicEncrypt (
33- this . privateKey ,
34- Buffer . from ( "This is a test!" )
35- ) ;
32+ this . privateKey = crypto . createPrivateKey ( pk ) ;
3633 }
3734 catch ( err ) {
3835 log . error ( `Failed to validate private key: (${ err . message } )` ) ;
3936 throw err ;
4037 }
38+ // Derive AES key from private key hash
39+ const hash = crypto . createHash ( 'sha256' ) ;
40+ // Re-export to have robustness against formatting/whitespace for same key
41+ hash . update ( this . privateKey . export ( {
42+ type : 'pkcs1' ,
43+ format : 'der'
44+ } ) ) ;
45+ this . secretKey = crypto . createSecretKey ( hash . digest ( ) ) ;
4146
4247 log . info ( `Private key loaded from ${ pkeyPath } - IRC password encryption enabled.` ) ;
4348 }
@@ -48,19 +53,42 @@ export class StringCrypto {
4853 }
4954
5055 public encrypt ( plaintext : string ) : string {
51- const salt = crypto . randomBytes ( 16 ) . toString ( 'base64' ) ;
52- return crypto . publicEncrypt (
53- this . privateKey ,
54- Buffer . from ( salt + ' ' + plaintext )
55- ) . toString ( 'base64' ) ;
56+ const iv = crypto . randomBytes ( 16 ) ;
57+ const cipher = crypto . createCipheriv (
58+ 'aes-256-gcm' ,
59+ this . secretKey ,
60+ iv ,
61+ { authTagLength : 16 }
62+ ) ;
63+ const encrypted = Buffer . concat ( [
64+ cipher . update ( plaintext ) ,
65+ cipher . final ( )
66+ ] ) ;
67+ return [
68+ cipher . getAuthTag ( ) ,
69+ iv ,
70+ encrypted
71+ ] . map ( x => x . toString ( 'base64' ) ) . join ( '|' ) ;
5672 }
5773
5874 public decrypt ( encryptedString : string ) : string {
75+ if ( encryptedString . includes ( '|' ) ) {
76+ const [ cipherTag , iv , encrypted ] = encryptedString . split ( '|' ) . map ( x => Buffer . from ( x , 'base64' ) )
77+ const decipher = crypto . createDecipheriv (
78+ 'aes-256-gcm' ,
79+ this . secretKey as any , // eslint-disable-line @typescript-eslint/no-explicit-any
80+ iv ,
81+ { authTagLength : 16 }
82+ ) ;
83+ decipher . setAuthTag ( cipherTag ) ;
84+ return [ decipher . update ( encrypted ) , decipher . final ( ) ] . join ( '' )
85+ }
86+ log . debug ( 'Could not decrypt string with derived secret key; falling back to asymmetric scheme' ) ;
5987 const decryptedPass = crypto . privateDecrypt (
6088 this . privateKey ,
6189 Buffer . from ( encryptedString , 'base64' )
6290 ) . toString ( ) ;
63- // Extract the password by removing the prefixed salt and seperating space
91+ // Extract the password by removing the prefixed salt and separating space
6492 return decryptedPass . split ( ' ' ) [ 1 ] ;
6593 }
6694}
0 commit comments