@@ -28,6 +28,7 @@ import { getBridgeVersion } from "matrix-appservice-bridge";
2828import { Provisioner } from "../provisioning/Provisioner" ;
2929import { IrcProvisioningError } from "../provisioning/Schema" ;
3030import { validateChannelName } from "../models/IrcRoom" ;
31+ import { X509Certificate , createPrivateKey } from "node:crypto" ;
3132
3233const log = logging ( "AdminRoomHandler" ) ;
3334
@@ -61,6 +62,33 @@ export function parseCommandFromEvent(event: { content?: { body?: unknown }}, pr
6162 return { cmd, args } ;
6263}
6364
65+
66+
67+ export function getKeyPairFromString ( keypairString : string ) {
68+ const keyStart = keypairString . indexOf ( '-----BEGIN PRIVATE KEY-----' ) ;
69+ const keyEnd = keypairString . indexOf ( '-----END PRIVATE KEY-----' ) ;
70+ const certStart = keypairString . indexOf ( '-----BEGIN CERTIFICATE-----' ) ;
71+ const certEnd = keypairString . indexOf ( '-----END CERTIFICATE-----' ) ;
72+ if ( certStart === - 1 ) {
73+ throw Error ( 'Missing BEGIN CERTIFICATE' ) ;
74+ }
75+ if ( certEnd === - 1 ) {
76+ throw Error ( 'Missing END CERTIFICATE' ) ;
77+ }
78+ if ( keyStart === - 1 ) {
79+ throw Error ( 'Missing BEGIN PRIVATE KEY' ) ;
80+ }
81+ if ( keyEnd === - 1 ) {
82+ throw Error ( 'Missing END PRIVATE KEY' ) ;
83+ }
84+ const certStr = keypairString . slice ( certStart , certEnd + '-----END CERTIFICATE-----' . length ) ;
85+ const privateKeyStr = keypairString . slice ( keyStart , keyEnd + '-----END CERTIFICATE-----' . length ) ;
86+ const privateKey = createPrivateKey ( privateKeyStr ) ;
87+ const cert = new X509Certificate ( certStr ) ;
88+ return { cert, privateKey } ;
89+
90+ }
91+
6492// This is just a length to avoid silly long usernames
6593const SANE_USERNAME_LENGTH = 64 ;
6694
@@ -75,6 +103,8 @@ const SANE_USERNAME_LENGTH = 64;
75103// (0x00 to 0x1F, plus DEL (0x7F)), as they are most likely mistakes.
76104const SASL_USERNAME_INVALID_CHARS_PATTERN = / [ \x00 - \x20 \x7F ] + / ; // eslint-disable-line
77105
106+ const CERT_FP_TIMEOUT_MS = 90 * 1000 ;
107+
78108interface Command {
79109 example : string ;
80110 summary : string ;
@@ -87,6 +117,10 @@ interface Heading {
87117
88118const COMMANDS : { [ command : string ] : Command | Heading } = {
89119 'Actions' : { heading : true } ,
120+ "certfp" : {
121+ example : `!certfp [irc.example.net]` ,
122+ summary : `Store a certificate for authenticating with the remote network` ,
123+ } ,
90124 "cmd" : {
91125 example : `!cmd [irc.example.net] COMMAND [arg0 [arg1 [...]]]` ,
92126 summary : "Issue a raw IRC command. These will not produce a reply." +
@@ -166,19 +200,28 @@ class ServerRequiredError extends Error {
166200const USER_FEATURES = [ "mentions" ] ;
167201export class AdminRoomHandler {
168202 private readonly botUser : MatrixUser ;
203+ private readonly roomIdsExpectingCertFp = new Map < string , ( certificate : string ) => void > ( ) ;
169204 constructor ( private ircBridge : IrcBridge , private matrixHandler : MatrixHandler ) {
170205 this . botUser = new MatrixUser ( ircBridge . appServiceUserId , undefined , false ) ;
171206 }
172207
173208 public async onAdminMessage ( req : BridgeRequest , event : MatrixSimpleMessage , adminRoom : MatrixRoom ) {
174209 req . log . info ( "Handling admin command from %s" , event . sender ) ;
210+
211+ const certFpFunction = this . roomIdsExpectingCertFp . get ( adminRoom . roomId ) ;
212+ if ( certFpFunction ) {
213+ this . roomIdsExpectingCertFp . delete ( adminRoom . roomId ) ;
214+ certFpFunction ( event . content . body ) ;
215+ return ;
216+ }
217+
175218 const parseResult = parseCommandFromEvent ( event ) ;
176219 let response : MatrixAction | void ;
177220 if ( parseResult ) {
178221 const { cmd, args } = parseResult ;
179222
180223 try {
181- response = await this . handleCommand ( cmd , args , req , event ) ;
224+ response = await this . handleCommand ( cmd , args , req , event , adminRoom ) ;
182225 }
183226 catch ( err ) {
184227 if ( err instanceof ServerRequiredError ) {
@@ -203,7 +246,9 @@ export class AdminRoomHandler {
203246 }
204247 }
205248
206- private async handleCommand ( cmd : string , args : string [ ] , req : BridgeRequest , event : MatrixSimpleMessage ) {
249+ private async handleCommand (
250+ cmd : string , args : string [ ] , req : BridgeRequest , event : MatrixSimpleMessage , adminRoom : MatrixRoom
251+ ) {
207252 const userPermission = this . getUserPermission ( event . sender ) ;
208253 const requiredPermission = ( COMMANDS [ cmd ] as Command | undefined ) ?. requiresPermission ;
209254 if ( requiredPermission && requiredPermission > userPermission ) {
@@ -216,6 +261,8 @@ export class AdminRoomHandler {
216261 return new MatrixAction ( ActionType . Notice , "You have been marked as active by the bridge" ) ;
217262 case "join" :
218263 return await this . handleJoin ( req , args , event . sender ) ;
264+ case "certfp" :
265+ return await this . handleCertfp ( req , args , event . sender , adminRoom ) ;
219266 case "cmd" :
220267 return await this . handleCmd ( req , args , event . sender ) ;
221268 case "whois" :
@@ -252,6 +299,106 @@ export class AdminRoomHandler {
252299 }
253300 }
254301
302+ private async getOrCreateClientConfig ( userId : string , server : IrcServer ) {
303+ const store = this . ircBridge . getStore ( ) ;
304+ const config = await store . getIrcClientConfig ( userId , server . domain ) ;
305+ if ( config ) {
306+ return config ;
307+ }
308+ return IrcClientConfig . newConfig (
309+ new MatrixUser ( userId ) , server . domain
310+ ) ;
311+ }
312+
313+ private async handleCertfp (
314+ req : BridgeRequest , args : string [ ] , sender : string , adminRoom : MatrixRoom ) : Promise < MatrixAction > {
315+ const server = this . extractServerFromArgs ( args ) ;
316+ req . log . info ( `${ sender } is attempting to store a cert for ${ server . domain } ` ) ;
317+ await this . ircBridge . sendMatrixAction (
318+ adminRoom , this . botUser , new MatrixAction (
319+ ActionType . Notice , `Please enter your certificate and private key (without formatting) for ${ server . getReadableName ( ) } .`
320+ )
321+ ) ;
322+ let certfp : string ;
323+ try {
324+ certfp = await new Promise < string > ( ( res , reject ) => {
325+ this . roomIdsExpectingCertFp . set ( adminRoom . roomId , res )
326+ setTimeout ( ( ) => {
327+ reject ( new Error ( 'Timeout' ) ) ;
328+ } , CERT_FP_TIMEOUT_MS ) ;
329+ } ) ;
330+ }
331+ catch ( ex ) {
332+ return new MatrixAction (
333+ ActionType . Notice , 'Timed out waiting for certificate' ,
334+ ) ;
335+ }
336+ finally {
337+ this . roomIdsExpectingCertFp . delete ( adminRoom . roomId ) ;
338+ }
339+ let privateKey , cert ;
340+ try {
341+ const pair = getKeyPairFromString ( certfp ) ;
342+ privateKey = pair . privateKey ;
343+ cert = pair . cert ;
344+ }
345+ catch ( ex ) {
346+ return new MatrixAction (
347+ ActionType . Notice , `Could not parse keypair: ${ ex . message } ` ,
348+ ) ;
349+ }
350+ if ( ! cert . checkPrivateKey ( privateKey ) ) {
351+ return new MatrixAction (
352+ ActionType . Notice , 'Public cert does not belong to private key.' ,
353+ ) ;
354+ }
355+ const now = new Date ( ) ;
356+ try {
357+ const validFrom = new Date ( cert . validFrom ) ;
358+ const validTo = new Date ( cert . validTo ) ;
359+
360+ if ( isNaN ( validFrom . getTime ( ) ) ) {
361+ return new MatrixAction (
362+ ActionType . Notice , 'Certificate validFrom was invalid.' ,
363+ ) ;
364+ }
365+ if ( isNaN ( validTo . getTime ( ) ) ) {
366+ return new MatrixAction (
367+ ActionType . Notice , 'Certificate validFrom was invalid.' ,
368+ ) ;
369+ }
370+
371+ if ( validFrom > now ) {
372+ return new MatrixAction (
373+ ActionType . Notice , 'Certificate is not valid yet.' ,
374+ ) ;
375+ }
376+ if ( now > validTo ) {
377+ return new MatrixAction (
378+ ActionType . Notice , 'Certificate has expired.' ,
379+ ) ;
380+ }
381+ }
382+ catch ( ex ) {
383+ return new MatrixAction (
384+ ActionType . Notice , 'Could not parse validFrom / validTo dates.' ,
385+ ) ;
386+ }
387+
388+ const clientConfig = await this . getOrCreateClientConfig ( sender , server ) ;
389+ clientConfig . setCertificate ( {
390+ cert : cert . toString ( ) ,
391+ key : privateKey . export ( { type : 'pkcs8' , format : 'pem' } ) . toString ( ) ,
392+ } ) ;
393+ await this . ircBridge . getStore ( ) . storeIrcClientConfig ( clientConfig ) ;
394+
395+
396+ return new MatrixAction (
397+ ActionType . Notice ,
398+ `Successfully stored certificate for ${ server . domain } . Use !reconnect to use this cert.`
399+ ) ;
400+ }
401+
255402 private async handlePlumb ( args : string [ ] , sender : string ) {
256403 const [ matrixRoomId , serverDomain , ircChannel ] = args ;
257404 const server = serverDomain && this . ircBridge . getServer ( serverDomain ) ;
@@ -557,12 +704,7 @@ export class AdminRoomHandler {
557704 ) ;
558705 }
559706 else {
560- let config = await store . getIrcClientConfig ( userId , server . domain ) ;
561- if ( ! config ) {
562- config = IrcClientConfig . newConfig (
563- new MatrixUser ( userId ) , server . domain
564- ) ;
565- }
707+ const config = await this . getOrCreateClientConfig ( userId , server ) ;
566708 config . setUsername ( username ) ;
567709 await this . ircBridge . getStore ( ) . storeIrcClientConfig ( config ) ;
568710 notice = new MatrixAction (
0 commit comments