diff options
-rw-r--r-- | ChangeLog | 1 | ||||
-rw-r--r-- | data/global.pl | 2 | ||||
-rw-r--r-- | lib/VNDB/DB/Users.pm | 50 | ||||
-rw-r--r-- | lib/VNDB/Handler/Users.pm | 11 | ||||
-rw-r--r-- | lib/VNDB/Util/Auth.pm | 86 | ||||
-rw-r--r-- | util/dump.sql | 12 | ||||
-rw-r--r-- | util/updates/update_2.6.sql | 13 |
7 files changed, 141 insertions, 34 deletions
@@ -1,5 +1,6 @@ 2.6 - ? - New screen resolutions: 1024x600 and 1600x1200 + - Rewritten authentication system 2.5 - 2009-07-09 - Hide NSFW images in diff viewer (unless NSFW warnings are disabled) diff --git a/data/global.pl b/data/global.pl index 83cb42fd..09382411 100644 --- a/data/global.pl +++ b/data/global.pl @@ -20,7 +20,7 @@ our %S = (%S, site_title => 'Yet another VNDB clone', skin_default => 'angel', cookie_domain => '.vndb.org', - cookie_key => 'any-private-string-here', + global_salt => 'any-private-string-here', source_url => 'http://git.blicky.net/vndb.git/?h=master', admin_email => 'contact@vndb.org', sharedmem_key => 'VNDB', diff --git a/lib/VNDB/DB/Users.pm b/lib/VNDB/DB/Users.pm index d5cdcc6c..eaed1234 100644 --- a/lib/VNDB/DB/Users.pm +++ b/lib/VNDB/DB/Users.pm @@ -5,7 +5,7 @@ use strict; use warnings; use Exporter 'import'; -our @EXPORT = qw|dbUserGet dbUserEdit dbUserAdd dbUserDel|; +our @EXPORT = qw|dbUserGet dbUserEdit dbUserAdd dbUserDel dbSessionAdd dbSessionDel dbSessionCheck|; # %options->{ username passwd mail order uid ip registered search results page what } @@ -75,7 +75,7 @@ sub dbUserEdit { my %h; defined $o{$_} && ($h{$_.' = ?'} = $o{$_}) - for (qw| username mail rank show_nsfw show_list skin customcss |); + for (qw| username mail rank show_nsfw show_list skin customcss salt |); $h{'passwd = decode(?, \'hex\')'} = $o{passwd} if defined $o{passwd}; @@ -88,11 +88,11 @@ sub dbUserEdit { } -# username, md5(pass), mail, [ip] +# username, pass(ecrypted), salt, mail, [ip] sub dbUserAdd { my($s, @o) = @_; - $s->dbExec(q|INSERT INTO users (username, passwd, mail, ip, registered) VALUES(?, decode(?, 'hex'), ?, ?, ?)|, - @o[0..2], $o[3]||$s->reqIP, time); + $s->dbExec(q|INSERT INTO users (username, passwd, salt, mail, ip, registered) VALUES(?, decode(?, 'hex'), ?, ?, ?, ?)|, + @o[0..3], $o[4]||$s->reqIP, time); } @@ -104,6 +104,7 @@ sub dbUserDel { q|DELETE FROM rlists WHERE uid = ?|, q|DELETE FROM wlists WHERE uid = ?|, q|DELETE FROM votes WHERE uid = ?|, + q|DELETE FROM sessions WHERE uid = ?|, q|UPDATE changes SET requester = 0 WHERE requester = ?|, q|UPDATE threads_posts SET uid = 0 WHERE uid = ?|, q|DELETE FROM users WHERE id = ?| @@ -111,4 +112,43 @@ sub dbUserDel { } +# Adds a session to the database +# If no expiration is supplied the database default is used +# uid, 40 character session token, expiration time (timestamp) +sub dbSessionAdd { + my($s, @o) = @_; + if (defined $o[2]) { + $s->dbExec(q|INSERT INTO sessions (uid, token, expiration) VALUES(?, ?, ?)|, + @o); + } else { + $s->dbExec(q|INSERT INTO sessions (uid, token) VALUES(?, ?)|, + @o); + } +} + + +# Deletes session(s) from the database +# If no token is supplied, all sessions for the uid are destroyed +# uid, token (optional) +sub dbSessionDel { + my($s, @o) = @_; + if (defined $o[1]) { + $s->dbExec(q|DELETE FROM sessions WHERE uid = ? AND token = ?|, + @o[0..1]); + } else { + $s->dbExec(q|DELETE FROM sessions WHERE uid = ?|, + $o[0]); + } +} + + +# Queries the database for the validity of a session +# Returns 1 if corresponding session found, 0 if not +# uid, token +sub dbSessionCheck { + my($s, @o) = @_; + return $s->dbRow(q|SELECT count(uid) AS count FROM sessions WHERE uid = ? AND token = ? LIMIT 1|, @o)->{count}||0; +} + + 1; diff --git a/lib/VNDB/Handler/Users.pm b/lib/VNDB/Handler/Users.pm index d4755ae1..982d6089 100644 --- a/lib/VNDB/Handler/Users.pm +++ b/lib/VNDB/Handler/Users.pm @@ -4,7 +4,6 @@ package VNDB::Handler::Users; use strict; use warnings; use YAWF ':html'; -use Digest::MD5 'md5_hex'; use VNDB::Func; @@ -183,7 +182,9 @@ sub newpass { if(!$frm->{_err}) { my @chars = ( 'A'..'Z', 'a'..'z', 0..9 ); my $pass = join '', map $chars[int rand $#chars+1], 0..8; - $self->dbUserEdit($u->{id}, passwd => md5_hex($pass)); + my %o; + ($o{passwd}, $o{salt}) = $self->authPreparePass($pass); + $self->dbUserEdit($u->{id}, %o); my $body = <<'__'; Hello %s, @@ -258,7 +259,8 @@ sub register { push @{$frm->{_err}}, 'oneaday' if !$frm->{_err} && $self->dbUserGet(ip => $self->reqIP, registered => time-24*3600)->[0]{id}; if(!$frm->{_err}) { - $self->dbUserAdd($frm->{usrname}, md5_hex($frm->{usrpass}), $frm->{mail}); + my ($pass, $salt) = $self->authPreparePass($frm->{usrpass}); + $self->dbUserAdd($frm->{usrname}, $pass, $salt, $frm->{mail}); return $self->authLogin($frm->{usrname}, $frm->{usrpass}, '/'); } } @@ -330,10 +332,11 @@ sub edit { $o{mail} = $frm->{mail}; $o{skin} = $frm->{skin}; $o{customcss} = $frm->{customcss}; - $o{passwd} = md5_hex($frm->{usrpass}) if $frm->{usrpass}; + ($o{passwd}, $o{salt}) = $self->authPreparePass($frm->{usrpass}) if $frm->{usrpass}; $o{show_list} = $frm->{flags_list} ? 1 : 0; $o{show_nsfw} = $frm->{flags_nsfw} ? 1 : 0; $self->dbUserEdit($uid, %o); + $self->dbSessionDel($uid) if $frm->{usrpass}; return $self->resRedirect("/u$uid/edit?d=1", 'post') if $uid != $self->authInfo->{id} || !$frm->{usrpass}; return $self->authLogin($frm->{usrname}||$u->{username}, $frm->{usrpass}, "/u$uid/edit?d=1"); } diff --git a/lib/VNDB/Util/Auth.pm b/lib/VNDB/Util/Auth.pm index cb0751e9..00700e6e 100644 --- a/lib/VNDB/Util/Auth.pm +++ b/lib/VNDB/Util/Auth.pm @@ -1,19 +1,17 @@ package VNDB::Util::Auth; -# This module is just a small improvement of the 1.x equivalent -# and is designed to work with the cookies and database of VNDB 1.x -# without modifications. A proper and more secure (incompatible) -# implementation should be written at some point. use strict; use warnings; use Exporter 'import'; -use Digest::MD5 'md5_hex'; -use Crypt::Lite; +use Digest::MD5 'md5'; +use Digest::SHA qw|sha1_hex sha256 sha256_hex|; +use Time::HiRes; +use POSIX 'strftime'; -our @EXPORT = qw| authInit authLogin authLogout authInfo authCan |; +our @EXPORT = qw| authInit authLogin authLogout authInfo authCan authPreparePass |; # initializes authentication information and checks the vndb_auth cookie @@ -22,12 +20,10 @@ sub authInit { $self->{_auth} = undef; my $cookie = $self->reqCookie('vndb_auth'); - return 0 if !$cookie; - my $str = Crypt::Lite->new()->decrypt($cookie, md5_hex($self->{cookie_key})); - return 0 if length($str) < 36; - my $pass = substr($str, 4, 32); - my $user = substr($str, 36); - _authCheck($self, $user, $pass); + return 0 if !$cookie || length($cookie) < 41; + my $token = substr($cookie, 0, 40); + my $uid = substr($cookie, 40); + $self->{_auth} = $self->dbUserGet(uid => $uid, what => 'mymessages')->[0] if $self->dbSessionCheck($uid, $token); } @@ -36,15 +32,21 @@ sub authInit { sub authLogin { my $self = shift; my $user = lc(scalar shift); - my $pass = md5_hex(shift); + my $pass = shift; my $to = shift; if(_authCheck($self, $user, $pass)) { - (my $cookie = Crypt::Lite->new()->encrypt("VNDB$pass$user", md5_hex($self->{cookie_key}))) =~ s/\r?\n//g; + my $token = sha1_hex(join('', Time::HiRes::gettimeofday()) . join('', map chr(rand(93)+33), 1..9)); + my $expiration = time + 31536000; # 1yr + my $cookie = $token . $self->{_auth}{id}; + $self->dbSessionAdd($self->{_auth}{id}, $token); + + my $expstr = strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime($expiration)); $self->resRedirect($to, 'post'); - $self->resHeader('Set-Cookie', "vndb_auth=$cookie; expires=Sat, 01-Jan-2030 00:00:00 GMT; path=/; domain=$self->{cookie_domain}"); + $self->resHeader('Set-Cookie', "vndb_auth=$cookie; expires=$expstr; path=/; domain=$self->{cookie_domain}"); return 1; } + return 0; } @@ -52,6 +54,14 @@ sub authLogin { # clears authentication cookie and redirects to / sub authLogout { my $self = shift; + + my $cookie = $self->reqCookie('vndb_auth'); + if ($cookie && length($cookie) >= 41) { + my $token = substr($cookie, 0, 40); + my $uid = substr($cookie, 40); + $self->dbSessionDel($uid, $token); + } + $self->resRedirect('/', 'temp'); $self->resHeader('Set-Cookie', "vndb_auth= ; expires=Sat, 01-Jan-2000 00:00:00 GMT; path=/; domain=$self->{cookie_domain}"); } @@ -75,20 +85,50 @@ sub authCan { # Checks for a valid login and writes information in _auth -# Arguments: user, md5_hex(pass) +# Arguments: user, pass # Returns: 1 if login is valid, 0 otherwise sub _authCheck { my($self, $user, $pass) = @_; - return 0 if - !$user || length($user) > 15 || length($user) < 2 - || !$pass || length($pass) != 32; + return 0 if !$user || length($user) > 15 || length($user) < 2 || !$pass; - my $d = $self->dbUserGet(username => $user, passwd => $pass, what => 'mymessages')->[0]; + my $d = $self->dbUserGet(username => $user, what => 'mymessages')->[0]; return 0 if !defined $d->{id} || !$d->{rank}; - $self->{_auth} = $d; - return 1; + if (_authEncryptPass($self, $pass, $d->{salt}, 1) eq $d->{passwd}) { + $self->{_auth} = $d; + return 1; + } + if (md5($pass) eq $d->{passwd}) { + $self->{_auth} = $d; + my %o; + ($o{passwd}, $o{salt}) = authPreparePass($self, $pass); + $self->dbUserEdit($d->{id}, %o); + return 1; + } + + return 0; +} + + +# Encryption algorithm for user passwords +# Arguments: self, pass, salt, binary mode +# Returns: encrypted password +sub _authEncryptPass{ + my ($self, $pass, $salt, $bin) = @_; + return sha256($self->{global_salt} . $pass . $salt) if $bin; + return sha256_hex($self->{global_salt} . $pass . $salt); +} + + +# Prepares a plaintext password for database storage +# Arguments: pass +# Returns: list (pass, salt) +sub authPreparePass{ + my($self, $pass) = @_; + my $salt = join '', map chr(rand(93)+33), 1..9; + my $hash = _authEncryptPass($self, $pass, $salt); + return ($hash, $salt); } diff --git a/util/dump.sql b/util/dump.sql index 84cbecbc..f7f9bb9e 100644 --- a/util/dump.sql +++ b/util/dump.sql @@ -161,6 +161,14 @@ CREATE TABLE screenshots ( height smallint NOT NULL DEFAULT 0 ); +-- sessions +CREATE TABLE sessions ( + uid integer NOT NULL, + token character(40) NOT NULL, + expiration timestamp NOT NULL DEFAULT (now() + '1 year'::interval), + PRIMARY KEY (uid, token) +); + -- stats_cache CREATE TABLE stats_cache ( section varchar(25) NOT NULL PRIMARY KEY, @@ -255,7 +263,8 @@ CREATE TABLE users ( skin varchar(128) NOT NULL DEFAULT '', customcss text NOT NULL DEFAULT '', ip inet NOT NULL DEFAULT '0.0.0.0', - c_tags integer NOT NULL DEFAULT 0 + c_tags integer NOT NULL DEFAULT 0, + salt character(9) NOT NULL DEFAULT '' ); -- vn @@ -365,6 +374,7 @@ ALTER TABLE releases_vn ADD FOREIGN KEY (rid) REFERENCES releases_r ALTER TABLE releases_vn ADD FOREIGN KEY (vid) REFERENCES vn (id); ALTER TABLE rlists ADD FOREIGN KEY (uid) REFERENCES users (id); ALTER TABLE rlists ADD FOREIGN KEY (rid) REFERENCES releases (id); +ALTER TABLE sessions ADD FOREIGN KEY (uid) REFERENCES users (id); ALTER TABLE tags ADD FOREIGN KEY (addedby) REFERENCES users (id); ALTER TABLE tags_aliases ADD FOREIGN KEY (tag) REFERENCES tags (id); ALTER TABLE tags_parents ADD FOREIGN KEY (tag) REFERENCES tags (id); diff --git a/util/updates/update_2.6.sql b/util/updates/update_2.6.sql index 567af268..67bbe584 100644 --- a/util/updates/update_2.6.sql +++ b/util/updates/update_2.6.sql @@ -1,5 +1,18 @@ +-- Create table for session data storage +CREATE TABLE sessions ( + uid integer NOT NULL REFERENCES users(id); + token character(40) NOT NULL, + expiration NOT NULL DEFAULT (NOW() + '1 year'::interval), + PRIMARY KEY (uid, token) +); + +-- Add column to users for salt storage +ALTER TABLE users ADD COLUMN salt character(9) NOT NULL DEFAULT ''::bpchar; + + + -- The anime table: -- - use timestamp data type for anime.lastfetch -- - allow NULL for all columns except id |