]> git.sesse.net Git - pr0n/blobdiff - perl/Sesse/pr0n/Common.pm
Save passwords as bcrypt instead of plain SHA-1 hashes (includes migration of old...
[pr0n] / perl / Sesse / pr0n / Common.pm
index 559d6339f38b206498902f801ffa721d777c6593..d03ed6d3a2ef79e342a73ff86837347ec0606369 100644 (file)
@@ -19,7 +19,7 @@ use DBD::Pg;
 use Image::Magick;
 use POSIX;
 use Digest::MD5;
-use Digest::SHA1;
+use Digest::SHA;
 use Digest::HMAC_SHA1;
 use MIME::Base64;
 use MIME::Types;
@@ -29,6 +29,7 @@ use Image::ExifTool;
 use HTML::Entities;
 use URI::Escape;
 use File::Basename;
+use Crypt::Eksblowfish::Bcrypt;
 
 BEGIN {
        use Exporter ();
@@ -39,7 +40,7 @@ BEGIN {
                require Sesse::pr0n::Config_local;
        };
 
-       $VERSION     = "v2.72";
+       $VERSION     = "v2.80";
        @ISA         = qw(Exporter);
        @EXPORT      = qw(&error &dberror);
        %EXPORT_TAGS = qw();
@@ -96,7 +97,7 @@ sub header {
                $quote = LWP::Simple::get("http://itk.samfundet.no/include/quotes.cli.php");
                $quote = "Error: Could not fetch quotes." if (!defined($quote));
        }
-       Sesse::pr0n::Templates::print_template($r, "header", { title => $title, quotes => Encode::decode_utf8($quote) });
+       Sesse::pr0n::Templates::print_template($r, "header", { title => $title, quotes => $quote });
 }
 
 sub footer {
@@ -308,6 +309,10 @@ sub update_image_info {
                
                # Tags
                my @tags = $exiftool->GetValue('Keywords', 'ValueConv');
+               if (scalar @tags == 0) {
+                       # This is XMP-dc:Subject, an RDF bag of tags.
+                       @tags = $exiftool->GetValue('Subject', 'ValueConv');
+               }
                $dbh->do('DELETE FROM tags WHERE image=?',
                        undef, $id)
                        or die "Couldn't delete old tag information in SQL: $!";
@@ -363,11 +368,11 @@ sub output_401 {
                # proxies etc. are being used), and we use HMAC instead of simple
                # hashing simply because that's a better signing method.
                #
-               # NOTE: For some weird reason, Digest::HMAC_SHA1 doesn't like taking
+               # NOTE: For some weird reason, Digest::HMAC_SHA doesn't like taking
                # the output from time directly (it gives a different response), so we
                # forcefully stringify the argument.
                my $ts = time;
-               my $nonce = Digest::HMAC_SHA1->hmac_sha1_hex($ts . "", $Sesse::pr0n::Config::db_password);
+               my $nonce = Digest::HMAC_SHA->hmac_sha1_hex($ts . "", $Sesse::pr0n::Config::db_password);
                my $stale_nonce_text = "";
                $stale_nonce_text = ", stale=\"true\"" if ($options{'StaleNonce'} // 0);
 
@@ -386,10 +391,18 @@ sub check_basic_auth {
 
        my ($raw_user, $pass) = split /:/, MIME::Base64::decode_base64($auth);
        my ($user, $takenby) = extract_takenby($raw_user);
-       
-       my $ref = $dbh->selectrow_hashref('SELECT sha1password,digest_ha1_hex FROM users WHERE username=? AND vhost=?',
+
+       my $ref = $dbh->selectrow_hashref('SELECT sha1password,cryptpassword,digest_ha1_hex FROM users WHERE username=? AND vhost=?',
                undef, $user, $r->get_server_name);
-       if (!defined($ref) || $ref->{'sha1password'} ne Digest::SHA1::sha1_base64($pass)) {
+       my ($sha1_matches, $bcrypt_matches) = (0, 0);
+       if (defined($ref) && defined($ref->{'sha1password'})) {
+               $sha1_matches = (Digest::SHA::sha1_base64($pass) eq $ref->{'sha1password'});
+       }
+       if (defined($ref) && defined($ref->{'cryptpassword'})) {
+               $bcrypt_matches = (Crypt::Eksblowfish::Bcrypt::bcrypt($pass, $ref->{'cryptpassword'}) eq $ref->{'cryptpassword'});
+       }
+
+       if (!defined($ref) || (!$sha1_matches && !$bcrypt_matches)) {
                $r->content_type('text/plain; charset=utf-8');
                $r->log->warn("Authentication failed for $user/$takenby");
                output_401($r);
@@ -406,9 +419,39 @@ sub check_basic_auth {
                $r->log->info("Updated Digest auth hash for for $user");
        }
 
+       # Make sure we can use bcrypt authentication in the future with this password.
+       # Also remove old-style SHA1 password when we migrate.
+       if (!$bcrypt_matches) {
+               my $salt = get_pseudorandom_bytes(16);  # Doesn't need to be cryptographically secur.
+               my $hash = "\$2a\$07\$" . Crypt::Eksblowfish::Bcrypt::en_base64($salt);
+               my $cryptpassword = Crypt::Eksblowfish::Bcrypt::bcrypt($pass, $hash);
+               $dbh->do('UPDATE users SET sha1password=NULL,cryptpassword=? WHERE username=? AND vhost=?',
+                       undef, $cryptpassword, $user, $r->get_server_name)
+                       or die "Couldn't update: " . $dbh->errstr;
+               $r->log->info("Updated bcrypt hash for $user");
+       }
+
        return ($user, $takenby);
 }
 
+sub get_pseudorandom_bytes {
+       my $num_left = shift;
+       my $bytes = "";
+       open my $randfh, "<", "/dev/urandom"
+               or die "/dev/urandom: $!";
+       binmode $randfh;
+       while ($num_left > 0) {
+               my $tmp;
+               if (sysread($randfh, $tmp, $num_left) == -1) {
+                       die "sysread(/dev/urandom): $!";
+               }
+               $bytes .= $tmp;
+               $num_left -= length($bytes);
+       }
+       close $randfh;
+       return $bytes;
+}
+
 sub check_digest_auth {
        my ($r, $auth) = @_;