xchat/etc/wyatt8740/plugins_extra/cap_sasl_xchat.pl

473 lines
10 KiB
Perl
Raw Permalink Normal View History

2024-10-12 09:38:51 +00:00
# based on cap_sasl.pl by Michael Tharp and Jilles Tjoelker
# ported to X-Chat 2 by Lian Wan Situ
#
# license: GNU General Public License
# Latest version at http://lwsitu.com/xchat/cap_sasl_xchat.pl
### Configuration #######
# How long to wait between authentication messages
my $AUTHENTICATION_TIMEOUT = 5;
### End Configuration ###
use strict;
use warnings;
use Xchat qw(:all);
use MIME::Base64;
register(
"CAP SASL",
"1.0300",
"Implements PLAIN SASL authentication mechanism for use with charybdis ircds, and enables CAP MULTI-PREFIX IDENTIFY-MSG",
\&cmd_sasl_save,
);
my %timeouts;
my %processing_cap;
hook_print( "Connected", \&event_connected );
hook_server( 'CAP', \&event_cap);
hook_server( 'AUTHENTICATE', \&event_authenticate);
hook_server( '900', sub {
cap_out( substr( $_[1][5], 1, ) );
timeout_remove();
return EAT_XCHAT;
});
hook_server( '903', \&event_saslsuccess);
hook_server( '904', \&event_saslend);
hook_server( '905', \&event_saslend);
hook_server( '906', \&event_saslend);
hook_server( '907', \&event_saslend);
hook_command( 'SASL', \&cmd_sasl, { help_text => cmd_sasl_help_text() } );
my $AUTH_TIMEOUT;
if( $AUTHENTICATION_TIMEOUT ) {
$AUTH_TIMEOUT = $AUTHENTICATION_TIMEOUT * 1_000;
} else {
$AUTH_TIMEOUT = 5_000;
}
my %sasl_auth = ();
my %mech = ();
sub send_raw {
commandf( "QUOTE %s", $_[0] );
}
sub cap {
send_raw( "CAP " . $_[0] );
}
sub cap_end{
delete $processing_cap{ get_info "id" };
cap( "END" );
}
sub connected {
my $flags = context_info->{flags};
return $flags & 1 || $flags & 2;
}
sub get_config_file {
return get_info( "xchatdirfs" )."/sasl.auth";
}
sub network_name {
return lc get_info( "network" );
}
# switch to the server tab for the current connection
# return true if successful
sub switch_to_server {
my $connection_id = shift;
for my $tab( get_list "channels") {
if( $tab->{id} == $connection_id && $tab->{type} == 1 ) {
return set_context( $tab->{context} );
}
}
return;
}
sub cap_out {
my $output = shift;
switch_to_server( get_info "id" );
prnt( $output );
}
sub event_connected {
cap( "LS" );
# reset everything for new connection
timeout_remove();
delete $processing_cap{ get_info( "id" ) };
return EAT_NONE;
}
sub event_cap {
my $tosend = '';
my $subcmd = uc $_[0][3];
my $caps = $_[1][4];
$caps =~ s/^://;
if ($subcmd eq 'LS') {
my $id = get_info "id";
if( $processing_cap{ $id } ) {
return EAT_XCHAT;
}
$processing_cap{ $id } = 1;
$tosend .= ' multi-prefix' if $caps =~ /\bmulti-prefix\b/xi;
if( $caps =~ /\bsasl\b/xi ) {
if( defined($sasl_auth{network_name()}) ) {
$tosend .= ' sasl';
} else {
cap_out( "\cC05SASL is supported but there is no authentication information set for this network(\cC02".network_name()."\cC05)." );
}
}
$tosend .= ' identify-msg' if $caps =~ /\bidentify-msg\b/;
$tosend =~ s/^ //;
cap_out( "CLICAP: supported by server: $caps" );
if ( connected() ) {
if ($tosend eq '') {
cap_end();
} else {
cap_out( "CLICAP: requesting: $tosend" );
cap( "REQ :$tosend" );
}
}
} elsif( $subcmd eq 'ACK' ) {
cap_out( "CLICAP: now enabled: $caps" );
if( $caps =~ /\bidentify-msg\b/i ) {
commandf( "RECV %s 290 %s :IDENTIFY-MSG",
$_[0][0], get_info( "nick" ) );
}
if( $caps =~ /\bsasl\b/i ) {
$sasl_auth{network_name()}{buffer} = '';
if($mech{$sasl_auth{network_name()}{mech}}) {
send_raw( "AUTHENTICATE "
. $sasl_auth{network_name()}{mech}
);
timeout_start();
} else {
cap_out( 'SASL: attempted to start unknown mechanism "%s"',
$sasl_auth{network_name()}{mech}
);
}
} elsif( connected() ) {
cap_end;
}
} elsif( $subcmd eq 'NAK' ) {
cap_out( "CLICAP: refused:$caps" );
if ( connected() ) {
cap_end;
}
} elsif( $subcmd eq 'LIST' ) {
cap_out( "CLICAP: currently enabled:$caps" );
}
return EAT_XCHAT;
}
sub event_authenticate {
my $args = $_[1][1] || "";
my $sasl = $sasl_auth{network_name()};
return EAT_XCHAT unless $sasl && $mech{$sasl->{mech}};
$sasl->{buffer} .= $args;
timeout_reset();
return EAT_XCHAT if length($args) == 400;
my $data = $sasl->{buffer} eq '+' ? '' : decode_base64($sasl->{buffer});
my $out = $mech{$sasl->{mech}}($sasl, $data);
$out = '' unless defined $out;
$out = $out eq '' ? '+' : encode_base64($out, '');
while(length $out >= 400) {
my $subout = substr($out, 0, 400, '');
send_raw("AUTHENTICATE $subout");
}
if(length $out) {
send_raw("AUTHENTICATE $out");
}else{ # Last piece was exactly 400 bytes, we have to send some padding to indicate we're done
send_raw("AUTHENTICATE +");
}
$sasl->{buffer} = '';
return EAT_XCHAT;
}
sub event_saslsuccess {
my $data = $_[1][3];
$data =~ s/^://;
cap_out( $data );
if( connected() ) {
cap_end();
}
return EAT_XCHAT;
}
sub event_saslend {
my $data = $_[1][1];
$data =~ s/^\S+ :?//;
if (connected()) {
cap_end();
}
return EAT_XCHAT;
}
sub timeout_start {
$timeouts{ context_info->{id} }
= hook_timer( $AUTH_TIMEOUT, sub { timeout(); return REMOVE; } );
}
sub timeout_remove {
unhook( $timeouts{ context_info->{id} } ) if $timeouts{ context_info->{id} };
}
sub timeout_reset {
timeout_remove();
timeout_start();
}
sub timeout {
my $id = get_info "id";
delete $processing_cap{ $id };
if( connected() ) {
cap_out( "SASL: authentication timed out" );
cap_end();
}
}
my %sasl_actions = (
load => \&cmd_sasl_load,
save => \&cmd_sasl_save,
set => \&cmd_sasl_set,
delete => \&cmd_sasl_delete,
show => \&cmd_sasl_show,
help => \&cmd_sasl_help,
mechanisms => \&cmd_sasl_mechanisms,
);
sub cmd_sasl {
my $action = $_[0][1];
if( $action and my $action_code = $sasl_actions{ $action } ) {
$action_code->( @_ );
} else {
$sasl_actions{ help }->( @_ );
}
return EAT_XCHAT;
}
sub cmd_sasl_help_text {
return <<"HELP_TEXT";
SASL [action] [action paramters]
actions:
load reload SASL information from disk
save save the current SASL information to disk
set set the SASL information for a particular network
set <net> <user> <password or keyfile> <mechanism>
you can also set <net> to -n or -- to use the name of the network from currently focused tab
you can also set <user> to -u or -- to use the current nick
delete delete the SASL information for a particular network
delete <net>
show display which networks have SASL information set
mechanisms display supported mechanisms
help show help message
HELP_TEXT
}
sub cmd_sasl_set {
my $data = $_[ 1 ][ 2 ] || "";
if( my ($net, $u, $p, $m) = $data =~ /^(\S+)\s+(\S+)\s+(\S+)(?:\s+(\S+))?$/ ) {
$m = "plain" unless defined $m;
$m = uc $m;
if( $mech{ $m } ) {
if( $net =~ /^-[-n]$/ ) {
$net = get_info( "network" );
unless ( defined $net ) {
prnt "SASL: Unable to get network name. Make sure the current tab is connected to a server.";
return;
}
}
$net = lc $net;
if( $u =~ /^-[-u]$/ ) {
$u = get_info( "nick" );
}
$sasl_auth{ $net }{ user } = $u;
$sasl_auth{ $net }{ password } = $p;
$sasl_auth{ $net }{ mech } = $m;
prnt( "SASL: added $net: [$m] $sasl_auth{$net}{user} *" );
} else {
prnt( "SASL: unknown mechanism $m" );
}
} elsif ( $data =~ /^(\S+)$/ ) {
$net = $1;
delete_net( $net );
} else {
prnt( "SASL: usage: /sasl set <net> <user> <password or keyfile> <mechanism>" );
}
}
sub delete_net {
my $net = lc shift;
if( $net =~ /^-[-n]$/ ) {
$net = lc get_info( "network" );
unless( defined $net ) {
prnt "SASL: Unable to get network name. Make sure the current tab is connected to a server.";
return;
}
}
if (defined($sasl_auth{$net})) {
delete $sasl_auth{$net};
prnt( "SASL: deleted $net" );
} else {
prnt( "SASL: no entry for $net" );
}
}
sub cmd_sasl_delete {
my $net = $_[0][2];
delete_net( $net );
}
sub cmd_sasl_show {
foreach my $net (keys %sasl_auth) {
prnt( "SASL: $net: [$sasl_auth{$net}{mech}] $sasl_auth{$net}{user} *" );
}
prnt("SASL: no networks defined") if !%sasl_auth;
}
sub cmd_sasl_save {
my $file = get_config_file();
if( open my $fh, ">", $file ) {
foreach my $net (keys %sasl_auth) {
printf $fh ("%s\t%s\t%s\t%s\n", lc $net, $sasl_auth{$net}{user}, $sasl_auth{$net}{password}, $sasl_auth{$net}{mech});
}
prnt( "SASL: auth saved to $file" );
} else {
prnt qq{Couldn't open '$file' to save auth data: $!};
}
}
sub cmd_sasl_load {
#my ($data, $server, $item) = @_;
my $file = get_config_file();
open FILE, "< $file" or return;
%sasl_auth = ();
while (<FILE>) {
chomp;
my ($net, $u, $p, $m) = split (/\t/, $_, 4);
$m ||= "PLAIN";
$net = lc $net;
if($mech{uc $m}) {
$sasl_auth{$net}{user} = $u;
$sasl_auth{$net}{password} = $p;
$sasl_auth{$net}{mech} = uc $m;
}else{
prnt( "SASL: unknown mechanism $m" );
}
}
close FILE;
prnt( "SASL: auth loaded from $file" );
}
sub cmd_sasl_mechanisms {
prnt( "SASL: mechanisms supported: " . join(" ", keys %mech) );
}
sub cmd_sasl_help {
prnt( cmd_sasl_help_text() );
}
$mech{PLAIN} = sub {
my($sasl, $data) = @_;
my $u = $sasl->{user};
my $p = $sasl->{password};
join("\0", $u, $u, $p);
};
# binary to BigInt
sub bin2bi {
return Crypt::OpenSSL::Bignum
->new_from_bin(shift)
->to_decimal;
}
# BigInt to binary
sub bi2bin {
return Crypt::OpenSSL::Bignum
->new_from_decimal((shift)->bstr)
->to_bin;
}
eval {
require Crypt::OpenSSL::Bignum;
require Crypt::DH;
require Crypt::Blowfish;
require Math::BigInt;
$mech{'DH-BLOWFISH'} = sub {
my($sasl, $data) = @_;
my $u = $sasl->{user};
my $pass = $sasl->{password};
# Generate private key and compute secret key
my($p, $g, $y) = unpack("(n/a*)3", $data);
my $dh = Crypt::DH->new(p => bin2bi($p), g => bin2bi($g));
$dh->generate_keys;
my $secret = bi2bin($dh->compute_secret(bin2bi($y)));
my $pubkey = bi2bin($dh->pub_key);
# Pad the password to the nearest multiple of blocksize and encrypt
$pass .= "\0";
$pass .= chr(rand(256)) while length($pass) % 8;
my $cipher = Crypt::Blowfish->new($secret);
my $crypted = '';
while(length $pass) {
my $clear = substr($pass, 0, 8, '');
$crypted .= $cipher->encrypt($clear);
}
pack("n/a*Z*a*", $pubkey, $u, $crypted);
};
};
cmd_sasl_load();
# vim: ts=4