From 993e5e3bbc3e2e6c9792a14e01efd7b57fbb74f0 Mon Sep 17 00:00:00 2001 From: Marco Dickert Date: Thu, 20 Jul 2017 14:23:33 +0200 Subject: [PATCH] add *real* htpasswd parser. closes #41 --- build/libifm.php | 141 +++++++++++++++++++++++++++++++++++++++++++++-- compiler.php | 4 +- ifm.php | 141 +++++++++++++++++++++++++++++++++++++++++++++-- src/htpasswd.php | 127 ++++++++++++++++++++++++++++++++++++++++++ src/main.php | 15 +++-- 5 files changed, 410 insertions(+), 18 deletions(-) create mode 100644 src/htpasswd.php diff --git a/build/libifm.php b/build/libifm.php index 0360db5..303daa5 100644 --- a/build/libifm.php +++ b/build/libifm.php @@ -15,12 +15,12 @@ ini_set( 'display_errors', 'OFF' ); class IFM { - const VERSION = '2.4.0'; + const VERSION = '2.4.2'; private $defaultconfig = array( // general config "auth" => 0, - "auth_source" => 'inlineadmin:$2y$10$0Bnm5L4wKFHRxJgNq.oZv.v7yXhkJZQvinJYR2p6X1zPvzyDRUVRC', + "auth_source" => 'inline;admin:$2y$10$0Bnm5L4wKFHRxJgNq.oZv.v7yXhkJZQvinJYR2p6X1zPvzyDRUVRC', "root_dir" => "", "tmp_dir" => "", "defaulttimezone" => "Europe/Berlin", @@ -1578,7 +1578,12 @@ function IFM( params ) { $this->getConfig(); } elseif( $_REQUEST["api"] == "getTemplates" ) { echo json_encode( $this->getTemplates() ); - } else { + } elseif( $_REQUEST["api"] == "logout" ) { + unset( $_SESSION ); + session_destroy(); + header( "Location: " . strtok( $_SERVER["REQUEST_URI"], '?' ) ); + exit( 0 ); + } else { if( isset( $_REQUEST["dir"] ) && $this->isPathValid( $_REQUEST["dir"] ) ) { switch( $_REQUEST["api"] ) { case "createDir": $this->createDir( $_REQUEST["dir"], $_REQUEST["dirname"] ); break; @@ -2140,8 +2145,8 @@ function IFM( params ) { break; case "file": if( @file_exists( $srcopt ) && @is_readable( $srcopt ) ) { - list( $uname, $hash ) = explode( ":", fgets( fopen( $srcopt, 'r' ) ) ); - return password_verify( $pass, trim( $hash ) ) ? ( $uname == $user ) : false; + $htpasswd = new Htpasswd( $srcopt ); + return $htpasswd->verify( $user, $pass ); } else { return false; } @@ -2801,3 +2806,129 @@ class IFMZip { } } } +/** + * htpasswd parser + */ + +class Htpasswd { + public $users = []; + + public function __construct( $filename="" ) { + if( $filename ) + $this->load( $filename ); + } + + /** + * Load a new htpasswd file + */ + public function load( $filename ) { + unset( $this->users ); + if( file_exists( $filename ) && is_readable( $filename ) ) { + $lines = file( $filename ); + foreach( $lines as $line ) { + list( $user, $pass ) = explode( ":", $line ); + $this->users[$user] = trim( $pass ); + } + return true; + } else + return false; + } + + public function getUsers() { + return array_keys( $this->users ); + } + + public function userExist( $user ) { + return isset( $this->users[ $user ] ); + } + + public function verify( $user, $pass ) { + if( isset( $this->users[$user] ) ) { + return $this->verifyPassword( $pass, $this->users[$user] ); + } else { + return false; + } + } + + public function verifyPassword( $pass, $hash ) { + if( substr( $hash, 0, 4 ) == '$2y$' ) { + return password_verify( $pass, $hash ); + } elseif( substr( $hash, 0, 6 ) == '$apr1$' ) { + $apr1 = new APR1_MD5(); + return $apr1->check( $pass, $hash ); + } elseif( substr( $hash, 0, 5 ) == '{SHA}' ) { + return base64_encode( sha1( $pass, TRUE ) ) == substr( $hash, 5 ); + } else { // assume CRYPT + return crypt( $pass, $hash ) == $hash; + } + } +} + +/** + * APR1_MD5 class + * + * Source: https://github.com/whitehat101/apr1-md5/blob/master/src/APR1_MD5.php + */ +class APR1_MD5 { + + const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + const APRMD5_ALPHABET = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + // Source/References for core algorithm: + // http://www.cryptologie.net/article/126/bruteforce-apr1-hashes/ + // http://svn.apache.org/viewvc/apr/apr-util/branches/1.3.x/crypto/apr_md5.c?view=co + // http://www.php.net/manual/en/function.crypt.php#73619 + // http://httpd.apache.org/docs/2.2/misc/password_encryptions.html + // Wikipedia + + public static function hash($mdp, $salt = null) { + if (is_null($salt)) + $salt = self::salt(); + $salt = substr($salt, 0, 8); + $max = strlen($mdp); + $context = $mdp.'$apr1$'.$salt; + $binary = pack('H32', md5($mdp.$salt.$mdp)); + for($i=$max; $i>0; $i-=16) + $context .= substr($binary, 0, min(16, $i)); + for($i=$max; $i>0; $i>>=1) + $context .= ($i & 1) ? chr(0) : $mdp[0]; + $binary = pack('H32', md5($context)); + for($i=0; $i<1000; $i++) { + $new = ($i & 1) ? $mdp : $binary; + if($i % 3) $new .= $salt; + if($i % 7) $new .= $mdp; + $new .= ($i & 1) ? $binary : $mdp; + $binary = pack('H32', md5($new)); + } + $hash = ''; + for ($i = 0; $i < 5; $i++) { + $k = $i+6; + $j = $i+12; + if($j == 16) $j = 5; + $hash = $binary[$i].$binary[$k].$binary[$j].$hash; + } + $hash = chr(0).chr(0).$binary[11].$hash; + $hash = strtr( + strrev(substr(base64_encode($hash), 2)), + self::BASE64_ALPHABET, + self::APRMD5_ALPHABET + ); + return '$apr1$'.$salt.'$'.$hash; + } + + // 8 character salts are the best. Don't encourage anything but the best. + public static function salt() { + $alphabet = self::APRMD5_ALPHABET; + $salt = ''; + for($i=0; $i<8; $i++) { + $offset = hexdec(bin2hex(openssl_random_pseudo_bytes(1))) % 64; + $salt .= $alphabet[$offset]; + } + return $salt; + } + + public static function check($plain, $hash) { + $parts = explode('$', $hash); + return self::hash($plain, $parts[2]) === $hash; + } +} diff --git a/compiler.php b/compiler.php index b942d8d..cc13c0d 100755 --- a/compiler.php +++ b/compiler.php @@ -9,7 +9,7 @@ chdir( realpath( dirname( __FILE__ ) ) ); $IFM_SRC_MAIN = "src/main.php"; -$IFM_SRC_PHPFILES = array( "src/ifmzip.php" ); +$IFM_SRC_PHPFILES = array( "src/ifmzip.php", "src/htpasswd.php" ); $IFM_SRC_JS = "src/ifm.js"; $IFM_BUILD_STANDALONE = "ifm.php"; @@ -51,8 +51,6 @@ $ifm->run(); /** * Build compressed standalone script - */ -/* * file_put_contents( $IFM_BUILD_STANDALONE_COMPRESSED, '' . gzencode( file_get_contents( "ifm.php", false, null, 5 ) ) ); */ diff --git a/ifm.php b/ifm.php index 45993cb..7bf8061 100644 --- a/ifm.php +++ b/ifm.php @@ -15,12 +15,12 @@ ini_set( 'display_errors', 'OFF' ); class IFM { - const VERSION = '2.4.0'; + const VERSION = '2.4.2'; private $defaultconfig = array( // general config "auth" => 0, - "auth_source" => 'inlineadmin:$2y$10$0Bnm5L4wKFHRxJgNq.oZv.v7yXhkJZQvinJYR2p6X1zPvzyDRUVRC', + "auth_source" => 'inline;admin:$2y$10$0Bnm5L4wKFHRxJgNq.oZv.v7yXhkJZQvinJYR2p6X1zPvzyDRUVRC', "root_dir" => "", "tmp_dir" => "", "defaulttimezone" => "Europe/Berlin", @@ -1578,7 +1578,12 @@ function IFM( params ) { $this->getConfig(); } elseif( $_REQUEST["api"] == "getTemplates" ) { echo json_encode( $this->getTemplates() ); - } else { + } elseif( $_REQUEST["api"] == "logout" ) { + unset( $_SESSION ); + session_destroy(); + header( "Location: " . strtok( $_SERVER["REQUEST_URI"], '?' ) ); + exit( 0 ); + } else { if( isset( $_REQUEST["dir"] ) && $this->isPathValid( $_REQUEST["dir"] ) ) { switch( $_REQUEST["api"] ) { case "createDir": $this->createDir( $_REQUEST["dir"], $_REQUEST["dirname"] ); break; @@ -2140,8 +2145,8 @@ function IFM( params ) { break; case "file": if( @file_exists( $srcopt ) && @is_readable( $srcopt ) ) { - list( $uname, $hash ) = explode( ":", fgets( fopen( $srcopt, 'r' ) ) ); - return password_verify( $pass, trim( $hash ) ) ? ( $uname == $user ) : false; + $htpasswd = new Htpasswd( $srcopt ); + return $htpasswd->verify( $user, $pass ); } else { return false; } @@ -2801,6 +2806,132 @@ class IFMZip { } } } +/** + * htpasswd parser + */ + +class Htpasswd { + public $users = []; + + public function __construct( $filename="" ) { + if( $filename ) + $this->load( $filename ); + } + + /** + * Load a new htpasswd file + */ + public function load( $filename ) { + unset( $this->users ); + if( file_exists( $filename ) && is_readable( $filename ) ) { + $lines = file( $filename ); + foreach( $lines as $line ) { + list( $user, $pass ) = explode( ":", $line ); + $this->users[$user] = trim( $pass ); + } + return true; + } else + return false; + } + + public function getUsers() { + return array_keys( $this->users ); + } + + public function userExist( $user ) { + return isset( $this->users[ $user ] ); + } + + public function verify( $user, $pass ) { + if( isset( $this->users[$user] ) ) { + return $this->verifyPassword( $pass, $this->users[$user] ); + } else { + return false; + } + } + + public function verifyPassword( $pass, $hash ) { + if( substr( $hash, 0, 4 ) == '$2y$' ) { + return password_verify( $pass, $hash ); + } elseif( substr( $hash, 0, 6 ) == '$apr1$' ) { + $apr1 = new APR1_MD5(); + return $apr1->check( $pass, $hash ); + } elseif( substr( $hash, 0, 5 ) == '{SHA}' ) { + return base64_encode( sha1( $pass, TRUE ) ) == substr( $hash, 5 ); + } else { // assume CRYPT + return crypt( $pass, $hash ) == $hash; + } + } +} + +/** + * APR1_MD5 class + * + * Source: https://github.com/whitehat101/apr1-md5/blob/master/src/APR1_MD5.php + */ +class APR1_MD5 { + + const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + const APRMD5_ALPHABET = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + // Source/References for core algorithm: + // http://www.cryptologie.net/article/126/bruteforce-apr1-hashes/ + // http://svn.apache.org/viewvc/apr/apr-util/branches/1.3.x/crypto/apr_md5.c?view=co + // http://www.php.net/manual/en/function.crypt.php#73619 + // http://httpd.apache.org/docs/2.2/misc/password_encryptions.html + // Wikipedia + + public static function hash($mdp, $salt = null) { + if (is_null($salt)) + $salt = self::salt(); + $salt = substr($salt, 0, 8); + $max = strlen($mdp); + $context = $mdp.'$apr1$'.$salt; + $binary = pack('H32', md5($mdp.$salt.$mdp)); + for($i=$max; $i>0; $i-=16) + $context .= substr($binary, 0, min(16, $i)); + for($i=$max; $i>0; $i>>=1) + $context .= ($i & 1) ? chr(0) : $mdp[0]; + $binary = pack('H32', md5($context)); + for($i=0; $i<1000; $i++) { + $new = ($i & 1) ? $mdp : $binary; + if($i % 3) $new .= $salt; + if($i % 7) $new .= $mdp; + $new .= ($i & 1) ? $binary : $mdp; + $binary = pack('H32', md5($new)); + } + $hash = ''; + for ($i = 0; $i < 5; $i++) { + $k = $i+6; + $j = $i+12; + if($j == 16) $j = 5; + $hash = $binary[$i].$binary[$k].$binary[$j].$hash; + } + $hash = chr(0).chr(0).$binary[11].$hash; + $hash = strtr( + strrev(substr(base64_encode($hash), 2)), + self::BASE64_ALPHABET, + self::APRMD5_ALPHABET + ); + return '$apr1$'.$salt.'$'.$hash; + } + + // 8 character salts are the best. Don't encourage anything but the best. + public static function salt() { + $alphabet = self::APRMD5_ALPHABET; + $salt = ''; + for($i=0; $i<8; $i++) { + $offset = hexdec(bin2hex(openssl_random_pseudo_bytes(1))) % 64; + $salt .= $alphabet[$offset]; + } + return $salt; + } + + public static function check($plain, $hash) { + $parts = explode('$', $hash); + return self::hash($plain, $parts[2]) === $hash; + } +} /** * start IFM diff --git a/src/htpasswd.php b/src/htpasswd.php new file mode 100644 index 0000000..4374c31 --- /dev/null +++ b/src/htpasswd.php @@ -0,0 +1,127 @@ +load( $filename ); + } + + /** + * Load a new htpasswd file + */ + public function load( $filename ) { + unset( $this->users ); + if( file_exists( $filename ) && is_readable( $filename ) ) { + $lines = file( $filename ); + foreach( $lines as $line ) { + list( $user, $pass ) = explode( ":", $line ); + $this->users[$user] = trim( $pass ); + } + return true; + } else + return false; + } + + public function getUsers() { + return array_keys( $this->users ); + } + + public function userExist( $user ) { + return isset( $this->users[ $user ] ); + } + + public function verify( $user, $pass ) { + if( isset( $this->users[$user] ) ) { + return $this->verifyPassword( $pass, $this->users[$user] ); + } else { + return false; + } + } + + public function verifyPassword( $pass, $hash ) { + if( substr( $hash, 0, 4 ) == '$2y$' ) { + return password_verify( $pass, $hash ); + } elseif( substr( $hash, 0, 6 ) == '$apr1$' ) { + $apr1 = new APR1_MD5(); + return $apr1->check( $pass, $hash ); + } elseif( substr( $hash, 0, 5 ) == '{SHA}' ) { + return base64_encode( sha1( $pass, TRUE ) ) == substr( $hash, 5 ); + } else { // assume CRYPT + return crypt( $pass, $hash ) == $hash; + } + } +} + +/** + * APR1_MD5 class + * + * Source: https://github.com/whitehat101/apr1-md5/blob/master/src/APR1_MD5.php + */ +class APR1_MD5 { + + const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + const APRMD5_ALPHABET = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + // Source/References for core algorithm: + // http://www.cryptologie.net/article/126/bruteforce-apr1-hashes/ + // http://svn.apache.org/viewvc/apr/apr-util/branches/1.3.x/crypto/apr_md5.c?view=co + // http://www.php.net/manual/en/function.crypt.php#73619 + // http://httpd.apache.org/docs/2.2/misc/password_encryptions.html + // Wikipedia + + public static function hash($mdp, $salt = null) { + if (is_null($salt)) + $salt = self::salt(); + $salt = substr($salt, 0, 8); + $max = strlen($mdp); + $context = $mdp.'$apr1$'.$salt; + $binary = pack('H32', md5($mdp.$salt.$mdp)); + for($i=$max; $i>0; $i-=16) + $context .= substr($binary, 0, min(16, $i)); + for($i=$max; $i>0; $i>>=1) + $context .= ($i & 1) ? chr(0) : $mdp[0]; + $binary = pack('H32', md5($context)); + for($i=0; $i<1000; $i++) { + $new = ($i & 1) ? $mdp : $binary; + if($i % 3) $new .= $salt; + if($i % 7) $new .= $mdp; + $new .= ($i & 1) ? $binary : $mdp; + $binary = pack('H32', md5($new)); + } + $hash = ''; + for ($i = 0; $i < 5; $i++) { + $k = $i+6; + $j = $i+12; + if($j == 16) $j = 5; + $hash = $binary[$i].$binary[$k].$binary[$j].$hash; + } + $hash = chr(0).chr(0).$binary[11].$hash; + $hash = strtr( + strrev(substr(base64_encode($hash), 2)), + self::BASE64_ALPHABET, + self::APRMD5_ALPHABET + ); + return '$apr1$'.$salt.'$'.$hash; + } + + // 8 character salts are the best. Don't encourage anything but the best. + public static function salt() { + $alphabet = self::APRMD5_ALPHABET; + $salt = ''; + for($i=0; $i<8; $i++) { + $offset = hexdec(bin2hex(openssl_random_pseudo_bytes(1))) % 64; + $salt .= $alphabet[$offset]; + } + return $salt; + } + + public static function check($plain, $hash) { + $parts = explode('$', $hash); + return self::hash($plain, $parts[2]) === $hash; + } +} diff --git a/src/main.php b/src/main.php index c672fda..f8b7f9f 100644 --- a/src/main.php +++ b/src/main.php @@ -15,12 +15,12 @@ ini_set( 'display_errors', 'OFF' ); class IFM { - const VERSION = '2.4.0'; + const VERSION = '2.4.2'; private $defaultconfig = array( // general config "auth" => 0, - "auth_source" => 'inlineadmin:$2y$10$0Bnm5L4wKFHRxJgNq.oZv.v7yXhkJZQvinJYR2p6X1zPvzyDRUVRC', + "auth_source" => 'inline;admin:$2y$10$0Bnm5L4wKFHRxJgNq.oZv.v7yXhkJZQvinJYR2p6X1zPvzyDRUVRC', "root_dir" => "", "tmp_dir" => "", "defaulttimezone" => "Europe/Berlin", @@ -131,7 +131,12 @@ class IFM { $this->getConfig(); } elseif( $_REQUEST["api"] == "getTemplates" ) { echo json_encode( $this->getTemplates() ); - } else { + } elseif( $_REQUEST["api"] == "logout" ) { + unset( $_SESSION ); + session_destroy(); + header( "Location: " . strtok( $_SERVER["REQUEST_URI"], '?' ) ); + exit( 0 ); + } else { if( isset( $_REQUEST["dir"] ) && $this->isPathValid( $_REQUEST["dir"] ) ) { switch( $_REQUEST["api"] ) { case "createDir": $this->createDir( $_REQUEST["dir"], $_REQUEST["dirname"] ); break; @@ -693,8 +698,8 @@ class IFM { break; case "file": if( @file_exists( $srcopt ) && @is_readable( $srcopt ) ) { - list( $uname, $hash ) = explode( ":", fgets( fopen( $srcopt, 'r' ) ) ); - return password_verify( $pass, trim( $hash ) ) ? ( $uname == $user ) : false; + $htpasswd = new Htpasswd( $srcopt ); + return $htpasswd->verify( $user, $pass ); } else { return false; }