From 96096920213b4f55dd47e82293bc65dbbddef591 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 11 Aug 2023 15:21:02 +0100 Subject: [PATCH] Split out cert setting for performance reasons --- src/bridge/AdminRoomHandler.ts | 7 +++---- src/datastore/DataStore.ts | 4 +++- src/datastore/NedbDataStore.ts | 11 ++++++++++- src/datastore/StringCrypto.ts | 7 +++++++ src/datastore/postgres/PgDataStore.ts | 25 +++++++++++++++++-------- 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/bridge/AdminRoomHandler.ts b/src/bridge/AdminRoomHandler.ts index 5b209d505..43decb92d 100644 --- a/src/bridge/AdminRoomHandler.ts +++ b/src/bridge/AdminRoomHandler.ts @@ -402,12 +402,11 @@ export class AdminRoomHandler { try { - const clientConfig = await this.getOrCreateClientConfig(sender, server); - clientConfig.setCertificate({ + const keypair = { cert: cert.toString(), key: privateKey.export({type: 'pkcs8', format: 'pem'}).toString(), - }); - await this.ircBridge.getStore().storeIrcClientConfig(clientConfig); + }; + await this.ircBridge.getStore().storeClientCert(sender, server.domain, keypair); } catch (ex) { req.log.error('Unable to store certificate for user', ex); diff --git a/src/datastore/DataStore.ts b/src/datastore/DataStore.ts index aae97b7cf..75df3a7c6 100644 --- a/src/datastore/DataStore.ts +++ b/src/datastore/DataStore.ts @@ -24,7 +24,7 @@ import { } from "matrix-appservice-bridge"; import { MatrixDirectoryVisibility } from "../bridge/IrcHandler"; import { IrcRoom } from "../models/IrcRoom"; -import { IrcClientConfig } from "../models/IrcClientConfig"; +import { IrcClientCertKeypair, IrcClientConfig } from "../models/IrcClientConfig"; import { IrcServer, IrcServerConfig } from "../irc/IrcServer"; export type RoomOrigin = "config"|"provision"|"alias"|"join"; @@ -175,6 +175,8 @@ export interface DataStore extends ProvisioningStore { removePass(userId: string, domain: string): Promise; + storeClientCert(userId: string, domain: string, keypair: IrcClientCertKeypair): Promise; + removeClientCert(userId: string, domain: string): Promise; getMatrixUserByUsername(domain: string, username: string): Promise; diff --git a/src/datastore/NedbDataStore.ts b/src/datastore/NedbDataStore.ts index a2517be12..96a94ce8d 100644 --- a/src/datastore/NedbDataStore.ts +++ b/src/datastore/NedbDataStore.ts @@ -16,7 +16,7 @@ limitations under the License. import { MatrixDirectoryVisibility } from "../bridge/IrcHandler"; import { IrcRoom } from "../models/IrcRoom"; -import { IrcClientConfig, IrcClientConfigSeralized } from "../models/IrcClientConfig" +import { IrcClientCertKeypair, IrcClientConfig, IrcClientConfigSeralized } from "../models/IrcClientConfig" import { getLogger } from "../logging"; import { @@ -701,6 +701,15 @@ export class NeDBDataStore implements DataStore { } } + public async storeClientCert(userId: string, domain: string, keypair: IrcClientCertKeypair): Promise { + const config = await this.getIrcClientConfig(userId, domain); + if (!config) { + throw new Error(`${userId} does not have an IRC client configured for ${domain}`); + } + config.setCertificate(keypair); + await this.storeIrcClientConfig(config); + } + public async removeClientCert(userId: string, domain: string): Promise { const config = await this.getIrcClientConfig(userId, domain); if (config) { diff --git a/src/datastore/StringCrypto.ts b/src/datastore/StringCrypto.ts index 366b4a718..877531ed3 100644 --- a/src/datastore/StringCrypto.ts +++ b/src/datastore/StringCrypto.ts @@ -86,9 +86,13 @@ export class StringCrypto { const password = randomBytes(32).toString(ENCRYPTED_ENCODING); const key = await scrypt(password, 'salt', 32) as Buffer; const iv = randomBytes(16); + const cipher = createCipheriv(algorithm, key, iv); cipher.setEncoding(ENCRYPTED_ENCODING); let encrypted = ''; + + // Large strings are encrypted as 'lg:encrypt($key_$iv):$encrypted_block' where the key_iv is further + // encrypted by the root private key. const secret = this.encrypt(`${key.toString(ENCRYPTED_ENCODING)}_${iv.toString(ENCRYPTED_ENCODING)}`); const streamPromise = new Promise((resolve, reject) => { cipher.on('error', (err) => reject(err)); @@ -96,6 +100,7 @@ export class StringCrypto { `lg:${secret}:${encrypted}` )); }); + cipher.on('data', (chunk) => { encrypted += chunk }); cipher.write(plaintext); cipher.end(); @@ -110,6 +115,7 @@ export class StringCrypto { const [keyB64, ivB64] = this.decrypt(keyPlusIvEnc).split('_'); const iv = Buffer.from(ivB64, ENCRYPTED_ENCODING); const key = Buffer.from(keyB64, ENCRYPTED_ENCODING); + const decipher = createDecipheriv(algorithm, key, iv); let decrypted = ''; decipher.on('data', (chunk) => { decrypted += chunk }); @@ -117,6 +123,7 @@ export class StringCrypto { decipher.on('error', (err) => reject(err)); decipher.on('end', () => resolve(decrypted)); }); + decipher.write(Buffer.from(data, ENCRYPTED_ENCODING)); decipher.end(); return streamPromise; diff --git a/src/datastore/postgres/PgDataStore.ts b/src/datastore/postgres/PgDataStore.ts index 8b73809cd..996b01f48 100644 --- a/src/datastore/postgres/PgDataStore.ts +++ b/src/datastore/postgres/PgDataStore.ts @@ -30,7 +30,7 @@ import { import { DataStore, RoomOrigin, ChannelMappings, UserFeatures } from "../DataStore"; import { MatrixDirectoryVisibility } from "../../bridge/IrcHandler"; import { IrcRoom } from "../../models/IrcRoom"; -import { IrcClientConfig, IrcClientConfigSeralized } from "../../models/IrcClientConfig"; +import { IrcClientCertKeypair, IrcClientConfig, IrcClientConfigSeralized } from "../../models/IrcClientConfig"; import { IrcServer, IrcServerConfig } from "../../irc/IrcServer"; import { getLogger } from "../../logging"; @@ -553,24 +553,17 @@ export class PgDataStore implements DataStore, ProvisioningStore { // We need to make sure we have a matrix user in the store. await this.pgPool.query("INSERT INTO matrix_users VALUES ($1, NULL) ON CONFLICT DO NOTHING", [userId]); let password = config.getPassword(); - const keypair: {cert?: string, key?: string} = { }; // This implies without a cryptostore these will be stored plain. if (password && this.cryptoStore) { password = this.cryptoStore.encrypt(password); } - if (config.certificate && this.cryptoStore) { - keypair.cert = config.certificate.cert; - keypair.key = await this.cryptoStore.encryptLargeString(config.certificate.key); - } const parameters = { user_id: userId, domain: config.getDomain(), // either use the decrypted password, or whatever is stored already. password, - cert: keypair.cert, - key: keypair.key, config: JSON.stringify(config.serialize(true)), }; const statement = PgDataStore.BuildUpsertStatement( @@ -667,6 +660,22 @@ export class PgDataStore implements DataStore, ProvisioningStore { [userId, domain]); } + public async storeClientCert(userId: string, domain: string, keypair: IrcClientCertKeypair): Promise { + if (!this.cryptoStore) { + throw Error("Password encryption is not configured.") + } + const key = this.cryptoStore.encrypt(keypair.key); + const parameters = { + user_id: userId, + domain, + cert: keypair.cert, + key, + }; + const statement = PgDataStore.BuildUpsertStatement("client_config", + "ON CONSTRAINT cons_client_config_unique", Object.keys(parameters)); + await this.pgPool.query(statement, Object.values(parameters)); + } + public async removeClientCert(userId: string, domain: string): Promise { await this.pgPool.query( "UPDATE client_config SET cert = NULL AND key = NULL WHERE user_id = $1 AND domain = $2",