Skip to content

Commit 7e28b54

Browse files
author
3nprob
committed
Migrate to symmetric encryption for passwords
* RSA is slow and can't encrypt longer texts than key length * Derive AES key from private key to avoid breaking config schema
1 parent 06e334a commit 7e28b54

File tree

1 file changed

+41
-13
lines changed

1 file changed

+41
-13
lines changed

src/datastore/StringCrypto.ts

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,28 @@ import { getLogger } from "../logging";
2121
const log = getLogger("CryptoStore");
2222

2323
export 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

Comments
 (0)