1 # based on cap_sasl.pl by Michael Tharp and Jilles Tjoelker
2 # ported to X-Chat 2 by Lian Wan Situ
4 # license: GNU General Public License
5 # Latest version at http://lwsitu.com/xchat/cap_sasl_xchat.pl
7 ### Configuration #######
8 # How long to wait between authentication messages
9 my $AUTHENTICATION_TIMEOUT = 15;
10 ### End Configuration ###
21 "Implements PLAIN SASL authentication mechanism for use with charybdis ircds, and enables CAP MULTI-PREFIX IDENTIFY-MSG",
28 hook_print
( "Connected", \
&event_connected
);
30 hook_server
( 'CAP', \
&event_cap
);
31 hook_server
( 'AUTHENTICATE', \
&event_authenticate
);
32 hook_server
( '900', sub {
33 cap_out
( substr( $_[1][5], 1, ) );
37 hook_server
( '903', \
&event_saslend
);
38 hook_server
( '904', \
&event_saslend
);
39 hook_server
( '905', \
&event_saslend
);
40 hook_server
( '906', \
&event_saslend
);
41 hook_server
( '907', \
&event_saslend
);
43 hook_command
( 'SASL', \
&cmd_sasl
, { help_text
=> cmd_sasl_help_text
() } );
46 if( $AUTHENTICATION_TIMEOUT ) {
47 $AUTH_TIMEOUT = $AUTHENTICATION_TIMEOUT * 1_000
;
49 $AUTH_TIMEOUT = 5_000
;
56 commandf
( "QUOTE %s", $_[0] );
60 send_raw
( "CAP " . $_[0] );
64 delete $processing_cap{ get_info
"id" };
69 my $flags = context_info
->{flags
};
70 return $flags & 1 || $flags & 2;
74 return get_info
( "xchatdirfs" )."/sasl.auth";
78 return lc get_info
( "network" );
81 # switch to the server tab for the current connection
82 # return true if successful
83 sub switch_to_server
{
84 my $connection_id = shift;
86 for my $tab( get_list
"channels") {
87 if( $tab->{id
} == $connection_id && $tab->{type
} == 1 ) {
88 return set_context
( $tab->{context
} );
97 switch_to_server
( get_info
"id" );
102 sub event_connected
{
105 # reset everything for new connection
107 delete $processing_cap{ get_info
( "id" ) };
113 my $subcmd = uc $_[0][3];
117 if ($subcmd eq 'LS') {
118 my $id = get_info
"id";
119 if( $processing_cap{ $id } ) {
122 $processing_cap{ $id } = 1;
123 $tosend .= ' multi-prefix' if $caps =~ /\bmulti-prefix\b/xi;
125 if( $caps =~ /\bsasl\b/xi ) {
126 if( defined($sasl_auth{network_name
()}) ) {
129 cap_out
( "\cC05SASL is supported but there is no authentication information set for this network(\cC02".network_name
()."\cC05)." );
133 $tosend .= ' identify-msg' if $caps =~ /\bidentify-msg\b/;
135 cap_out
( "CLICAP: supported by server: $caps" );
141 cap_out
( "CLICAP: requesting: $tosend" );
142 cap
( "REQ :$tosend" );
145 } elsif( $subcmd eq 'ACK' ) {
146 cap_out
( "CLICAP: now enabled: $caps" );
148 if( $caps =~ /\bidentify-msg\b/i ) {
149 commandf
( "RECV %s 290 %s :IDENTIFY-MSG",
150 $_[0][0], get_info
( "nick" ) );
153 if( $caps =~ /\bsasl\b/i ) {
154 $sasl_auth{network_name
()}{buffer
} = '';
155 if($mech{$sasl_auth{network_name
()}{mech
}}) {
156 send_raw
( "AUTHENTICATE "
157 . $sasl_auth{network_name
()}{mech
}
162 cap_out
( 'SASL: attempted to start unknown mechanism "%s"',
163 $sasl_auth{network_name
()}{mech
}
166 } elsif( connected
() ) {
169 } elsif( $subcmd eq 'NAK' ) {
170 cap_out
( "CLICAP: refused:$caps" );
174 } elsif( $subcmd eq 'LIST' ) {
175 cap_out
( "CLICAP: currently enabled:$caps" );
181 sub event_authenticate
{
182 my $args = $_[1][1] || "";
184 my $sasl = $sasl_auth{network_name
()};
185 return EAT_XCHAT
unless $sasl && $mech{$sasl->{mech
}};
187 $sasl->{buffer
} .= $args;
189 return EAT_XCHAT
if length($args) == 400;
191 my $data = $sasl->{buffer
} eq '+' ?
'' : decode_base64
($sasl->{buffer
});
192 my $out = $mech{$sasl->{mech
}}($sasl, $data);
193 $out = '' unless defined $out;
194 $out = $out eq '' ?
'+' : encode_base64
($out, '');
196 while(length $out >= 400) {
197 my $subout = substr($out, 0, 400, '');
198 send_raw
("AUTHENTICATE $subout");
201 send_raw
("AUTHENTICATE $out");
202 }else{ # Last piece was exactly 400 bytes, we have to send some padding to indicate we're done
203 send_raw
("AUTHENTICATE +");
206 $sasl->{buffer
} = '';
212 $data =~ s/^\S+ :?//;
222 $timeouts{ context_info
->{id
} }
223 = hook_timer
( $AUTH_TIMEOUT, sub { timeout
(); return REMOVE
; } );
227 unhook
( $timeouts{ context_info
->{id
} } ) if $timeouts{ context_info
->{id
} };
236 my $id = get_info
"id";
237 delete $processing_cap{ $id };
240 cap_out
( "SASL: authentication timed out" );
246 load
=> \
&cmd_sasl_load
,
247 save
=> \
&cmd_sasl_save
,
248 set
=> \
&cmd_sasl_set
,
249 delete => \
&cmd_sasl_delete
,
250 show
=> \
&cmd_sasl_show
,
251 help
=> \
&cmd_sasl_help
,
252 mechanisms
=> \
&cmd_sasl_mechanisms
,
256 my $action = $_[0][1];
258 if( $action and my $action_code = $sasl_actions{ $action } ) {
259 $action_code->( @_ );
261 $sasl_actions{ help
}->( @_ );
267 sub cmd_sasl_help_text
{
268 return <<"HELP_TEXT";
269 SASL [action] [action paramters]
271 load reload SASL information from disk
272 save save the current SASL information to disk
273 set set the SASL information for a particular network
274 set <net> <user> <passord or keyfile> <mechanism>
275 delete delete the SASL information for a particular network
278 show display which networks have SASL information set
279 mechanisms display supported mechanisms
281 help show help message
287 my $data = $_[1][2] || "";
289 if (my($net, $u, $p, $m) = $data =~ /^(\S+) (\S+) (\S+) (\S+)$/) {
292 $sasl_auth{$net}{user} = $u;
293 $sasl_auth{$net}{password} = $p;
294 $sasl_auth{$net}{mech} = uc $m;
295 prnt( "SASL: added $net: [$m] $sasl_auth{$net}{user} *" );
297 prnt( "SASL: unknown mechanism $m" );
299 } elsif( $data =~ /^(\S+)$/) {
301 if (defined($sasl_auth{$net})) {
302 delete $sasl_auth{$net};
303 prnt( "SASL: deleted $net" );
305 prnt( "SASL: no entry for $net" );
308 prnt( "SASL: usage: /sasl set <net> <user> <password or keyfile> <mechanism>" );
312 sub cmd_sasl_delete {
316 delete $sasl_auth{$net};
320 foreach my $net (keys %sasl_auth) {
321 prnt( "SASL: $net: [$sasl_auth{$net}{mech}] $sasl_auth{$net}{user} *" );
323 prnt("SASL: no networks defined") if !%sasl_auth;
327 my $file = get_config_file();
329 if( open my $fh, ">", $file ) {
331 foreach my $net (keys %sasl_auth) {
332 printf $fh ("%s\t%s\t%s\t%s\n", lc $net, $sasl_auth{$net}{user}, $sasl_auth{$net}{password}, $sasl_auth{$net}{mech});
335 prnt( "SASL: auth saved to $file" );
337 prnt qq{Couldn't open '$file' to save auth data: $!};
342 #my ($data, $server, $item) = @_;
343 my $file = get_config_file();
345 open FILE, "< $file" or return;
349 my ($net, $u, $p, $m) = split (/\t/, $_, 4);
353 $sasl_auth{$net}{user} = $u;
354 $sasl_auth{$net}{password} = $p;
355 $sasl_auth{$net}{mech} = uc $m;
357 prnt( "SASL: unknown mechanism $m" );
361 prnt( "SASL: auth loaded from $file" );
364 sub cmd_sasl_mechanisms {
365 prnt( "SASL: mechanisms supported: " . join(" ", keys %mech) );
369 prnt( cmd_sasl_help_text() );
373 my($sasl, $data) = @_;
374 my $u = $sasl->{user};
375 my $p = $sasl->{password};
377 join("\0", $u, $u, $p);
382 return Crypt::OpenSSL::Bignum
383 ->new_from_bin(shift)
389 return Crypt::OpenSSL::Bignum
390 ->new_from_decimal((shift)->bstr)
395 require Crypt::OpenSSL::Bignum;
397 require Crypt::Blowfish;
398 require Math::BigInt;
400 $mech{'DH-BLOWFISH'} = sub {
401 my($sasl, $data) = @_;
402 my $u = $sasl->{user};
403 my $pass = $sasl->{password};
405 # Generate private key and compute secret key
406 my($p, $g, $y) = unpack("(n/a*)3", $data);
407 my $dh = Crypt::DH->new(p => bin2bi($p), g => bin2bi($g));
410 my $secret = bi2bin($dh->compute_secret(bin2bi($y)));
411 my $pubkey = bi2bin($dh->pub_key);
413 # Pad the password to the nearest multiple of blocksize and encrypt
415 $pass .= chr(rand(256)) while length($pass) % 8;
417 my $cipher = Crypt::Blowfish->new($secret);
419 while(length $pass) {
420 my $clear = substr($pass, 0, 8, '');
421 $crypted .= $cipher->encrypt($clear);
424 pack("n/a*Z*a*", $pubkey, $u, $crypted);