diff --git a/cpanfile b/cpanfile index 2b0fd6fa..d6408b39 100644 --- a/cpanfile +++ b/cpanfile @@ -12,6 +12,7 @@ requires 'perl' => '5.020000'; # PBot core requires 'Cache::FileCache'; requires 'Carp'; +requires 'Crypt::SaltedHash'; requires 'DateTime'; requires 'DateTime::Format::Duration'; requires 'DateTime::Format::Flexible'; diff --git a/doc/Admin.md b/doc/Admin.md index 337b275e..57e6e88e 100644 --- a/doc/Admin.md +++ b/doc/Admin.md @@ -109,7 +109,7 @@ Parameter | Description `hostmasks` | The hostmasks from which this user is recognized/allowed to login from (e.g., `somenick!*@*.somedomain.com` or `*!*@unaffiliated/someuser`). Can be a comma-separated list of values. `channels` | The channels this user belongs to; use `global` for all channels. Can be a comma-separated list of values. `capabilities` | A comma-separated list of [user-capabilities](#user-capabilities) for this user. -`password` | The password the user will use to login (from `/msg`, obviously). Generates a random password if omitted. Users may view and set their password by using the [`my`](Commands.md#my) command. +`password` | The password the user will use to login (from `/msg`, obviously). Users may update their password by using the [`my`](Commands.md#my) command once logged in. ### userdel Removes a user from PBot. diff --git a/doc/FAQ.md b/doc/FAQ.md index f10094bf..89e58840 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -101,13 +101,19 @@ to set the `preserve_whitespace` [factoid metadata](Factoids.md#factoid-metadata ## How do I change my password? If you have a NickServ account or a unique hostmask, you don't need a PBot password. -The `stayloggedin` metadata on your user account can be set instead. +The `autologin` and `stayloggedin` metadata on your user account can be set instead. But if you prefer to be safe instead of sorry, use the [`my`](Commands.md#my) command -to set the `password` user metadata for your user account. Your hostmask must match the -user account. +to set the `password` and unset the `autologin` and `stayloggedin` metadata for your +user account. Your hostmask must match the user account and you must be logged in. my password + my autologin 0 + my stayloggedin 0 + +If you are unable to log in, ask an admin to set a temporary password for you +with the [`userset`](Admin.md#userset) command. Log in with the temporary +password and then use the above commands to update your password. ## How do I make PBot remember my `date` timezone? Use the [`my`](Commands.md#my) command to set the `timezone` user metadata for your diff --git a/doc/QuickStart.md b/doc/QuickStart.md index daef8b4f..f0ba4171 100644 --- a/doc/QuickStart.md +++ b/doc/QuickStart.md @@ -362,7 +362,7 @@ command in the PBot terminal console. Its usage is: Suppose your nick is `Bob` and your hostmask is `Bob!~user@some.domain.com`. Use the following command: - useradd Bob Bob!~user@*.domain.com global botowner + useradd Bob Bob!~user@*.domain.com global botowner This will create a user account named `Bob` with the `botowner` [user-capability](Admin.md#user-capabilities) that can administrate all channels. Note the wildcard replacing `some` in `some.domain.com`. Now as long as @@ -372,16 +372,14 @@ It is very important that user account hostmasks are defined as strictly or as n as possible to match only the person it is intended for. Ideally, the user would have a NickServ account, a user-cloak given by the staff of the IRC server or a unique DNS name. -In your own IRC client, connected using the hostmask we just added, type the -following command, in a private `/query` or `/msg`: - - my password - -This will show you the randomly generated password that was assigned to your -user account. You can change it -- if you want to -- with: +You can change your password with: my password +or + + userset Bob password + Then you can login with: login @@ -389,8 +387,30 @@ Then you can login with: Now you can use `/msg` in your own IRC client to administrate PBot, instead of the terminal console. +If you want to autologin without typing a password, first ensure your hostmask is unique -- preferably +by using a NickServ vhost/cloak or a reverse-DNS name. Then set the following metadata on your account: + + userset Bob autologin 1 + +If you want to remain permanently logged in, ensure your hostmask is unique and set the following metadata on your account: + + userset Bob stayloggedin 1 + ### Adding other users and admins -To add users to PBot, use the [`useradd`](Admin.md#useradd) command. Its usage is: +Users may create their own unprivileged accounts by using the [`my`](Commands.md#my) command. It will automatically +set their username, hostmask, channel and log them into it. + +Users added this way will have `autologin` and `stayloggedin` enabled. If they feel their hostmask is insecure, they +can disable `autologin` and `stayloggedin` with: + + my autologin 0 + my stayloggedin 0 + +And then update their login password with: + + my password + +Alternatively, you can manually add users to PBot with the [`useradd`](Admin.md#useradd) command. Its usage is: useradd [channels [capabilities [password]]] @@ -399,14 +419,25 @@ The `hostmasks` and `channels` arguments can be a comma-separated list of values If you omit the `capabilities` argument, the user will be a normal unprivileged user. See [user-capabilities](Admin.md#user-capabilities) for more information about user-capabilities. -If you omit the `password` argument, a random password will be generated. The user -can use the [`my`](Commands.md#my) command to view or change it. - Users may view and change their own metadata by using the [`my`](Commands.md#my) command, provided their hostmask matches the user account. my [key [value]] +Admins may change a user's password with: + + userset password + +If the user has a unique hostmask, preferable via a NickServ vhost/cloak or a reverse-DNS name, they +may prefer to use passwordless autologin via: + + userset autologin 1 + +If the user has a unique hostmask, preferable via a NickServ vhost/cloak or a reverse-DNS name, they +may prefer to remain permanently logged in via: + + userset stayloggedin 1 + For more information, see the [Admin documentation](Admin.md). ### Adding channels diff --git a/lib/PBot/Core/Commands/Users.pm b/lib/PBot/Core/Commands/Users.pm index c5d64d6b..65e6b497 100644 --- a/lib/PBot/Core/Commands/Users.pm +++ b/lib/PBot/Core/Commands/Users.pm @@ -237,6 +237,10 @@ sub cmd_userset($self, $context) { return "To set the $key capability your user account must also have it." unless $self->{pbot}->{capabilities}->userhas($u, 'botowner'); } + if ($key eq 'password' and defined $value) { + $value = $self->{pbot}->{users}->digest_password($value); + } + my $result = $self->{pbot}->{users}->{storage}->set($name, $key, $value); $result =~ s/^password: .*;?$/password: ;/m; @@ -351,6 +355,10 @@ sub cmd_my($self, $context) { $result = "Usage: my [value]; "; } + if ($key eq 'password' and defined $value) { + $value = $self->{pbot}->{users}->digest_password($value); + } + $result .= $self->{pbot}->{users}->{storage}->set($name, $key, $value); $result =~ s/^password: .*;?$/password: ;/m; return $result; diff --git a/lib/PBot/Core/Users.pm b/lib/PBot/Core/Users.pm index 17f0895b..e2c7cce2 100644 --- a/lib/PBot/Core/Users.pm +++ b/lib/PBot/Core/Users.pm @@ -10,6 +10,8 @@ use parent 'PBot::Core::Class'; use PBot::Imports; +use Crypt::SaltedHash; + sub initialize($self, %conf) { $self->{storage} = PBot::Core::Storage::HashObject->new( pbot => $conf{pbot}, @@ -27,6 +29,7 @@ sub add_user($self, $name, $channels, $hostmasks, $capabilities = 'none', $passw $channels = 'global' if $channels !~ m/^#/; $password //= $self->{pbot}->random_nick(16); + $password = $self->digest_password($password); my $data = { channels => $channels, @@ -177,7 +180,7 @@ sub login($self, $channel, $hostmask, $password = undef) { return "You do not have a user account$channel_text."; } - if (defined $password and $user->{password} ne $password) { + if (defined $password and !Crypt::SaltedHash->validate($user->{password}, $password)) { $self->{pbot}->{logger}->log("Bad login password for $channel $hostmask\n"); return "I don't think so."; } @@ -218,4 +221,10 @@ sub get_loggedin_user_metadata($self, $channel, $hostmask, $key) { return undef; } +sub digest_password($self, $password) { + my $csh = Crypt::SaltedHash->new(algorithm => 'SHA-512'); + $csh->add($password); + return $csh->generate; +} + 1; diff --git a/lib/PBot/VERSION.pm b/lib/PBot/VERSION.pm index eb645ef5..1d0d26f1 100644 --- a/lib/PBot/VERSION.pm +++ b/lib/PBot/VERSION.pm @@ -25,8 +25,8 @@ use PBot::Imports; # These are set by the /misc/update_version script use constant { BUILD_NAME => "PBot", - BUILD_REVISION => 4761, - BUILD_DATE => "2024-06-12", + BUILD_REVISION => 4762, + BUILD_DATE => "2024-06-22", }; sub initialize {} diff --git a/updates/4762_hash_passwords.pl b/updates/4762_hash_passwords.pl new file mode 100755 index 00000000..bb0e3a29 --- /dev/null +++ b/updates/4762_hash_passwords.pl @@ -0,0 +1,60 @@ +#!/usr/bin/env perl + +# Replaces user cleartext passwords with salted hashes. +# +# This was way overdue. User passwords are no longer stored as cleartext. +# +# Why did it take me so long to finally get around to hashing passwords +# properly, you might ask. The reason why this wasn't done sooner is because +# all of my users used hostmask-based `autologin`. The passwords that PBot +# randomly generated were ignored and never used. +# +# I do regret that it took me so long to get around to this, for those of you +# who might be using custom passwords instead of hostmask-based `autologin`. + +use warnings; +use strict; + +BEGIN { + use File::Basename; + my $location = -l __FILE__ ? dirname readlink __FILE__ : dirname __FILE__; + unshift @INC, $location; +} + +use lib4422::HashObject; +use lib3503::PBot; + +use Crypt::SaltedHash; + +my ($data_dir, $version, $last_update) = @ARGV; + +print "Hashing passwords ... version: $version, last_update: $last_update, data_dir: $data_dir\n"; + +my $pbot = lib3503::PBot->new(); + +my $users = lib4422::HashObject->new(name => 'Users', filename => "$data_dir/users", pbot => $pbot); + +$users->load; + +if (not keys $users->{hash}->%*) { + die "No users loaded"; +} + +print "Updating users:\n"; + +foreach my $user (keys %{$users->{hash}}) { + if ($user eq '$metadata$') { + $users->{hash}->{$user}->{update_version} = 4762; + next; + } + + print " $user ..."; + my $csh = Crypt::SaltedHash->new(algorithm => 'SHA-512'); + $csh->add($users->{hash}->{$user}->{password}); + $users->{hash}->{$user}->{password} = $csh->generate; + print " done\n"; +} + +$users->save; +print "Done.\n"; +exit 0;