From 6722fd7f8d7373bcdaac174a745048a8374d0e84 Mon Sep 17 00:00:00 2001 From: Pragmatic Software Date: Sat, 22 Jun 2024 22:38:15 -0700 Subject: [PATCH] Store user passwords as salted hash digests This was way overdue. User passwords are no longer stored as cleartext. When PBot is restarted after applying this commit, all stored passwords will be converted to salted hash digests. The `useradd`, `userset` and `my` commands will now hash passwords. 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`. --- cpanfile | 1 + doc/Admin.md | 2 +- doc/FAQ.md | 12 +++++-- doc/QuickStart.md | 55 +++++++++++++++++++++++------- lib/PBot/Core/Commands/Users.pm | 8 +++++ lib/PBot/Core/Users.pm | 11 +++++- lib/PBot/VERSION.pm | 4 +-- updates/4762_hash_passwords.pl | 60 +++++++++++++++++++++++++++++++++ 8 files changed, 134 insertions(+), 19 deletions(-) create mode 100755 updates/4762_hash_passwords.pl 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;