crypted admin password support
authorTony Cook <tony@develop-help.com>
Mon, 2 Aug 2010 06:07:13 +0000 (06:07 +0000)
committertony <tony@45cb6cf1-00bc-42d2-bb5a-07f51df49f94>
Mon, 2 Aug 2010 06:07:13 +0000 (06:07 +0000)
15 files changed:
MANIFEST
schema/bse.sql
site/cgi-bin/modules/BSE/AdminLogon.pm
site/cgi-bin/modules/BSE/AdminUsers.pm
site/cgi-bin/modules/BSE/ChangePW.pm
site/cgi-bin/modules/BSE/DB/Mysql.pm
site/cgi-bin/modules/BSE/Password.pod [new file with mode: 0644]
site/cgi-bin/modules/BSE/Password/Crypt.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/Password/CryptMD5.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/Password/CryptSHA256.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/Password/Plain.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/Passwords.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/TB/AdminUser.pm
site/cgi-bin/modules/BSE/TB/AdminUsers.pm
site/util/mysql.str

index 21b3945..0b7ef98 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -116,6 +116,12 @@ site/cgi-bin/modules/BSE/Message.pm
 site/cgi-bin/modules/BSE/MessageScanner.pm
 site/cgi-bin/modules/BSE/NLFilter/SQL.pm
 site/cgi-bin/modules/BSE/NotifyFiles.pm
+site/cgi-bin/modules/BSE/Password.pod
+site/cgi-bin/modules/BSE/Password/Crypt.pm
+site/cgi-bin/modules/BSE/Password/CryptMD5.pm
+site/cgi-bin/modules/BSE/Password/CryptSHA256.pm
+site/cgi-bin/modules/BSE/Password/Plain.pm
+site/cgi-bin/modules/BSE/Passwords.pm
 site/cgi-bin/modules/BSE/Permissions.pm
 site/cgi-bin/modules/BSE/ProductImportXLS.pm
 site/cgi-bin/modules/BSE/Report.pm
index 5e59fc8..7b33f11 100644 (file)
@@ -698,8 +698,9 @@ create table admin_users (
   base_id integer not null,
   logon varchar(60) not null,
   name varchar(255) not null,
-  password varchar(80) not null,
+  password varchar(255) not null,
   perm_map varchar(255) not null,
+  password_type varchar(20) not null default 'plain',
   primary key (base_id),
   unique (logon)
 );
index be73ae5..8c0abcc 100644 (file)
@@ -130,7 +130,13 @@ sub req_logon {
     and return $class->_service_error($req, undef, \%errors, "FIELD");
   require BSE::TB::AdminUsers;
   my $user = BSE::TB::AdminUsers->getBy(logon=>$logon);
-  $user && $user->{password} eq $password
+  my $error;
+  my $match = $user->check_password($password, \$error);
+  if (!$match && $error eq "LOAD") {
+    $errors{logon} = "Could not load password check module for type ".$user->password_type;
+    return $class->_service_error($req, undef, \%errors, "FIELD");
+  }
+  $user && $match
     or return $class->_service_error($req, "Invalid logon or password", {}, "INVALID");
   $req->session->{adminuserid} = $user->{id};
   delete $req->session->{csrfp};
index 6beb836..f3316f8 100644 (file)
@@ -99,11 +99,26 @@ sub _save_htusers {
     $$rmsg = "Cannot create work userfile $work: $!";
     return;
   }
+  my $bad_count = 0;
+  my $bad_user;
   for my $user (@users) {
-    my $salt = join '', @saltchars[rand(@saltchars), rand(@saltchars)];
-    my $cryptpw = crypt $user->{password}, $salt;
+    my $cryptpw;
+    if ($user->password_type eq "plain") {
+      my $salt = join '', @saltchars[rand(@saltchars), rand(@saltchars)];
+      $cryptpw = crypt $user->{password}, $salt;
+    }
+    elsif ($user->password_type =~ /^crypt/) {
+      $cryptpw = $user->password;
+    }
+    else {
+      $bad_user = $user;
+      ++$bad_count;
+    }
     print USERS "$user->{logon}:$cryptpw\n";
   }
+  if ($bad_count) {
+    $req->flash("Cannot handle password type " . $bad_user->password_type . " in userfile for up to $bad_count users");
+  }
   close USERS;
   chmod 0644, $work;
   unless (rename $work, $userfile) {
@@ -263,9 +278,7 @@ sub req_adduser {
      password => $password,
      perm_map => '',
     );
-  my @cols = BSE::TB::AdminUser->columns;
-  shift @cols;
-  my $user = BSE::TB::AdminUsers->add(@user{@cols});
+  my $user = BSE::TB::AdminUsers->make(%user);
 
   my $msg = "User $logon created";
 
@@ -297,7 +310,7 @@ sub req_addgroup {
     or $errors{template_set} = 
       $req->text(bse_invalid_group_template_set =>
                 'Please select a valid template_set');
-                                         
+
   keys %errors
     and return $class->req_addgroupform($req, undef, \%errors);
   my $old = BSE::TB::AdminGroups->getBy(name=>$name)
@@ -664,7 +677,7 @@ sub req_saveuser {
      && length $password) {
     if (length $confirm) {
       if ($password eq $confirm) {
-       $user->{password} = $password;
+       $user->changepw($password);
       }
       else {
        $errors{confirm} = "Password and confirmation password didn't match"
index cf5d449..5dfe3a5 100644 (file)
@@ -63,7 +63,7 @@ sub req_change {
     $errors{oldpassword} = "Enter your current password";
   }
   else {
-    $oldpw eq $user->{password}
+    $user->check_password($oldpw)
       or $errors{oldpassword} = "Your old password is incorrect";
   }
   if (!defined $newpw || $newpw eq '') {
@@ -79,7 +79,7 @@ sub req_change {
   keys %errors
     and return $class->req_form($req, undef, \%errors);
 
-  $user->{password} = $newpw;
+  $user->changepw($newpw);
   $user->save;
 
   my $r = $cgi->param('r');
index 1b199d4..6f5ccd4 100644 (file)
@@ -293,8 +293,8 @@ SQL
 select bs.*, us.* from admin_base bs, admin_users us
   where bs.id = us.base_id and bs.id = ?
 SQL
-   addAdminUser => 'insert into admin_users values(?,?,?,?,?)',
-   replaceAdminUser => 'replace into admin_users values(?,?,?,?,?)',
+   addAdminUser => 'insert into admin_users values(?,?,?,?,?,?)',
+   replaceAdminUser => 'replace into admin_users values(?,?,?,?,?,?)',
    deleteAdminUser => 'delete from admin_users where base_id = ?',
    adminUsersGroups => <<SQL,
 select bs.*, gr.*
diff --git a/site/cgi-bin/modules/BSE/Password.pod b/site/cgi-bin/modules/BSE/Password.pod
new file mode 100644 (file)
index 0000000..d5cf5e9
--- /dev/null
@@ -0,0 +1,40 @@
+=head1 NAME
+
+BSE::Password - interface for password hashing and checking.
+
+=head1 SYNOPSIS
+
+  my $pass_handler = BSE::Password::Foo->new;
+  my $hash = $pass_handler->hash($password);
+  if ($pass_handler->check($hash, $password)) {
+    # success
+  }
+
+=head1 DESCRIPTION
+
+This module implements a simple mechanism for hashing passwords.
+
+Sub-classes should implement three methods:
+
+=over
+
+=item new()
+
+Create a new handler.  Should return nothing if this mechanism is not
+available, eg. if modules required to implement it aren't available.
+
+=item hash($password)
+
+Return a hash to store for later checks.
+
+=item check($hash, $password)
+
+Check that the password matches the hash.  Return true on a match.
+
+=back
+
+=head1 AUTHOR
+
+Tony Cook <tony@develop-help.com>
+
+=cut
diff --git a/site/cgi-bin/modules/BSE/Password/Crypt.pm b/site/cgi-bin/modules/BSE/Password/Crypt.pm
new file mode 100644 (file)
index 0000000..5feb3fb
--- /dev/null
@@ -0,0 +1,28 @@
+package BSE::Password::Crypt;
+use strict;
+
+sub new {
+  my ($class) = @_;
+
+  return bless {}, $class;
+}
+
+sub _salt {
+  return join '', ('.', '/', 0..9, 'A'..'Z', 'a'..'z')[rand 64, rand 64]
+}
+
+sub hash {
+  my ($self, $password) = @_;
+
+  my $salt = $self->_salt;
+
+  return crypt($password, $salt);
+}
+
+sub check {
+  my ($self, $hash, $password) = @_;
+
+  return crypt($password, $hash) eq $hash;
+}
+
+1;
diff --git a/site/cgi-bin/modules/BSE/Password/CryptMD5.pm b/site/cgi-bin/modules/BSE/Password/CryptMD5.pm
new file mode 100644 (file)
index 0000000..f8cd27a
--- /dev/null
@@ -0,0 +1,19 @@
+package BSE::Password::CryptMD5;
+use strict;
+use base "BSE::Password::Crypt";
+
+sub new {
+  my ($class) = @_;
+
+  crypt("test", '$1$00000000') eq '$1$00000000$/6RgkLRMOYgcMlYPGNjSy.'
+    or return;
+
+  return $class->SUPER::new();
+}
+
+sub _salt {
+  return '$1$' . join "",
+    ('.', '/', 0..9, 'A'..'Z', 'a'..'z')[map rand 64, 1..8]
+}
+
+1;
diff --git a/site/cgi-bin/modules/BSE/Password/CryptSHA256.pm b/site/cgi-bin/modules/BSE/Password/CryptSHA256.pm
new file mode 100644 (file)
index 0000000..14d5ff1
--- /dev/null
@@ -0,0 +1,19 @@
+package BSE::Password::CryptSHA256;
+use strict;
+use base "BSE::Password::Crypt";
+
+sub new {
+  my ($class) = @_;
+
+  crypt("test", '$5$00000000') eq '$5$00000000$rlV/Cvf6KAPRr28eKJSvYDnUm9rmJztOXHH0jx7zhnB'
+    or return;
+
+  return $class->SUPER::new();
+}
+
+sub _salt {
+  return '$5$' . join "",
+    ('.', '/', 0..9, 'A'..'Z', 'a'..'z')[map rand 64, 1..16]
+}
+
+1;
diff --git a/site/cgi-bin/modules/BSE/Password/Plain.pm b/site/cgi-bin/modules/BSE/Password/Plain.pm
new file mode 100644 (file)
index 0000000..ae81407
--- /dev/null
@@ -0,0 +1,22 @@
+package BSE::Password::Plain;
+use strict;
+
+sub new {
+  my ($class) = @_;
+
+  return bless {}, $class;
+}
+
+sub hash {
+  my ($self, $password) = @_;
+
+  return $password;
+}
+
+sub check {
+  my ($self, $hash, $password) = @_;
+
+  return $hash eq $password;
+}
+
+1;
diff --git a/site/cgi-bin/modules/BSE/Passwords.pm b/site/cgi-bin/modules/BSE/Passwords.pm
new file mode 100644 (file)
index 0000000..493ab0b
--- /dev/null
@@ -0,0 +1,62 @@
+package BSE::Passwords;
+use strict;
+
+# wrapper around using the BSE::Password classes
+
+sub new_password_hash {
+  my ($self, $password) = @_;
+
+  my ($obj, $name) = $self->new_password_handler;
+
+  return ( $obj->hash($password), $name );
+}
+
+sub check_password_hash {
+  my ($self, $hash, $type, $password, $error) = @_;
+
+  my $obj = $self->_load($type);
+  unless ($obj) {
+    $$error = "LOAD";
+    return;
+  }
+
+  unless ($obj->check($hash, $password)) {
+    $$error = "INVALID";
+    return;
+  }
+
+  return 1;
+}
+
+sub new_password_handler {
+  my ($self) = @_;
+
+  my $cfg = BSE::Cfg->single;
+  my @handlers = split /,/, $cfg->entry("basic", "passwords", "cryptSHA256,cryptMD5,crypt,plain");
+
+  for my $handler (@handlers) {
+    my $obj = $self->_load($handler);
+    $obj and return ($obj, $handler);
+  }
+
+  require BSE::Password::Plain;
+  return ( BSE::Password::Plain->new, "plain" );
+}
+
+sub _load {
+  my ($self, $handler) = @_;
+
+  my $class = "BSE::Password::\u$handler";
+  my $file = "BSE/Password/\u$handler.pm";
+
+  eval { require $file; 1 }
+    or return;
+
+  my $obj = $class->new
+    or return;
+
+  return $obj;
+}
+
+1;
+
index 9e98cad..4a0617c 100644 (file)
@@ -4,7 +4,7 @@ use base qw(BSE::TB::AdminBase);
 
 sub columns {
   return ($_[0]->SUPER::columns,
-         qw/base_id logon name password perm_map/);
+         qw/base_id logon name password perm_map password_type/);
 }
 
 sub bases {
@@ -27,5 +27,25 @@ sub groups {
   BSE::TB::AdminGroups->getSpecial(forUser => $self->{id});
 }
 
+sub changepw {
+  my ($self, $password) = @_;
+
+  require BSE::Passwords;
+
+  my ($hash, $type) = BSE::Passwords->new_password_hash($password);
+
+  $self->set_password($hash);
+  $self->set_password_type($type);
+
+  1;
+}
+
+sub check_password {
+  my ($self, $password, $error) = @_;
+
+  require BSE::Passwords;
+  return BSE::Passwords->check_password_hash($self->password, $self->password_type, $password, \$error);
+}
+
 1;
 
index a62f682..de323c4 100644 (file)
@@ -7,4 +7,17 @@ sub rowClass {
   return 'BSE::TB::AdminUser';
 }
 
+sub make {
+  my ($self, %opts) = @_;
+
+  require BSE::Passwords;
+  my $password = delete $opts{password};
+  my ($hash, $type) = BSE::Passwords->new_password_hash($password);
+
+  $opts{password} = $hash;
+  $opts{password_type} = $type;
+
+  return $self->SUPER::make(%opts);
+}
+
 1;
index 495e902..6370717 100644 (file)
@@ -23,8 +23,9 @@ Table admin_users
 Column base_id;int(11);NO;NULL;
 Column logon;varchar(60);NO;NULL;
 Column name;varchar(255);NO;NULL;
-Column password;varchar(80);NO;NULL;
+Column password;varchar(255);NO;NULL;
 Column perm_map;varchar(255);NO;NULL;
+Column password_type;varchar(20);NO;plain;
 Index PRIMARY;1;[base_id]
 Index logon;1;[logon]
 Table article