Merge branch 'pliablepixels-crypt-replacement3' into storageareas

This commit is contained in:
Isaac Connor 2019-05-23 15:21:58 -04:00
commit 4466ef13fd
3 changed files with 64 additions and 68 deletions

2
.gitmodules vendored
View File

@ -7,7 +7,7 @@
url = https://github.com/ZoneMinder/CakePHP-Enum-Behavior.git url = https://github.com/ZoneMinder/CakePHP-Enum-Behavior.git
[submodule "third_party/bcrypt"] [submodule "third_party/bcrypt"]
path = third_party/bcrypt path = third_party/bcrypt
url = https://github.com/pliablepixels/libbcrypt url = https://github.com/ZoneMinder/libbcrypt
[submodule "third_party/jwt-cpp"] [submodule "third_party/jwt-cpp"]
path = third_party/jwt-cpp path = third_party/jwt-cpp
url = https://github.com/Thalhammer/jwt-cpp url = https://github.com/Thalhammer/jwt-cpp

View File

@ -112,21 +112,23 @@ sub interpret_messages {
my @results; my @results;
foreach my $response ( @responses ) { foreach my $response ( @responses ) {
if($verbose) { if ( $verbose ) {
print "Received message:\n" . $response . "\n"; print "Received message:\n" . $response . "\n";
} }
my $result = deserialize_message($svc_discover, $response); my $result = deserialize_message($svc_discover, $response);
if(not $result) { if ( not $result ) {
print "Error deserializing message. No message returned from deserializer.\n" if $verbose; print "Error deserializing message. No message returned from deserializer.\n" if $verbose;
next; next;
} }
my $xaddr; my $xaddr;
foreach my $l_xaddr (split ' ', $result->get_ProbeMatch()->get_XAddrs()) { my $probe_match = $result->get_ProbeMatch();
next if ! $probe_match;
foreach my $l_xaddr (split ' ', $probe_match->get_XAddrs()) {
# find IPv4 address # find IPv4 address
print "l_xaddr = $l_xaddr\n" if $verbose; print "l_xaddr = $l_xaddr\n" if $verbose;
if($l_xaddr =~ m|//[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[:/]|) { if ( $l_xaddr =~ m|//[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[:/]| ) {
$xaddr = $l_xaddr; $xaddr = $l_xaddr;
last; last;
} else { } else {

View File

@ -26,25 +26,23 @@ use \Firebase\JWT\JWT;
// this function migrates mysql hashing to bcrypt, if you are using PHP >= 5.5 // this function migrates mysql hashing to bcrypt, if you are using PHP >= 5.5
// will be called after successful login, only if mysql hashing is detected // will be called after successful login, only if mysql hashing is detected
function migrateHash($user, $pass) { function migrateHash($user, $pass) {
if (function_exists('password_hash')) { if ( function_exists('password_hash') ) {
ZM\Info ("Migrating $user to bcrypt scheme"); ZM\Info("Migrating $user to bcrypt scheme");
// let it generate its own salt, and ensure bcrypt as PASSWORD_DEFAULT may change later // let it generate its own salt, and ensure bcrypt as PASSWORD_DEFAULT may change later
// we can modify this later to support argon2 etc as switch to its own password signature detection // we can modify this later to support argon2 etc as switch to its own password signature detection
$bcrypt_hash = password_hash($pass, PASSWORD_BCRYPT); $bcrypt_hash = password_hash($pass, PASSWORD_BCRYPT);
//ZM\Info ("hased bcrypt $pass is $bcrypt_hash"); //ZM\Info ("hased bcrypt $pass is $bcrypt_hash");
$update_password_sql = 'UPDATE Users SET Password=\''.$bcrypt_hash.'\' WHERE Username=\''.$user.'\''; $update_password_sql = 'UPDATE Users SET Password=\''.$bcrypt_hash.'\' WHERE Username=\''.$user.'\'';
ZM\Info ($update_password_sql); ZM\Info($update_password_sql);
dbQuery($update_password_sql); dbQuery($update_password_sql);
} # Since password field has changed, existing auth_hash is no longer valid
else { generateAuthHash(ZM_AUTH_HASH_IPS, true);
} else {
ZM\Info ('Cannot migrate password scheme to bcrypt, as you are using PHP < 5.3'); ZM\Info('Cannot migrate password scheme to bcrypt, as you are using PHP < 5.3');
return; return;
} }
} }
// core function used to login a user to PHP. Is also used for cake sessions for the API // core function used to login a user to PHP. Is also used for cake sessions for the API
function userLogin($username='', $password='', $passwordHashed=false, $apiLogin = false) { function userLogin($username='', $password='', $passwordHashed=false, $apiLogin = false) {
@ -58,7 +56,7 @@ function userLogin($username='', $password='', $passwordHashed=false, $apiLogin
// if true, a popup will display after login // if true, a popup will display after login
// lets validate reCaptcha if it exists // lets validate reCaptcha if it exists
// this only applies if it userLogin was not called from API layer // this only applies if it userLogin was not called from API layer
if (!$apiLogin if ( !$apiLogin
&& defined('ZM_OPT_USE_GOOG_RECAPTCHA') && defined('ZM_OPT_USE_GOOG_RECAPTCHA')
&& defined('ZM_OPT_GOOG_RECAPTCHA_SECRETKEY') && defined('ZM_OPT_GOOG_RECAPTCHA_SECRETKEY')
&& defined('ZM_OPT_GOOG_RECAPTCHA_SITEKEY') && defined('ZM_OPT_GOOG_RECAPTCHA_SITEKEY')
@ -73,17 +71,17 @@ function userLogin($username='', $password='', $passwordHashed=false, $apiLogin
'remoteip' => $_SERVER['REMOTE_ADDR'] 'remoteip' => $_SERVER['REMOTE_ADDR']
); );
$res = do_post_request($url, http_build_query($fields)); $res = do_post_request($url, http_build_query($fields));
$responseData = json_decode($res,true); $responseData = json_decode($res, true);
// credit: https://github.com/google/recaptcha/blob/master/src/ReCaptcha/Response.php // credit: https://github.com/google/recaptcha/blob/master/src/ReCaptcha/Response.php
// if recaptcha resulted in error, we might have to deny login // if recaptcha resulted in error, we might have to deny login
if ( isset($responseData['success']) && $responseData['success'] == false ) { if ( isset($responseData['success']) && ($responseData['success'] == false) ) {
// PP - before we deny auth, let's make sure the error was not 'invalid secret' // PP - before we deny auth, let's make sure the error was not 'invalid secret'
// because that means the user did not configure the secret key correctly // because that means the user did not configure the secret key correctly
// in this case, we prefer to let him login in and display a message to correct // in this case, we prefer to let him login in and display a message to correct
// the key. Unfortunately, there is no way to check for invalid site key in code // the key. Unfortunately, there is no way to check for invalid site key in code
// as it produces the same error as when you don't answer a recaptcha // as it produces the same error as when you don't answer a recaptcha
if ( isset($responseData['error-codes']) && is_array($responseData['error-codes']) ) { if ( isset($responseData['error-codes']) && is_array($responseData['error-codes']) ) {
if ( !in_array('invalid-input-secret',$responseData['error-codes']) ) { if ( !in_array('invalid-input-secret', $responseData['error-codes']) ) {
Error('reCaptcha authentication failed'); Error('reCaptcha authentication failed');
return null; return null;
} else { } else {
@ -102,18 +100,18 @@ function userLogin($username='', $password='', $passwordHashed=false, $apiLogin
// First retrieve the stored password // First retrieve the stored password
// and move password hashing to application space // and move password hashing to application space
$saved_user_details = dbFetchOne ($sql, NULL, $sql_values); $saved_user_details = dbFetchOne($sql, NULL, $sql_values);
$password_correct = false; $password_correct = false;
$password_type = NULL; $password_type = NULL;
if ($saved_user_details) { if ( $saved_user_details ) {
// if the API layer asked us to login, make sure the user // if the API layer asked us to login, make sure the user
// has API enabled (admin may have banned API for this user) // has API enabled (admin may have banned API for this user)
if ($apiLogin) { if ( $apiLogin ) {
if ($saved_user_details['APIEnabled'] != 1) { if ( $saved_user_details['APIEnabled'] != 1 ) {
ZM\Error ("API disabled for: $username"); ZM\Error("API disabled for: $username");
$_SESSION['loginFailed'] = true; $_SESSION['loginFailed'] = true;
unset($user); unset($user);
return false; return false;
@ -121,45 +119,39 @@ function userLogin($username='', $password='', $passwordHashed=false, $apiLogin
} }
$saved_password = $saved_user_details['Password']; $saved_password = $saved_user_details['Password'];
if ($saved_password[0] == '*') { if ( $saved_password[0] == '*' ) {
// We assume we don't need to support mysql < 4.1 // We assume we don't need to support mysql < 4.1
// Starting MY SQL 4.1, mysql concats a '*' in front of its password hash // Starting MY SQL 4.1, mysql concats a '*' in front of its password hash
// https://blog.pythian.com/hashing-algorithm-in-mysql-password-2/ // https://blog.pythian.com/hashing-algorithm-in-mysql-password-2/
ZM\Logger::Debug ('Saved password is using MYSQL password function'); ZM\Logger::Debug('Saved password is using MYSQL password function');
$input_password_hash ='*'.strtoupper(sha1(sha1($password, true))); $input_password_hash = '*'.strtoupper(sha1(sha1($password, true)));
$password_correct = ($saved_password == $input_password_hash); $password_correct = ($saved_password == $input_password_hash);
$password_type = 'mysql'; $password_type = 'mysql';
} } else if ( preg_match('/^\$2[ayb]\$.+$/', $saved_password) ) {
elseif (preg_match('/^\$2[ayb]\$.+$/', $saved_password)) {
ZM\Logger::Debug('bcrypt signature found, assumed bcrypt password'); ZM\Logger::Debug('bcrypt signature found, assumed bcrypt password');
$password_type='bcrypt'; $password_type = 'bcrypt';
$password_correct = $passwordHashed? ($password == $saved_password) : password_verify($password, $saved_password); $password_correct = $passwordHashed ? ($password == $saved_password) : password_verify($password, $saved_password);
} }
// zmupdate.pl adds a '-ZM-' prefix to overlay encrypted passwords // zmupdate.pl adds a '-ZM-' prefix to overlay encrypted passwords
// this is done so that we don't spend cycles doing two bcrypt password_verify calls // this is done so that we don't spend cycles doing two bcrypt password_verify calls
// for every wrong password entered. This will only be invoked for passwords zmupdate.pl has // for every wrong password entered. This will only be invoked for passwords zmupdate.pl has
// overlay hashed // overlay hashed
elseif (substr($saved_password, 0,4) == '-ZM-') { else if ( substr($saved_password, 0,4) == '-ZM-' ) {
ZM\Logger::Debug("Detected bcrypt overlay hashing for $username"); ZM\Logger::Debug("Detected bcrypt overlay hashing for $username");
ZM\Info("Detected bcrypt overlay hashing for $username"); $bcrypt_hash = substr($saved_password, 4);
$bcrypt_hash = substr ($saved_password, 4); $mysql_encoded_password = '*'.strtoupper(sha1(sha1($password, true)));
$mysql_encoded_password ='*'.strtoupper(sha1(sha1($password, true)));
ZM\Logger::Debug("Comparing password $mysql_encoded_password to bcrypt hash: $bcrypt_hash"); ZM\Logger::Debug("Comparing password $mysql_encoded_password to bcrypt hash: $bcrypt_hash");
ZM\Info("Comparing password $mysql_encoded_password to bcrypt hash: $bcrypt_hash");
$password_correct = password_verify($mysql_encoded_password, $bcrypt_hash); $password_correct = password_verify($mysql_encoded_password, $bcrypt_hash);
$password_type = "mysql"; // so we can migrate later down $password_type = 'mysql'; // so we can migrate later down
} else {
} // we really should nag the user not to use plain
else { ZM\Warning ('assuming plain text password as signature is not known. Please do not use plain, it is very insecure');
// we really should nag the user not to use plain $password_type = 'plain';
ZM\Warning ('assuming plain text password as signature is not known. Please do not use plain, it is very insecure'); $password_correct = ($saved_password == $password);
$password_type = 'plain';
$password_correct = ($saved_password == $password);
} }
} else { } else {
ZM\Error ("Could not retrieve user $username details"); ZM\Error("Could not retrieve user $username details");
$_SESSION['loginFailed'] = true; $_SESSION['loginFailed'] = true;
unset($user); unset($user);
return false; return false;
@ -172,10 +164,10 @@ function userLogin($username='', $password='', $passwordHashed=false, $apiLogin
} }
$_SESSION['remoteAddr'] = $_SERVER['REMOTE_ADDR']; // To help prevent session hijacking $_SESSION['remoteAddr'] = $_SERVER['REMOTE_ADDR']; // To help prevent session hijacking
if ($password_correct) { if ( $password_correct ) {
ZM\Info("Login successful for user \"$username\""); ZM\Info("Login successful for user \"$username\"");
$user = $saved_user_details; $user = $saved_user_details;
if ($password_type == 'mysql') { if ( $password_type == 'mysql' ) {
ZM\Info('Migrating password, if possible for future logins'); ZM\Info('Migrating password, if possible for future logins');
migrateHash($username, $password); migrateHash($username, $password);
} }
@ -224,42 +216,41 @@ function validateToken ($token, $allowed_token_type='access') {
$jwt_payload = json_decode(json_encode($decoded_token), true); $jwt_payload = json_decode(json_encode($decoded_token), true);
$type = $jwt_payload['type']; $type = $jwt_payload['type'];
if ($type != $allowed_token_type) { if ( $type != $allowed_token_type ) {
if ($allowed_token_type == 'access') { if ( $allowed_token_type == 'access' ) {
// give a hint that the user is not doing it right // give a hint that the user is not doing it right
ZM\Error ('Please do not use refresh tokens for this operation'); ZM\Error('Please do not use refresh tokens for this operation');
} }
return array (false, "Incorrect token type"); return array (false, 'Incorrect token type');
} }
$username = $jwt_payload['user']; $username = $jwt_payload['user'];
$sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username = ?'; $sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username = ?';
$sql_values = array($username); $sql_values = array($username);
$saved_user_details = dbFetchOne ($sql, NULL, $sql_values); $saved_user_details = dbFetchOne($sql, NULL, $sql_values);
if ($saved_user_details) { if ( $saved_user_details ) {
$issuedAt = $jwt_payload['iat']; $issuedAt = $jwt_payload['iat'];
$minIssuedAt = $saved_user_details['TokenMinExpiry']; $minIssuedAt = $saved_user_details['TokenMinExpiry'];
if ($issuedAt < $minIssuedAt) { if ( $issuedAt < $minIssuedAt ) {
ZM\Error ("Token revoked for \"$username\". Please generate a new token"); ZM\Error("Token revoked for $username. Please generate a new token");
$_SESSION['loginFailed'] = true; $_SESSION['loginFailed'] = true;
unset($user); unset($user);
return array(false, "Token revoked. Please re-generate"); return array(false, 'Token revoked. Please re-generate');
} }
$user = $saved_user_details; $user = $saved_user_details;
return array($user, "OK"); return array($user, 'OK');
} else { } else {
ZM\Error ("Could not retrieve user \"$username\" details"); ZM\Error("Could not retrieve user $username details");
$_SESSION['loginFailed'] = true; $_SESSION['loginFailed'] = true;
unset($user); unset($user);
return array(false, "No such user/credentials"); return array(false, 'No such user/credentials');
} }
} // end function validateToken($token, $allowed_token_type='access')
}
function getAuthUser($auth) { function getAuthUser($auth) {
if ( ZM_OPT_USE_AUTH && ZM_AUTH_RELAY == 'hashed' && !empty($auth) ) { if ( ZM_OPT_USE_AUTH && ZM_AUTH_RELAY == 'hashed' && !empty($auth) ) {
@ -299,8 +290,6 @@ function getAuthUser($auth) {
return false; return false;
} // end getAuthUser($auth) } // end getAuthUser($auth)
function generateAuthHash($useRemoteAddr, $force=false) { function generateAuthHash($useRemoteAddr, $force=false) {
if ( ZM_OPT_USE_AUTH and ZM_AUTH_RELAY == 'hashed' and isset($_SESSION['username']) and $_SESSION['passwordHash'] ) { if ( ZM_OPT_USE_AUTH and ZM_AUTH_RELAY == 'hashed' and isset($_SESSION['username']) and $_SESSION['passwordHash'] ) {
$time = time(); $time = time();
@ -367,9 +356,15 @@ if ( ZM_OPT_USE_AUTH ) {
} }
if ( isset($_SESSION['username']) ) { if ( isset($_SESSION['username']) ) {
# Need to refresh permissions and validate that the user still exists if ( ZM_AUTH_HASH_LOGINS ) {
$sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username=?'; # Extra validation, if logged in, then the auth hash will be set in the session, so we can validate it.
$user = dbFetchOne($sql, NULL, array($_SESSION['username'])); # This prevent session modification to switch users
$user = getAuthUser($_SESSION['AuthHash'.$_SESSION['remoteAddr']);
} else {
# Need to refresh permissions and validate that the user still exists
$sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username=?';
$user = dbFetchOne($sql, NULL, array($_SESSION['username']));
}
} }
if ( ZM_AUTH_RELAY == 'plain' ) { if ( ZM_AUTH_RELAY == 'plain' ) {
@ -379,7 +374,6 @@ if ( ZM_OPT_USE_AUTH ) {
$_SESSION['remoteAddr'] = $_SERVER['REMOTE_ADDR']; // To help prevent session hijacking $_SESSION['remoteAddr'] = $_SERVER['REMOTE_ADDR']; // To help prevent session hijacking
if ( ZM_AUTH_HASH_LOGINS && empty($user) && !empty($_REQUEST['auth']) ) { if ( ZM_AUTH_HASH_LOGINS && empty($user) && !empty($_REQUEST['auth']) ) {
if ( $authUser = getAuthUser($_REQUEST['auth']) ) { if ( $authUser = getAuthUser($_REQUEST['auth']) ) {
userLogin($authUser['Username'], $authUser['Password'], true); userLogin($authUser['Username'], $authUser['Password'], true);
} }
@ -387,7 +381,7 @@ if ( ZM_OPT_USE_AUTH ) {
userLogin($_REQUEST['username'], $_REQUEST['password'], false); userLogin($_REQUEST['username'], $_REQUEST['password'], false);
} }
if ( ZM_AUTH_HASH_LOGINS && empty($user) && !empty($_REQUEST['token']) ) { if (empty($user) && !empty($_REQUEST['token']) ) {
$ret = validateToken($_REQUEST['token'], 'access'); $ret = validateToken($_REQUEST['token'], 'access');
$user = $ret[0]; $user = $ret[0];