Skip to content

Commit

Permalink
Migrate to symmetric encryption for passwords
Browse files Browse the repository at this point in the history
* RSA is slow and can't encrypt longer texts than key length
* Derive AES key from private key to avoid breaking config schema
  • Loading branch information
3nprob committed Aug 18, 2021
1 parent 06e334a commit 4030d79
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 13 deletions.
1 change: 1 addition & 0 deletions changelog.d/1462.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Change encryption algorihm for user passwords from RSA to AES (but derive AES key from RSA key).
54 changes: 41 additions & 13 deletions src/datastore/StringCrypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,28 @@ import { getLogger } from "../logging";
const log = getLogger("CryptoStore");

export class StringCrypto {
private privateKey!: string;
private secretKey!: crypto.KeyObject;
private privateKey!: crypto.KeyObject;

public load(pkeyPath: string) {
try {
this.privateKey = fs.readFileSync(pkeyPath, "utf8").toString();
const pk = fs.readFileSync(pkeyPath, "utf8").toString();

// Test whether key is a valid PEM key (publicEncrypt does internal validation)
try {
crypto.publicEncrypt(
this.privateKey,
Buffer.from("This is a test!")
);
this.privateKey = crypto.createPrivateKey(pk);
}
catch (err) {
log.error(`Failed to validate private key: (${err.message})`);
throw err;
}
// Derive AES key from private key hash
const hash = crypto.createHash('sha256');
// Re-export to have robustness against formatting/whitespace for same key
hash.update(this.privateKey.export({
type: 'pkcs1',
format: 'der'
}));
this.secretKey = crypto.createSecretKey(hash.digest());

log.info(`Private key loaded from ${pkeyPath} - IRC password encryption enabled.`);
}
Expand All @@ -48,19 +53,42 @@ export class StringCrypto {
}

public encrypt(plaintext: string): string {
const salt = crypto.randomBytes(16).toString('base64');
return crypto.publicEncrypt(
this.privateKey,
Buffer.from(salt + ' ' + plaintext)
).toString('base64');
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(
'aes-256-gcm',
this.secretKey,
iv,
{authTagLength: 16}
);
const encrypted = Buffer.concat([
cipher.update(plaintext),
cipher.final()
]);
return [
cipher.getAuthTag(),
iv,
encrypted
].map(x => x.toString('base64')).join('|');
}

public decrypt(encryptedString: string): string {
if (encryptedString.includes('|')) {
const [cipherTag, iv, encrypted] = encryptedString.split('|').map(x => Buffer.from(x, 'base64'))
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
this.secretKey as any, // eslint-disable-line @typescript-eslint/no-explicit-any
iv,
{authTagLength: 16}
);
decipher.setAuthTag(cipherTag);
return [decipher.update(encrypted), decipher.final()].join('')
}
log.debug('Could not decrypt string with derived secret key; falling back to asymmetric scheme');
const decryptedPass = crypto.privateDecrypt(
this.privateKey,
Buffer.from(encryptedString, 'base64')
).toString();
// Extract the password by removing the prefixed salt and seperating space
// Extract the password by removing the prefixed salt and separating space
return decryptedPass.split(' ')[1];
}
}

0 comments on commit 4030d79

Please sign in to comment.