+ my ($raw_user, $pass) = split /:/, MIME::Base64::decode_base64($auth);
+ my ($user, $takenby) = extract_takenby($raw_user);
+
+ my $ref = $dbh->selectrow_hashref('SELECT sha1password,cryptpassword,digest_ha1_hex FROM users WHERE username=? AND vhost=?',
+ undef, $user, $r->get_server_name);
+ 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);
+ return undef;
+ }
+ $r->log->info("Authentication succeeded for $user/$takenby");
+
+ # Make sure we can use Digest authentication in the future with this password.
+ my $ha1 = Digest::MD5::md5_hex($user . ':pr0n.sesse.net:' . $pass);
+ if (!defined($ref->{'digest_ha1_hex'}) || $ref->{'digest_ha1_hex'} ne $ha1) {
+ $dbh->do('UPDATE users SET digest_ha1_hex=? WHERE username=? AND vhost=?',
+ undef, $ha1, $user, $r->get_server_name)
+ or die "Couldn't update: " . $dbh->errstr;
+ $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) = @_;
+
+ # We're a bit more liberal than RFC2069 in the parsing here, allowing
+ # quoted strings everywhere.
+ my %auth = ();
+ while ($auth =~ s/^ ([a-zA-Z]+) # key
+ =
+ (
+ [^",]* # either something that doesn't contain comma or quotes
+ |
+ " ( [^"\\] | \\ . ) * " # or a full quoted string
+ )
+ (?: (?: , \s* ) + | $ ) # delimiter(s), or end of string
+ //x) {
+ my ($key, $value) = ($1, $2);
+ if ($value =~ /^"(.*)"$/) {
+ $value = $1;
+ $value =~ s/\\(.)/$1/g;
+ }
+ $auth{$key} = $value;
+ }
+ unless (exists($auth{'username'}) &&
+ exists($auth{'uri'}) &&
+ exists($auth{'nonce'}) &&
+ exists($auth{'opaque'}) &&
+ exists($auth{'response'})) {
+ output_401($r);
+ return undef;
+ }
+ if ($r->uri ne $auth{'uri'}) {
+ output_401($r);
+ return undef;
+ }
+
+ # Verify that the opaque data does indeed look like a timestamp, and that the nonce
+ # is indeed a signed version of it.
+ if ($auth{'opaque'} !~ /^\d+$/) {
+ output_401($r);
+ return undef;
+ }
+ my $compare_nonce = Digest::HMAC_SHA1->hmac_sha1_hex($auth{'opaque'}, $Sesse::pr0n::Config::db_password);
+ if ($auth{'nonce'} ne $compare_nonce) {
+ output_401($r);
+ return undef;
+ }
+
+ # Now look up the user's HA1 from the database, and calculate HA2.
+ my ($user, $takenby) = extract_takenby($auth{'username'});
+ my $ref = $dbh->selectrow_hashref('SELECT digest_ha1_hex FROM users WHERE username=? AND vhost=?',
+ undef, $user, $r->get_server_name);
+ if (!defined($ref)) {
+ output_401($r);
+ return undef;
+ }
+ if (!defined($ref->{'digest_ha1_hex'}) || $ref->{'digest_ha1_hex'} !~ /^[0-9a-f]{32}$/) {
+ # A user that exists but has empty HA1 is a user that's not
+ # ready for digest auth, so we hack it and resend 401,
+ # only this time without digest auth.
+ output_401($r, DigestAuth => 0);
+ return undef;
+ }
+ my $ha1 = $ref->{'digest_ha1_hex'};
+ my $ha2 = Digest::MD5::md5_hex($r->method . ':' . $auth{'uri'});
+ my $response;
+ if (exists($auth{'qop'}) && $auth{'qop'} eq 'auth') {
+ unless (exists($auth{'nc'}) && exists($auth{'cnonce'})) {
+ output_401($r);
+ return undef;
+ }
+
+ $response = $ha1;
+ $response .= ':' . $auth{'nonce'};
+ $response .= ':' . $auth{'nc'};
+ $response .= ':' . $auth{'cnonce'};
+ $response .= ':' . $auth{'qop'};
+ $response .= ':' . $ha2;
+ } else {
+ $response = $ha1;
+ $response .= ':' . $auth{'nonce'};
+ $response .= ':' . $ha2;
+ }
+ if ($auth{'response'} ne Digest::MD5::md5_hex($response)) {
+ output_401($r);
+ return undef;
+ }
+
+ # OK, everything is good, and there's only one thing we need to check: That the nonce
+ # isn't too old. If it is, but everything else is ok, we tell the browser that and it
+ # will re-encrypt with the new nonce.
+ my $timediff = time - $auth{'opaque'};
+ if ($timediff < 0 || $timediff > 300) {
+ output_401($r, StaleNonce => 1);
+ return undef;
+ }
+
+ return ($user, $takenby);
+}
+
+sub extract_takenby {
+ my ($user) = shift;