FHEMWEB - salted basic auth

Begonnen von wa, 04 Januar 2014, 02:46:19

Vorheriges Thema - Nächstes Thema

wa

Hallo!

Ich wollte mal etwas gegen die Speicherung der Klartextpasswörter unternehmen, also habe ich mal meine 01_FHEMWEB gepatcht:

--- 01_FHEMWEB - Kopie.pm Sun Sep 29 12:46:41 2013
+++ 01_FHEMWEB.pm Sat Jan 04 15:37:10 2014
@@ -118,7 +118,8 @@ FHEMWEB_Initialize($)
     "stylesheetPrefix touchpad:deprecated smallscreen:deprecated ".
     "basicAuth basicAuthMsg hiddenroom hiddengroup HTTPS allowfrom CORS:0,1 ".
     "refresh longpoll:0,1 longpollSVG:1,0 redirectCmds:0,1 reverseLogs:0,1 ".
-    "menuEntries roomIcons SVGcache iconPath";
+    "menuEntries roomIcons SVGcache iconPath ".
+ "saltedBasicAuth saltedBasicAuthCosts";

   ###############
   # Initialize internal structures
@@ -151,9 +152,9 @@ FW_SecurityCheck($$)
             !grep(m/^INITIALIZED$/, @{$dev->{CHANGED}}));
   my $motd = AttrVal("global", "motd", "");
   if($motd =~ "^SecurityCheck") {
-    my @list = grep { !AttrVal($_, "basicAuth", undef) }
+    my @list = grep { !AttrVal($_, "basicAuth", undef) && !AttrVal($_, "saltedBasicAuth",undef) }
                devspec2array("TYPE=FHEMWEB");
-    $motd .= (join(",", sort @list)." has no basicAuth attribute.\n")
+    $motd .= (join(",", sort @list)." has no basicAuth or saltedBasicAuth attribute.\n")
         if(@list);
     $attr{global}{motd} = $motd;
   }
@@ -270,10 +271,69 @@ FW_Read($)


   #############################
-  # BASIC HTTP AUTH
-  my $basicAuth = AttrVal($FW_wname, "basicAuth", undef);
+  # SALTED BASIC HTTP AUTH
   my @headerOptions = grep /OPTIONS/, @FW_httpheader;
-  if($basicAuth) {
+  my $saltedBasicAuth = AttrVal($FW_wname, "saltedBasicAuth", undef);
+  if($saltedBasicAuth) {
+ if($headerOptions[0]) {
+      print $c "HTTP/1.1 200 OK\r\n",
+             $FW_headercors,
+             "Content-Length: 0\r\n\r\n";
+      delete $hash->{CONTENT_LENGTH};
+      delete $hash->{BUF};
+      return;
+      exit(1);
+    };
+ my $pwok=0;
+ my $bcryptCost=AttrVal($FW_wname,"saltedBasicAuthCosts",2);
+    my @authLine = grep /Authorization: Basic/, @FW_httpheader;
+    my $secret = $authLine[0];
+    if($secret) {
+ $secret =~ s/^Authorization: Basic //;
+ eval "use MIME::Base64";
+ if($@) {
+ Log3 $FW_wname, 1, $@;
+   } else {
+ eval "use Digest::Bcrypt";
+ if($@) {
+ Log3 $FW_wname, 1, $@;
+ } else {
+ my ($user, $password) = split(":", decode_base64($secret));
+ my @saltedSecrets = split(",",$saltedBasicAuth);
+ foreach(@saltedSecrets) {
+ my ($username,$salt,$storedBcrypt) = split(":",$_);
+ if(!($username eq $user)) {
+ next;
+ }
+ $salt=decode_base64($salt);
+ my $bcrypt=Digest::Bcrypt->new();
+ $bcrypt->cost($bcryptCost);
+ $bcrypt->salt($salt);
+ $bcrypt->add($password);
+ my $bcryptResult=$bcrypt->hexdigest;
+ $pwok=($storedBcrypt eq ($bcryptResult));
+ if($pwok) {
+ last;
+ }
+ }
+ }
+ }
+ }
+    if(!$pwok) {
+      my $msg = AttrVal($FW_wname, "basicAuthMsg", "Fhem: login required");
+      print $c "HTTP/1.1 401 Authorization Required\r\n",
+             "WWW-Authenticate: Basic realm=\"$msg\"\r\n",
+             $FW_headercors,
+             "Content-Length: 0\r\n\r\n";
+      delete $hash->{CONTENT_LENGTH};
+      delete $hash->{BUF};
+      return;
+    };
+  }
+  #############################
+  # BASIC HTTP AUTH 
+  my $basicAuth = AttrVal($FW_wname, "basicAuth", undef);
+  if($basicAuth && !$saltedBasicAuth) {
     my @authLine = grep /Authorization: Basic/, @FW_httpheader;
     my $secret = $authLine[0];
     $secret =~ s/^Authorization: Basic // if($secret);

Benötigt das Modul Digest::Bcrypt!

Funktioniert auch wunderbar, wenn man basicAuth benutzt, bleibt alles beim alten (sofern saltedBasicAuth nicht vorhanden ist, wird es ignoriert).
Ich habe nur eine Frage, habe das mit $headerOptions[0] im basicAuth-Fall nicht ganz verstanden, warum gibt es da diese oder-Bedingung (Zeile 281 in der Originaldatei)? Ich habe jetzt den OPTIONS-Fall einfach nach vorne geschoben, damit der direkt beantwortet wird.

Man hätte das ganze natürlich auch in den basicAuth-Code integrieren können, ich habe das jetzt mal mit Absicht voneinander getrennt.

Man kann auch mehrere Nutzer anlegen, indem man die Authentifizierungsstrings mittels "," voneinander trennt. Ein Beispielstring in der fhem.cfg sähe so aus:

attr WEB saltedBasicAuth admin:vXsk8G4y+YZ6hfeF3zYvTg==:ff8ee299328d214a1d679541ecda4ddc3dbcf28469e9ee

Das Format ist:

<userPW>:=<Nutername>:<Salt>:<Hex-Output von Bcrypt>
attr <DEVICE> saltedBasicAuth <userPW>,<userPW>,...


Habe außerdem noch eine Utils-Methode geschrieben, die mir den benötigten String bei Eingabe erzeugt (3. Parameter ist optional), Ausgabe landet im Log, das kann man natürlich anpassen. Aufruf mittels { createSaltedAuthString("USERNAME","PASSWORT") } bzw. { createSaltedAuthString("USERNAME","PASSWORT",COST) }:


sub
createSaltedAuthString(@)
{
my ($user,$password,$cost) = @_;
if(!$cost) {
$cost=2;
}
eval 'use Digest::Bcrypt';
if($@) {
Log 1, $@;
} else {
eval 'use MIME::Base64';
if($@) {
Log 1, $@;
}
else {
eval 'use Crypt::Random qw( makerandom_octet )';
if($@) {
Log 1, $@;
}
else {
my $bcrypt=Digest::Bcrypt->new();
my $salt=makerandom_octet(Length=>16);
$bcrypt->salt($salt);
$bcrypt->cost($cost);
$bcrypt->add($password);
my $saltedPassword=$bcrypt->hexdigest;
$salt=encode_base64($salt);
my $constructedUserPassString=$user.':'.$salt.':'.$saltedPassword;
Log 1, "saltedBasicAuth-String: $constructedUserPassString";
}
}
}
}

Benötigt das Modul Crypt::Random und Digest::Bcrypt

Ich habe die cost-Funktion von bcrypt mal auf 2 gestellt, das ist natürlich etwas schwach, nur, wenn man es höher stellt wird es langsam, denn man braucht den Aufwand auf Server-Seite für jeden Aufruf, da ja FHEMWEB keine Session-IDs, Cookies oder so etwas vergibt.

Ahso, ohne SSL-Nutzung ist das ganze natürlich nicht so sinnvoll, aber das ist ja vermutlich klar.

Falls irgendwer irgendwas davon übernehmen möchte, kann er das gerne tun. :-)

Edit:
Habe den Code geändert damit es keinen Bug mehr gibt, wenn man denselben Nutzernamen mit 2 verschiedenen Passwörtern nutzen möchte.
Habe außerdem die überflüssige Base64-Codierung entfernt und eine Beschreibung sowie ein Beispiel in der fhem.cfg hinzugefügt.