]> git.99rst.org Git - irssi-scripts.irssi.org.git/commitdiff
Add cap_sasl.pl
authorMantas Mikulėnas <redacted>
Thu, 18 Dec 2014 07:12:50 +0000 (09:12 +0200)
committerMantas Mikulėnas <redacted>
Thu, 18 Dec 2014 16:22:13 +0000 (18:22 +0200)
This is a copy of Atheme's `contrib/cap_sasl.pl` 1.8a.

_data/scripts.yaml
scripts/cap_sasl.pl [new file with mode: 0644]

index 821d037edc579fe6b99be97f90c81a831d7b3e5b..5944567cc5edef16220716fe54e70ef6bb176cfe 100644 (file)
   url: "http://scripts.irssi.org/"
   version: "1.0"
 
+- authors: "Michael Tharp (gxti), Jilles Tjoelker (jilles), Mantas Mikulėnas (grawity)"
+  commands: "sasl"
+  contact: "grawity@gmail.com"
+  description: "Implements SASL authentication (and multi-prefix as free bonus)"
+  filename: "cap_sasl.pl"
+  modified: "2014-12-18 05:37:00"
+  modules: "Crypt::PK::ECC MIME::Base64"
+  license: "GPLv2"
+  name: "cap_sasl"
+  url: "https://nullroute.eu.org/~grawity/irssi.html"
+  version: "1.8"
+
 - authors: "ZaMz0n"
   commands: "cddb"
   contact: "zamzon@freakpower.com"
diff --git a/scripts/cap_sasl.pl b/scripts/cap_sasl.pl
new file mode 100644 (file)
index 0000000..ad6df45
--- /dev/null
@@ -0,0 +1,385 @@
+use strict;
+use Irssi;
+use MIME::Base64;
+use vars qw($VERSION %IRSSI);
+use constant CHALLENGE_SIZE => 32;
+
+$VERSION = "1.8a";
+%IRSSI = (
+    authors     => 'Michael Tharp (gxti), Jilles Tjoelker (jilles), Mantas Mikulėnas (grawity)',
+    contact     => 'grawity@gmail.com',
+    name        => 'cap_sasl.pl',
+    description => 'Implements SASL authentication and enables CAP "multi-prefix"',
+    license     => 'GPLv2',
+    url         => 'http://ircv3.atheme.org/extensions/sasl-3.1',
+);
+
+my %sasl_auth = ();
+my %mech = ();
+
+sub timeout;
+
+sub server_connected {
+       my $server = shift;
+       if (uc $server->{chat_type} eq 'IRC') {
+               $server->send_raw_now("CAP LS");
+       }
+}
+
+sub event_cap {
+       my ($server, $args, $nick, $address) = @_;
+       my ($subcmd, $caps, $tosend, $sasl);
+
+       $tosend = '';
+       $sasl = $sasl_auth{$server->{tag}};
+       if ($args =~ /^\S+ (\S+) :(.*)$/) {
+               $subcmd = uc $1;
+               $caps = ' '.$2.' ';
+               if ($subcmd eq 'LS') {
+                       $tosend .= ' multi-prefix' if $caps =~ / multi-prefix /i;
+                       $tosend .= ' sasl' if $caps =~ / sasl /i && defined($sasl);
+                       $tosend =~ s/^ //;
+                       $server->print('', "CLICAP: supported by server:$caps");
+                       if (!$server->{connected}) {
+                               if ($tosend eq '') {
+                                       $server->send_raw_now("CAP END");
+                               } else {
+                                       $server->print('', "CLICAP: requesting: $tosend");
+                                       $server->send_raw_now("CAP REQ :$tosend");
+                               }
+                       }
+                       Irssi::signal_stop();
+               } elsif ($subcmd eq 'ACK') {
+                       $server->print('', "CLICAP: now enabled:$caps");
+                       if ($caps =~ / sasl /i) {
+                               $sasl->{buffer} = '';
+                               $sasl->{step} = 0;
+                               if ($mech{$sasl->{mech}}) {
+                                       $server->send_raw_now("AUTHENTICATE " . $sasl->{mech});
+                                       Irssi::timeout_add_once(7500, \&timeout, $server->{tag});
+                               } else {
+                                       $server->print('', 'SASL: attempted to start unknown mechanism "' . $sasl->{mech} . '"');
+                               }
+                       }
+                       elsif (!$server->{connected}) {
+                               $server->send_raw_now("CAP END");
+                       }
+                       Irssi::signal_stop();
+               } elsif ($subcmd eq 'NAK') {
+                       $server->print('', "CLICAP: refused:$caps");
+                       if (!$server->{connected}) {
+                               $server->send_raw_now("CAP END");
+                       }
+                       Irssi::signal_stop();
+               } elsif ($subcmd eq 'LIST') {
+                       $server->print('', "CLICAP: currently enabled:$caps");
+                       Irssi::signal_stop();
+               }
+       }
+}
+
+sub event_authenticate {
+       my ($server, $args, $nick, $address) = @_;
+       my $sasl = $sasl_auth{$server->{tag}};
+       return unless $sasl && $mech{$sasl->{mech}};
+
+       $sasl->{buffer} .= $args;
+       return if length($args) == 400;
+
+       my $data = ($sasl->{buffer} eq '+') ? '' : decode_base64($sasl->{buffer});
+       my $out = $mech{$sasl->{mech}}($sasl, $data);
+
+       if (defined $out) {
+               $out = ($out eq '') ? '+' : encode_base64($out, '');
+               while (length $out >= 400) {
+                       my $subout = substr($out, 0, 400, '');
+                       $server->send_raw_now("AUTHENTICATE $subout");
+               }
+               if (length $out) {
+                       $server->send_raw_now("AUTHENTICATE $out");
+               } else {
+                       # Last piece was exactly 400 bytes, we have to send
+                       # some padding to indicate we're done.
+                       $server->send_raw_now("AUTHENTICATE +");
+               }
+       } else {
+               $server->send_raw_now("AUTHENTICATE *");
+       }
+
+       $sasl->{buffer} = "";
+       Irssi::signal_stop();
+}
+
+sub event_saslend {
+       my ($server, $args, $nick, $address) = @_;
+
+       my $data = $args;
+       $data =~ s/^\S+ :?//;
+       # need this to see it, ?? -- jilles
+
+       $server->print('', $data);
+       if (!$server->{connected}) {
+               $server->send_raw_now("CAP END");
+       }
+}
+
+sub event_saslfail {
+       my ($server, $args, $nick, $address) = @_;
+
+       my $data = $args;
+       $data =~ s/^\S+ :?//;
+
+       if (Irssi::settings_get_bool('sasl_disconnect_on_fail')) {
+               $server->print('', "$data - disconnecting from server", MSGLEVEL_CLIENTERROR);
+               $server->disconnect();
+       } else {
+               $server->print('', "$data - continuing anyway");
+               if (!$server->{connected}) {
+                       $server->send_raw_now("CAP END");
+               }
+       }
+}
+
+sub timeout {
+       my $tag = shift;
+       my $server = Irssi::server_find_tag($tag);
+       if ($server && !$server->{connected}) {
+               $server->print('', "SASL: authentication timed out", MSGLEVEL_CLIENTERROR);
+               $server->send_raw_now("CAP END");
+       }
+}
+
+sub cmd_sasl {
+       my ($data, $server, $item) = @_;
+
+       if ($data ne '') {
+               Irssi::command_runsub ('sasl', $data, $server, $item);
+       } else {
+               cmd_sasl_show(@_);
+       }
+}
+
+sub cmd_sasl_set {
+       my ($data, $server, $item) = @_;
+
+       if (my ($net, $u, $p, $m) = $data =~ /^(\S+) (\S+) (\S+) (\S+)$/) {
+               if ($mech{uc $m}) {
+                       $sasl_auth{$net}{user} = $u;
+                       $sasl_auth{$net}{password} = $p;
+                       $sasl_auth{$net}{mech} = uc $m;
+                       Irssi::print("SASL: added $net: [$m] $sasl_auth{$net}{user} *");
+               } else {
+                       Irssi::print("SASL: unknown mechanism $m", MSGLEVEL_CLIENTERROR);
+               }
+       } elsif ($data =~ /^(\S+)$/) {
+               $net = $1;
+               if (defined($sasl_auth{$net})) {
+                       delete $sasl_auth{$net};
+                       Irssi::print("SASL: deleted $net");
+               } else {
+                       Irssi::print("SASL: no entry for $net");
+               }
+       } else {
+               Irssi::print("SASL: usage: /sasl set <net> <user> <password or keyfile> <mechanism>");
+       }
+}
+
+sub cmd_sasl_show {
+       #my ($data, $server, $item) = @_;
+       my @nets = keys %sasl_auth;
+       for my $net (@nets) {
+               Irssi::print("SASL: $net: [$sasl_auth{$net}{mech}] $sasl_auth{$net}{user} *");
+       }
+       Irssi::print("SASL: no networks defined") if !@nets;
+}
+
+sub cmd_sasl_save {
+       #my ($data, $server, $item) = @_;
+       my $file = Irssi::get_irssi_dir()."/sasl.auth";
+       if (open(my $fh, ">", $file)) {
+               chmod(0600, $file);
+               for my $net (keys %sasl_auth) {
+                       printf $fh ("%s\t%s\t%s\t%s\n",
+                               $net,
+                               $sasl_auth{$net}{user},
+                               $sasl_auth{$net}{password},
+                               $sasl_auth{$net}{mech});
+               }
+               close($fh);
+               Irssi::print("SASL: auth saved to '$file'");
+       } else {
+               Irssi::print("SASL: couldn't access '$file': $@");
+       }
+}
+
+sub cmd_sasl_load {
+       #my ($data, $server, $item) = @_;
+       my $file = Irssi::get_irssi_dir()."/sasl.auth";
+       if (open(my $fh, "<", $file)) {
+               %sasl_auth = ();
+               while (<$fh>) {
+                       chomp;
+                       my ($net, $u, $p, $m) = split(/\t/, $_, 4);
+                       $m ||= "PLAIN";
+                       if ($mech{uc $m}) {
+                               $sasl_auth{$net}{user} = $u;
+                               $sasl_auth{$net}{password} = $p;
+                               $sasl_auth{$net}{mech} = uc $m;
+                       } else {
+                               Irssi::print("SASL: unknown mechanism $m", MSGLEVEL_CLIENTERROR);
+                       }
+               }
+               close($fh);
+               Irssi::print("SASL: cap_sasl $VERSION, auth loaded from '$file'");
+       }
+}
+
+sub cmd_sasl_mechanisms {
+       Irssi::print("SASL: mechanisms supported: " . join(", ", sort keys %mech));
+}
+
+Irssi::settings_add_bool('server', 'sasl_disconnect_on_fail', 1);
+
+Irssi::signal_add_first('server connected', \&server_connected);
+Irssi::signal_add('event cap', \&event_cap);
+Irssi::signal_add('event authenticate', \&event_authenticate);
+Irssi::signal_add('event 903', \&event_saslend);
+Irssi::signal_add('event 904', \&event_saslfail);
+Irssi::signal_add('event 905', \&event_saslend);
+Irssi::signal_add('event 906', \&event_saslfail);
+Irssi::signal_add('event 907', \&event_saslend);
+
+Irssi::command_bind('sasl', \&cmd_sasl);
+Irssi::command_bind('sasl load', \&cmd_sasl_load);
+Irssi::command_bind('sasl save', \&cmd_sasl_save);
+Irssi::command_bind('sasl set', \&cmd_sasl_set);
+Irssi::command_bind('sasl show', \&cmd_sasl_show);
+Irssi::command_bind('sasl mechanisms', \&cmd_sasl_mechanisms);
+
+$mech{PLAIN} = sub {
+       my ($sasl, $data) = @_;
+       my $u = $sasl->{user};
+       my $p = $sasl->{password};
+       return join("\0", $u, $u, $p);
+};
+
+$mech{EXTERNAL} = sub {
+       my ($sasl, $data) = @_;
+       return $sasl->{user} // "";
+};
+
+if (eval {require Crypt::PK::ECC}) {
+       $mech{'ECDSA-NIST256P-CHALLENGE'} = sub {
+               my ($sasl, $data) = @_;
+               my $u = $sasl->{user};
+               my $k = $sasl->{password};
+               if ($k !~ m!^/!) {
+                       $k = Irssi::get_irssi_dir()."/".$k;
+               }
+               if (!-f $k) {
+                       Irssi::print("SASL: key file '$k' not found", MSGLEVEL_CLIENTERROR);
+                       return;
+               }
+               my $pk = eval {Crypt::PK::ECC->new($k)};
+               if ($@ || !$pk || !$pk->is_private) {
+                       Irssi::print("SASL: no private key in file '$k'", MSGLEVEL_CLIENTERROR);
+                       return;
+               }
+               my $step = ++$sasl->{step};
+               if ($step == 1) {
+                       if (length $data == CHALLENGE_SIZE) {
+                               my $sig = $pk->sign_hash($data);
+                               return $u."\0".$u."\0".$sig;
+                       } elsif (length $data) {
+                               return;
+                       } else {
+                               return $u."\0".$u;
+                       }
+               }
+               elsif ($step == 2) {
+                       if (length $data == CHALLENGE_SIZE) {
+                               return $pk->sign_hash($data);
+                       } else {
+                               return;
+                       }
+               }
+       };
+
+       sub cmd_sasl_keygen {
+               my ($data, $server, $witem) = @_;
+
+               my $mech = "ECDSA-NIST256P-CHALLENGE";
+               my $net;
+               my $print;
+
+               if ($server) {
+                       $net = $server->{tag};
+                       $print = sub { $server->print("", shift) };
+               } else {
+                       $net = $data;
+                       $print = sub { Irssi::print(shift) };
+               }
+
+               if (!length $net) {
+                       $print->("Please connect to a server first.");
+                       return;
+               }
+
+               my $f_name = lc "sasl-ecdsa-$net";
+                  $f_name =~ s![ /]+!_!g;
+               my $f_priv = Irssi::get_irssi_dir()."/$f_name.key";
+               my $f_pub  = Irssi::get_irssi_dir()."/$f_name.pub";
+               if (-e $f_priv) {
+                       $print->("SASL: refusing to overwrite '$f_priv'");
+                       return;
+               }
+
+               $print->("SASL: generating keypair for '$net'...");
+               my $pk = Crypt::PK::ECC->new;
+               $pk->generate_key("prime256v1");
+
+               my $priv = $pk->export_key_pem("private");
+               my $pub = encode_base64($pk->export_key_raw("public_compressed"), "");
+               my $cmd = "/msg NickServ SET PROPERTY pubkey $pub";
+
+               if (open(my $fh, ">", $f_priv)) {
+                       chmod(0600, $f_priv);
+                       print $fh $priv;
+                       close($fh);
+                       $print->("SASL: wrote private key to '$f_priv'");
+               } else {
+                       $print->("SASL: could not write '$f_priv': $!");
+                       return;
+               }
+
+               if (open(my $fh, ">", $f_pub)) {
+                       print $fh $pub."\n";
+                       close($fh);
+               } else {
+                       $print->("SASL: could not write '$f_pub': $!");
+               }
+
+               if ($server) {
+                       $print->("SASL: updating your Irssi settings...");
+                       $sasl_auth{$net}{user} //= $server->{nick};
+                       $sasl_auth{$net}{password} = "$f_name.key";
+                       $sasl_auth{$net}{mech} = $mech;
+                       cmd_sasl_save(@_);
+
+                       $print->("SASL: submitting key to NickServ...");
+                       $server->command($cmd);
+               } else {
+                       $print->("SASL: update your Irssi settings:");
+                       $print->("%P/sasl set $net <nick> $f_name.key $mech");
+
+                       $print->("SASL: submit your public key to $net:");
+                       $print->("%P$cmd");
+               }
+       }
+
+       Irssi::command_bind('sasl keygen', \&cmd_sasl_keygen);
+};
+
+cmd_sasl_load();
+
+# vim: ts=4:sw=4
git clone https://git.99rst.org/PROJECT