diff --git a/changelog.d/1463.feature b/changelog.d/1463.feature new file mode 100644 index 000000000..7639a1500 --- /dev/null +++ b/changelog.d/1463.feature @@ -0,0 +1 @@ +Enable per-client SASL certificates diff --git a/config.sample.yaml b/config.sample.yaml index 9b513033f..64e2ca2af 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -164,6 +164,17 @@ ircService: # real matrix users in them, even if there is a mapping for the channel. # Default: true joinChannelsIfNoUsers: true + # + # Explicit key/cert to use when connecting. Optional. + # When setting up with https://freenode.net/kb/answer/certfp , you can copy these from the .pem file + #sslKey: | + # -----BEGIN PRIVATE KEY----- + # ... + # -----END PRIVATE KEY----- + #saslCert: | + # -----BEGIN CERTIFICATE----- + # ... + # -----END CERTIFICATE----- # Configuration for PMs / private 1:1 communications between users. privateMessages: diff --git a/config.schema.yml b/config.schema.yml index 66194157e..fbc47d674 100644 --- a/config.schema.yml +++ b/config.schema.yml @@ -257,6 +257,10 @@ properties: type: "string" joinChannelsIfNoUsers: type: "boolean" + saslKey: + type: "string" + saslCert: + type: "string" privateMessages: type: "object" properties: diff --git a/package-lock.json b/package-lock.json index a4ea2a67a..f54c82a73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "matrix-appservice-irc", - "version": "0.33.1", + "version": "0.34.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "matrix-appservice-irc", - "version": "0.33.1", + "version": "0.34.0", "license": "Apache-2.0", "dependencies": { "@sentry/node": "^6.17.9", @@ -18,7 +18,7 @@ "logform": "^2.4.0", "matrix-appservice-bridge": "^3.2.0", "matrix-bot-sdk": "0.5.19", - "matrix-org-irc": "^1.2.0", + "matrix-org-irc": "^1.2.1", "nopt": "^3.0.1", "p-queue": "^6.6.2", "pg": "^8.7.3", @@ -40,7 +40,7 @@ "@types/extend": "^3.0.1", "@types/he": "^1.1.2", "@types/nedb": "^1.8.12", - "@types/node": "^14", + "@types/node": "^16", "@types/nopt": "^3.0.29", "@types/pg": "^8.6.4", "@types/sanitize-html": "^2.6.2", @@ -54,7 +54,7 @@ "typescript": "^4.5.5" }, "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/@alloc/quick-lru": { @@ -813,9 +813,9 @@ } }, "node_modules/@types/node": { - "version": "14.17.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.19.tgz", - "integrity": "sha512-jjYI6NkyfXykucU6ELEoT64QyKOdvaA6enOqKtP4xUsGY0X0ZUZz29fUmrTRo+7v7c6TgDu82q3GHHaCEkqZwA==" + "version": "16.11.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.33.tgz", + "integrity": "sha512-0PJ0vg+JyU0MIan58IOIFRtSvsb7Ri+7Wltx2qAg94eMOrpg4+uuP3aUHCpxXc1i0jCXiC+zIamSZh3l9AbcQA==" }, "node_modules/@types/nopt": { "version": "3.0.29", @@ -3918,9 +3918,9 @@ } }, "node_modules/matrix-org-irc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/matrix-org-irc/-/matrix-org-irc-1.2.0.tgz", - "integrity": "sha512-RnfeR9FimJJD/iOWw0GiV7NIPRmBJobvFasUgjVmGre9A4qJ9klHIDOlQ5vXIoPPMjzG8XXuAf4WHgMCNBfZkQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/matrix-org-irc/-/matrix-org-irc-1.2.1.tgz", + "integrity": "sha512-x7SoeIOP+Z6R2s8PJqhM89OZNsS2RO4srx7c3JGa/VN6rtJ1AMLEyW4EPCVh09tGiTvmbit9KJysjLvFQPx9KA==", "dependencies": { "chardet": "^1.3.0", "iconv-lite": "^0.6.2", @@ -6760,9 +6760,9 @@ } }, "@types/node": { - "version": "14.17.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.19.tgz", - "integrity": "sha512-jjYI6NkyfXykucU6ELEoT64QyKOdvaA6enOqKtP4xUsGY0X0ZUZz29fUmrTRo+7v7c6TgDu82q3GHHaCEkqZwA==" + "version": "16.11.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.33.tgz", + "integrity": "sha512-0PJ0vg+JyU0MIan58IOIFRtSvsb7Ri+7Wltx2qAg94eMOrpg4+uuP3aUHCpxXc1i0jCXiC+zIamSZh3l9AbcQA==" }, "@types/nopt": { "version": "3.0.29", @@ -9149,9 +9149,9 @@ } }, "matrix-org-irc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/matrix-org-irc/-/matrix-org-irc-1.2.0.tgz", - "integrity": "sha512-RnfeR9FimJJD/iOWw0GiV7NIPRmBJobvFasUgjVmGre9A4qJ9klHIDOlQ5vXIoPPMjzG8XXuAf4WHgMCNBfZkQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/matrix-org-irc/-/matrix-org-irc-1.2.1.tgz", + "integrity": "sha512-x7SoeIOP+Z6R2s8PJqhM89OZNsS2RO4srx7c3JGa/VN6rtJ1AMLEyW4EPCVh09tGiTvmbit9KJysjLvFQPx9KA==", "requires": { "chardet": "^1.3.0", "iconv-lite": "^0.6.2", diff --git a/package.json b/package.json index f94cfc996..be10eb061 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "matrix-appservice-irc", - "version": "0.33.1", + "version": "0.34.0", "description": "An IRC Bridge for Matrix", "main": "app.js", "bin": "./bin/matrix-appservice-irc", "engines": { - "node": ">=14" + "node": ">=16" }, "scripts": { "prepare": "npm run build", @@ -34,7 +34,7 @@ "he": "^1.2.0", "logform": "^2.4.0", "matrix-appservice-bridge": "^3.2.0", - "matrix-org-irc": "^1.2.0", + "matrix-org-irc": "^1.2.1", "matrix-bot-sdk": "0.5.19", "nopt": "^3.0.1", "p-queue": "^6.6.2", @@ -54,7 +54,7 @@ "@types/extend": "^3.0.1", "@types/he": "^1.1.2", "@types/nedb": "^1.8.12", - "@types/node": "^14", + "@types/node": "^16", "@types/nopt": "^3.0.29", "@types/pg": "^8.6.4", "@types/sanitize-html": "^2.6.2", diff --git a/src/bridge/AdminRoomHandler.ts b/src/bridge/AdminRoomHandler.ts index d9fdd2301..4594a5f2e 100644 --- a/src/bridge/AdminRoomHandler.ts +++ b/src/bridge/AdminRoomHandler.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import * as crypto from "crypto"; import { BridgeRequest } from "../models/BridgeRequest"; import { MatrixRoom, MatrixUser } from "matrix-appservice-bridge"; import { IrcBridge } from "./IrcBridge"; @@ -91,6 +92,22 @@ const COMMANDS: {[command: string]: Command|Heading} = { example: `!username [irc.example.net] username`, summary: "Store a username to use for future connections.", }, + "!storecert": { + example: `!storecert irc.example.net] -----BEGIN CERTIFICATE-----[...]`, + summary: `Store a SASL certificate for CertFP`, + }, + "!storekey": { + example: `!storekey [irc.example.net] -----BEGIN PRIVATE KEY-----[...]`, + summary: `Store a SASL private key for CertFP`, + }, + "!removecert": { + example: `!removecert [irc.example.net]`, + summary: `Remove a previously stored SASL certificate`, + }, + "!removekey": { + example: `!removekey [irc.example.net]`, + summary: `Remove a previously stored SASL private key`, + }, 'Info': { heading: true}, "!bridgeversion": { example: `!bridgeversion`, @@ -176,6 +193,14 @@ export class AdminRoomHandler { return await this.handleStorePass(req, args, event.sender); case "!removepass": return await this.handleRemovePass(args, event.sender); + case "!storekey": + return await this.handleStoreKey(req, args, event.sender); + case "!storecert": + return await this.handleStoreCert(req, args, event.sender); + case "!removekey": + return await this.handleRemoveKey(args, event.sender); + case "!removecert": + return await this.handleRemoveCert(args, event.sender); case "!listrooms": return await this.handleListRooms(args, event.sender); case "!quit": @@ -469,7 +494,7 @@ export class AdminRoomHandler { let notice; try { - // Allow passwords with spaces + // Allow usernames with spaces const username = args[0]?.trim(); if (!username) { notice = new MatrixAction( @@ -563,6 +588,143 @@ export class AdminRoomHandler { } } + private async handleStoreKey(req: BridgeRequest, args: string[], userId: string) { + const server = this.extractServerFromArgs(args); + const domain = server.domain; + let notice; + + try { + const key = args.join(' ').replace(/(-----([A-Z ]*)-----)\s*/g, '\n$1\n').trim().replace('\n\n', '\n'); + if (key.length === 0) { + notice = new MatrixAction( + "notice", + "Format: '!storekey key' or '!storepass irc.server.name key'\n" + ); + } + else { + try { + const pk = crypto.createPrivateKey(key); + const config = await this.ircBridge.getStore().getIrcClientConfig(userId, server.domain); + if (config) { + const cert = config.getSASLCert(); + if (cert) { + const c = new crypto.X509Certificate(cert); + if (!c.checkPrivateKey(pk)) { + return new MatrixAction( + "notice", + "Private key does not match stored certificate. " + + "To store a new pair, first call !removecert.\n" + ); + } + } + } + } + catch (err) { + throw new Error(`Invalid private key: ${err.message})`); + } + await this.ircBridge.getStore().storeKey(userId, domain, key); + notice = new MatrixAction( + "notice", `Successfully stored SASL key for ${domain}. Use !reconnect to reauthenticate.` + ); + } + } + catch (err) { + req.log.error(err.stack); + return new MatrixAction( + "notice", `Failed to store SASL key: ${err.message}` + ); + } + return notice; + } + + private async handleRemoveKey(args: string[], userId: string) { + const server = this.extractServerFromArgs(args); + + try { + await this.ircBridge.getStore().removeKey(userId, server.domain); + return new MatrixAction( + "notice", `Successfully removed SASL key.` + ); + } + catch (err) { + return new MatrixAction( + "notice", `Failed to remove SASL key: ${err.message}` + ); + } + } + + private async handleStoreCert(req: BridgeRequest, args: string[], userId: string) { + const server = this.extractServerFromArgs(args); + const domain = server.domain; + let notice; + + try { + const cert = args.join(' ').replace(/(-----([A-Z ]*)-----)\s*/g, '\n$1\n').trim().replace('\n\n', '\n'); + if (cert.length === 0) { + notice = new MatrixAction( + "notice", + "Format: '!storecert cert' or '!storecert irc.server.name cert'\n" + ); + } + else { + let config = await this.ircBridge.getStore().getIrcClientConfig(userId, server.domain); + if (!config) { + config = IrcClientConfig.newConfig( + new MatrixUser(userId), server.domain + ); + } + let c: crypto.X509Certificate; + try { + c = new crypto.X509Certificate(cert); + const pk = config.getSASLKey(); + if (pk) { + if (!c.checkPrivateKey(crypto.createPrivateKey(pk))) { + return new MatrixAction( + "notice", + "Certificate does not match stored private key. " + + "To store a new pair, first call !removekey.\n" + ); + } + } + } + catch (err) { + throw new Error(`Invalid certificate: ${err.message})`); + } + const fingerprint512 = crypto.createHash('sha512').update(c.raw).digest('hex') + .replace(/:/g, '').toLowerCase(); + config.setSASLCert(cert); + await this.ircBridge.getStore().storeIrcClientConfig(config); + notice = new MatrixAction( + "notice", `Successfully stored SASL cert for ${domain} with fingerprint ${fingerprint512}.\n' + + 'Use !reconnect to reauthenticate.` + ); + } + } + catch (err) { + req.log.error(err.stack); + return new MatrixAction( + "notice", `Failed to store SASL cert: ${err.message}` + ); + } + return notice; + } + + private async handleRemoveCert(args: string[], userId: string) { + const server = this.extractServerFromArgs(args); + + try { + await this.ircBridge.getStore().removeCert(userId, server.domain); + return new MatrixAction( + "notice", `Successfully removed SASL cert.` + ); + } + catch (err) { + return new MatrixAction( + "notice", `Failed to remove SASL cert: ${err.message}` + ); + } + } + private async handleListRooms(args: string[], sender: string) { const server = this.extractServerFromArgs(args); diff --git a/src/bridge/RoomConfig.ts b/src/bridge/RoomConfig.ts index 447afff8c..d0ddb4845 100644 --- a/src/bridge/RoomConfig.ts +++ b/src/bridge/RoomConfig.ts @@ -58,7 +58,7 @@ export class RoomConfig { // We don't want to spend too long trying to fetch the state, so return null. return Promise.race([ internalFunc(), - new Promise(res => setTimeout(res, STATE_TIMEOUT_MS)), + new Promise(res => setTimeout(res as unknown as ((args: void) => void), STATE_TIMEOUT_MS)), // We *never* want this function to throw, as it's critical for the bridging of messages. // Instead we return null for any errors. ]).catch(ex => { diff --git a/src/datastore/DataStore.ts b/src/datastore/DataStore.ts index c67f5f1f4..599b5ba44 100644 --- a/src/datastore/DataStore.ts +++ b/src/datastore/DataStore.ts @@ -169,6 +169,14 @@ export interface DataStore { removePass(userId: string, domain: string): Promise; + storeKey(userId: string, domain: string, key: string): Promise; + + removeKey(userId: string, domain: string): Promise; + + storeCert(userId: string, domain: string, cert: string): Promise; + + removeCert(userId: string, domain: string): Promise; + getMatrixUserByUsername(domain: string, username: string): Promise; getCountForUsernamePrefix(domain: string, usernamePrefix: string): Promise; diff --git a/src/datastore/NedbDataStore.ts b/src/datastore/NedbDataStore.ts index a5ea2390a..ebc726f6d 100644 --- a/src/datastore/NedbDataStore.ts +++ b/src/datastore/NedbDataStore.ts @@ -575,6 +575,14 @@ export class NeDBDataStore implements DataStore { const decryptedPass = this.cryptoStore.decrypt(encryptedPass); clientConfig.setPassword(decryptedPass); } + const encryptedKey = clientConfig.getSASLKey(); + if (encryptedKey) { + if (!this.cryptoStore) { + throw new Error(`Cannot decrypt SASL key of ${userId} - no private key`); + } + const decryptedKey = this.cryptoStore.decrypt(encryptedKey); + clientConfig.setPassword(decryptedKey); + } return clientConfig; } @@ -604,6 +612,16 @@ export class NeDBDataStore implements DataStore { // Store the encrypted password, ready for the db config.setPassword(encryptedPass); } + const saslKey = config.getSASLKey(); + if (saslKey) { + if (!this.cryptoStore) { + throw new Error( + 'Cannot store plaintext private keys' + ); + } + const encryptedKey = this.cryptoStore.encrypt(saslKey); + config.setSASLKey(encryptedKey); + } userConfig[config.getDomain().replace(/\./g, "_")] = config.serialize(); user.set("client_config", userConfig); await this.userStore.setMatrixUser(user); @@ -648,6 +666,39 @@ export class NeDBDataStore implements DataStore { } } + public async storeKey(userId: string, domain: string, key: string) { + const config = await this.getIrcClientConfig(userId, domain); + if (!config) { + throw new Error(`${userId} does not have an IRC client configured for ${domain}`); + } + config.setSASLKey(key); + await this.storeIrcClientConfig(config); + } + + public async removeKey(userId: string, domain: string) { + const config = await this.getIrcClientConfig(userId, domain); + if (config) { + config.setSASLKey(); + await this.storeIrcClientConfig(config); + } + } + + public async storeCert(userId: string, domain: string, cert: string) { + const config = await this.getIrcClientConfig(userId, domain); + if (!config) { + throw new Error(`${userId} does not have an IRC client configured for ${domain}`); + } + config.setSASLCert(cert); + await this.storeIrcClientConfig(config); + } + + public async removeCert(userId: string, domain: string) { + const config = await this.getIrcClientConfig(userId, domain); + if (config) { + config.setSASLCert(); + await this.storeIrcClientConfig(config); + } + } public async getMatrixUserByUsername(domain: string, username: string): Promise { const domainKey = domain.replace(/\./g, "_"); const matrixUsers = await this.userStore.getByMatrixData({ diff --git a/src/datastore/postgres/PgDataStore.ts b/src/datastore/postgres/PgDataStore.ts index e007f3c6c..f63363c12 100644 --- a/src/datastore/postgres/PgDataStore.ts +++ b/src/datastore/postgres/PgDataStore.ts @@ -54,7 +54,7 @@ interface RoomRecord { export class PgDataStore implements DataStore { private serverMappings: {[domain: string]: IrcServer} = {}; - public static readonly LATEST_SCHEMA = 8; + public static readonly LATEST_SCHEMA = 9; private pgPool: Pool; private hasEnded = false; private cryptoStore?: StringCrypto; @@ -502,7 +502,7 @@ export class PgDataStore implements DataStore { public async getIrcClientConfig(userId: string, domain: string): Promise { const res = await this.pgPool.query( - "SELECT config, password FROM client_config WHERE user_id = $1 and domain = $2", + "SELECT config, password, sasl_cert, sasl_key FROM client_config WHERE user_id = $1 and domain = $2", [ userId, domain @@ -515,6 +515,10 @@ export class PgDataStore implements DataStore { if (row.password && this.cryptoStore) { config.password = this.cryptoStore.decrypt(row.password); } + if (row.sasl_key && this.cryptoStore) { + config.saslKey = this.cryptoStore.decrypt(row.sasl_key); + } + config.saslCert = row.sasl_cert; return new IrcClientConfig(userId, domain, config); } @@ -530,11 +534,16 @@ export class PgDataStore implements DataStore { if (password && this.cryptoStore) { password = this.cryptoStore.encrypt(password); } + let saslKey = config.getSASLKey(); + if (saslKey && this.cryptoStore) { + saslKey = this.cryptoStore.encrypt(saslKey); + } const parameters = { user_id: userId, domain: config.getDomain(), - // either use the decrypted password, or whatever is stored already. password, + sasl_key: saslKey, + sasl_cert: config.getSASLCert(), config: JSON.stringify(config.serialize(true)), }; const statement = PgDataStore.BuildUpsertStatement( @@ -613,6 +622,45 @@ export class PgDataStore implements DataStore { [userId, domain]); } + public async storeKey(userId: string, domain: string, key: string, encrypt = true): Promise { + let sasl_key = key; + if (encrypt) { + if (!this.cryptoStore) { + throw Error("Password encryption is not configured.") + } + sasl_key = this.cryptoStore.encrypt(sasl_key); + } + const parameters = { + user_id: userId, + domain, + sasl_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 removeKey(userId: string, domain: string): Promise { + await this.pgPool.query("UPDATE client_config SET sasl_key = NULL WHERE user_id = $1 AND domain = $2", + [userId, domain]); + } + + public async storeCert(userId: string, domain: string, cert: string): Promise { + const parameters = { + user_id: userId, + domain, + sasl_cert: cert, + }; + 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 removeCert(userId: string, domain: string): Promise { + await this.pgPool.query("UPDATE client_config SET sasl_cert = NULL WHERE user_id = $1 AND domain = $2", + [userId, domain]); + } + public async getMatrixUserByUsername(domain: string, username: string): Promise { // This will need a join const res = await this.pgPool.query( diff --git a/src/datastore/postgres/schema/v9.ts b/src/datastore/postgres/schema/v9.ts new file mode 100644 index 000000000..c83c1e33f --- /dev/null +++ b/src/datastore/postgres/schema/v9.ts @@ -0,0 +1,9 @@ +import { PoolClient } from "pg"; + +export async function runSchema(connection: PoolClient) { + await connection.query(` + ALTER TABLE client_config ADD COLUMN sasl_cert TEXT; + ALTER TABLE client_config ADD COLUMN sasl_key TEXT; + `); +} + diff --git a/src/irc/BridgedClient.ts b/src/irc/BridgedClient.ts index 43c8b8e6c..5f3609466 100644 --- a/src/irc/BridgedClient.ts +++ b/src/irc/BridgedClient.ts @@ -85,6 +85,8 @@ export class BridgedClient extends EventEmitter { private _nick: string; public readonly id: string; private readonly password?: string; + private readonly saslKey?: string; + private readonly saslCert?: string; private lastActionTs: number; private _explicitDisconnect = false; private _disconnectReason: string|null = null; @@ -135,9 +137,16 @@ export class BridgedClient extends EventEmitter { throw Error("Could not determine nick for user"); } this._nick = BridgedClient.getValidNick(chosenNick, false, this.state); - this.password = ( - clientConfig.getPassword() ? clientConfig.getPassword() : server.config.password - ); + if (isBot) { + this.password = clientConfig.getPassword() || server.config.password; + this.saslKey = clientConfig.getSASLKey() || server.config.botConfig.saslKey; + this.saslCert = clientConfig.getSASLCert() || server.config.botConfig.saslCert; + } + else { + this.password = clientConfig.getPassword(); + this.saslKey = clientConfig.getSASLKey(); + this.saslCert = clientConfig.getSASLCert(); + } this.lastActionTs = Date.now(); this.connectDefer = promiseutil.defer(); @@ -250,6 +259,10 @@ export class BridgedClient extends EventEmitter { this.server.getIpv6Prefix() ? this.clientConfig.getIpv6Address() : undefined ), encodingFallback: this.encodingFallback, + secure: { + key: this.saslKey, + cert: this.saslCert, + }, }, this.server.homeserverDomain, (inst: ConnectionInstance) => { diff --git a/src/irc/ConnectionInstance.ts b/src/irc/ConnectionInstance.ts index a0d48b4ad..907f0b13d 100644 --- a/src/irc/ConnectionInstance.ts +++ b/src/irc/ConnectionInstance.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Client } from "matrix-org-irc"; +import { Client, IrcClientOpts } from "matrix-org-irc"; import * as promiseutil from "../promiseutil"; import Scheduler from "./Scheduler"; import * as logging from "../logging"; @@ -67,6 +67,8 @@ export interface ConnectionOpts { username?: string; nick: string; secure?: { + cert?: string; + key?: string; ca?: string; }; encodingFallback: string; @@ -374,7 +376,7 @@ export class ConnectionInstance { if (!opts.nick || !server) { throw new Error("Bad inputs. Nick: " + opts.nick); } - const connectionOpts = { + const connectionOpts: IrcClientOpts = { userName: opts.username, realName: opts.realname, password: opts.password, @@ -390,10 +392,16 @@ export class ConnectionInstance { family: (server.getIpv6Prefix() || server.getIpv6Only() ? 6 : null) as 6|null, bustRfc3484: true, sasl: opts.password ? server.useSasl() : false, - secure: server.useSsl() ? server.getSecureOptions() : undefined, + secure: server.useSsl() ? { + ...server.getSecureOptions(), + ...opts.secure + } : undefined, encodingFallback: opts.encodingFallback }; + if (typeof connectionOpts.secure === 'object' && connectionOpts.secure.key && connectionOpts.secure.cert) { + connectionOpts.saslType = 'EXTERNAL'; + } // Returns: A promise which resolves to a ConnectionInstance const retryConnection = () => { const nodeClient = new Client( diff --git a/src/irc/IrcServer.ts b/src/irc/IrcServer.ts index d8abfe28d..3a9012ff1 100644 --- a/src/irc/IrcServer.ts +++ b/src/irc/IrcServer.ts @@ -73,6 +73,8 @@ export interface IrcServerConfig { enabled: boolean; password?: string; username: string; + saslKey?: string; + saslCert?: string; }; privateMessages: { enabled: boolean; @@ -387,7 +389,7 @@ export class IrcServer { public createBotIrcClientConfig() { return IrcClientConfig.newConfig( null, this.domain, this.config.botConfig.nick, this.config.botConfig.username, - this.config.botConfig.password + this.config.botConfig.password, this.config.botConfig.saslKey, this.config.botConfig.saslCert ); } diff --git a/src/models/IrcClientConfig.ts b/src/models/IrcClientConfig.ts index efe386d2b..160aa1a39 100644 --- a/src/models/IrcClientConfig.ts +++ b/src/models/IrcClientConfig.ts @@ -21,6 +21,8 @@ export interface IrcClientConfigSeralized { password?: string; nick?: string; ipv6?: string; + saslKey?: string; + saslCert?: string; } /** @@ -66,6 +68,22 @@ export class IrcClientConfig { return this.config.password; } + public setSASLKey(saslKey?: string) { + this.config.saslKey = saslKey; + } + + public getSASLKey(): string|undefined { + return this.config.saslKey; + } + + public setSASLCert(saslCert?: string) { + this.config.saslCert = saslCert; + } + + public getSASLCert(): string|undefined { + return this.config.saslCert; + } + public setDesiredNick(nick: string) { this.config.nick = nick; } @@ -86,6 +104,8 @@ export class IrcClientConfig { if (removePassword) { const clone = JSON.parse(JSON.stringify(this.config)); delete clone.password; + delete clone.saslKey; + delete clone.saslCert; return clone; } return this.config; @@ -97,16 +117,20 @@ export class IrcClientConfig { nick: this.config.nick, ipv6: this.config.ipv6, password: this.config.password ? '' : undefined, + saslKey: this.config.saslKey ? '' : undefined, }; return this.userId + "=>" + this.domain + "=" + JSON.stringify(redactedConfig); } public static newConfig(matrixUser: MatrixUser|null, domain: string, - nick?: string, username?: string, password?: string) { + nick?: string, username?: string, password?: string, + saslKey?: string, saslCert?: string) { return new IrcClientConfig(matrixUser ? matrixUser.getId() : null, domain, { - nick: nick, - username: username, - password: password + nick, + username, + password, + saslKey, + saslCert }); } } diff --git a/src/models/IrcUser.ts b/src/models/IrcUser.ts index d5ca714a0..70c5a0647 100644 --- a/src/models/IrcUser.ts +++ b/src/models/IrcUser.ts @@ -27,19 +27,25 @@ export class IrcUser extends RemoteUser { * @param {boolean} isVirtual : True if the user is not a real IRC user. * @param {string} password : The password to give to NickServ. * @param {string} username : The username of the client (for ident) + * @param {string} saslKey : The private key for SASL external auth + * @param {string} saslCert : The certifcate for SASL external auth */ constructor( public readonly server: IrcServer, public readonly nick: string, public readonly isVirtual: boolean, public readonly password: string|null = null, + public readonly saslKey: string|null = null, + public readonly saslCert: string|null = null, username: string|null = null) { super(server.domain + "__@__" + nick, { domain: server.domain, nick: nick, isVirtual: Boolean(isVirtual), password: password || null, - username: username || null + username: username || null, + saslKey: saslKey || null, + saslCert: saslCert || null }); }