From a5de45419f5b723a4f947705af1afe9f1ed3ada2 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 1 May 2019 13:08:52 -0400 Subject: [PATCH 01/90] added sha1 and bcrypt submodules --- .gitmodules | 6 ++++++ CMakeLists.txt | 1 + third_party/bcrypt | 1 + third_party/sha1 | 1 + 4 files changed, 9 insertions(+) create mode 160000 third_party/bcrypt create mode 160000 third_party/sha1 diff --git a/.gitmodules b/.gitmodules index eb0e282a2..473f65c73 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,9 @@ [submodule "web/api/app/Plugin/CakePHP-Enum-Behavior"] path = web/api/app/Plugin/CakePHP-Enum-Behavior url = https://github.com/ZoneMinder/CakePHP-Enum-Behavior.git +[submodule "third_party/bcrypt"] + path = third_party/bcrypt + url = https://github.com/trusch/libbcrypt +[submodule "third_party/sha1"] + path = third_party/sha1 + url = https://github.com/vog/sha1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 9646ffc3e..5d8506cc0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -870,6 +870,7 @@ include(Pod2Man) ADD_MANPAGE_TARGET() # Process subdirectories +add_subdirectory(third_party/bcrypt) add_subdirectory(src) add_subdirectory(scripts) add_subdirectory(db) diff --git a/third_party/bcrypt b/third_party/bcrypt new file mode 160000 index 000000000..180cd3372 --- /dev/null +++ b/third_party/bcrypt @@ -0,0 +1 @@ +Subproject commit 180cd3372609b2539fe9c916f15c8ef8a2aef5f2 diff --git a/third_party/sha1 b/third_party/sha1 new file mode 160000 index 000000000..68a099035 --- /dev/null +++ b/third_party/sha1 @@ -0,0 +1 @@ +Subproject commit 68a0990352c04de43c494e8381264c27ed0b8e7e From c4b1bc19e01011722a91accc3a17725d1e9a3811 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 1 May 2019 13:15:07 -0400 Subject: [PATCH 02/90] added bcrypt and sha to src build process --- src/CMakeLists.txt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e7e3157eb..0aad53020 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,6 +6,10 @@ configure_file(zm_config.h.in "${CMAKE_CURRENT_BINARY_DIR}/zm_config.h" @ONLY) # Group together all the source files that are used by all the binaries (zmc, zma, zmu, zms etc) set(ZM_BIN_SRC_FILES zm_box.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp zm_config.cpp zm_coord.cpp zm_curl_camera.cpp zm.cpp zm_db.cpp zm_logger.cpp zm_event.cpp zm_frame.cpp zm_eventstream.cpp zm_exception.cpp zm_file_camera.cpp zm_ffmpeg_input.cpp zm_ffmpeg_camera.cpp zm_group.cpp zm_image.cpp zm_jpeg.cpp zm_libvlc_camera.cpp zm_local_camera.cpp zm_monitor.cpp zm_monitorstream.cpp zm_ffmpeg.cpp zm_mpeg.cpp zm_packet.cpp zm_packetqueue.cpp zm_poly.cpp zm_regexp.cpp zm_remote_camera.cpp zm_remote_camera_http.cpp zm_remote_camera_nvsocket.cpp zm_remote_camera_rtsp.cpp zm_rtp.cpp zm_rtp_ctrl.cpp zm_rtp_data.cpp zm_rtp_source.cpp zm_rtsp.cpp zm_rtsp_auth.cpp zm_sdp.cpp zm_signal.cpp zm_stream.cpp zm_swscale.cpp zm_thread.cpp zm_time.cpp zm_timer.cpp zm_user.cpp zm_utils.cpp zm_video.cpp zm_videostore.cpp zm_zone.cpp zm_storage.cpp) + +# includes and linkages to 3rd party libraries/src +set (ZM_BIN_THIRDPARTY_SRC_FILES ../third_party/sha1/sha1.cpp) + # A fix for cmake recompiling the source files for every target. add_library(zm STATIC ${ZM_BIN_SRC_FILES}) @@ -14,10 +18,13 @@ add_executable(zma zma.cpp) add_executable(zmu zmu.cpp) add_executable(zms zms.cpp) +#include_directories(../third_party/sha1 ../third_party/bcrypt/include/bcrypt) +link_directories(../third_party/bcrypt) + target_link_libraries(zmc zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS}) target_link_libraries(zma zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS}) -target_link_libraries(zmu zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS}) -target_link_libraries(zms zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS}) +target_link_libraries(zmu zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS} bcrypt) +target_link_libraries(zms zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS} bcrypt) # Generate man files for the binaries destined for the bin folder FOREACH(CBINARY zma zmc zmu) From 1ba1bf0c45891ac0f7b68716b77e1b5811b19f40 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 1 May 2019 13:18:51 -0400 Subject: [PATCH 03/90] added test sha1 and bcrypt code to validate working --- src/zm_user.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/zm_user.cpp b/src/zm_user.cpp index 46ee2cdf1..a710188cc 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -26,6 +26,8 @@ #include #include #include +#include "BCrypt.hpp" +#include "sha1.hpp" #include "zm_utils.h" @@ -95,6 +97,16 @@ User *zmLoadUser( const char *username, const char *password ) { // According to docs, size of safer_whatever must be 2*length+1 due to unicode conversions + null terminator. mysql_real_escape_string(&dbconn, safer_username, username, username_length ); + BCrypt bcrypt; + std::string ptest = "test"; + std::string hash = bcrypt.generateHash(ptest); + Info ("ZM_USER TEST: BCRYPT WORKED AND PRODUCED %s", hash.c_str()); + + SHA1 sha1_checksum; + sha1_checksum.update (ptest); + hash = sha1_checksum.final(); + Info ("ZM_USER TEST: SHA1 WORKED AND PRODUCED %s", hash.c_str()); + if ( password ) { int password_length = strlen(password); char *safer_password = new char[(password_length * 2) + 1]; From 887912e7adfe16af3a47a0a50140eb972e0a5b40 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 1 May 2019 13:22:24 -0400 Subject: [PATCH 04/90] bcrypt auth migration in PHP land --- web/includes/auth.php | 91 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 77 insertions(+), 14 deletions(-) diff --git a/web/includes/auth.php b/web/includes/auth.php index 6b061f7fc..ef1871164 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -20,16 +20,39 @@ // require_once('session.php'); +// 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 +function migrateHash($user, $pass) { + if (function_exists('password_hash')) { + ZM\Info ("Migrating $user to bcrypt scheme"); + // 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 + $bcrypt_hash = password_hash($pass, PASSWORD_BCRYPT); + //ZM\Info ("hased bcrypt $pass is $bcrypt_hash"); + $update_password_sql = 'UPDATE Users SET Password=\''.$bcrypt_hash.'\' WHERE Username=\''.$user.'\''; + ZM\Info ($update_password_sql); + dbQuery($update_password_sql); + } + else { + // Not really an error, so an info + // there is also a compat library https://github.com/ircmaxell/password_compat + // not sure if its worth it. Do a lot of people really use PHP < 5.5? + ZM\Info ('Cannot migrate password scheme to bcrypt, as you are using PHP < 5.5'); + return; + } + +} + +// core function used to login a user to PHP. Is also used for cake sessions for the API function userLogin($username='', $password='', $passwordHashed=false) { global $user; - if ( !$username and isset($_REQUEST['username']) ) $username = $_REQUEST['username']; if ( !$password and isset($_REQUEST['password']) ) $password = $_REQUEST['password']; // if true, a popup will display after login - // PP - lets validate reCaptcha if it exists + // lets validate reCaptcha if it exists if ( defined('ZM_OPT_USE_GOOG_RECAPTCHA') && defined('ZM_OPT_GOOG_RECAPTCHA_SECRETKEY') && defined('ZM_OPT_GOOG_RECAPTCHA_SITEKEY') @@ -64,28 +87,68 @@ function userLogin($username='', $password='', $passwordHashed=false) { } // end if success==false } // end if using reCaptcha - $sql = 'SELECT * FROM Users WHERE Enabled=1'; - $sql_values = NULL; - if ( ZM_AUTH_TYPE == 'builtin' ) { - if ( $passwordHashed ) { - $sql .= ' AND Username=? AND Password=?'; - } else { - $sql .= ' AND Username=? AND Password=password(?)'; + // coming here means we need to authenticate the user + // if captcha existed, it was passed + + $sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username = ?'; + $sql_values = array($username); + + // First retrieve the stored password + // and move password hashing to application space + + $saved_user_details = dbFetchOne ($sql, NULL, $sql_values); + $password_correct = false; + $password_type = NULL; + + if ($saved_user_details) { + $saved_password = $saved_user_details['Password']; + if ($saved_password[0] == '*') { + // 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 + // https://blog.pythian.com/hashing-algorithm-in-mysql-password-2/ + ZM\Logger::Debug ('Saved password is using MYSQL password function'); + $input_password_hash ='*'.strtoupper(sha1(sha1($password, true))); + $password_correct = ($saved_password == $input_password_hash); + $password_type = 'mysql'; + + } + else { + // bcrypt can have multiple signatures + if (preg_match('/^\$2[ayb]\$.+$/', $saved_password)) { + + ZM\Logger::Debug ('bcrypt signature found, assumed bcrypt password'); + $password_type='bcrypt'; + $password_correct = password_verify($password, $saved_password); + } + else { + // we really should nag the user not to use plain + ZM\Warning ('assuming plain text password as signature is not known. Please do not use plain, it is very insecure'); + $password_type = 'plain'; + $password_correct = ($saved_password == $password); + } + } - $sql_values = array($username, $password); } else { - $sql .= ' AND Username=?'; - $sql_values = array($username); + ZM\Error ("Could not retrieve user $username details"); + $_SESSION['loginFailed'] = true; + unset($user); + return; } + $close_session = 0; if ( !is_session_started() ) { session_start(); $close_session = 1; } $_SESSION['remoteAddr'] = $_SERVER['REMOTE_ADDR']; // To help prevent session hijacking - if ( $dbUser = dbFetchOne($sql, NULL, $sql_values) ) { + + if ($password_correct) { ZM\Info("Login successful for user \"$username\""); - $user = $dbUser; + $user = $saved_user_details; + if ($password_type == 'mysql') { + ZM\Info ('Migrating password, if possible for future logins'); + migrateHash($username, $password); + } unset($_SESSION['loginFailed']); if ( ZM_AUTH_TYPE == 'builtin' ) { $_SESSION['passwordHash'] = $user['Password']; From ddb7752226766eb8298f91bef928c59fc620f761 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 1 May 2019 13:24:50 -0400 Subject: [PATCH 05/90] added include path --- src/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0aad53020..a56cb36ba 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,7 +18,7 @@ add_executable(zma zma.cpp) add_executable(zmu zmu.cpp) add_executable(zms zms.cpp) -#include_directories(../third_party/sha1 ../third_party/bcrypt/include/bcrypt) +include_directories(../third_party/sha1 ../third_party/bcrypt/include/bcrypt) link_directories(../third_party/bcrypt) target_link_libraries(zmc zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS}) From dd63fe86cedfbad6d6763905a18db1a120d0efd6 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 1 May 2019 13:28:39 -0400 Subject: [PATCH 06/90] add sha source --- src/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a56cb36ba..ecd42b874 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,7 +11,7 @@ set(ZM_BIN_SRC_FILES zm_box.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp zm_conf set (ZM_BIN_THIRDPARTY_SRC_FILES ../third_party/sha1/sha1.cpp) # A fix for cmake recompiling the source files for every target. -add_library(zm STATIC ${ZM_BIN_SRC_FILES}) +add_library(zm STATIC ${ZM_BIN_SRC_FILES} ${ZM_BIN_THIRDPARTY_SRC_FILES}) add_executable(zmc zmc.cpp) add_executable(zma zma.cpp) From 07be830f94dbd63ac3cfd368cec8b8b899be005b Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 1 May 2019 13:35:18 -0400 Subject: [PATCH 07/90] added bcrypt to others --- src/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ecd42b874..3abfa5266 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,8 +21,8 @@ add_executable(zms zms.cpp) include_directories(../third_party/sha1 ../third_party/bcrypt/include/bcrypt) link_directories(../third_party/bcrypt) -target_link_libraries(zmc zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS}) -target_link_libraries(zma zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS}) +target_link_libraries(zmc zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS} bcrypt) +target_link_libraries(zma zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS} brycpt) target_link_libraries(zmu zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS} bcrypt) target_link_libraries(zms zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS} bcrypt) From 8bbddadc12e6226b3ddfc0928873d01f46a6ee8d Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 1 May 2019 13:43:41 -0400 Subject: [PATCH 08/90] put link_dir ahead of add_executable --- src/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3abfa5266..8b73ae3cc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,6 +7,7 @@ configure_file(zm_config.h.in "${CMAKE_CURRENT_BINARY_DIR}/zm_config.h" @ONLY) set(ZM_BIN_SRC_FILES zm_box.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp zm_config.cpp zm_coord.cpp zm_curl_camera.cpp zm.cpp zm_db.cpp zm_logger.cpp zm_event.cpp zm_frame.cpp zm_eventstream.cpp zm_exception.cpp zm_file_camera.cpp zm_ffmpeg_input.cpp zm_ffmpeg_camera.cpp zm_group.cpp zm_image.cpp zm_jpeg.cpp zm_libvlc_camera.cpp zm_local_camera.cpp zm_monitor.cpp zm_monitorstream.cpp zm_ffmpeg.cpp zm_mpeg.cpp zm_packet.cpp zm_packetqueue.cpp zm_poly.cpp zm_regexp.cpp zm_remote_camera.cpp zm_remote_camera_http.cpp zm_remote_camera_nvsocket.cpp zm_remote_camera_rtsp.cpp zm_rtp.cpp zm_rtp_ctrl.cpp zm_rtp_data.cpp zm_rtp_source.cpp zm_rtsp.cpp zm_rtsp_auth.cpp zm_sdp.cpp zm_signal.cpp zm_stream.cpp zm_swscale.cpp zm_thread.cpp zm_time.cpp zm_timer.cpp zm_user.cpp zm_utils.cpp zm_video.cpp zm_videostore.cpp zm_zone.cpp zm_storage.cpp) +link_directories(../third_party/bcrypt) # includes and linkages to 3rd party libraries/src set (ZM_BIN_THIRDPARTY_SRC_FILES ../third_party/sha1/sha1.cpp) @@ -19,7 +20,6 @@ add_executable(zmu zmu.cpp) add_executable(zms zms.cpp) include_directories(../third_party/sha1 ../third_party/bcrypt/include/bcrypt) -link_directories(../third_party/bcrypt) target_link_libraries(zmc zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS} bcrypt) target_link_libraries(zma zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS} brycpt) From ca24b504d42a44e1effad5e0493ff5cbea30987a Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 1 May 2019 13:46:54 -0400 Subject: [PATCH 09/90] fixed typo --- src/CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8b73ae3cc..bba97d661 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,12 +7,12 @@ configure_file(zm_config.h.in "${CMAKE_CURRENT_BINARY_DIR}/zm_config.h" @ONLY) set(ZM_BIN_SRC_FILES zm_box.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp zm_config.cpp zm_coord.cpp zm_curl_camera.cpp zm.cpp zm_db.cpp zm_logger.cpp zm_event.cpp zm_frame.cpp zm_eventstream.cpp zm_exception.cpp zm_file_camera.cpp zm_ffmpeg_input.cpp zm_ffmpeg_camera.cpp zm_group.cpp zm_image.cpp zm_jpeg.cpp zm_libvlc_camera.cpp zm_local_camera.cpp zm_monitor.cpp zm_monitorstream.cpp zm_ffmpeg.cpp zm_mpeg.cpp zm_packet.cpp zm_packetqueue.cpp zm_poly.cpp zm_regexp.cpp zm_remote_camera.cpp zm_remote_camera_http.cpp zm_remote_camera_nvsocket.cpp zm_remote_camera_rtsp.cpp zm_rtp.cpp zm_rtp_ctrl.cpp zm_rtp_data.cpp zm_rtp_source.cpp zm_rtsp.cpp zm_rtsp_auth.cpp zm_sdp.cpp zm_signal.cpp zm_stream.cpp zm_swscale.cpp zm_thread.cpp zm_time.cpp zm_timer.cpp zm_user.cpp zm_utils.cpp zm_video.cpp zm_videostore.cpp zm_zone.cpp zm_storage.cpp) -link_directories(../third_party/bcrypt) # includes and linkages to 3rd party libraries/src set (ZM_BIN_THIRDPARTY_SRC_FILES ../third_party/sha1/sha1.cpp) # A fix for cmake recompiling the source files for every target. add_library(zm STATIC ${ZM_BIN_SRC_FILES} ${ZM_BIN_THIRDPARTY_SRC_FILES}) +link_directories(../third_party/bcrypt) add_executable(zmc zmc.cpp) add_executable(zma zma.cpp) @@ -21,8 +21,8 @@ add_executable(zms zms.cpp) include_directories(../third_party/sha1 ../third_party/bcrypt/include/bcrypt) -target_link_libraries(zmc zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS} bcrypt) -target_link_libraries(zma zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS} brycpt) +target_link_libraries(zmc zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS}) +target_link_libraries(zma zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS}) target_link_libraries(zmu zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS} bcrypt) target_link_libraries(zms zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS} bcrypt) From c663246f0a003e3f000a6615996fea5cb248c412 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 1 May 2019 14:22:10 -0400 Subject: [PATCH 10/90] try add_library instead --- CMakeLists.txt | 2 +- src/CMakeLists.txt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d8506cc0..d2e01a7e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -870,7 +870,7 @@ include(Pod2Man) ADD_MANPAGE_TARGET() # Process subdirectories -add_subdirectory(third_party/bcrypt) +#add_subdirectory(third_party/bcrypt) add_subdirectory(src) add_subdirectory(scripts) add_subdirectory(db) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bba97d661..c64fd9778 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,10 +9,13 @@ set(ZM_BIN_SRC_FILES zm_box.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp zm_conf # includes and linkages to 3rd party libraries/src set (ZM_BIN_THIRDPARTY_SRC_FILES ../third_party/sha1/sha1.cpp) +add_library(bcrypt GLOBAL SHARED IMPORTED) +set_target_properties( bcrypt PROPERTIES IMPORTED_LOCATION ../third_party/bcrypt/libbcrypt.so ) + # A fix for cmake recompiling the source files for every target. add_library(zm STATIC ${ZM_BIN_SRC_FILES} ${ZM_BIN_THIRDPARTY_SRC_FILES}) -link_directories(../third_party/bcrypt) +#link_directories(../third_party/bcrypt) add_executable(zmc zmc.cpp) add_executable(zma zma.cpp) From 65a57feedbc3d5a7b31c6c332feaeb7c5f18152f Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 1 May 2019 14:30:00 -0400 Subject: [PATCH 11/90] absolute path --- CMakeLists.txt | 2 +- src/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d2e01a7e8..5d8506cc0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -870,7 +870,7 @@ include(Pod2Man) ADD_MANPAGE_TARGET() # Process subdirectories -#add_subdirectory(third_party/bcrypt) +add_subdirectory(third_party/bcrypt) add_subdirectory(src) add_subdirectory(scripts) add_subdirectory(db) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c64fd9778..41403d9ab 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,7 +15,7 @@ set_target_properties( bcrypt PROPERTIES IMPORTED_LOCATION ../third_party/bcrypt # A fix for cmake recompiling the source files for every target. add_library(zm STATIC ${ZM_BIN_SRC_FILES} ${ZM_BIN_THIRDPARTY_SRC_FILES}) -#link_directories(../third_party/bcrypt) +link_directories(/home/pp/source/pp_ZoneMinder.git/third_party/bcrypt) add_executable(zmc zmc.cpp) add_executable(zma zma.cpp) From 45b78141243c986179b65d00aee369db5b0c241d Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 1 May 2019 14:33:36 -0400 Subject: [PATCH 12/90] absolute path --- src/CMakeLists.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 41403d9ab..c45faf232 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,8 +9,6 @@ set(ZM_BIN_SRC_FILES zm_box.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp zm_conf # includes and linkages to 3rd party libraries/src set (ZM_BIN_THIRDPARTY_SRC_FILES ../third_party/sha1/sha1.cpp) -add_library(bcrypt GLOBAL SHARED IMPORTED) -set_target_properties( bcrypt PROPERTIES IMPORTED_LOCATION ../third_party/bcrypt/libbcrypt.so ) # A fix for cmake recompiling the source files for every target. From d252a8ba306a47f8b8d35d6d35905971a8e7ad1b Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Thu, 2 May 2019 10:52:21 -0400 Subject: [PATCH 13/90] build bcrypt as static --- CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d8506cc0..0973f8726 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -870,7 +870,13 @@ include(Pod2Man) ADD_MANPAGE_TARGET() # Process subdirectories + +# build a bcrypt static library +set(BUILD_SHARED_LIBS_SAVED "${BUILD_SHARED_LIBS}") +set(BUILD_SHARED_LIBS OFF) add_subdirectory(third_party/bcrypt) +set(BUILD_SHARED_LIBS "${BUILD_SHARED_LIBS_SAVED}") + add_subdirectory(src) add_subdirectory(scripts) add_subdirectory(db) From 72325d12b72b37a861d0f951900a629b1a0effb2 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Fri, 3 May 2019 11:40:35 -0400 Subject: [PATCH 14/90] move to wrapper --- .gitmodules | 3 -- src/CMakeLists.txt | 2 +- src/zm_crypt.cpp | 74 ++++++++++++++++++++++++++++++++++++++++++++++ src/zm_crypt.h | 29 ++++++++++++++++++ src/zm_user.cpp | 49 ++++++++++++------------------ 5 files changed, 123 insertions(+), 34 deletions(-) create mode 100644 src/zm_crypt.cpp create mode 100644 src/zm_crypt.h diff --git a/.gitmodules b/.gitmodules index 473f65c73..978afe56a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,9 +5,6 @@ [submodule "web/api/app/Plugin/CakePHP-Enum-Behavior"] path = web/api/app/Plugin/CakePHP-Enum-Behavior url = https://github.com/ZoneMinder/CakePHP-Enum-Behavior.git -[submodule "third_party/bcrypt"] - path = third_party/bcrypt - url = https://github.com/trusch/libbcrypt [submodule "third_party/sha1"] path = third_party/sha1 url = https://github.com/vog/sha1 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c45faf232..4a55ce095 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,7 +4,7 @@ configure_file(zm_config.h.in "${CMAKE_CURRENT_BINARY_DIR}/zm_config.h" @ONLY) # Group together all the source files that are used by all the binaries (zmc, zma, zmu, zms etc) -set(ZM_BIN_SRC_FILES zm_box.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp zm_config.cpp zm_coord.cpp zm_curl_camera.cpp zm.cpp zm_db.cpp zm_logger.cpp zm_event.cpp zm_frame.cpp zm_eventstream.cpp zm_exception.cpp zm_file_camera.cpp zm_ffmpeg_input.cpp zm_ffmpeg_camera.cpp zm_group.cpp zm_image.cpp zm_jpeg.cpp zm_libvlc_camera.cpp zm_local_camera.cpp zm_monitor.cpp zm_monitorstream.cpp zm_ffmpeg.cpp zm_mpeg.cpp zm_packet.cpp zm_packetqueue.cpp zm_poly.cpp zm_regexp.cpp zm_remote_camera.cpp zm_remote_camera_http.cpp zm_remote_camera_nvsocket.cpp zm_remote_camera_rtsp.cpp zm_rtp.cpp zm_rtp_ctrl.cpp zm_rtp_data.cpp zm_rtp_source.cpp zm_rtsp.cpp zm_rtsp_auth.cpp zm_sdp.cpp zm_signal.cpp zm_stream.cpp zm_swscale.cpp zm_thread.cpp zm_time.cpp zm_timer.cpp zm_user.cpp zm_utils.cpp zm_video.cpp zm_videostore.cpp zm_zone.cpp zm_storage.cpp) +set(ZM_BIN_SRC_FILES zm_box.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp zm_config.cpp zm_coord.cpp zm_curl_camera.cpp zm.cpp zm_db.cpp zm_logger.cpp zm_event.cpp zm_frame.cpp zm_eventstream.cpp zm_exception.cpp zm_file_camera.cpp zm_ffmpeg_input.cpp zm_ffmpeg_camera.cpp zm_group.cpp zm_image.cpp zm_jpeg.cpp zm_libvlc_camera.cpp zm_local_camera.cpp zm_monitor.cpp zm_monitorstream.cpp zm_ffmpeg.cpp zm_mpeg.cpp zm_packet.cpp zm_packetqueue.cpp zm_poly.cpp zm_regexp.cpp zm_remote_camera.cpp zm_remote_camera_http.cpp zm_remote_camera_nvsocket.cpp zm_remote_camera_rtsp.cpp zm_rtp.cpp zm_rtp_ctrl.cpp zm_rtp_data.cpp zm_rtp_source.cpp zm_rtsp.cpp zm_rtsp_auth.cpp zm_sdp.cpp zm_signal.cpp zm_stream.cpp zm_swscale.cpp zm_thread.cpp zm_time.cpp zm_timer.cpp zm_user.cpp zm_utils.cpp zm_video.cpp zm_videostore.cpp zm_zone.cpp zm_storage.cpp zm_crypt.cpp) # includes and linkages to 3rd party libraries/src diff --git a/src/zm_crypt.cpp b/src/zm_crypt.cpp new file mode 100644 index 000000000..9a8cee0ad --- /dev/null +++ b/src/zm_crypt.cpp @@ -0,0 +1,74 @@ +#include "zm.h" +# include "zm_crypt.h" +#include + + + +//https://stackoverflow.com/a/46403026/1361529 +char char2int(char input) { + if (input >= '0' && input <= '9') + return input - '0'; + else if (input >= 'A' && input <= 'F') + return input - 'A' + 10; + else if (input >= 'a' && input <= 'f') + return input - 'a' + 10; + else + return input; // this really should not happen + +} +std::string hex2str(std::string &hex) { + std::string out; + out.resize(hex.size() / 2 + hex.size() % 2); + std::string::iterator it = hex.begin(); + std::string::iterator out_it = out.begin(); + if (hex.size() % 2 != 0) { + *out_it++ = char(char2int(*it++)); + } + + for (; it < hex.end() - 1; it++) { + *out_it++ = char2int(*it++) << 4 | char2int(*it); + }; + + return out; +} + + +bool verifyPassword(const char *input_password, const char *db_password_hash) { + bool password_correct = false; + if (strlen(db_password_hash ) < 4) { + // actually, shoud be more, but this is min. for next code + Error ("DB Password is too short or invalid to check"); + return false; + } + if (db_password_hash[0] == '*') { + // MYSQL PASSWORD + Info ("%s is an MD5 encoded password", db_password_hash); + SHA1 checksum; + + // next few lines do '*'+SHA1(raw(SHA1(password))) + // which is MYSQL >=4.1 PASSWORD algorithm + checksum.update(input_password); + std::string interim_hash = checksum.final(); + std::string binary_hash = hex2str(interim_hash); // get interim hash + checksum.update(binary_hash); + interim_hash = checksum.final(); + std::string final_hash = "*" + interim_hash; + std::transform(final_hash.begin(), final_hash.end(), final_hash.begin(), ::toupper); + + Info ("Computed password_hash:%s, stored password_hash:%s", final_hash.c_str(), db_password_hash); + password_correct = (std::string(db_password_hash) == final_hash); + } + else if ((db_password_hash[0] == '$') && (db_password_hash[1]== '2') + &&(db_password_hash[3] == '$')) { + // BCRYPT + Info ("%s is a Bcrypt password", db_password_hash); + BCrypt bcrypt; + std::string input_hash = bcrypt.generateHash(std::string(input_password)); + password_correct = bcrypt.validatePassword(std::string(input_password), std::string(db_password_hash)); + } + else { + // plain + password_correct = (strcmp(input_password, db_password_hash) == 0); + } + return password_correct; +} \ No newline at end of file diff --git a/src/zm_crypt.h b/src/zm_crypt.h new file mode 100644 index 000000000..b893f7940 --- /dev/null +++ b/src/zm_crypt.h @@ -0,0 +1,29 @@ +// +// ZoneMinder General Utility Functions, $Date$, $Revision$ +// Copyright (C) 2001-2008 Philip Coombes +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// + +#ifndef ZM_CRYPT_H +#define ZM_CRYPT_H + +#include +#include "BCrypt.hpp" +#include "sha1.hpp" + +bool verifyPassword( const char *input_password, const char *db_password_hash); + +#endif // ZM_CRYPT_H \ No newline at end of file diff --git a/src/zm_user.cpp b/src/zm_user.cpp index a710188cc..d6194b802 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -26,10 +26,10 @@ #include #include #include -#include "BCrypt.hpp" -#include "sha1.hpp" + #include "zm_utils.h" +#include "zm_crypt.h" User::User() { id = 0; @@ -97,34 +97,15 @@ User *zmLoadUser( const char *username, const char *password ) { // According to docs, size of safer_whatever must be 2*length+1 due to unicode conversions + null terminator. mysql_real_escape_string(&dbconn, safer_username, username, username_length ); - BCrypt bcrypt; - std::string ptest = "test"; - std::string hash = bcrypt.generateHash(ptest); - Info ("ZM_USER TEST: BCRYPT WORKED AND PRODUCED %s", hash.c_str()); - SHA1 sha1_checksum; - sha1_checksum.update (ptest); - hash = sha1_checksum.final(); - Info ("ZM_USER TEST: SHA1 WORKED AND PRODUCED %s", hash.c_str()); + snprintf(sql, sizeof(sql), + "SELECT Id, Username, Password, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds" + " FROM Users where Username = '%s' and Enabled = 1", safer_username ); - if ( password ) { - int password_length = strlen(password); - char *safer_password = new char[(password_length * 2) + 1]; - mysql_real_escape_string(&dbconn, safer_password, password, password_length); - snprintf(sql, sizeof(sql), - "SELECT Id, Username, Password, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds" - " FROM Users WHERE Username = '%s' AND Password = password('%s') AND Enabled = 1", - safer_username, safer_password ); - delete safer_password; - } else { - snprintf(sql, sizeof(sql), - "SELECT Id, Username, Password, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds" - " FROM Users where Username = '%s' and Enabled = 1", safer_username ); - } if ( mysql_query(&dbconn, sql) ) { Error("Can't run query: %s", mysql_error(&dbconn)); - exit(mysql_errno(&dbconn)); + exit(mysql_errno(&dbconn)); } MYSQL_RES *result = mysql_store_result(&dbconn); @@ -143,12 +124,20 @@ User *zmLoadUser( const char *username, const char *password ) { MYSQL_ROW dbrow = mysql_fetch_row(result); User *user = new User(dbrow); - Info("Authenticated user '%s'", user->getUsername()); + Info ("Retrieved password for user:%s as %s", user->getUsername(), user->getPassword()); - mysql_free_result(result); - delete safer_username; - - return user; + if (verifyPassword(password, user->getPassword())) { + Info("Authenticated user '%s'", user->getUsername()); + mysql_free_result(result); + delete safer_username; + return user; + } + else { + Warning("Unable to authenticate user %s", username); + mysql_free_result(result); + return NULL; + } + } // Function to validate an authentication string From 18c5b2da2aec6b04c61112bfeef807e14c67feef Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Fri, 3 May 2019 11:43:38 -0400 Subject: [PATCH 15/90] move to fork --- .gitmodules | 3 +++ third_party/bcrypt | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 978afe56a..c4cc3d6d1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,3 +8,6 @@ [submodule "third_party/sha1"] path = third_party/sha1 url = https://github.com/vog/sha1 +[submodule "third_party/bcrypt"] + path = third_party/bcrypt + url = https://github.com/pliablepixels/libbcrypt diff --git a/third_party/bcrypt b/third_party/bcrypt index 180cd3372..be171cd75 160000 --- a/third_party/bcrypt +++ b/third_party/bcrypt @@ -1 +1 @@ -Subproject commit 180cd3372609b2539fe9c916f15c8ef8a2aef5f2 +Subproject commit be171cd75dd65e06315a67c7dcdb8e1bbc1dabd4 From ca2e7ea97c953b11cbd8261227d7b598ef05640f Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Fri, 3 May 2019 12:01:13 -0400 Subject: [PATCH 16/90] logs tweak --- src/zm_crypt.cpp | 9 +++++---- src/zm_crypt.h | 2 +- src/zm_user.cpp | 5 ++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/zm_crypt.cpp b/src/zm_crypt.cpp index 9a8cee0ad..19fa6dd9d 100644 --- a/src/zm_crypt.cpp +++ b/src/zm_crypt.cpp @@ -33,7 +33,7 @@ std::string hex2str(std::string &hex) { } -bool verifyPassword(const char *input_password, const char *db_password_hash) { +bool verifyPassword(const char *username, const char *input_password, const char *db_password_hash) { bool password_correct = false; if (strlen(db_password_hash ) < 4) { // actually, shoud be more, but this is min. for next code @@ -42,7 +42,7 @@ bool verifyPassword(const char *input_password, const char *db_password_hash) { } if (db_password_hash[0] == '*') { // MYSQL PASSWORD - Info ("%s is an MD5 encoded password", db_password_hash); + Info ("%s is using an MD5 encoded password", username); SHA1 checksum; // next few lines do '*'+SHA1(raw(SHA1(password))) @@ -55,19 +55,20 @@ bool verifyPassword(const char *input_password, const char *db_password_hash) { std::string final_hash = "*" + interim_hash; std::transform(final_hash.begin(), final_hash.end(), final_hash.begin(), ::toupper); - Info ("Computed password_hash:%s, stored password_hash:%s", final_hash.c_str(), db_password_hash); + Debug (5, "Computed password_hash:%s, stored password_hash:%s", final_hash.c_str(), db_password_hash); password_correct = (std::string(db_password_hash) == final_hash); } else if ((db_password_hash[0] == '$') && (db_password_hash[1]== '2') &&(db_password_hash[3] == '$')) { // BCRYPT - Info ("%s is a Bcrypt password", db_password_hash); + Info ("%s is using a bcrypt encoded password", username); BCrypt bcrypt; std::string input_hash = bcrypt.generateHash(std::string(input_password)); password_correct = bcrypt.validatePassword(std::string(input_password), std::string(db_password_hash)); } else { // plain + Warning ("%s is using a plain text password, please do not use plain text", username); password_correct = (strcmp(input_password, db_password_hash) == 0); } return password_correct; diff --git a/src/zm_crypt.h b/src/zm_crypt.h index b893f7940..ad4aa06da 100644 --- a/src/zm_crypt.h +++ b/src/zm_crypt.h @@ -24,6 +24,6 @@ #include "BCrypt.hpp" #include "sha1.hpp" -bool verifyPassword( const char *input_password, const char *db_password_hash); +bool verifyPassword( const char *username, const char *input_password, const char *db_password_hash); #endif // ZM_CRYPT_H \ No newline at end of file diff --git a/src/zm_user.cpp b/src/zm_user.cpp index d6194b802..8766a2e8f 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -124,9 +124,8 @@ User *zmLoadUser( const char *username, const char *password ) { MYSQL_ROW dbrow = mysql_fetch_row(result); User *user = new User(dbrow); - Info ("Retrieved password for user:%s as %s", user->getUsername(), user->getPassword()); - - if (verifyPassword(password, user->getPassword())) { + + if (verifyPassword(username, password, user->getPassword())) { Info("Authenticated user '%s'", user->getUsername()); mysql_free_result(result); delete safer_username; From 7603e94e90eb68bdd4d60511f6b064eeb3820247 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Fri, 3 May 2019 16:57:43 -0400 Subject: [PATCH 17/90] added lib-ssl/dev for JWT signing --- distros/debian/control | 2 ++ 1 file changed, 2 insertions(+) diff --git a/distros/debian/control b/distros/debian/control index 4c23ab367..2b46f44e1 100644 --- a/distros/debian/control +++ b/distros/debian/control @@ -26,6 +26,7 @@ Build-Depends: debhelper (>= 9), cmake , libdata-dump-perl, libclass-std-fast-perl, libsoap-wsdl-perl, libio-socket-multicast-perl, libdigest-sha-perl , libsys-cpu-perl, libsys-meminfo-perl , libdata-uuid-perl + , libssl-dev Standards-Version: 3.9.4 Package: zoneminder @@ -51,6 +52,7 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} , zip , libvlccore5 | libvlccore7 | libvlccore8, libvlc5 , libpolkit-gobject-1-0, php5-gd + , libssl Recommends: mysql-server | mariadb-server Description: Video camera security and surveillance solution ZoneMinder is intended for use in single or multi-camera video security From d952fe7117c6c557c0fc200e4e4a0e23822067dc Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sat, 4 May 2019 11:52:53 -0400 Subject: [PATCH 18/90] Moved to openSSL SHA1, initial JWT plugin --- .gitmodules | 6 ++--- src/CMakeLists.txt | 9 +++---- src/zm_crypt.cpp | 60 ++++++++++++++++----------------------------- src/zm_crypt.h | 5 +++- third_party/jwt-cpp | 1 + 5 files changed, 33 insertions(+), 48 deletions(-) create mode 160000 third_party/jwt-cpp diff --git a/.gitmodules b/.gitmodules index c4cc3d6d1..f5bcf359d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,9 +5,9 @@ [submodule "web/api/app/Plugin/CakePHP-Enum-Behavior"] path = web/api/app/Plugin/CakePHP-Enum-Behavior url = https://github.com/ZoneMinder/CakePHP-Enum-Behavior.git -[submodule "third_party/sha1"] - path = third_party/sha1 - url = https://github.com/vog/sha1 [submodule "third_party/bcrypt"] path = third_party/bcrypt url = https://github.com/pliablepixels/libbcrypt +[submodule "third_party/jwt-cpp"] + path = third_party/jwt-cpp + url = https://github.com/Thalhammer/jwt-cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4a55ce095..efdb5224d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,12 +7,9 @@ configure_file(zm_config.h.in "${CMAKE_CURRENT_BINARY_DIR}/zm_config.h" @ONLY) set(ZM_BIN_SRC_FILES zm_box.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp zm_config.cpp zm_coord.cpp zm_curl_camera.cpp zm.cpp zm_db.cpp zm_logger.cpp zm_event.cpp zm_frame.cpp zm_eventstream.cpp zm_exception.cpp zm_file_camera.cpp zm_ffmpeg_input.cpp zm_ffmpeg_camera.cpp zm_group.cpp zm_image.cpp zm_jpeg.cpp zm_libvlc_camera.cpp zm_local_camera.cpp zm_monitor.cpp zm_monitorstream.cpp zm_ffmpeg.cpp zm_mpeg.cpp zm_packet.cpp zm_packetqueue.cpp zm_poly.cpp zm_regexp.cpp zm_remote_camera.cpp zm_remote_camera_http.cpp zm_remote_camera_nvsocket.cpp zm_remote_camera_rtsp.cpp zm_rtp.cpp zm_rtp_ctrl.cpp zm_rtp_data.cpp zm_rtp_source.cpp zm_rtsp.cpp zm_rtsp_auth.cpp zm_sdp.cpp zm_signal.cpp zm_stream.cpp zm_swscale.cpp zm_thread.cpp zm_time.cpp zm_timer.cpp zm_user.cpp zm_utils.cpp zm_video.cpp zm_videostore.cpp zm_zone.cpp zm_storage.cpp zm_crypt.cpp) -# includes and linkages to 3rd party libraries/src -set (ZM_BIN_THIRDPARTY_SRC_FILES ../third_party/sha1/sha1.cpp) - # A fix for cmake recompiling the source files for every target. -add_library(zm STATIC ${ZM_BIN_SRC_FILES} ${ZM_BIN_THIRDPARTY_SRC_FILES}) +add_library(zm STATIC ${ZM_BIN_SRC_FILES}) link_directories(/home/pp/source/pp_ZoneMinder.git/third_party/bcrypt) add_executable(zmc zmc.cpp) @@ -20,7 +17,9 @@ add_executable(zma zma.cpp) add_executable(zmu zmu.cpp) add_executable(zms zms.cpp) -include_directories(../third_party/sha1 ../third_party/bcrypt/include/bcrypt) +# JWT is a header only library. +include_directories(../third_party/bcrypt/include/bcrypt) +include_directories(../third_party/jwt-cpp/include/jwt-cpp) target_link_libraries(zmc zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS}) target_link_libraries(zma zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS}) diff --git a/src/zm_crypt.cpp b/src/zm_crypt.cpp index 19fa6dd9d..cf707aff0 100644 --- a/src/zm_crypt.cpp +++ b/src/zm_crypt.cpp @@ -4,37 +4,22 @@ -//https://stackoverflow.com/a/46403026/1361529 -char char2int(char input) { - if (input >= '0' && input <= '9') - return input - '0'; - else if (input >= 'A' && input <= 'F') - return input - 'A' + 10; - else if (input >= 'a' && input <= 'f') - return input - 'a' + 10; - else - return input; // this really should not happen + +std::string createToken() { + std::string token = jwt::create() + .set_issuer("auth0") + //.set_expires_at(jwt::date(expiresAt)) + //.set_issued_at(jwt::date(tp)) + //.set_issued_at(jwt::date(std::chrono::system_clock::now())) + //.set_expires_at(jwt::date(std::chrono::system_clock::now()+std::chrono::seconds{EXPIRY})) + .sign(jwt::algorithm::hs256{"secret"}); + return token; } -std::string hex2str(std::string &hex) { - std::string out; - out.resize(hex.size() / 2 + hex.size() % 2); - std::string::iterator it = hex.begin(); - std::string::iterator out_it = out.begin(); - if (hex.size() % 2 != 0) { - *out_it++ = char(char2int(*it++)); - } - - for (; it < hex.end() - 1; it++) { - *out_it++ = char2int(*it++) << 4 | char2int(*it); - }; - - return out; -} - bool verifyPassword(const char *username, const char *input_password, const char *db_password_hash) { bool password_correct = false; + Info ("JWT created as %s",createToken().c_str()); if (strlen(db_password_hash ) < 4) { // actually, shoud be more, but this is min. for next code Error ("DB Password is too short or invalid to check"); @@ -43,20 +28,17 @@ bool verifyPassword(const char *username, const char *input_password, const char if (db_password_hash[0] == '*') { // MYSQL PASSWORD Info ("%s is using an MD5 encoded password", username); - SHA1 checksum; + unsigned char digest_interim[SHA_DIGEST_LENGTH]; + unsigned char digest_final[SHA_DIGEST_LENGTH]; + SHA1((unsigned char*)&input_password, strlen((const char *) input_password), (unsigned char*)&digest_interim); + SHA1((unsigned char*)&digest_interim, strlen((const char *)digest_interim), (unsigned char*)&digest_final); + char final_hash[SHA_DIGEST_LENGTH * 2 +2]; + for(int i = 0; i < SHA_DIGEST_LENGTH; i++) + sprintf(&final_hash[i*2], "%02X", (unsigned int)digest_final[i]); - // next few lines do '*'+SHA1(raw(SHA1(password))) - // which is MYSQL >=4.1 PASSWORD algorithm - checksum.update(input_password); - std::string interim_hash = checksum.final(); - std::string binary_hash = hex2str(interim_hash); // get interim hash - checksum.update(binary_hash); - interim_hash = checksum.final(); - std::string final_hash = "*" + interim_hash; - std::transform(final_hash.begin(), final_hash.end(), final_hash.begin(), ::toupper); - - Debug (5, "Computed password_hash:%s, stored password_hash:%s", final_hash.c_str(), db_password_hash); - password_correct = (std::string(db_password_hash) == final_hash); + Info ("Computed password_hash:%s, stored password_hash:%s", final_hash, db_password_hash); + Debug (5, "Computed password_hash:%s, stored password_hash:%s", final_hash, db_password_hash); + password_correct = (strcmp(db_password_hash, final_hash)==0); } else if ((db_password_hash[0] == '$') && (db_password_hash[1]== '2') &&(db_password_hash[3] == '$')) { diff --git a/src/zm_crypt.h b/src/zm_crypt.h index ad4aa06da..a1e8945e4 100644 --- a/src/zm_crypt.h +++ b/src/zm_crypt.h @@ -20,10 +20,13 @@ #ifndef ZM_CRYPT_H #define ZM_CRYPT_H + #include +#include #include "BCrypt.hpp" -#include "sha1.hpp" +#include "jwt.h" bool verifyPassword( const char *username, const char *input_password, const char *db_password_hash); +std::string createToken(); #endif // ZM_CRYPT_H \ No newline at end of file diff --git a/third_party/jwt-cpp b/third_party/jwt-cpp new file mode 160000 index 000000000..dd0337e64 --- /dev/null +++ b/third_party/jwt-cpp @@ -0,0 +1 @@ +Subproject commit dd0337e64c19b5c6290b30429a9eedafadcae4b7 From 4c51747171eb54ee359c03476c377f68c8c4c610 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sat, 4 May 2019 11:57:30 -0400 Subject: [PATCH 19/90] removed vog --- third_party/sha1 | 1 - 1 file changed, 1 deletion(-) delete mode 160000 third_party/sha1 diff --git a/third_party/sha1 b/third_party/sha1 deleted file mode 160000 index 68a099035..000000000 --- a/third_party/sha1 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 68a0990352c04de43c494e8381264c27ed0b8e7e From 983e050fd7e4370d68cbbed061c6438803ec2c94 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sat, 4 May 2019 15:20:31 -0400 Subject: [PATCH 20/90] fixed SHA1 algo --- src/zm_crypt.cpp | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/zm_crypt.cpp b/src/zm_crypt.cpp index cf707aff0..266105c65 100644 --- a/src/zm_crypt.cpp +++ b/src/zm_crypt.cpp @@ -28,13 +28,27 @@ bool verifyPassword(const char *username, const char *input_password, const char if (db_password_hash[0] == '*') { // MYSQL PASSWORD Info ("%s is using an MD5 encoded password", username); + + SHA_CTX ctx1, ctx2; unsigned char digest_interim[SHA_DIGEST_LENGTH]; unsigned char digest_final[SHA_DIGEST_LENGTH]; - SHA1((unsigned char*)&input_password, strlen((const char *) input_password), (unsigned char*)&digest_interim); - SHA1((unsigned char*)&digest_interim, strlen((const char *)digest_interim), (unsigned char*)&digest_final); + + //get first iteration + SHA1_Init(&ctx1); + SHA1_Update(&ctx1, input_password, strlen(input_password)); + SHA1_Final(digest_interim, &ctx1); + + //2nd iteration + SHA1_Init(&ctx2); + SHA1_Update(&ctx2, digest_interim,SHA_DIGEST_LENGTH); + SHA1_Final (digest_final, &ctx2) + char final_hash[SHA_DIGEST_LENGTH * 2 +2]; + final_hash[0]='*'; + //convert to hex for(int i = 0; i < SHA_DIGEST_LENGTH; i++) - sprintf(&final_hash[i*2], "%02X", (unsigned int)digest_final[i]); + sprintf(&final_hash[i*2]+1, "%02X", (unsigned int)digest_final[i]); + final_hash[SHA_DIGEST_LENGTH *2 + 1]=0; Info ("Computed password_hash:%s, stored password_hash:%s", final_hash, db_password_hash); Debug (5, "Computed password_hash:%s, stored password_hash:%s", final_hash, db_password_hash); From 4c8d20db64f97b8a2ca86ea49529390c887383eb Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sat, 4 May 2019 15:27:00 -0400 Subject: [PATCH 21/90] typo --- src/zm_crypt.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zm_crypt.cpp b/src/zm_crypt.cpp index 266105c65..6c6f4c7c1 100644 --- a/src/zm_crypt.cpp +++ b/src/zm_crypt.cpp @@ -41,7 +41,7 @@ bool verifyPassword(const char *username, const char *input_password, const char //2nd iteration SHA1_Init(&ctx2); SHA1_Update(&ctx2, digest_interim,SHA_DIGEST_LENGTH); - SHA1_Final (digest_final, &ctx2) + SHA1_Final (digest_final, &ctx2); char final_hash[SHA_DIGEST_LENGTH * 2 +2]; final_hash[0]='*'; From 725c3c50ed6238410994d48988659b3a28bcc22a Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 5 May 2019 07:08:25 -0400 Subject: [PATCH 22/90] use php-jwt, use proper way to add PHP modules, via composer --- web/composer.json | 5 + web/composer.lock | 64 +++ web/includes/auth.php | 17 + web/vendor/autoload.php | 7 + web/vendor/composer/ClassLoader.php | 445 ++++++++++++++++++ web/vendor/composer/LICENSE | 21 + web/vendor/composer/autoload_classmap.php | 9 + web/vendor/composer/autoload_namespaces.php | 9 + web/vendor/composer/autoload_psr4.php | 10 + web/vendor/composer/autoload_real.php | 52 ++ web/vendor/composer/autoload_static.php | 31 ++ web/vendor/composer/installed.json | 50 ++ web/vendor/firebase/php-jwt/LICENSE | 30 ++ web/vendor/firebase/php-jwt/README.md | 200 ++++++++ web/vendor/firebase/php-jwt/composer.json | 29 ++ .../php-jwt/src/BeforeValidException.php | 7 + .../firebase/php-jwt/src/ExpiredException.php | 7 + web/vendor/firebase/php-jwt/src/JWT.php | 379 +++++++++++++++ .../php-jwt/src/SignatureInvalidException.php | 7 + 19 files changed, 1379 insertions(+) create mode 100644 web/composer.json create mode 100644 web/composer.lock create mode 100644 web/vendor/autoload.php create mode 100644 web/vendor/composer/ClassLoader.php create mode 100644 web/vendor/composer/LICENSE create mode 100644 web/vendor/composer/autoload_classmap.php create mode 100644 web/vendor/composer/autoload_namespaces.php create mode 100644 web/vendor/composer/autoload_psr4.php create mode 100644 web/vendor/composer/autoload_real.php create mode 100644 web/vendor/composer/autoload_static.php create mode 100644 web/vendor/composer/installed.json create mode 100644 web/vendor/firebase/php-jwt/LICENSE create mode 100644 web/vendor/firebase/php-jwt/README.md create mode 100644 web/vendor/firebase/php-jwt/composer.json create mode 100644 web/vendor/firebase/php-jwt/src/BeforeValidException.php create mode 100644 web/vendor/firebase/php-jwt/src/ExpiredException.php create mode 100644 web/vendor/firebase/php-jwt/src/JWT.php create mode 100644 web/vendor/firebase/php-jwt/src/SignatureInvalidException.php diff --git a/web/composer.json b/web/composer.json new file mode 100644 index 000000000..cc22311b1 --- /dev/null +++ b/web/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "firebase/php-jwt": "^5.0" + } +} diff --git a/web/composer.lock b/web/composer.lock new file mode 100644 index 000000000..b0b368b4f --- /dev/null +++ b/web/composer.lock @@ -0,0 +1,64 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "7f97fc9c4d2beaf06d019ba50f7efcbc", + "packages": [ + { + "name": "firebase/php-jwt", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "9984a4d3a32ae7673d6971ea00bae9d0a1abba0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/9984a4d3a32ae7673d6971ea00bae9d0a1abba0e", + "reference": "9984a4d3a32ae7673d6971ea00bae9d0a1abba0e", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": " 4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "time": "2017-06-27T22:17:23+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/web/includes/auth.php b/web/includes/auth.php index ef1871164..52391d435 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -19,6 +19,9 @@ // // require_once('session.php'); +require_once ('../vendor/autoload.php'); + +use \Firebase\JWT\JWT; // 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 @@ -45,7 +48,21 @@ function migrateHash($user, $pass) { // core function used to login a user to PHP. Is also used for cake sessions for the API function userLogin($username='', $password='', $passwordHashed=false) { + global $user; + + $key = "example_key"; + $token = array( + "iss" => "http://example.org", + "aud" => "http://example.com", + "iat" => 1356999524, + "nbf" => 1357000000 + ); + $jwt = JWT::encode($token, $key); + + ZM\Info ("JWT token is $jwt"); + + if ( !$username and isset($_REQUEST['username']) ) $username = $_REQUEST['username']; if ( !$password and isset($_REQUEST['password']) ) diff --git a/web/vendor/autoload.php b/web/vendor/autoload.php new file mode 100644 index 000000000..034205792 --- /dev/null +++ b/web/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/web/vendor/composer/LICENSE b/web/vendor/composer/LICENSE new file mode 100644 index 000000000..f27399a04 --- /dev/null +++ b/web/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/web/vendor/composer/autoload_classmap.php b/web/vendor/composer/autoload_classmap.php new file mode 100644 index 000000000..7a91153b0 --- /dev/null +++ b/web/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ + array($vendorDir . '/firebase/php-jwt/src'), +); diff --git a/web/vendor/composer/autoload_real.php b/web/vendor/composer/autoload_real.php new file mode 100644 index 000000000..accbcefb3 --- /dev/null +++ b/web/vendor/composer/autoload_real.php @@ -0,0 +1,52 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit254e25e69fe049d603f41f5fd853ef2b::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/web/vendor/composer/autoload_static.php b/web/vendor/composer/autoload_static.php new file mode 100644 index 000000000..2709f5803 --- /dev/null +++ b/web/vendor/composer/autoload_static.php @@ -0,0 +1,31 @@ + + array ( + 'Firebase\\JWT\\' => 13, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Firebase\\JWT\\' => + array ( + 0 => __DIR__ . '/..' . '/firebase/php-jwt/src', + ), + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit254e25e69fe049d603f41f5fd853ef2b::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit254e25e69fe049d603f41f5fd853ef2b::$prefixDirsPsr4; + + }, null, ClassLoader::class); + } +} diff --git a/web/vendor/composer/installed.json b/web/vendor/composer/installed.json new file mode 100644 index 000000000..5b2924c21 --- /dev/null +++ b/web/vendor/composer/installed.json @@ -0,0 +1,50 @@ +[ + { + "name": "firebase/php-jwt", + "version": "v5.0.0", + "version_normalized": "5.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "9984a4d3a32ae7673d6971ea00bae9d0a1abba0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/9984a4d3a32ae7673d6971ea00bae9d0a1abba0e", + "reference": "9984a4d3a32ae7673d6971ea00bae9d0a1abba0e", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": " 4.8.35" + }, + "time": "2017-06-27T22:17:23+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt" + } +] diff --git a/web/vendor/firebase/php-jwt/LICENSE b/web/vendor/firebase/php-jwt/LICENSE new file mode 100644 index 000000000..cb0c49b33 --- /dev/null +++ b/web/vendor/firebase/php-jwt/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2011, Neuman Vong + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Neuman Vong nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/web/vendor/firebase/php-jwt/README.md b/web/vendor/firebase/php-jwt/README.md new file mode 100644 index 000000000..b1a7a3a20 --- /dev/null +++ b/web/vendor/firebase/php-jwt/README.md @@ -0,0 +1,200 @@ +[![Build Status](https://travis-ci.org/firebase/php-jwt.png?branch=master)](https://travis-ci.org/firebase/php-jwt) +[![Latest Stable Version](https://poser.pugx.org/firebase/php-jwt/v/stable)](https://packagist.org/packages/firebase/php-jwt) +[![Total Downloads](https://poser.pugx.org/firebase/php-jwt/downloads)](https://packagist.org/packages/firebase/php-jwt) +[![License](https://poser.pugx.org/firebase/php-jwt/license)](https://packagist.org/packages/firebase/php-jwt) + +PHP-JWT +======= +A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to [RFC 7519](https://tools.ietf.org/html/rfc7519). + +Installation +------------ + +Use composer to manage your dependencies and download PHP-JWT: + +```bash +composer require firebase/php-jwt +``` + +Example +------- +```php + "http://example.org", + "aud" => "http://example.com", + "iat" => 1356999524, + "nbf" => 1357000000 +); + +/** + * IMPORTANT: + * You must specify supported algorithms for your application. See + * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 + * for a list of spec-compliant algorithms. + */ +$jwt = JWT::encode($token, $key); +$decoded = JWT::decode($jwt, $key, array('HS256')); + +print_r($decoded); + +/* + NOTE: This will now be an object instead of an associative array. To get + an associative array, you will need to cast it as such: +*/ + +$decoded_array = (array) $decoded; + +/** + * You can add a leeway to account for when there is a clock skew times between + * the signing and verifying servers. It is recommended that this leeway should + * not be bigger than a few minutes. + * + * Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef + */ +JWT::$leeway = 60; // $leeway in seconds +$decoded = JWT::decode($jwt, $key, array('HS256')); + +?> +``` +Example with RS256 (openssl) +---------------------------- +```php + "example.org", + "aud" => "example.com", + "iat" => 1356999524, + "nbf" => 1357000000 +); + +$jwt = JWT::encode($token, $privateKey, 'RS256'); +echo "Encode:\n" . print_r($jwt, true) . "\n"; + +$decoded = JWT::decode($jwt, $publicKey, array('RS256')); + +/* + NOTE: This will now be an object instead of an associative array. To get + an associative array, you will need to cast it as such: +*/ + +$decoded_array = (array) $decoded; +echo "Decode:\n" . print_r($decoded_array, true) . "\n"; +?> +``` + +Changelog +--------- + +#### 5.0.0 / 2017-06-26 +- Support RS384 and RS512. + See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)! +- Add an example for RS256 openssl. + See [#125](https://github.com/firebase/php-jwt/pull/125). Thanks [@akeeman](https://github.com/akeeman)! +- Detect invalid Base64 encoding in signature. + See [#162](https://github.com/firebase/php-jwt/pull/162). Thanks [@psignoret](https://github.com/psignoret)! +- Update `JWT::verify` to handle OpenSSL errors. + See [#159](https://github.com/firebase/php-jwt/pull/159). Thanks [@bshaffer](https://github.com/bshaffer)! +- Add `array` type hinting to `decode` method + See [#101](https://github.com/firebase/php-jwt/pull/101). Thanks [@hywak](https://github.com/hywak)! +- Add all JSON error types. + See [#110](https://github.com/firebase/php-jwt/pull/110). Thanks [@gbalduzzi](https://github.com/gbalduzzi)! +- Bugfix 'kid' not in given key list. + See [#129](https://github.com/firebase/php-jwt/pull/129). Thanks [@stampycode](https://github.com/stampycode)! +- Miscellaneous cleanup, documentation and test fixes. + See [#107](https://github.com/firebase/php-jwt/pull/107), [#115](https://github.com/firebase/php-jwt/pull/115), + [#160](https://github.com/firebase/php-jwt/pull/160), [#161](https://github.com/firebase/php-jwt/pull/161), and + [#165](https://github.com/firebase/php-jwt/pull/165). Thanks [@akeeman](https://github.com/akeeman), + [@chinedufn](https://github.com/chinedufn), and [@bshaffer](https://github.com/bshaffer)! + +#### 4.0.0 / 2016-07-17 +- Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)! +- Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)! +- Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)! +- Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)! + +#### 3.0.0 / 2015-07-22 +- Minimum PHP version updated from `5.2.0` to `5.3.0`. +- Add `\Firebase\JWT` namespace. See +[#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to +[@Dashron](https://github.com/Dashron)! +- Require a non-empty key to decode and verify a JWT. See +[#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to +[@sjones608](https://github.com/sjones608)! +- Cleaner documentation blocks in the code. See +[#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to +[@johanderuijter](https://github.com/johanderuijter)! + +#### 2.2.0 / 2015-06-22 +- Add support for adding custom, optional JWT headers to `JWT::encode()`. See +[#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to +[@mcocaro](https://github.com/mcocaro)! + +#### 2.1.0 / 2015-05-20 +- Add support for adding a leeway to `JWT:decode()` that accounts for clock skew +between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)! +- Add support for passing an object implementing the `ArrayAccess` interface for +`$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)! + +#### 2.0.0 / 2015-04-01 +- **Note**: It is strongly recommended that you update to > v2.0.0 to address + known security vulnerabilities in prior versions when both symmetric and + asymmetric keys are used together. +- Update signature for `JWT::decode(...)` to require an array of supported + algorithms to use when verifying token signatures. + + +Tests +----- +Run the tests using phpunit: + +```bash +$ pear install PHPUnit +$ phpunit --configuration phpunit.xml.dist +PHPUnit 3.7.10 by Sebastian Bergmann. +..... +Time: 0 seconds, Memory: 2.50Mb +OK (5 tests, 5 assertions) +``` + +New Lines in private keys +----- + +If your private key contains `\n` characters, be sure to wrap it in double quotes `""` +and not single quotes `''` in order to properly interpret the escaped characters. + +License +------- +[3-Clause BSD](http://opensource.org/licenses/BSD-3-Clause). diff --git a/web/vendor/firebase/php-jwt/composer.json b/web/vendor/firebase/php-jwt/composer.json new file mode 100644 index 000000000..b76ffd191 --- /dev/null +++ b/web/vendor/firebase/php-jwt/composer.json @@ -0,0 +1,29 @@ +{ + "name": "firebase/php-jwt", + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "license": "BSD-3-Clause", + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "require-dev": { + "phpunit/phpunit": " 4.8.35" + } +} diff --git a/web/vendor/firebase/php-jwt/src/BeforeValidException.php b/web/vendor/firebase/php-jwt/src/BeforeValidException.php new file mode 100644 index 000000000..a6ee2f7c6 --- /dev/null +++ b/web/vendor/firebase/php-jwt/src/BeforeValidException.php @@ -0,0 +1,7 @@ + + * @author Anant Narayanan + * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD + * @link https://github.com/firebase/php-jwt + */ +class JWT +{ + + /** + * When checking nbf, iat or expiration times, + * we want to provide some extra leeway time to + * account for clock skew. + */ + public static $leeway = 0; + + /** + * Allow the current timestamp to be specified. + * Useful for fixing a value within unit testing. + * + * Will default to PHP time() value if null. + */ + public static $timestamp = null; + + public static $supported_algs = array( + 'HS256' => array('hash_hmac', 'SHA256'), + 'HS512' => array('hash_hmac', 'SHA512'), + 'HS384' => array('hash_hmac', 'SHA384'), + 'RS256' => array('openssl', 'SHA256'), + 'RS384' => array('openssl', 'SHA384'), + 'RS512' => array('openssl', 'SHA512'), + ); + + /** + * Decodes a JWT string into a PHP object. + * + * @param string $jwt The JWT + * @param string|array $key The key, or map of keys. + * If the algorithm used is asymmetric, this is the public key + * @param array $allowed_algs List of supported verification algorithms + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * + * @return object The JWT's payload as a PHP object + * + * @throws UnexpectedValueException Provided JWT was invalid + * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed + * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' + * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' + * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim + * + * @uses jsonDecode + * @uses urlsafeB64Decode + */ + public static function decode($jwt, $key, array $allowed_algs = array()) + { + $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp; + + if (empty($key)) { + throw new InvalidArgumentException('Key may not be empty'); + } + $tks = explode('.', $jwt); + if (count($tks) != 3) { + throw new UnexpectedValueException('Wrong number of segments'); + } + list($headb64, $bodyb64, $cryptob64) = $tks; + if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) { + throw new UnexpectedValueException('Invalid header encoding'); + } + if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { + throw new UnexpectedValueException('Invalid claims encoding'); + } + if (false === ($sig = static::urlsafeB64Decode($cryptob64))) { + throw new UnexpectedValueException('Invalid signature encoding'); + } + if (empty($header->alg)) { + throw new UnexpectedValueException('Empty algorithm'); + } + if (empty(static::$supported_algs[$header->alg])) { + throw new UnexpectedValueException('Algorithm not supported'); + } + if (!in_array($header->alg, $allowed_algs)) { + throw new UnexpectedValueException('Algorithm not allowed'); + } + if (is_array($key) || $key instanceof \ArrayAccess) { + if (isset($header->kid)) { + if (!isset($key[$header->kid])) { + throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); + } + $key = $key[$header->kid]; + } else { + throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); + } + } + + // Check the signature + if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { + throw new SignatureInvalidException('Signature verification failed'); + } + + // Check if the nbf if it is defined. This is the time that the + // token can actually be used. If it's not yet that time, abort. + if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { + throw new BeforeValidException( + 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) + ); + } + + // Check that this token has been created before 'now'. This prevents + // using tokens that have been created for later use (and haven't + // correctly used the nbf claim). + if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { + throw new BeforeValidException( + 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat) + ); + } + + // Check if this token has expired. + if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { + throw new ExpiredException('Expired token'); + } + + return $payload; + } + + /** + * Converts and signs a PHP object or array into a JWT string. + * + * @param object|array $payload PHP object or array + * @param string $key The secret key. + * If the algorithm used is asymmetric, this is the private key + * @param string $alg The signing algorithm. + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * @param mixed $keyId + * @param array $head An array with header elements to attach + * + * @return string A signed JWT + * + * @uses jsonEncode + * @uses urlsafeB64Encode + */ + public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null) + { + $header = array('typ' => 'JWT', 'alg' => $alg); + if ($keyId !== null) { + $header['kid'] = $keyId; + } + if ( isset($head) && is_array($head) ) { + $header = array_merge($head, $header); + } + $segments = array(); + $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); + $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); + $signing_input = implode('.', $segments); + + $signature = static::sign($signing_input, $key, $alg); + $segments[] = static::urlsafeB64Encode($signature); + + return implode('.', $segments); + } + + /** + * Sign a string with a given key and algorithm. + * + * @param string $msg The message to sign + * @param string|resource $key The secret key + * @param string $alg The signing algorithm. + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * + * @return string An encrypted message + * + * @throws DomainException Unsupported algorithm was specified + */ + public static function sign($msg, $key, $alg = 'HS256') + { + if (empty(static::$supported_algs[$alg])) { + throw new DomainException('Algorithm not supported'); + } + list($function, $algorithm) = static::$supported_algs[$alg]; + switch($function) { + case 'hash_hmac': + return hash_hmac($algorithm, $msg, $key, true); + case 'openssl': + $signature = ''; + $success = openssl_sign($msg, $signature, $key, $algorithm); + if (!$success) { + throw new DomainException("OpenSSL unable to sign data"); + } else { + return $signature; + } + } + } + + /** + * Verify a signature with the message, key and method. Not all methods + * are symmetric, so we must have a separate verify and sign method. + * + * @param string $msg The original message (header and body) + * @param string $signature The original signature + * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key + * @param string $alg The algorithm + * + * @return bool + * + * @throws DomainException Invalid Algorithm or OpenSSL failure + */ + private static function verify($msg, $signature, $key, $alg) + { + if (empty(static::$supported_algs[$alg])) { + throw new DomainException('Algorithm not supported'); + } + + list($function, $algorithm) = static::$supported_algs[$alg]; + switch($function) { + case 'openssl': + $success = openssl_verify($msg, $signature, $key, $algorithm); + if ($success === 1) { + return true; + } elseif ($success === 0) { + return false; + } + // returns 1 on success, 0 on failure, -1 on error. + throw new DomainException( + 'OpenSSL error: ' . openssl_error_string() + ); + case 'hash_hmac': + default: + $hash = hash_hmac($algorithm, $msg, $key, true); + if (function_exists('hash_equals')) { + return hash_equals($signature, $hash); + } + $len = min(static::safeStrlen($signature), static::safeStrlen($hash)); + + $status = 0; + for ($i = 0; $i < $len; $i++) { + $status |= (ord($signature[$i]) ^ ord($hash[$i])); + } + $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash)); + + return ($status === 0); + } + } + + /** + * Decode a JSON string into a PHP object. + * + * @param string $input JSON string + * + * @return object Object representation of JSON string + * + * @throws DomainException Provided string was invalid JSON + */ + public static function jsonDecode($input) + { + if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { + /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you + * to specify that large ints (like Steam Transaction IDs) should be treated as + * strings, rather than the PHP default behaviour of converting them to floats. + */ + $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING); + } else { + /** Not all servers will support that, however, so for older versions we must + * manually detect large ints in the JSON string and quote them (thus converting + *them to strings) before decoding, hence the preg_replace() call. + */ + $max_int_length = strlen((string) PHP_INT_MAX) - 1; + $json_without_bigints = preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input); + $obj = json_decode($json_without_bigints); + } + + if (function_exists('json_last_error') && $errno = json_last_error()) { + static::handleJsonError($errno); + } elseif ($obj === null && $input !== 'null') { + throw new DomainException('Null result with non-null input'); + } + return $obj; + } + + /** + * Encode a PHP object into a JSON string. + * + * @param object|array $input A PHP object or array + * + * @return string JSON representation of the PHP object or array + * + * @throws DomainException Provided object could not be encoded to valid JSON + */ + public static function jsonEncode($input) + { + $json = json_encode($input); + if (function_exists('json_last_error') && $errno = json_last_error()) { + static::handleJsonError($errno); + } elseif ($json === 'null' && $input !== null) { + throw new DomainException('Null result with non-null input'); + } + return $json; + } + + /** + * Decode a string with URL-safe Base64. + * + * @param string $input A Base64 encoded string + * + * @return string A decoded string + */ + public static function urlsafeB64Decode($input) + { + $remainder = strlen($input) % 4; + if ($remainder) { + $padlen = 4 - $remainder; + $input .= str_repeat('=', $padlen); + } + return base64_decode(strtr($input, '-_', '+/')); + } + + /** + * Encode a string with URL-safe Base64. + * + * @param string $input The string you want encoded + * + * @return string The base64 encode of what you passed in + */ + public static function urlsafeB64Encode($input) + { + return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); + } + + /** + * Helper method to create a JSON error. + * + * @param int $errno An error number from json_last_error() + * + * @return void + */ + private static function handleJsonError($errno) + { + $messages = array( + JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', + JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', + JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', + JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', + JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3 + ); + throw new DomainException( + isset($messages[$errno]) + ? $messages[$errno] + : 'Unknown JSON error: ' . $errno + ); + } + + /** + * Get the number of bytes in cryptographic strings. + * + * @param string + * + * @return int + */ + private static function safeStrlen($str) + { + if (function_exists('mb_strlen')) { + return mb_strlen($str, '8bit'); + } + return strlen($str); + } +} diff --git a/web/vendor/firebase/php-jwt/src/SignatureInvalidException.php b/web/vendor/firebase/php-jwt/src/SignatureInvalidException.php new file mode 100644 index 000000000..27332b21b --- /dev/null +++ b/web/vendor/firebase/php-jwt/src/SignatureInvalidException.php @@ -0,0 +1,7 @@ + Date: Sun, 5 May 2019 07:50:52 -0400 Subject: [PATCH 23/90] fixed module path --- web/.gitignore | 2 +- web/includes/auth.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/.gitignore b/web/.gitignore index 90d971d4b..354e5470b 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -4,8 +4,8 @@ /app/tmp /lib/Cake/Console/Templates/skel/tmp/ /plugins -/vendors /build +/vendors /dist /tags /app/webroot/events diff --git a/web/includes/auth.php b/web/includes/auth.php index 52391d435..7c8c24527 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -19,7 +19,7 @@ // // require_once('session.php'); -require_once ('../vendor/autoload.php'); +require_once(__DIR__.'/../vendor/autoload.php'); use \Firebase\JWT\JWT; From a55a11dad1249d6db7a0cfb578e5752cf6bb9186 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 5 May 2019 11:24:55 -0400 Subject: [PATCH 24/90] first attempt to fix cast error --- .gitmodules | 3 --- web/includes/auth.php | 33 ++++++++++++++++++++++----------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/.gitmodules b/.gitmodules index f5bcf359d..2ec483d25 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,6 +8,3 @@ [submodule "third_party/bcrypt"] path = third_party/bcrypt url = https://github.com/pliablepixels/libbcrypt -[submodule "third_party/jwt-cpp"] - path = third_party/jwt-cpp - url = https://github.com/Thalhammer/jwt-cpp diff --git a/web/includes/auth.php b/web/includes/auth.php index 7c8c24527..3f575d4c2 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -51,16 +51,7 @@ function userLogin($username='', $password='', $passwordHashed=false) { global $user; - $key = "example_key"; - $token = array( - "iss" => "http://example.org", - "aud" => "http://example.com", - "iat" => 1356999524, - "nbf" => 1357000000 - ); - $jwt = JWT::encode($token, $key); - - ZM\Info ("JWT token is $jwt"); + if ( !$username and isset($_REQUEST['username']) ) @@ -233,8 +224,28 @@ function getAuthUser($auth) { function generateAuthHash($useRemoteAddr, $force=false) { if ( ZM_OPT_USE_AUTH and ZM_AUTH_RELAY == 'hashed' and isset($_SESSION['username']) and $_SESSION['passwordHash'] ) { - # regenerate a hash at half the liftetime of a hash, an hour is 3600 so half is 1800 $time = time(); + $key = ZM_AUTH_HASH_SECRET; + $issuedAt = time(); + $expireAt = $issuedAt + ZM_AUTH_HASH_TTL * 3600; + + + $token = array( + "iss" => "ZoneMinder", + "iat" => $issuedAt, + "exp" => $expireAt + + ); + + if ($useRemoteAddr) { + $token['remote_addr'] = $_SESSION['remoteAddr']; + } + + + $jwt = JWT::encode($token, $key); + + ZM\Info ("JWT token is $jwt"); + $mintime = $time - ( ZM_AUTH_HASH_TTL * 1800 ); if ( $force or ( !isset($_SESSION['AuthHash'.$_SESSION['remoteAddr']]) ) or ( $_SESSION['AuthHashGeneratedAt'] < $mintime ) ) { From e9d3dd1987cc8c658607508df9b681183cdcfbd7 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 5 May 2019 11:26:01 -0400 Subject: [PATCH 25/90] own fork --- third_party/jwt-cpp | 1 - 1 file changed, 1 deletion(-) delete mode 160000 third_party/jwt-cpp diff --git a/third_party/jwt-cpp b/third_party/jwt-cpp deleted file mode 160000 index dd0337e64..000000000 --- a/third_party/jwt-cpp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit dd0337e64c19b5c6290b30429a9eedafadcae4b7 From 31c7cc31a28197795f0c8bcbd202de5bf2364e95 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 5 May 2019 11:26:20 -0400 Subject: [PATCH 26/90] own fork --- .gitmodules | 3 +++ third_party/jwt-cpp | 1 + 2 files changed, 4 insertions(+) create mode 160000 third_party/jwt-cpp diff --git a/.gitmodules b/.gitmodules index 2ec483d25..0bbfbb368 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,3 +8,6 @@ [submodule "third_party/bcrypt"] path = third_party/bcrypt url = https://github.com/pliablepixels/libbcrypt +[submodule "third_party/jwt-cpp"] + path = third_party/jwt-cpp + url = https://github.com/pliablepixels/jwt-cpp diff --git a/third_party/jwt-cpp b/third_party/jwt-cpp new file mode 160000 index 000000000..3dbc5a092 --- /dev/null +++ b/third_party/jwt-cpp @@ -0,0 +1 @@ +Subproject commit 3dbc5a0929aa3e53a47cbffac546f3d7877c41b5 From 37040f33a824f6075c32ca4834ae500e250cc76e Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 5 May 2019 12:49:33 -0400 Subject: [PATCH 27/90] add composer vendor directory --- web/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/CMakeLists.txt b/web/CMakeLists.txt index 50e5f9998..b3d097739 100644 --- a/web/CMakeLists.txt +++ b/web/CMakeLists.txt @@ -9,7 +9,7 @@ add_subdirectory(tools/mootools) configure_file(includes/config.php.in "${CMAKE_CURRENT_BINARY_DIR}/includes/config.php" @ONLY) # Install the web files -install(DIRECTORY api ajax css fonts graphics includes js lang skins tools views DESTINATION "${ZM_WEBDIR}" PATTERN "*.in" EXCLUDE PATTERN "*Make*" EXCLUDE PATTERN "*cmake*" EXCLUDE) +install(DIRECTORY vendor api ajax css fonts graphics includes js lang skins tools views DESTINATION "${ZM_WEBDIR}" PATTERN "*.in" EXCLUDE PATTERN "*Make*" EXCLUDE PATTERN "*cmake*" EXCLUDE) install(FILES index.php robots.txt DESTINATION "${ZM_WEBDIR}") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/includes/config.php" DESTINATION "${ZM_WEBDIR}/includes") From ca3f65deef7f7664cef0af55c4e874dcd8b3e51f Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 5 May 2019 14:32:09 -0400 Subject: [PATCH 28/90] go back to jwt-cpp as PR merged --- .gitmodules | 3 --- third_party/jwt-cpp | 1 - web/includes/auth.php | 3 ++- 3 files changed, 2 insertions(+), 5 deletions(-) delete mode 160000 third_party/jwt-cpp diff --git a/.gitmodules b/.gitmodules index 0bbfbb368..2ec483d25 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,6 +8,3 @@ [submodule "third_party/bcrypt"] path = third_party/bcrypt url = https://github.com/pliablepixels/libbcrypt -[submodule "third_party/jwt-cpp"] - path = third_party/jwt-cpp - url = https://github.com/pliablepixels/jwt-cpp diff --git a/third_party/jwt-cpp b/third_party/jwt-cpp deleted file mode 160000 index 3dbc5a092..000000000 --- a/third_party/jwt-cpp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3dbc5a0929aa3e53a47cbffac546f3d7877c41b5 diff --git a/web/includes/auth.php b/web/includes/auth.php index 3f575d4c2..b6340bcfc 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -233,7 +233,8 @@ function generateAuthHash($useRemoteAddr, $force=false) { $token = array( "iss" => "ZoneMinder", "iat" => $issuedAt, - "exp" => $expireAt + "exp" => $expireAt, + "user" => $_SESSION['username'] ); From 37f915ec0f912aca5a5fd2945ad4e5d1a11954d2 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 5 May 2019 14:32:54 -0400 Subject: [PATCH 29/90] moved to jwt-cpp after PR merge --- .gitmodules | 3 +++ third_party/jwt-cpp | 1 + 2 files changed, 4 insertions(+) create mode 160000 third_party/jwt-cpp diff --git a/.gitmodules b/.gitmodules index 2ec483d25..f5bcf359d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,3 +8,6 @@ [submodule "third_party/bcrypt"] path = third_party/bcrypt url = https://github.com/pliablepixels/libbcrypt +[submodule "third_party/jwt-cpp"] + path = third_party/jwt-cpp + url = https://github.com/Thalhammer/jwt-cpp diff --git a/third_party/jwt-cpp b/third_party/jwt-cpp new file mode 160000 index 000000000..bfca4f6a8 --- /dev/null +++ b/third_party/jwt-cpp @@ -0,0 +1 @@ +Subproject commit bfca4f6a87bfd9d9a259939d0524169827a3a862 From 0bbc58297138856b9015f03b1d49e04ab5b14ba3 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Tue, 7 May 2019 15:03:13 -0400 Subject: [PATCH 30/90] New token= query for JWT --- web/api/app/Controller/AppController.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/web/api/app/Controller/AppController.php b/web/api/app/Controller/AppController.php index 51575f055..840a14f58 100644 --- a/web/api/app/Controller/AppController.php +++ b/web/api/app/Controller/AppController.php @@ -73,19 +73,27 @@ class AppController extends Controller { $mUser = $this->request->query('user') ? $this->request->query('user') : $this->request->data('user'); $mPassword = $this->request->query('pass') ? $this->request->query('pass') : $this->request->data('pass'); - $mAuth = $this->request->query('auth') ? $this->request->query('auth') : $this->request->data('auth'); + $mToken = $this->request->query('token') ? $this->request->query('token') : $this->request->data('token'); if ( $mUser and $mPassword ) { - $user = userLogin($mUser, $mPassword); + $user = userLogin($mUser, $mPassword, true); if ( !$user ) { throw new UnauthorizedException(__('User not found or incorrect password')); return; } - } else if ( $mAuth ) { - $user = getAuthUser($mAuth); + } else if ( $mToken ) { + $ret = validateToken($mToken); + $user = $ret[0]; + $retstatus = $ret[1]; if ( !$user ) { - throw new UnauthorizedException(__('Invalid Auth Key')); + throw new UnauthorizedException(__($retstatus)); return; + } else if ( $mAuth ) { + $user = getAuthUser($mAuth); + if ( !$user ) { + throw new UnauthorizedException(__('Invalid Auth Key')); + return; + } } } // We need to reject methods that are not authenticated From d36c1f5d3ce9b55713d0600354d81296c988e5af Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Tue, 7 May 2019 15:04:12 -0400 Subject: [PATCH 31/90] Add JWT token creation, move old code to a different function for future deprecation, simplified code for ZM_XX parameter reading --- web/api/app/Controller/HostController.php | 97 +++++++++++++++-------- 1 file changed, 63 insertions(+), 34 deletions(-) diff --git a/web/api/app/Controller/HostController.php b/web/api/app/Controller/HostController.php index 05b2ed3fa..d2a9cb203 100644 --- a/web/api/app/Controller/HostController.php +++ b/web/api/app/Controller/HostController.php @@ -31,17 +31,22 @@ class HostController extends AppController { } function login() { - $cred = $this->_getCredentials(); + $cred_depr = $this->_getCredentialsDeprecated(); $ver = $this->_getVersion(); $this->set(array( - 'credentials' => $cred[0], - 'append_password'=>$cred[1], + 'token'=>$cred[0], + 'token_expires'=>$cred[1] * 3600, // takes AUTH_HASH_TTL || 2 hrs as the default + 'credentials'=>$cred_depr[0], + 'append_password'=>$cred_depr[1], 'version' => $ver[0], 'apiversion' => $ver[1], - '_serialize' => array('credentials', - 'append_password', + '_serialize' => array( + 'token', + 'token_expires', 'version', + 'credentials', + 'append_password', 'apiversion' ))); } // end function login() @@ -56,41 +61,65 @@ class HostController extends AppController { )); } // end function logout() - - private function _getCredentials() { + + private function _getCredentialsDeprecated() { $credentials = ''; $appendPassword = 0; - $this->loadModel('Config'); - $isZmAuth = $this->Config->find('first',array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_OPT_USE_AUTH')))['Config']['Value']; - - if ( $isZmAuth ) { - // In future, we may want to completely move to AUTH_HASH_LOGINS and return &auth= for all cases - require_once __DIR__ .'/../../../includes/auth.php'; # in the event we directly call getCredentials.json - - $zmAuthRelay = $this->Config->find('first',array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_AUTH_RELAY')))['Config']['Value']; - if ( $zmAuthRelay == 'hashed' ) { - $zmAuthHashIps = $this->Config->find('first',array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_AUTH_HASH_IPS')))['Config']['Value']; - // make sure auth is regenerated each time we call this API - $credentials = 'auth='.generateAuthHash($zmAuthHashIps,true); - } else { - // user will need to append the store password here + if (ZM_OPT_USE_AUTH) { + require_once __DIR__ .'/../../../includes/auth.php'; + if (ZM_AUTH_RELAY=='hashed') { + $credentials = 'auth='.generateAuthHash(ZM_AUTH_HASH_IPS,true); + } + else { $credentials = 'user='.$this->Session->read('Username').'&pass='; $appendPassword = 1; } + return array($credentials, $appendPassword); } - return array($credentials, $appendPassword); - } // end function _getCredentials - - function getCredentials() { - // ignore debug warnings from other functions - $this->view='Json'; - $val = $this->_getCredentials(); - $this->set(array( - 'credentials'=> $val[0], - 'append_password'=>$val[1], - '_serialize' => array('credentials', 'append_password') - ) ); } + + private function _getCredentials() { + $credentials = ''; + $this->loadModel('Config'); + + $isZmAuth = ZM_OPT_USE_AUTH; + $jwt = ''; + $ttl = ''; + + if ( $isZmAuth ) { + require_once __DIR__ .'/../../../includes/auth.php'; + require_once __DIR__.'/../../../vendor/autoload.php'; + $zmAuthRelay = ZM_AUTH_RELAY; + $zmAuthHashIps = NULL; + if ( $zmAuthRelay == 'hashed' ) { + $zmAuthHashIps = ZM_AUTH_HASH_IPS; + } + + $key = ZM_AUTH_HASH_SECRET; + if ($zmAuthHashIps) { + $key = $key . $_SERVER['REMOTE_ADDR']; + } + $issuedAt = time(); + $ttl = ZM_AUTH_HASH_TTL || 2; + + // print ("relay=".$zmAuthRelay." haship=".$zmAuthHashIps." remote ip=".$_SERVER['REMOTE_ADDR']); + + $expireAt = $issuedAt + $ttl * 3600; + $expireAt = $issuedAt + 30; // TEST REMOVE + + $token = array( + "iss" => "ZoneMinder", + "iat" => $issuedAt, + "exp" => $expireAt, + "user" => $_SESSION['username'] + ); + + //use \Firebase\JWT\JWT; + $jwt = \Firebase\JWT\JWT::encode($token, $key, 'HS256'); + + } + return array($jwt, $ttl); + } // end function _getCredentials // If $mid is set, only return disk usage for that monitor // Else, return an array of total disk usage, and per-monitor @@ -169,7 +198,7 @@ class HostController extends AppController { private function _getVersion() { $version = Configure::read('ZM_VERSION'); - $apiversion = '1.0'; + $apiversion = '2.0'; return array($version, $apiversion); } From e8f79f32549aef1cd4a1c4fbcb81ad008df67944 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Tue, 7 May 2019 15:04:51 -0400 Subject: [PATCH 32/90] JWT integration, validate JWT token via validateToken --- web/includes/auth.php | 83 +++++++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 27 deletions(-) diff --git a/web/includes/auth.php b/web/includes/auth.php index b6340bcfc..07e4be65f 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -46,14 +46,12 @@ function migrateHash($user, $pass) { } + // core function used to login a user to PHP. Is also used for cake sessions for the API -function userLogin($username='', $password='', $passwordHashed=false) { +function userLogin($username='', $password='', $passwordHashed=false, $apiLogin = false) { global $user; - - - if ( !$username and isset($_REQUEST['username']) ) $username = $_REQUEST['username']; if ( !$password and isset($_REQUEST['password']) ) @@ -61,7 +59,9 @@ function userLogin($username='', $password='', $passwordHashed=false) { // if true, a popup will display after login // lets validate reCaptcha if it exists - if ( defined('ZM_OPT_USE_GOOG_RECAPTCHA') + // this only applies if it userLogin was not called from API layer + if (!$apiLogin + && defined('ZM_OPT_USE_GOOG_RECAPTCHA') && defined('ZM_OPT_GOOG_RECAPTCHA_SECRETKEY') && defined('ZM_OPT_GOOG_RECAPTCHA_SITEKEY') && ZM_OPT_USE_GOOG_RECAPTCHA @@ -76,7 +76,7 @@ function userLogin($username='', $password='', $passwordHashed=false) { ); $res = do_post_request($url, http_build_query($fields)); $responseData = json_decode($res,true); - // PP - 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 ( isset($responseData['success']) && $responseData['success'] == false ) { // PP - before we deny auth, let's make sure the error was not 'invalid secret' @@ -140,7 +140,7 @@ function userLogin($username='', $password='', $passwordHashed=false) { ZM\Error ("Could not retrieve user $username details"); $_SESSION['loginFailed'] = true; unset($user); - return; + return false; } $close_session = 0; @@ -184,6 +184,53 @@ function userLogout() { zm_session_clear(); } + +function validateToken ($token) { + global $user; + $key = ZM_AUTH_HASH_SECRET; + if (ZM_AUTH_HASH_IPS) $key .= $_SERVER['REMOTE_ADDR']; + try { + $decoded_token = JWT::decode($token, $key, array('HS256')); + } catch (Exception $e) { + ZM\Error("Unable to authenticate user. error decoding JWT token:".$e->getMessage()); + + return array(false, $e->getMessage()); + } + + // convert from stdclass to array + $jwt_payload = json_decode(json_encode($decoded_token), true); + $username = $jwt_payload['user']; + $sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username = ?'; + $sql_values = array($username); + + $saved_user_details = dbFetchOne ($sql, NULL, $sql_values); + + if ($saved_user_details) { + $user = $saved_user_details; + return array($user, "OK"); + } else { + ZM\Error ("Could not retrieve user $username details"); + $_SESSION['loginFailed'] = true; + unset($user); + return array(false, "No such user/credentials"); + } + + + // We are NOT checking against session username for now... + /* + // at this stage, token is valid, but lets validate user with session user + ZM\Info ("JWT user is ".$jwt['user']); + if ($jwt['user'] != $_SESSION['username']) { + ZM\Error ("Unable to authenticate user. Token doesn't belong to current user"); + return false; + } else { + ZM\Info ("Token validated for user:".$_SESSION['username']); + return $user; + } + */ + +} + function getAuthUser($auth) { if ( ZM_OPT_USE_AUTH && ZM_AUTH_RELAY == 'hashed' && !empty($auth) ) { $remoteAddr = ''; @@ -222,31 +269,13 @@ function getAuthUser($auth) { return false; } // end getAuthUser($auth) + + function generateAuthHash($useRemoteAddr, $force=false) { if ( ZM_OPT_USE_AUTH and ZM_AUTH_RELAY == 'hashed' and isset($_SESSION['username']) and $_SESSION['passwordHash'] ) { $time = time(); - $key = ZM_AUTH_HASH_SECRET; - $issuedAt = time(); - $expireAt = $issuedAt + ZM_AUTH_HASH_TTL * 3600; - $token = array( - "iss" => "ZoneMinder", - "iat" => $issuedAt, - "exp" => $expireAt, - "user" => $_SESSION['username'] - - ); - - if ($useRemoteAddr) { - $token['remote_addr'] = $_SESSION['remoteAddr']; - } - - - $jwt = JWT::encode($token, $key); - - ZM\Info ("JWT token is $jwt"); - $mintime = $time - ( ZM_AUTH_HASH_TTL * 1800 ); if ( $force or ( !isset($_SESSION['AuthHash'.$_SESSION['remoteAddr']]) ) or ( $_SESSION['AuthHashGeneratedAt'] < $mintime ) ) { From b293592e4c1655b9a5f1a79bd201a9ea33a75468 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 8 May 2019 10:55:32 -0400 Subject: [PATCH 33/90] added token validation to zms/zmu/zmuser --- src/zm_user.cpp | 64 +++++++++++++++++++++++ src/zm_user.h | 1 + src/zms.cpp | 13 ++++- src/zmu.cpp | 12 ++++- web/api/app/Controller/HostController.php | 2 +- web/includes/auth.php | 14 ----- 6 files changed, 87 insertions(+), 19 deletions(-) diff --git a/src/zm_user.cpp b/src/zm_user.cpp index 8766a2e8f..c0f622b15 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -139,6 +139,70 @@ User *zmLoadUser( const char *username, const char *password ) { } +User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { + std::string key = config.auth_hash_secret; + std::string remote_addr = ""; + if (use_remote_addr) { + remote_addr = std::string(getenv( "REMOTE_ADDR" )); + if ( remote_addr == "" ) { + Warning( "Can't determine remote address, using null" ); + remote_addr = ""; + } + key += remote_addr; + } + + + Info ("Inside zmLoadTokenUser, formed key=%s", key.c_str()); + + auto decoded = jwt::decode(jwt_token_str); + + auto verifier = jwt::verify() + .allow_algorithm(jwt::algorithm::hs256{ key }) + .with_issuer("ZoneMinder"); + try { + verifier.verify(decoded); + } + catch (const Exception &e) { + Error( "Unable to verify token: %s", e.getMessage().c_str() ); + return 0; + } + // token is valid and not expired + if (decoded.has_payload_claim("user")) { + + // We only need to check if user is enabled in DB and pass on + // correct access permissions + std::string username = decoded.get_payload_claim("user").as_string(); + Info ("Got %s as user claim from token", username.c_str()); + char sql[ZM_SQL_MED_BUFSIZ] = ""; + snprintf(sql, sizeof(sql), + "SELECT Id, Username, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds" + " FROM Users where Username = '%s' and Enabled = 1", username.c_str() ); + + MYSQL_RES *result = mysql_store_result(&dbconn); + if ( !result ) { + Error("Can't use query result: %s", mysql_error(&dbconn)); + exit(mysql_errno(&dbconn)); + } + int n_users = mysql_num_rows(result); + + if ( n_users != 1 ) { + mysql_free_result(result); + Warning("Unable to authenticate user %s", username); + return NULL; + } + + MYSQL_ROW dbrow = mysql_fetch_row(result); + User *user = new User(dbrow); + Info ("Authenticated user '%s' via token", username.c_str()); + return user; + + } + else { + Error ("User not found in claim"); + return 0; + } +} + // Function to validate an authentication string User *zmLoadAuthUser( const char *auth, bool use_remote_addr ) { #if HAVE_DECL_MD5 || HAVE_DECL_GNUTLS_FINGERPRINT diff --git a/src/zm_user.h b/src/zm_user.h index 00c61185b..04842b318 100644 --- a/src/zm_user.h +++ b/src/zm_user.h @@ -77,6 +77,7 @@ public: User *zmLoadUser( const char *username, const char *password=0 ); User *zmLoadAuthUser( const char *auth, bool use_remote_addr ); +User *zmLoadTokenUser( std::string jwt, bool use_remote_addr); bool checkUser ( const char *username); bool checkPass (const char *password); diff --git a/src/zms.cpp b/src/zms.cpp index 0a3712938..0d4a22f45 100644 --- a/src/zms.cpp +++ b/src/zms.cpp @@ -70,6 +70,7 @@ int main( int argc, const char *argv[] ) { std::string username; std::string password; char auth[64] = ""; + std::string jwt_token_str = ""; unsigned int connkey = 0; unsigned int playback_buffer = 0; @@ -158,6 +159,10 @@ int main( int argc, const char *argv[] ) { playback_buffer = atoi(value); } else if ( !strcmp( name, "auth" ) ) { strncpy( auth, value, sizeof(auth)-1 ); + } else if ( !strcmp( name, "token" ) ) { + jwt_token_str = value; + Info("ZMS: JWT token found: %s", jwt_token_str.c_str()); + } else if ( !strcmp( name, "user" ) ) { username = UriDecode( value ); } else if ( !strcmp( name, "pass" ) ) { @@ -181,11 +186,15 @@ int main( int argc, const char *argv[] ) { if ( config.opt_use_auth ) { User *user = 0; - if ( strcmp(config.auth_relay, "none") == 0 ) { + if (jwt_token_str != "") { + user = zmLoadTokenUser(jwt_token_str, config.auth_hash_ips); + + } + else if ( strcmp(config.auth_relay, "none") == 0 ) { if ( checkUser(username.c_str()) ) { user = zmLoadUser(username.c_str()); } else { - Error("") + Error("Bad username"); } } else { diff --git a/src/zmu.cpp b/src/zmu.cpp index 2ad1471d5..4750e40e0 100644 --- a/src/zmu.cpp +++ b/src/zmu.cpp @@ -138,6 +138,7 @@ void Usage(int status=-1) { " -U, --username : When running in authenticated mode the username and\n" " -P, --password : password combination of the given user\n" " -A, --auth : Pass authentication hash string instead of user details\n" + " -T, --token : Pass JWT token string instead of user details\n" "", stderr ); exit(status); @@ -263,6 +264,7 @@ int main(int argc, char *argv[]) { char *username = 0; char *password = 0; char *auth = 0; + std::string jwt_token_str = ""; #if ZM_HAS_V4L #if ZM_HAS_V4L2 int v4lVersion = 2; @@ -378,6 +380,9 @@ int main(int argc, char *argv[]) { case 'A': auth = optarg; break; + case 'T': + jwt_token_str = std::string(optarg); + break; #if ZM_HAS_V4L case 'V': v4lVersion = (atoi(optarg)==1)?1:2; @@ -438,10 +443,13 @@ int main(int argc, char *argv[]) { user = zmLoadUser(username); } else { - if ( !(username && password) && !auth ) { - Error("Username and password or auth string must be supplied"); + if ( !(username && password) && !auth && (jwt_token_str=="")) { + Error("Username and password or auth/token string must be supplied"); exit_zmu(-1); } + if (jwt_token_str != "") { + user = zmLoadTokenUser(jwt_token_str, false); + } if ( auth ) { user = zmLoadAuthUser(auth, false); } diff --git a/web/api/app/Controller/HostController.php b/web/api/app/Controller/HostController.php index d2a9cb203..0a24b5e85 100644 --- a/web/api/app/Controller/HostController.php +++ b/web/api/app/Controller/HostController.php @@ -105,7 +105,7 @@ class HostController extends AppController { // print ("relay=".$zmAuthRelay." haship=".$zmAuthHashIps." remote ip=".$_SERVER['REMOTE_ADDR']); $expireAt = $issuedAt + $ttl * 3600; - $expireAt = $issuedAt + 30; // TEST REMOVE + $expireAt = $issuedAt + 60; // TEST REMOVE $token = array( "iss" => "ZoneMinder", diff --git a/web/includes/auth.php b/web/includes/auth.php index 07e4be65f..1e6f9c3a7 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -215,20 +215,6 @@ function validateToken ($token) { return array(false, "No such user/credentials"); } - - // We are NOT checking against session username for now... - /* - // at this stage, token is valid, but lets validate user with session user - ZM\Info ("JWT user is ".$jwt['user']); - if ($jwt['user'] != $_SESSION['username']) { - ZM\Error ("Unable to authenticate user. Token doesn't belong to current user"); - return false; - } else { - ZM\Info ("Token validated for user:".$_SESSION['username']); - return $user; - } - */ - } function getAuthUser($auth) { From bb18c305abe2375a700ea4bcbb2d54ed42baf7ba Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 8 May 2019 11:08:27 -0400 Subject: [PATCH 34/90] add token to command line for zmu --- src/zmu.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/zmu.cpp b/src/zmu.cpp index 4750e40e0..07f9ae8aa 100644 --- a/src/zmu.cpp +++ b/src/zmu.cpp @@ -243,6 +243,7 @@ int main(int argc, char *argv[]) { {"username", 1, 0, 'U'}, {"password", 1, 0, 'P'}, {"auth", 1, 0, 'A'}, + {"token", 1, 0, 'T'}, {"version", 1, 0, 'V'}, {"help", 0, 0, 'h'}, {"list", 0, 0, 'l'}, @@ -275,7 +276,7 @@ int main(int argc, char *argv[]) { while (1) { int option_index = 0; - int c = getopt_long(argc, argv, "d:m:vsEDLurwei::S:t::fz::ancqhlB::C::H::O::U:P:A:V:", long_options, &option_index); + int c = getopt_long(argc, argv, "d:m:vsEDLurwei::S:t::fz::ancqhlB::C::H::O::U:P:A:V:T:", long_options, &option_index); if ( c == -1 ) { break; } From 3a67217972d73d2ddcfe5a222604cdbd04b084ff Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 8 May 2019 11:29:34 -0400 Subject: [PATCH 35/90] move decode inside try/catch --- src/zm_user.cpp | 86 +++++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/src/zm_user.cpp b/src/zm_user.cpp index c0f622b15..907ca6536 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -150,57 +150,59 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { } key += remote_addr; } - Info ("Inside zmLoadTokenUser, formed key=%s", key.c_str()); - auto decoded = jwt::decode(jwt_token_str); - - auto verifier = jwt::verify() - .allow_algorithm(jwt::algorithm::hs256{ key }) - .with_issuer("ZoneMinder"); try { + + auto decoded = jwt::decode(jwt_token_str); + auto verifier = jwt::verify() + .allow_algorithm(jwt::algorithm::hs256{ key }) + .with_issuer("ZoneMinder"); + verifier.verify(decoded); - } + + // token is valid and not expired + if (decoded.has_payload_claim("user")) { + + // We only need to check if user is enabled in DB and pass on + // correct access permissions + std::string username = decoded.get_payload_claim("user").as_string(); + Info ("Got %s as user claim from token", username.c_str()); + char sql[ZM_SQL_MED_BUFSIZ] = ""; + snprintf(sql, sizeof(sql), + "SELECT Id, Username, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds" + " FROM Users where Username = '%s' and Enabled = 1", username.c_str() ); + + MYSQL_RES *result = mysql_store_result(&dbconn); + if ( !result ) { + Error("Can't use query result: %s", mysql_error(&dbconn)); + exit(mysql_errno(&dbconn)); + } + int n_users = mysql_num_rows(result); + + if ( n_users != 1 ) { + mysql_free_result(result); + Warning("Unable to authenticate user %s", username); + return NULL; + } + + MYSQL_ROW dbrow = mysql_fetch_row(result); + User *user = new User(dbrow); + Info ("Authenticated user '%s' via token", username.c_str()); + return user; + + } + else { + Error ("User not found in claim"); + return 0; + } + + } catch (const Exception &e) { Error( "Unable to verify token: %s", e.getMessage().c_str() ); return 0; } - // token is valid and not expired - if (decoded.has_payload_claim("user")) { - - // We only need to check if user is enabled in DB and pass on - // correct access permissions - std::string username = decoded.get_payload_claim("user").as_string(); - Info ("Got %s as user claim from token", username.c_str()); - char sql[ZM_SQL_MED_BUFSIZ] = ""; - snprintf(sql, sizeof(sql), - "SELECT Id, Username, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds" - " FROM Users where Username = '%s' and Enabled = 1", username.c_str() ); - - MYSQL_RES *result = mysql_store_result(&dbconn); - if ( !result ) { - Error("Can't use query result: %s", mysql_error(&dbconn)); - exit(mysql_errno(&dbconn)); - } - int n_users = mysql_num_rows(result); - - if ( n_users != 1 ) { - mysql_free_result(result); - Warning("Unable to authenticate user %s", username); - return NULL; - } - - MYSQL_ROW dbrow = mysql_fetch_row(result); - User *user = new User(dbrow); - Info ("Authenticated user '%s' via token", username.c_str()); - return user; - - } - else { - Error ("User not found in claim"); - return 0; - } } // Function to validate an authentication string From 04c3bebef92f96f7d58629e037a3f90c5134280e Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 8 May 2019 11:44:15 -0400 Subject: [PATCH 36/90] exception handling for try/catch --- src/zm_user.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/zm_user.cpp b/src/zm_user.cpp index 907ca6536..e79332cb3 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -199,10 +199,15 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { } } - catch (const Exception &e) { - Error( "Unable to verify token: %s", e.getMessage().c_str() ); + catch (const std::exception &e) { + Error("Unable to verify token: %s", e.what()); return 0; } + catch (...) { + Error ("unknown exception"); + + } + return 0; } // Function to validate an authentication string From 3c6d0131ffd232d5349a3395387690c40e8dd03a Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 8 May 2019 12:06:37 -0400 Subject: [PATCH 37/90] fix db read, forgot to exec query --- src/zm_user.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/zm_user.cpp b/src/zm_user.cpp index e79332cb3..0c8201fd2 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -172,7 +172,12 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { char sql[ZM_SQL_MED_BUFSIZ] = ""; snprintf(sql, sizeof(sql), "SELECT Id, Username, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds" - " FROM Users where Username = '%s' and Enabled = 1", username.c_str() ); + " FROM Users WHERE Username = '%s' and Enabled = 1", username.c_str() ); + + if ( mysql_query(&dbconn, sql) ) { + Error("Can't run query: %s", mysql_error(&dbconn)); + exit(mysql_errno(&dbconn)); + } MYSQL_RES *result = mysql_store_result(&dbconn); if ( !result ) { From 27e6e46f849974c363145285851732f049a8f8f9 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 8 May 2019 12:11:32 -0400 Subject: [PATCH 38/90] remove allowing auth_hash_ip for token --- src/zms.cpp | 3 ++- web/api/app/Controller/HostController.php | 21 ++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/zms.cpp b/src/zms.cpp index 0d4a22f45..8442b6a65 100644 --- a/src/zms.cpp +++ b/src/zms.cpp @@ -187,7 +187,8 @@ int main( int argc, const char *argv[] ) { User *user = 0; if (jwt_token_str != "") { - user = zmLoadTokenUser(jwt_token_str, config.auth_hash_ips); + //user = zmLoadTokenUser(jwt_token_str, config.auth_hash_ips); + user = zmLoadTokenUser(jwt_token_str, false); } else if ( strcmp(config.auth_relay, "none") == 0 ) { diff --git a/web/api/app/Controller/HostController.php b/web/api/app/Controller/HostController.php index 0a24b5e85..c867be605 100644 --- a/web/api/app/Controller/HostController.php +++ b/web/api/app/Controller/HostController.php @@ -82,23 +82,26 @@ class HostController extends AppController { $credentials = ''; $this->loadModel('Config'); - $isZmAuth = ZM_OPT_USE_AUTH; $jwt = ''; $ttl = ''; - if ( $isZmAuth ) { + if ( ZM_OPT_USE_AUTH ) { require_once __DIR__ .'/../../../includes/auth.php'; require_once __DIR__.'/../../../vendor/autoload.php'; - $zmAuthRelay = ZM_AUTH_RELAY; - $zmAuthHashIps = NULL; - if ( $zmAuthRelay == 'hashed' ) { - $zmAuthHashIps = ZM_AUTH_HASH_IPS; - } + $key = ZM_AUTH_HASH_SECRET; - if ($zmAuthHashIps) { + + /* we won't support AUTH_HASH_IPS in token mode + reasons: + a) counter-intuitive for mobile consumers + b) zmu will never be able to to validate via a token if we sign + it after appending REMOTE_ADDR + + if (ZM_AUTH_HASH_IPS) { $key = $key . $_SERVER['REMOTE_ADDR']; - } + }*/ + $issuedAt = time(); $ttl = ZM_AUTH_HASH_TTL || 2; From bc050fe330026c9b66c88babd0f3417c850311cb Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 8 May 2019 13:38:42 -0400 Subject: [PATCH 39/90] support refresh tokens as well for increased security --- src/zm_user.cpp | 14 ++++++ web/api/app/Controller/AppController.php | 17 +++++-- web/api/app/Controller/HostController.php | 56 ++++++++++++++--------- web/includes/auth.php | 13 +++++- 4 files changed, 75 insertions(+), 25 deletions(-) diff --git a/src/zm_user.cpp b/src/zm_user.cpp index 0c8201fd2..eac3a4b0f 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -163,6 +163,19 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { verifier.verify(decoded); // token is valid and not expired + + if (decoded.has_payload_claim("type")) { + std::string type = decoded.get_payload_claim("type").as_string(); + if (type != "access") { + Error ("Only access tokens are allowed. Please do not use refresh tokens"); + return 0; + } + } + else { + // something is wrong. All ZM tokens have type + Error ("Missing token type. This should not happen"); + return 0; + } if (decoded.has_payload_claim("user")) { // We only need to check if user is enabled in DB and pass on @@ -195,6 +208,7 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { MYSQL_ROW dbrow = mysql_fetch_row(result); User *user = new User(dbrow); Info ("Authenticated user '%s' via token", username.c_str()); + mysql_free_result(result); return user; } diff --git a/web/api/app/Controller/AppController.php b/web/api/app/Controller/AppController.php index 840a14f58..23bd528b5 100644 --- a/web/api/app/Controller/AppController.php +++ b/web/api/app/Controller/AppController.php @@ -82,19 +82,30 @@ class AppController extends Controller { return; } } else if ( $mToken ) { - $ret = validateToken($mToken); + // if you pass a token to login, we should only allow + // refresh tokens to regenerate new access and refresh tokens + if ( !strcasecmp($this->params->action, 'login') ) { + $only_allow_token_type='refresh'; + } else { + // for any other methods, don't allow refresh tokens + // they are supposed to be infrequently used for security + // purposes + $only_allow_token_type='access'; + + } + $ret = validateToken($mToken, $only_allow_token_type); $user = $ret[0]; $retstatus = $ret[1]; if ( !$user ) { throw new UnauthorizedException(__($retstatus)); return; - } else if ( $mAuth ) { + } + } else if ( $mAuth ) { $user = getAuthUser($mAuth); if ( !$user ) { throw new UnauthorizedException(__('Invalid Auth Key')); return; } - } } // We need to reject methods that are not authenticated // besides login and logout diff --git a/web/api/app/Controller/HostController.php b/web/api/app/Controller/HostController.php index c867be605..747403dc3 100644 --- a/web/api/app/Controller/HostController.php +++ b/web/api/app/Controller/HostController.php @@ -35,15 +35,19 @@ class HostController extends AppController { $cred_depr = $this->_getCredentialsDeprecated(); $ver = $this->_getVersion(); $this->set(array( - 'token'=>$cred[0], - 'token_expires'=>$cred[1] * 3600, // takes AUTH_HASH_TTL || 2 hrs as the default + 'access_token'=>$cred[0], + 'access_token_expires'=>$cred[1], + 'refresh_token'=>$cred[2], + 'refresh_token_expires'=>$cred[3], 'credentials'=>$cred_depr[0], 'append_password'=>$cred_depr[1], 'version' => $ver[0], 'apiversion' => $ver[1], '_serialize' => array( - 'token', - 'token_expires', + 'access_token', + 'access_token_expires', + 'refresh_token', + 'refresh_token_expires', 'version', 'credentials', 'append_password', @@ -82,9 +86,6 @@ class HostController extends AppController { $credentials = ''; $this->loadModel('Config'); - $jwt = ''; - $ttl = ''; - if ( ZM_OPT_USE_AUTH ) { require_once __DIR__ .'/../../../includes/auth.php'; require_once __DIR__.'/../../../vendor/autoload.php'; @@ -102,27 +103,40 @@ class HostController extends AppController { $key = $key . $_SERVER['REMOTE_ADDR']; }*/ - $issuedAt = time(); - $ttl = ZM_AUTH_HASH_TTL || 2; + $access_issued_at = time(); + $access_ttl = (ZM_AUTH_HASH_TTL || 2) * 3600; - // print ("relay=".$zmAuthRelay." haship=".$zmAuthHashIps." remote ip=".$_SERVER['REMOTE_ADDR']); - - $expireAt = $issuedAt + $ttl * 3600; - $expireAt = $issuedAt + 60; // TEST REMOVE + // by default access token will expire in 2 hrs + // you can change it by changing the value of ZM_AUTH_HASH_TLL + $access_expire_at = $access_issued_at + $access_ttl; + $access_expire_at = $access_issued_at + 60; // TEST, REMOVE - $token = array( + $access_token = array( "iss" => "ZoneMinder", - "iat" => $issuedAt, - "exp" => $expireAt, - "user" => $_SESSION['username'] + "iat" => $access_issued_at, + "exp" => $access_expire_at, + "user" => $_SESSION['username'], + "type" => "access" ); - //use \Firebase\JWT\JWT; - $jwt = \Firebase\JWT\JWT::encode($token, $key, 'HS256'); + $jwt_access_token = \Firebase\JWT\JWT::encode($access_token, $key, 'HS256'); + + $refresh_issued_at = time(); + $refresh_ttl = 24 * 3600; // 1 day + + $refresh_expire_at = $refresh_issued_at + $refresh_ttl; + $refresh_token = array( + "iss" => "ZoneMinder", + "iat" => $refresh_issued_at, + "exp" => $refresh_expire_at, + "user" => $_SESSION['username'], + "type" => "refresh" + ); + $jwt_refresh_token = \Firebase\JWT\JWT::encode($refresh_token, $key, 'HS256'); } - return array($jwt, $ttl); - } // end function _getCredentials + return array($jwt_access_token, $access_ttl, $jwt_refresh_token, $refresh_ttl); + } // If $mid is set, only return disk usage for that monitor // Else, return an array of total disk usage, and per-monitor diff --git a/web/includes/auth.php b/web/includes/auth.php index 1e6f9c3a7..03e0fe0c6 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -185,7 +185,8 @@ function userLogout() { } -function validateToken ($token) { +function validateToken ($token, $allowed_token_type='access') { + global $user; $key = ZM_AUTH_HASH_SECRET; if (ZM_AUTH_HASH_IPS) $key .= $_SERVER['REMOTE_ADDR']; @@ -199,6 +200,16 @@ function validateToken ($token) { // convert from stdclass to array $jwt_payload = json_decode(json_encode($decoded_token), true); + + $type = $jwt_payload['type']; + if ($type != $allowed_token_type) { + if ($allowed_token_type == 'access') { + // give a hint that the user is not doing it right + ZM\Error ('Please do not use refresh tokens for this operation'); + } + return array (false, "Incorrect token type"); + } + $username = $jwt_payload['user']; $sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username = ?'; $sql_values = array($username); From f9730bb46b1a5ee3a31d849b3c057335aafbe58f Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 8 May 2019 14:07:48 -0400 Subject: [PATCH 40/90] remove auth_hash_ip --- web/includes/auth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/includes/auth.php b/web/includes/auth.php index 03e0fe0c6..c446d0024 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -189,7 +189,7 @@ function validateToken ($token, $allowed_token_type='access') { global $user; $key = ZM_AUTH_HASH_SECRET; - if (ZM_AUTH_HASH_IPS) $key .= $_SERVER['REMOTE_ADDR']; + //if (ZM_AUTH_HASH_IPS) $key .= $_SERVER['REMOTE_ADDR']; try { $decoded_token = JWT::decode($token, $key, array('HS256')); } catch (Exception $e) { From 0bc96dfe83b0ef0f51d97d0b44236a11caf8a2ec Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 8 May 2019 14:26:16 -0400 Subject: [PATCH 41/90] Error out if used did not create an AUTH_HASH_SECRET --- web/api/app/Controller/HostController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/api/app/Controller/HostController.php b/web/api/app/Controller/HostController.php index 747403dc3..91e0093a1 100644 --- a/web/api/app/Controller/HostController.php +++ b/web/api/app/Controller/HostController.php @@ -90,8 +90,10 @@ class HostController extends AppController { require_once __DIR__ .'/../../../includes/auth.php'; require_once __DIR__.'/../../../vendor/autoload.php'; - $key = ZM_AUTH_HASH_SECRET; + if (!$key) { + throw new ForbiddenException(__('Please create a valid AUTH_HASH_SECRET in ZoneMinder')); + } /* we won't support AUTH_HASH_IPS in token mode reasons: From c41a2d067cb641f217efb2e22ae225998eddf8ba Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 8 May 2019 14:29:44 -0400 Subject: [PATCH 42/90] fixed type conversion --- src/zm_user.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zm_user.cpp b/src/zm_user.cpp index eac3a4b0f..06ec3f76a 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -201,7 +201,7 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { if ( n_users != 1 ) { mysql_free_result(result); - Warning("Unable to authenticate user %s", username); + Warning("Unable to authenticate user %s", username.c_str()); return NULL; } From 1770ebea23594d95ad31abfd9396466c6ae7fc25 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 8 May 2019 15:26:51 -0400 Subject: [PATCH 43/90] make sure refresh token login doesn't generate another refresh token --- web/api/app/Controller/HostController.php | 100 +++++++++++++++------- 1 file changed, 67 insertions(+), 33 deletions(-) diff --git a/web/api/app/Controller/HostController.php b/web/api/app/Controller/HostController.php index 91e0093a1..55fedd281 100644 --- a/web/api/app/Controller/HostController.php +++ b/web/api/app/Controller/HostController.php @@ -31,28 +31,57 @@ class HostController extends AppController { } function login() { - $cred = $this->_getCredentials(); $cred_depr = $this->_getCredentialsDeprecated(); $ver = $this->_getVersion(); - $this->set(array( - 'access_token'=>$cred[0], - 'access_token_expires'=>$cred[1], - 'refresh_token'=>$cred[2], - 'refresh_token_expires'=>$cred[3], - 'credentials'=>$cred_depr[0], - 'append_password'=>$cred_depr[1], - 'version' => $ver[0], - 'apiversion' => $ver[1], - '_serialize' => array( - 'access_token', - 'access_token_expires', - 'refresh_token', - 'refresh_token_expires', - 'version', - 'credentials', - 'append_password', - 'apiversion' - ))); + + $mUser = $this->request->query('user') ? $this->request->query('user') : $this->request->data('user'); + $mPassword = $this->request->query('pass') ? $this->request->query('pass') : $this->request->data('pass'); + $mToken = $this->request->query('token') ? $this->request->query('token') : $this->request->data('token'); + + if ($mUser && $mPassword) { + $cred = $this->_getCredentials(true); + // if you authenticated via user/pass then generate new refresh + $this->set(array( + 'access_token'=>$cred[0], + 'access_token_expires'=>$cred[1], + 'refresh_token'=>$cred[2], + 'refresh_token_expires'=>$cred[3], + 'credentials'=>$cred_depr[0], + 'append_password'=>$cred_depr[1], + 'version' => $ver[0], + 'apiversion' => $ver[1], + '_serialize' => array( + 'access_token', + 'access_token_expires', + 'refresh_token', + 'refresh_token_expires', + 'version', + 'credentials', + 'append_password', + 'apiversion' + ))); + } + else { + $cred = $this->_getCredentials(false); + $this->set(array( + 'access_token'=>$cred[0], + 'access_token_expires'=>$cred[1], + 'credentials'=>$cred_depr[0], + 'append_password'=>$cred_depr[1], + 'version' => $ver[0], + 'apiversion' => $ver[1], + '_serialize' => array( + 'access_token', + 'access_token_expires', + 'version', + 'credentials', + 'append_password', + 'apiversion' + ))); + + } + + } // end function login() // clears out session @@ -82,7 +111,7 @@ class HostController extends AppController { } } - private function _getCredentials() { + private function _getCredentials($generate_refresh_token=false) { $credentials = ''; $this->loadModel('Config'); @@ -123,19 +152,24 @@ class HostController extends AppController { $jwt_access_token = \Firebase\JWT\JWT::encode($access_token, $key, 'HS256'); - $refresh_issued_at = time(); - $refresh_ttl = 24 * 3600; // 1 day - - $refresh_expire_at = $refresh_issued_at + $refresh_ttl; - $refresh_token = array( - "iss" => "ZoneMinder", - "iat" => $refresh_issued_at, - "exp" => $refresh_expire_at, - "user" => $_SESSION['username'], - "type" => "refresh" - ); - $jwt_refresh_token = \Firebase\JWT\JWT::encode($refresh_token, $key, 'HS256'); + $jwt_refresh_token = ""; + $refresh_ttl = 0; + if ($generate_refresh_token) { + $refresh_issued_at = time(); + $refresh_ttl = 24 * 3600; // 1 day + + $refresh_expire_at = $refresh_issued_at + $refresh_ttl; + $refresh_token = array( + "iss" => "ZoneMinder", + "iat" => $refresh_issued_at, + "exp" => $refresh_expire_at, + "user" => $_SESSION['username'], + "type" => "refresh" + ); + $jwt_refresh_token = \Firebase\JWT\JWT::encode($refresh_token, $key, 'HS256'); + } + } return array($jwt_access_token, $access_ttl, $jwt_refresh_token, $refresh_ttl); } From 2212244882ef6dca0acf51f0149fcaf6fd251857 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 8 May 2019 15:47:38 -0400 Subject: [PATCH 44/90] fix absolute path --- src/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index efdb5224d..3ea9d9647 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,7 +10,7 @@ set(ZM_BIN_SRC_FILES zm_box.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp zm_conf # A fix for cmake recompiling the source files for every target. add_library(zm STATIC ${ZM_BIN_SRC_FILES}) -link_directories(/home/pp/source/pp_ZoneMinder.git/third_party/bcrypt) +link_directories(../third_party/bcrypt) add_executable(zmc zmc.cpp) add_executable(zma zma.cpp) From 4ab0c35962926e7149eccdc6bc415b048d5ef223 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 8 May 2019 16:45:28 -0400 Subject: [PATCH 45/90] move JWT/Bcrypt inside zm_crypt --- src/zm_crypt.cpp | 56 ++++++++++++++++++++++------ src/zm_crypt.h | 5 +-- src/zm_user.cpp | 95 +++++++++++++++--------------------------------- 3 files changed, 76 insertions(+), 80 deletions(-) diff --git a/src/zm_crypt.cpp b/src/zm_crypt.cpp index 6c6f4c7c1..3a3f66aeb 100644 --- a/src/zm_crypt.cpp +++ b/src/zm_crypt.cpp @@ -1,25 +1,59 @@ #include "zm.h" # include "zm_crypt.h" +#include "BCrypt.hpp" +#include "jwt.h" #include +// returns username if valid, "" if not +std::string verifyToken(std::string jwt_token_str, std::string key) { + std::string username = ""; + try { + // is it decodable? + auto decoded = jwt::decode(jwt_token_str); + auto verifier = jwt::verify() + .allow_algorithm(jwt::algorithm::hs256{ key }) + .with_issuer("ZoneMinder"); + + // signature verified? + verifier.verify(decoded); + // make sure it has fields we need + if (decoded.has_payload_claim("type")) { + std::string type = decoded.get_payload_claim("type").as_string(); + if (type != "access") { + Error ("Only access tokens are allowed. Please do not use refresh tokens"); + return ""; + } + } + else { + // something is wrong. All ZM tokens have type + Error ("Missing token type. This should not happen"); + return ""; + } + if (decoded.has_payload_claim("user")) { + username = decoded.get_payload_claim("user").as_string(); + Info ("Got %s as user claim from token", username.c_str()); + } + else { + Error ("User not found in claim"); + return ""; + } + } // try + catch (const std::exception &e) { + Error("Unable to verify token: %s", e.what()); + return ""; + } + catch (...) { + Error ("unknown exception"); + return ""; - -std::string createToken() { - std::string token = jwt::create() - .set_issuer("auth0") - //.set_expires_at(jwt::date(expiresAt)) - //.set_issued_at(jwt::date(tp)) - //.set_issued_at(jwt::date(std::chrono::system_clock::now())) - //.set_expires_at(jwt::date(std::chrono::system_clock::now()+std::chrono::seconds{EXPIRY})) - .sign(jwt::algorithm::hs256{"secret"}); - return token; + } + return username; } bool verifyPassword(const char *username, const char *input_password, const char *db_password_hash) { bool password_correct = false; - Info ("JWT created as %s",createToken().c_str()); if (strlen(db_password_hash ) < 4) { // actually, shoud be more, but this is min. for next code Error ("DB Password is too short or invalid to check"); diff --git a/src/zm_crypt.h b/src/zm_crypt.h index a1e8945e4..8fb50cf00 100644 --- a/src/zm_crypt.h +++ b/src/zm_crypt.h @@ -23,10 +23,9 @@ #include #include -#include "BCrypt.hpp" -#include "jwt.h" + bool verifyPassword( const char *username, const char *input_password, const char *db_password_hash); -std::string createToken(); +std::string verifyToken(std::string token, std::string key); #endif // ZM_CRYPT_H \ No newline at end of file diff --git a/src/zm_user.cpp b/src/zm_user.cpp index 06ec3f76a..7ac934d2a 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -142,6 +142,7 @@ User *zmLoadUser( const char *username, const char *password ) { User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { std::string key = config.auth_hash_secret; std::string remote_addr = ""; + if (use_remote_addr) { remote_addr = std::string(getenv( "REMOTE_ADDR" )); if ( remote_addr == "" ) { @@ -153,82 +154,44 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { Info ("Inside zmLoadTokenUser, formed key=%s", key.c_str()); - try { + std::string username = verifyToken(jwt_token_str, key); + if (username != "") { + char sql[ZM_SQL_MED_BUFSIZ] = ""; + snprintf(sql, sizeof(sql), + "SELECT Id, Username, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds" + " FROM Users WHERE Username = '%s' and Enabled = 1", username.c_str() ); - auto decoded = jwt::decode(jwt_token_str); - auto verifier = jwt::verify() - .allow_algorithm(jwt::algorithm::hs256{ key }) - .with_issuer("ZoneMinder"); - - verifier.verify(decoded); - - // token is valid and not expired - - if (decoded.has_payload_claim("type")) { - std::string type = decoded.get_payload_claim("type").as_string(); - if (type != "access") { - Error ("Only access tokens are allowed. Please do not use refresh tokens"); - return 0; - } + if ( mysql_query(&dbconn, sql) ) { + Error("Can't run query: %s", mysql_error(&dbconn)); + exit(mysql_errno(&dbconn)); } - else { - // something is wrong. All ZM tokens have type - Error ("Missing token type. This should not happen"); - return 0; + + MYSQL_RES *result = mysql_store_result(&dbconn); + if ( !result ) { + Error("Can't use query result: %s", mysql_error(&dbconn)); + exit(mysql_errno(&dbconn)); } - if (decoded.has_payload_claim("user")) { + int n_users = mysql_num_rows(result); - // We only need to check if user is enabled in DB and pass on - // correct access permissions - std::string username = decoded.get_payload_claim("user").as_string(); - Info ("Got %s as user claim from token", username.c_str()); - char sql[ZM_SQL_MED_BUFSIZ] = ""; - snprintf(sql, sizeof(sql), - "SELECT Id, Username, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds" - " FROM Users WHERE Username = '%s' and Enabled = 1", username.c_str() ); - - if ( mysql_query(&dbconn, sql) ) { - Error("Can't run query: %s", mysql_error(&dbconn)); - exit(mysql_errno(&dbconn)); - } - - MYSQL_RES *result = mysql_store_result(&dbconn); - if ( !result ) { - Error("Can't use query result: %s", mysql_error(&dbconn)); - exit(mysql_errno(&dbconn)); - } - int n_users = mysql_num_rows(result); - - if ( n_users != 1 ) { - mysql_free_result(result); - Warning("Unable to authenticate user %s", username.c_str()); - return NULL; - } - - MYSQL_ROW dbrow = mysql_fetch_row(result); - User *user = new User(dbrow); - Info ("Authenticated user '%s' via token", username.c_str()); + if ( n_users != 1 ) { mysql_free_result(result); - return user; - - } - else { - Error ("User not found in claim"); - return 0; + Warning("Unable to authenticate user %s", username.c_str()); + return NULL; } - } - catch (const std::exception &e) { - Error("Unable to verify token: %s", e.what()); - return 0; - } - catch (...) { - Error ("unknown exception"); + MYSQL_ROW dbrow = mysql_fetch_row(result); + User *user = new User(dbrow); + Info ("Authenticated user '%s' via token", username.c_str()); + mysql_free_result(result); + return user; } - return 0; + else { + return NULL; + } + } - + // Function to validate an authentication string User *zmLoadAuthUser( const char *auth, bool use_remote_addr ) { #if HAVE_DECL_MD5 || HAVE_DECL_GNUTLS_FINGERPRINT From 0bb5ff934e7e5c96884368510ce15ac71f038cb0 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 8 May 2019 19:17:31 -0400 Subject: [PATCH 46/90] move sha headers out --- src/zm_crypt.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/zm_crypt.cpp b/src/zm_crypt.cpp index 3a3f66aeb..6d9af1460 100644 --- a/src/zm_crypt.cpp +++ b/src/zm_crypt.cpp @@ -3,6 +3,7 @@ #include "BCrypt.hpp" #include "jwt.h" #include +#include // returns username if valid, "" if not From 8461852e2777cec3fe494b89c60dd3bf41870bd9 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 8 May 2019 19:32:58 -0400 Subject: [PATCH 47/90] move out sha header --- src/zm_crypt.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zm_crypt.h b/src/zm_crypt.h index 8fb50cf00..fd3bd7e85 100644 --- a/src/zm_crypt.h +++ b/src/zm_crypt.h @@ -22,7 +22,7 @@ #include -#include + bool verifyPassword( const char *username, const char *input_password, const char *db_password_hash); From 95b448abdde69f1513347ef39eb604a133910c87 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Fri, 10 May 2019 11:25:55 -0400 Subject: [PATCH 48/90] handle case when supplied password is hashed, fix wrong params in AppController --- web/api/app/Controller/AppController.php | 3 ++- web/includes/auth.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/web/api/app/Controller/AppController.php b/web/api/app/Controller/AppController.php index 23bd528b5..1264dc25c 100644 --- a/web/api/app/Controller/AppController.php +++ b/web/api/app/Controller/AppController.php @@ -76,7 +76,8 @@ class AppController extends Controller { $mToken = $this->request->query('token') ? $this->request->query('token') : $this->request->data('token'); if ( $mUser and $mPassword ) { - $user = userLogin($mUser, $mPassword, true); + // log (user, pass, nothashed, api based login so skip recaptcha) + $user = userLogin($mUser, $mPassword, false, true); if ( !$user ) { throw new UnauthorizedException(__('User not found or incorrect password')); return; diff --git a/web/includes/auth.php b/web/includes/auth.php index c446d0024..6a076ec5d 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -126,7 +126,7 @@ function userLogin($username='', $password='', $passwordHashed=false, $apiLogin ZM\Logger::Debug ('bcrypt signature found, assumed bcrypt password'); $password_type='bcrypt'; - $password_correct = password_verify($password, $saved_password); + $password_correct = $passwordHashed? ($password == $saved_password) : password_verify($password, $saved_password); } else { // we really should nag the user not to use plain @@ -346,6 +346,7 @@ if ( ZM_OPT_USE_AUTH ) { $_SESSION['remoteAddr'] = $_SERVER['REMOTE_ADDR']; // To help prevent session hijacking if ( ZM_AUTH_HASH_LOGINS && empty($user) && !empty($_REQUEST['auth']) ) { + if ( $authUser = getAuthUser($_REQUEST['auth']) ) { userLogin($authUser['Username'], $authUser['Password'], true); } From e6b7af4583647f4a392dee8279acba4b6dcbcd82 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Fri, 10 May 2019 15:11:35 -0400 Subject: [PATCH 49/90] initial baby step for api tab --- scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in | 2 +- web/lang/en_gb.php | 1 + web/skins/classic/views/options.php | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in index c6ec697f1..0e52cbe0c 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in @@ -394,7 +394,7 @@ our @options = ( if you are exposing your ZM instance on the Internet. `, type => $types{boolean}, - category => 'system', + category => 'API', }, { name => 'ZM_OPT_USE_EVENTNOTIFICATION', diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index 945d26bea..09f3c09a9 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -104,6 +104,7 @@ $SLANG = array( 'All' => 'All', 'AnalysisFPS' => 'Analysis FPS', 'AnalysisUpdateDelay' => 'Analysis Update Delay', + 'API' => 'API', 'Apply' => 'Apply', 'ApplyingStateChange' => 'Applying State Change', 'ArchArchived' => 'Archived Only', diff --git a/web/skins/classic/views/options.php b/web/skins/classic/views/options.php index f7440d92e..561964a85 100644 --- a/web/skins/classic/views/options.php +++ b/web/skins/classic/views/options.php @@ -29,6 +29,7 @@ $tabs = array(); $tabs['skins'] = translate('Display'); $tabs['system'] = translate('System'); $tabs['config'] = translate('Config'); +$tabs['config'] = translate('API'); $tabs['servers'] = translate('Servers'); $tabs['storage'] = translate('Storage'); $tabs['web'] = translate('Web'); From ae14be916c5a282ce23f8f67f81aa559382e86c2 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sat, 11 May 2019 13:39:40 -0400 Subject: [PATCH 50/90] initial plumbing to introduce token expiry and API bans per user --- db/zm_create.sql.in | 4 +++- src/zm_crypt.cpp | 23 ++++++++++++++------ src/zm_crypt.h | 2 +- src/zm_user.cpp | 16 ++++++++++++-- version | 2 +- web/includes/auth.php | 24 +++++++++++++++++++++ web/lang/en_gb.php | 1 + web/skins/classic/views/options.php | 33 +++++++++++++++++++++++++++-- 8 files changed, 91 insertions(+), 14 deletions(-) diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index 787854091..e0af53f1f 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -245,7 +245,7 @@ DROP TABLE IF EXISTS `Events_Week`; CREATE TABLE `Events_Week` ( `EventId` BIGINT unsigned NOT NULL, `MonitorId` int(10) unsigned NOT NULL, - `StartTime` datetime default NULL, + `StartTime` datetime default NULL,M `DiskSpace` bigint default NULL, PRIMARY KEY (`EventId`), KEY `Events_Week_MonitorId_idx` (`MonitorId`), @@ -640,6 +640,8 @@ CREATE TABLE `Users` ( `System` enum('None','View','Edit') NOT NULL default 'None', `MaxBandwidth` varchar(16), `MonitorIds` text, + `TokenMinExpiry` BIGINT UNSIGNED NOT NULL DEFAULT 0, + `APIEnabled` tinyint(3) UNSIGNED NOT NULL default 1, PRIMARY KEY (`Id`), UNIQUE KEY `UC_Username` (`Username`) ) ENGINE=@ZM_MYSQL_ENGINE@; diff --git a/src/zm_crypt.cpp b/src/zm_crypt.cpp index 6d9af1460..845582137 100644 --- a/src/zm_crypt.cpp +++ b/src/zm_crypt.cpp @@ -7,8 +7,9 @@ // returns username if valid, "" if not -std::string verifyToken(std::string jwt_token_str, std::string key) { +std::pair verifyToken(std::string jwt_token_str, std::string key) { std::string username = ""; + int token_issued_at = 0; try { // is it decodable? auto decoded = jwt::decode(jwt_token_str); @@ -24,13 +25,13 @@ std::string verifyToken(std::string jwt_token_str, std::string key) { std::string type = decoded.get_payload_claim("type").as_string(); if (type != "access") { Error ("Only access tokens are allowed. Please do not use refresh tokens"); - return ""; + return std::make_pair("",0); } } else { // something is wrong. All ZM tokens have type Error ("Missing token type. This should not happen"); - return ""; + return std::make_pair("",0); } if (decoded.has_payload_claim("user")) { username = decoded.get_payload_claim("user").as_string(); @@ -38,19 +39,27 @@ std::string verifyToken(std::string jwt_token_str, std::string key) { } else { Error ("User not found in claim"); - return ""; + return std::make_pair("",0); + } + + if (decoded.has_payload_claim("iat")) { + token_issued_at = (unsigned int) (decoded.get_payload_claim("iat").as_int()); + } + else { + Error ("IAT not found in claim. This should not happen"); + return std::make_pair("",0); } } // try catch (const std::exception &e) { Error("Unable to verify token: %s", e.what()); - return ""; + return std::make_pair("",0); } catch (...) { Error ("unknown exception"); - return ""; + return std::make_pair("",0); } - return username; + return std::make_pair(username,token_issued_at); } bool verifyPassword(const char *username, const char *input_password, const char *db_password_hash) { diff --git a/src/zm_crypt.h b/src/zm_crypt.h index fd3bd7e85..340abc36c 100644 --- a/src/zm_crypt.h +++ b/src/zm_crypt.h @@ -27,5 +27,5 @@ bool verifyPassword( const char *username, const char *input_password, const char *db_password_hash); -std::string verifyToken(std::string token, std::string key); +std::pair verifyToken(std::string token, std::string key); #endif // ZM_CRYPT_H \ No newline at end of file diff --git a/src/zm_user.cpp b/src/zm_user.cpp index 7ac934d2a..b8009c221 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -154,7 +154,10 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { Info ("Inside zmLoadTokenUser, formed key=%s", key.c_str()); - std::string username = verifyToken(jwt_token_str, key); + std::pair ans = verifyToken(jwt_token_str, key); + std::string username = ans.first; + unsigned int iat = ans.second; + if (username != "") { char sql[ZM_SQL_MED_BUFSIZ] = ""; snprintf(sql, sizeof(sql), @@ -175,12 +178,21 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { if ( n_users != 1 ) { mysql_free_result(result); - Warning("Unable to authenticate user %s", username.c_str()); + Error("Unable to authenticate user %s", username.c_str()); return NULL; } MYSQL_ROW dbrow = mysql_fetch_row(result); User *user = new User(dbrow); + unsigned int stored_iat = strtoul(dbrow[14], NULL,0 ); + + if (stored_iat > iat ) { // admin revoked tokens + mysql_free_result(result); + Error("Token was revoked for %s", username.c_str()); + return NULL; + } + + Info ("Got stored expiry time of %u",stored_iat); Info ("Authenticated user '%s' via token", username.c_str()); mysql_free_result(result); return user; diff --git a/version b/version index 692c2e30d..c64ec5337 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.33.8 +1.33.9 diff --git a/web/includes/auth.php b/web/includes/auth.php index 6a076ec5d..559b478ce 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -109,6 +109,19 @@ function userLogin($username='', $password='', $passwordHashed=false, $apiLogin $password_type = NULL; if ($saved_user_details) { + + // if the API layer asked us to login, make sure the user + // has API enabled (admin may have banned API for this user) + + if ($apiLogin) { + if ($saved_user_details['APIEnabled'] != 1) { + ZM\Error ("API disabled for: $username"); + $_SESSION['loginFailed'] = true; + unset($user); + return false; + } + } + $saved_password = $saved_user_details['Password']; if ($saved_password[0] == '*') { // We assume we don't need to support mysql < 4.1 @@ -217,6 +230,17 @@ function validateToken ($token, $allowed_token_type='access') { $saved_user_details = dbFetchOne ($sql, NULL, $sql_values); if ($saved_user_details) { + + $issuedAt = $jwt_payload['iat']; + $minIssuedAt = $saved_user_details['TokenMinExpiry']; + + if ($issuedAt < $minIssuedAt) { + ZM\Error ("Token revoked for $username. Please generate a new token"); + $_SESSION['loginFailed'] = true; + unset($user); + return array(false, "Token revoked. Please re-generate"); + } + $user = $saved_user_details; return array($user, "OK"); } else { diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index 09f3c09a9..e81f7fe6f 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -421,6 +421,7 @@ $SLANG = array( 'Images' => 'Images', 'Include' => 'Include', 'In' => 'In', + 'InvalidateTokens' => 'Invalidate all generated tokens', 'Inverted' => 'Inverted', 'Iris' => 'Iris', 'KeyString' => 'Key String', diff --git a/web/skins/classic/views/options.php b/web/skins/classic/views/options.php index 561964a85..90d9a5b02 100644 --- a/web/skins/classic/views/options.php +++ b/web/skins/classic/views/options.php @@ -29,7 +29,7 @@ $tabs = array(); $tabs['skins'] = translate('Display'); $tabs['system'] = translate('System'); $tabs['config'] = translate('Config'); -$tabs['config'] = translate('API'); +$tabs['API'] = translate('API'); $tabs['servers'] = translate('Servers'); $tabs['storage'] = translate('Storage'); $tabs['web'] = translate('Web'); @@ -134,7 +134,8 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI -
@@ -424,6 +425,32 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI + + + + HELLO BABY! + + >
+
+ + + + + +
@@ -432,6 +459,8 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI } ?> + + From 91b6d0103cd032c1721f2d24b66a1f4e8d661ed1 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sat, 11 May 2019 13:41:19 -0400 Subject: [PATCH 51/90] remove M typo --- db/zm_create.sql.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index e0af53f1f..653e95cc6 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -245,7 +245,7 @@ DROP TABLE IF EXISTS `Events_Week`; CREATE TABLE `Events_Week` ( `EventId` BIGINT unsigned NOT NULL, `MonitorId` int(10) unsigned NOT NULL, - `StartTime` datetime default NULL,M + `StartTime` datetime default NULL, `DiskSpace` bigint default NULL, PRIMARY KEY (`EventId`), KEY `Events_Week_MonitorId_idx` (`MonitorId`), From 2ee466f5e445944c8934e760081c79ca6f29de43 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sat, 11 May 2019 14:08:49 -0400 Subject: [PATCH 52/90] display user table in api --- web/skins/classic/views/options.php | 48 ++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/web/skins/classic/views/options.php b/web/skins/classic/views/options.php index 90d9a5b02..3c0bd809e 100644 --- a/web/skins/classic/views/options.php +++ b/web/skins/classic/views/options.php @@ -430,25 +430,57 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI if ($tab == 'API') { ?> - HELLO BABY! +
- >
+ >

+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
From 88d50ec9cac7f18fdd9f4affcaf7be5e779d5c72 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sat, 11 May 2019 15:47:57 -0400 Subject: [PATCH 53/90] added revoke all tokens code, removed test code --- web/api/app/Controller/HostController.php | 2 +- web/skins/classic/views/options.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/web/api/app/Controller/HostController.php b/web/api/app/Controller/HostController.php index 55fedd281..7b8c7c0ff 100644 --- a/web/api/app/Controller/HostController.php +++ b/web/api/app/Controller/HostController.php @@ -140,7 +140,7 @@ class HostController extends AppController { // by default access token will expire in 2 hrs // you can change it by changing the value of ZM_AUTH_HASH_TLL $access_expire_at = $access_issued_at + $access_ttl; - $access_expire_at = $access_issued_at + 60; // TEST, REMOVE + //$access_expire_at = $access_issued_at + 60; // TEST, REMOVE $access_token = array( "iss" => "ZoneMinder", diff --git a/web/skins/classic/views/options.php b/web/skins/classic/views/options.php index 3c0bd809e..e9245f10e 100644 --- a/web/skins/classic/views/options.php +++ b/web/skins/classic/views/options.php @@ -439,6 +439,8 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI Date: Sat, 11 May 2019 15:58:07 -0400 Subject: [PATCH 54/90] use strtoul for conversion --- src/zm_crypt.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/zm_crypt.cpp b/src/zm_crypt.cpp index 845582137..fe0b2b418 100644 --- a/src/zm_crypt.cpp +++ b/src/zm_crypt.cpp @@ -43,7 +43,11 @@ std::pair verifyToken(std::string jwt_token_str, std } if (decoded.has_payload_claim("iat")) { - token_issued_at = (unsigned int) (decoded.get_payload_claim("iat").as_int()); + + + std::string iat_str = decoded.get_payload_claim("iat").as_string(); + Info ("Got IAT token=%s", iat_str); + token_issued_at = strtoul(iat_str, NULL,0 ); } else { Error ("IAT not found in claim. This should not happen"); From 053e57af62c79953f6a0c0f913a529d6fc54d088 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sat, 11 May 2019 16:01:05 -0400 Subject: [PATCH 55/90] use strtoul for conversion --- src/zm_crypt.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zm_crypt.cpp b/src/zm_crypt.cpp index fe0b2b418..5ef2433d4 100644 --- a/src/zm_crypt.cpp +++ b/src/zm_crypt.cpp @@ -47,7 +47,7 @@ std::pair verifyToken(std::string jwt_token_str, std std::string iat_str = decoded.get_payload_claim("iat").as_string(); Info ("Got IAT token=%s", iat_str); - token_issued_at = strtoul(iat_str, NULL,0 ); + token_issued_at = strtoul(iat_str.c_str(), NULL,0 ); } else { Error ("IAT not found in claim. This should not happen"); From 3e66be27a80215ca72a00945ac01644cdc1f99e5 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sat, 11 May 2019 16:09:42 -0400 Subject: [PATCH 56/90] use strtoul for conversion --- src/zm_crypt.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zm_crypt.cpp b/src/zm_crypt.cpp index 5ef2433d4..72fba3294 100644 --- a/src/zm_crypt.cpp +++ b/src/zm_crypt.cpp @@ -46,7 +46,7 @@ std::pair verifyToken(std::string jwt_token_str, std std::string iat_str = decoded.get_payload_claim("iat").as_string(); - Info ("Got IAT token=%s", iat_str); + Info ("Got IAT token=%s", iat_str.c_str()); token_issued_at = strtoul(iat_str.c_str(), NULL,0 ); } else { From 6ab31dfe4b8b42a84bf4ecdcf8b100fa24002436 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 12 May 2019 05:03:16 -0400 Subject: [PATCH 57/90] more fixes --- src/zm_crypt.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/zm_crypt.cpp b/src/zm_crypt.cpp index 72fba3294..e9877ffd3 100644 --- a/src/zm_crypt.cpp +++ b/src/zm_crypt.cpp @@ -9,7 +9,7 @@ // returns username if valid, "" if not std::pair verifyToken(std::string jwt_token_str, std::string key) { std::string username = ""; - int token_issued_at = 0; + unsigned int token_issued_at = 0; try { // is it decodable? auto decoded = jwt::decode(jwt_token_str); @@ -43,11 +43,9 @@ std::pair verifyToken(std::string jwt_token_str, std } if (decoded.has_payload_claim("iat")) { - - - std::string iat_str = decoded.get_payload_claim("iat").as_string(); - Info ("Got IAT token=%s", iat_str.c_str()); - token_issued_at = strtoul(iat_str.c_str(), NULL,0 ); + token_issued_at = (unsigned int) (decoded.get_payload_claim("iat").as_int()); + Info ("Got IAT token=%u", iat); + } else { Error ("IAT not found in claim. This should not happen"); From 1f22c38453ff6d0c1c12a08fedbc72ea9d27a733 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 12 May 2019 05:10:20 -0400 Subject: [PATCH 58/90] more fixes --- src/zm_crypt.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zm_crypt.cpp b/src/zm_crypt.cpp index e9877ffd3..c37ab25bc 100644 --- a/src/zm_crypt.cpp +++ b/src/zm_crypt.cpp @@ -44,7 +44,7 @@ std::pair verifyToken(std::string jwt_token_str, std if (decoded.has_payload_claim("iat")) { token_issued_at = (unsigned int) (decoded.get_payload_claim("iat").as_int()); - Info ("Got IAT token=%u", iat); + Info ("Got IAT token=%u", token_issued_at); } else { From 225893fcd63e27f6e4246dae079937176f0b931a Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 12 May 2019 05:50:19 -0400 Subject: [PATCH 59/90] add mintokenexpiry to DB seek --- src/zm_user.cpp | 5 +++-- web/api/app/Controller/AppController.php | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/zm_user.cpp b/src/zm_user.cpp index b8009c221..f37233553 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -161,7 +161,7 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { if (username != "") { char sql[ZM_SQL_MED_BUFSIZ] = ""; snprintf(sql, sizeof(sql), - "SELECT Id, Username, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds" + "SELECT Id, Username, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds, MinTokenExpiry" " FROM Users WHERE Username = '%s' and Enabled = 1", username.c_str() ); if ( mysql_query(&dbconn, sql) ) { @@ -184,7 +184,8 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { MYSQL_ROW dbrow = mysql_fetch_row(result); User *user = new User(dbrow); - unsigned int stored_iat = strtoul(dbrow[14], NULL,0 ); + Info ("DB 9=%s", dbrow[9]); + unsigned int stored_iat = strtoul(dbrow[9], NULL,0 ); if (stored_iat > iat ) { // admin revoked tokens mysql_free_result(result); diff --git a/web/api/app/Controller/AppController.php b/web/api/app/Controller/AppController.php index 1264dc25c..65585ff61 100644 --- a/web/api/app/Controller/AppController.php +++ b/web/api/app/Controller/AppController.php @@ -79,7 +79,7 @@ class AppController extends Controller { // log (user, pass, nothashed, api based login so skip recaptcha) $user = userLogin($mUser, $mPassword, false, true); if ( !$user ) { - throw new UnauthorizedException(__('User not found or incorrect password')); + throw new UnauthorizedException(__('Incorrect credentials or API disabled')); return; } } else if ( $mToken ) { From 849995876794cc13bd2cfa37d9b52d30d29c22f9 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 12 May 2019 05:57:17 -0400 Subject: [PATCH 60/90] typo --- src/zm_user.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zm_user.cpp b/src/zm_user.cpp index f37233553..84536c16e 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -161,7 +161,7 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { if (username != "") { char sql[ZM_SQL_MED_BUFSIZ] = ""; snprintf(sql, sizeof(sql), - "SELECT Id, Username, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds, MinTokenExpiry" + "SELECT Id, Username, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds, TokenMinExpiry" " FROM Users WHERE Username = '%s' and Enabled = 1", username.c_str() ); if ( mysql_query(&dbconn, sql) ) { From a9d601e5aecaebeda97a30f21f86cb4798294502 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 12 May 2019 10:56:17 -0400 Subject: [PATCH 61/90] add ability to revoke tokens and enable/disable APIs per user --- web/includes/actions/options.php | 1 + web/lang/en_gb.php | 1 + web/skins/classic/views/options.php | 115 ++++++++++++++++------------ 3 files changed, 68 insertions(+), 49 deletions(-) diff --git a/web/includes/actions/options.php b/web/includes/actions/options.php index 0c80bacf0..2f98b4a95 100644 --- a/web/includes/actions/options.php +++ b/web/includes/actions/options.php @@ -75,6 +75,7 @@ if ( $action == 'delete' ) { case 'config' : $restartWarning = true; break; + case 'API': case 'web' : case 'tools' : break; diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index e81f7fe6f..b5fa70168 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -660,6 +660,7 @@ $SLANG = array( 'RestrictedMonitors' => 'Restricted Monitors', 'ReturnDelay' => 'Return Delay', 'ReturnLocation' => 'Return Location', + 'RevokeAllTokens' => 'Revoke All Tokens' 'Rewind' => 'Rewind', 'RotateLeft' => 'Rotate Left', 'RotateRight' => 'Rotate Right', diff --git a/web/skins/classic/views/options.php b/web/skins/classic/views/options.php index e9245f10e..c4a630ade 100644 --- a/web/skins/classic/views/options.php +++ b/web/skins/classic/views/options.php @@ -430,60 +430,77 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI if ($tab == 'API') { ?> - -
- >

-
- - - - -
- - - - - - - - - - - - - - - - - - - - - - + + +
+ + "; + dbQuery('UPDATE Users SET TokenMinExpiry=? WHERE Id=?', array($minTime, $markUid)); + } + foreach( $_REQUEST["apiUids"] as $markUid ) { + dbQuery('UPDATE Users SET APIEnabled=1 WHERE Id=?', array($markUid)); + // echo "UPDATE Users SET APIEnabled=1"." WHERE Id=".$markUid."
"; + } + echo "Updated."; + } + + if(array_key_exists('revokeAllTokens',$_POST)){ + revokeAllTokens(); + } + + if(array_key_exists('updateSelected',$_POST)){ + updateSelected(); + } + ?> + + +

+ + + +
+ + + + + + + + + + + + + + +
/>
+
From c1891e35b9c23eb5b73480af36c180dd1609ed5c Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 12 May 2019 12:15:08 -0400 Subject: [PATCH 62/90] moved API enable back to system --- .../lib/ZoneMinder/ConfigData.pm.in | 2 +- web/skins/classic/views/options.php | 160 +++++++++--------- 2 files changed, 83 insertions(+), 79 deletions(-) diff --git a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in index 0e52cbe0c..c6ec697f1 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in @@ -394,7 +394,7 @@ our @options = ( if you are exposing your ZM instance on the Internet. `, type => $types{boolean}, - category => 'API', + category => 'system', }, { name => 'ZM_OPT_USE_EVENTNOTIFICATION', diff --git a/web/skins/classic/views/options.php b/web/skins/classic/views/options.php index c4a630ade..18b608155 100644 --- a/web/skins/classic/views/options.php +++ b/web/skins/classic/views/options.php @@ -311,8 +311,88 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI
-APIs are disabled. To enable, please turn on OPT_USE_API in Options->System
"; + } + else { + ?> + +
+
+ + "; + dbQuery('UPDATE Users SET TokenMinExpiry=? WHERE Id=?', array($minTime, $markUid)); + } + foreach( $_REQUEST["apiUids"] as $markUid ) { + dbQuery('UPDATE Users SET APIEnabled=1 WHERE Id=?', array($markUid)); + // echo "UPDATE Users SET APIEnabled=1"." WHERE Id=".$markUid."
"; + } + echo "Updated."; + } + + if(array_key_exists('revokeAllTokens',$_POST)){ + revokeAllTokens(); + } + + if(array_key_exists('updateSelected',$_POST)){ + updateSelected(); + } + ?> + + +

+ + + + + + + + + + + + + + + + + + + + +
/>
+
+ + + - - - - -
-
- - "; - dbQuery('UPDATE Users SET TokenMinExpiry=? WHERE Id=?', array($minTime, $markUid)); - } - foreach( $_REQUEST["apiUids"] as $markUid ) { - dbQuery('UPDATE Users SET APIEnabled=1 WHERE Id=?', array($markUid)); - // echo "UPDATE Users SET APIEnabled=1"." WHERE Id=".$markUid."
"; - } - echo "Updated."; - } - - if(array_key_exists('revokeAllTokens',$_POST)){ - revokeAllTokens(); - } - - if(array_key_exists('updateSelected',$_POST)){ - updateSelected(); - } - ?> - - -

- - - - - - - - - - - - - - - - - - - - -
/>
-
- - -
From 9998c2610112470bfddd9214631442c73f89ab6f Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 12 May 2019 12:21:49 -0400 Subject: [PATCH 63/90] comma --- web/lang/en_gb.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index b5fa70168..e0ee8f39f 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -660,7 +660,7 @@ $SLANG = array( 'RestrictedMonitors' => 'Restricted Monitors', 'ReturnDelay' => 'Return Delay', 'ReturnLocation' => 'Return Location', - 'RevokeAllTokens' => 'Revoke All Tokens' + 'RevokeAllTokens' => 'Revoke All Tokens', 'Rewind' => 'Rewind', 'RotateLeft' => 'Rotate Left', 'RotateRight' => 'Rotate Right', From 91dd6630b59f962f6062402b191c165c5e2c6029 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 12 May 2019 12:34:55 -0400 Subject: [PATCH 64/90] enable API options only if API enabled --- web/skins/classic/views/options.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/skins/classic/views/options.php b/web/skins/classic/views/options.php index 18b608155..465da0a02 100644 --- a/web/skins/classic/views/options.php +++ b/web/skins/classic/views/options.php @@ -316,7 +316,7 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI } else if ($tab == 'API') { $apiEnabled = dbFetchOne("SELECT Value FROM Config WHERE Name='ZM_OPT_USE_API'"); - if (!$apiEnabled) { + if ($apiEnabled['Value']!='1') { echo "
APIs are disabled. To enable, please turn on OPT_USE_API in Options->System
"; } else { From d7dbaf52d410bb76bbdfb6a07e701ef9cab7db4d Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 12 May 2019 13:01:29 -0400 Subject: [PATCH 65/90] move user creation to bcrypt --- web/includes/actions/user.php | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/web/includes/actions/user.php b/web/includes/actions/user.php index af569627f..bacf68698 100644 --- a/web/includes/actions/user.php +++ b/web/includes/actions/user.php @@ -28,8 +28,18 @@ if ( $action == 'user' ) { $types = array(); $changes = getFormChanges($dbUser, $_REQUEST['newUser'], $types); - if ( $_REQUEST['newUser']['Password'] ) - $changes['Password'] = 'Password = password('.dbEscape($_REQUEST['newUser']['Password']).')'; + if (function_exists ('password_hash')) { + $pass_hash = '"'.password_hash($pass, PASSWORD_BCRYPT).'"'; + } else { + $pass_hash = ' PASSWORD('.dbEscape($_REQUEST['newUser']['Password']).') '; + ZM\Info ('Cannot use bcrypt as you are using PHP < 5.5'); + } + + if ( $_REQUEST['newUser']['Password'] ) { + $changes['Password'] = 'Password = '.$pass_hash; + ZM\Info ("PASS CMD=".$changes['Password']); + } + else unset($changes['Password']); @@ -53,8 +63,19 @@ if ( $action == 'user' ) { $types = array(); $changes = getFormChanges($dbUser, $_REQUEST['newUser'], $types); - if ( !empty($_REQUEST['newUser']['Password']) ) - $changes['Password'] = 'Password = password('.dbEscape($_REQUEST['newUser']['Password']).')'; + if (function_exists ('password_hash')) { + $pass_hash = '"'.password_hash($pass, PASSWORD_BCRYPT).'"'; + } else { + $pass_hash = ' PASSWORD('.dbEscape($_REQUEST['newUser']['Password']).') '; + ZM\Info ('Cannot use bcrypt as you are using PHP < 5.5'); + } + + + if ( !empty($_REQUEST['newUser']['Password']) ) { + ZM\Info ("PASS CMD=".$changes['Password']); + $changes['Password'] = 'Password = '.$pass_hash; + } + else unset($changes['Password']); if ( count($changes) ) { From adb01c4d0e33fa17715371c390f276a3af5c3bc7 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 12 May 2019 13:57:25 -0400 Subject: [PATCH 66/90] added password_compat for PHP >=5.3 <5.5 --- web/composer.json | 3 +- web/composer.lock | 46 ++- web/includes/actions/user.php | 2 +- web/includes/auth.php | 6 +- web/skins/classic/views/options.php | 4 +- web/vendor/composer/ClassLoader.php | 4 +- web/vendor/composer/LICENSE | 69 +++- web/vendor/composer/autoload_files.php | 10 + web/vendor/composer/autoload_real.php | 18 + web/vendor/composer/autoload_static.php | 4 + web/vendor/composer/installed.json | 44 +++ .../ircmaxell/password-compat/LICENSE.md | 7 + .../ircmaxell/password-compat/composer.json | 20 ++ .../password-compat/lib/password.php | 314 ++++++++++++++++++ .../password-compat/version-test.php | 6 + 15 files changed, 528 insertions(+), 29 deletions(-) create mode 100644 web/vendor/composer/autoload_files.php create mode 100644 web/vendor/ircmaxell/password-compat/LICENSE.md create mode 100644 web/vendor/ircmaxell/password-compat/composer.json create mode 100644 web/vendor/ircmaxell/password-compat/lib/password.php create mode 100644 web/vendor/ircmaxell/password-compat/version-test.php diff --git a/web/composer.json b/web/composer.json index cc22311b1..968d1d4cb 100644 --- a/web/composer.json +++ b/web/composer.json @@ -1,5 +1,6 @@ { "require": { - "firebase/php-jwt": "^5.0" + "firebase/php-jwt": "^5.0", + "ircmaxell/password-compat": "^1.0" } } diff --git a/web/composer.lock b/web/composer.lock index b0b368b4f..b260d2e5a 100644 --- a/web/composer.lock +++ b/web/composer.lock @@ -1,10 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "7f97fc9c4d2beaf06d019ba50f7efcbc", + "content-hash": "5759823f1f047089a354efaa25903378", "packages": [ { "name": "firebase/php-jwt", @@ -51,6 +51,48 @@ "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", "homepage": "https://github.com/firebase/php-jwt", "time": "2017-06-27T22:17:23+00:00" + }, + { + "name": "ircmaxell/password-compat", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/ircmaxell/password_compat.git", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "type": "library", + "autoload": { + "files": [ + "lib/password.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anthony Ferrara", + "email": "ircmaxell@php.net", + "homepage": "http://blog.ircmaxell.com" + } + ], + "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", + "homepage": "https://github.com/ircmaxell/password_compat", + "keywords": [ + "hashing", + "password" + ], + "time": "2014-11-20T16:49:30+00:00" } ], "packages-dev": [], diff --git a/web/includes/actions/user.php b/web/includes/actions/user.php index bacf68698..2b520cd10 100644 --- a/web/includes/actions/user.php +++ b/web/includes/actions/user.php @@ -67,7 +67,7 @@ if ( $action == 'user' ) { $pass_hash = '"'.password_hash($pass, PASSWORD_BCRYPT).'"'; } else { $pass_hash = ' PASSWORD('.dbEscape($_REQUEST['newUser']['Password']).') '; - ZM\Info ('Cannot use bcrypt as you are using PHP < 5.5'); + ZM\Info ('Cannot use bcrypt as you are using PHP < 5.3'); } diff --git a/web/includes/auth.php b/web/includes/auth.php index 559b478ce..643feb952 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -37,10 +37,8 @@ function migrateHash($user, $pass) { dbQuery($update_password_sql); } else { - // Not really an error, so an info - // there is also a compat library https://github.com/ircmaxell/password_compat - // not sure if its worth it. Do a lot of people really use PHP < 5.5? - ZM\Info ('Cannot migrate password scheme to bcrypt, as you are using PHP < 5.5'); + + ZM\Info ('Cannot migrate password scheme to bcrypt, as you are using PHP < 5.3'); return; } diff --git a/web/skins/classic/views/options.php b/web/skins/classic/views/options.php index 465da0a02..d7b264834 100644 --- a/web/skins/classic/views/options.php +++ b/web/skins/classic/views/options.php @@ -314,7 +314,7 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI APIs are disabled. To enable, please turn on OPT_USE_API in Options->System"; @@ -377,7 +377,7 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI foreach( dbFetchAll($sql) as $row ) { ?> - + /> diff --git a/web/vendor/composer/ClassLoader.php b/web/vendor/composer/ClassLoader.php index fce8549f0..dc02dfb11 100644 --- a/web/vendor/composer/ClassLoader.php +++ b/web/vendor/composer/ClassLoader.php @@ -279,7 +279,7 @@ class ClassLoader */ public function setApcuPrefix($apcuPrefix) { - $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null; } /** @@ -377,7 +377,7 @@ class ClassLoader $subPath = $class; while (false !== $lastPos = strrpos($subPath, '\\')) { $subPath = substr($subPath, 0, $lastPos); - $search = $subPath . '\\'; + $search = $subPath.'\\'; if (isset($this->prefixDirsPsr4[$search])) { $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); foreach ($this->prefixDirsPsr4[$search] as $dir) { diff --git a/web/vendor/composer/LICENSE b/web/vendor/composer/LICENSE index f27399a04..f0157a6ed 100644 --- a/web/vendor/composer/LICENSE +++ b/web/vendor/composer/LICENSE @@ -1,21 +1,56 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: Composer +Upstream-Contact: Jordi Boggiano +Source: https://github.com/composer/composer -Copyright (c) Nils Adermann, Jordi Boggiano +Files: * +Copyright: 2016, Nils Adermann + 2016, Jordi Boggiano +License: Expat -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: +Files: src/Composer/Util/TlsHelper.php +Copyright: 2016, Nils Adermann + 2016, Jordi Boggiano + 2013, Evan Coury +License: Expat and BSD-2-Clause -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +License: BSD-2-Clause + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + . + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + . + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +License: Expat + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is furnished + to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. diff --git a/web/vendor/composer/autoload_files.php b/web/vendor/composer/autoload_files.php new file mode 100644 index 000000000..eb2e8068e --- /dev/null +++ b/web/vendor/composer/autoload_files.php @@ -0,0 +1,10 @@ + $vendorDir . '/ircmaxell/password-compat/lib/password.php', +); diff --git a/web/vendor/composer/autoload_real.php b/web/vendor/composer/autoload_real.php index accbcefb3..6d63dc4f7 100644 --- a/web/vendor/composer/autoload_real.php +++ b/web/vendor/composer/autoload_real.php @@ -47,6 +47,24 @@ class ComposerAutoloaderInit254e25e69fe049d603f41f5fd853ef2b $loader->register(true); + if ($useStaticLoader) { + $includeFiles = Composer\Autoload\ComposerStaticInit254e25e69fe049d603f41f5fd853ef2b::$files; + } else { + $includeFiles = require __DIR__ . '/autoload_files.php'; + } + foreach ($includeFiles as $fileIdentifier => $file) { + composerRequire254e25e69fe049d603f41f5fd853ef2b($fileIdentifier, $file); + } + return $loader; } } + +function composerRequire254e25e69fe049d603f41f5fd853ef2b($fileIdentifier, $file) +{ + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + require $file; + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + } +} diff --git a/web/vendor/composer/autoload_static.php b/web/vendor/composer/autoload_static.php index 2709f5803..980a5a0d7 100644 --- a/web/vendor/composer/autoload_static.php +++ b/web/vendor/composer/autoload_static.php @@ -6,6 +6,10 @@ namespace Composer\Autoload; class ComposerStaticInit254e25e69fe049d603f41f5fd853ef2b { + public static $files = array ( + 'e40631d46120a9c38ea139981f8dab26' => __DIR__ . '/..' . '/ircmaxell/password-compat/lib/password.php', + ); + public static $prefixLengthsPsr4 = array ( 'F' => array ( diff --git a/web/vendor/composer/installed.json b/web/vendor/composer/installed.json index 5b2924c21..0e2ed23cf 100644 --- a/web/vendor/composer/installed.json +++ b/web/vendor/composer/installed.json @@ -46,5 +46,49 @@ ], "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", "homepage": "https://github.com/firebase/php-jwt" + }, + { + "name": "ircmaxell/password-compat", + "version": "v1.0.4", + "version_normalized": "1.0.4.0", + "source": { + "type": "git", + "url": "https://github.com/ircmaxell/password_compat.git", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "time": "2014-11-20T16:49:30+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "lib/password.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anthony Ferrara", + "email": "ircmaxell@php.net", + "homepage": "http://blog.ircmaxell.com" + } + ], + "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", + "homepage": "https://github.com/ircmaxell/password_compat", + "keywords": [ + "hashing", + "password" + ] } ] diff --git a/web/vendor/ircmaxell/password-compat/LICENSE.md b/web/vendor/ircmaxell/password-compat/LICENSE.md new file mode 100644 index 000000000..1efc565fc --- /dev/null +++ b/web/vendor/ircmaxell/password-compat/LICENSE.md @@ -0,0 +1,7 @@ +Copyright (c) 2012 Anthony Ferrara + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/web/vendor/ircmaxell/password-compat/composer.json b/web/vendor/ircmaxell/password-compat/composer.json new file mode 100644 index 000000000..822fd1ffb --- /dev/null +++ b/web/vendor/ircmaxell/password-compat/composer.json @@ -0,0 +1,20 @@ +{ + "name": "ircmaxell/password-compat", + "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", + "keywords": ["password", "hashing"], + "homepage": "https://github.com/ircmaxell/password_compat", + "license": "MIT", + "authors": [ + { + "name": "Anthony Ferrara", + "email": "ircmaxell@php.net", + "homepage": "http://blog.ircmaxell.com" + } + ], + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "autoload": { + "files": ["lib/password.php"] + } +} diff --git a/web/vendor/ircmaxell/password-compat/lib/password.php b/web/vendor/ircmaxell/password-compat/lib/password.php new file mode 100644 index 000000000..cc6896c1d --- /dev/null +++ b/web/vendor/ircmaxell/password-compat/lib/password.php @@ -0,0 +1,314 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @copyright 2012 The Authors + */ + +namespace { + + if (!defined('PASSWORD_BCRYPT')) { + /** + * PHPUnit Process isolation caches constants, but not function declarations. + * So we need to check if the constants are defined separately from + * the functions to enable supporting process isolation in userland + * code. + */ + define('PASSWORD_BCRYPT', 1); + define('PASSWORD_DEFAULT', PASSWORD_BCRYPT); + define('PASSWORD_BCRYPT_DEFAULT_COST', 10); + } + + if (!function_exists('password_hash')) { + + /** + * Hash the password using the specified algorithm + * + * @param string $password The password to hash + * @param int $algo The algorithm to use (Defined by PASSWORD_* constants) + * @param array $options The options for the algorithm to use + * + * @return string|false The hashed password, or false on error. + */ + function password_hash($password, $algo, array $options = array()) { + if (!function_exists('crypt')) { + trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING); + return null; + } + if (is_null($password) || is_int($password)) { + $password = (string) $password; + } + if (!is_string($password)) { + trigger_error("password_hash(): Password must be a string", E_USER_WARNING); + return null; + } + if (!is_int($algo)) { + trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING); + return null; + } + $resultLength = 0; + switch ($algo) { + case PASSWORD_BCRYPT: + $cost = PASSWORD_BCRYPT_DEFAULT_COST; + if (isset($options['cost'])) { + $cost = $options['cost']; + if ($cost < 4 || $cost > 31) { + trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING); + return null; + } + } + // The length of salt to generate + $raw_salt_len = 16; + // The length required in the final serialization + $required_salt_len = 22; + $hash_format = sprintf("$2y$%02d$", $cost); + // The expected length of the final crypt() output + $resultLength = 60; + break; + default: + trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING); + return null; + } + $salt_requires_encoding = false; + if (isset($options['salt'])) { + switch (gettype($options['salt'])) { + case 'NULL': + case 'boolean': + case 'integer': + case 'double': + case 'string': + $salt = (string) $options['salt']; + break; + case 'object': + if (method_exists($options['salt'], '__tostring')) { + $salt = (string) $options['salt']; + break; + } + case 'array': + case 'resource': + default: + trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING); + return null; + } + if (PasswordCompat\binary\_strlen($salt) < $required_salt_len) { + trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", PasswordCompat\binary\_strlen($salt), $required_salt_len), E_USER_WARNING); + return null; + } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) { + $salt_requires_encoding = true; + } + } else { + $buffer = ''; + $buffer_valid = false; + if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) { + $buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM); + if ($buffer) { + $buffer_valid = true; + } + } + if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) { + $buffer = openssl_random_pseudo_bytes($raw_salt_len); + if ($buffer) { + $buffer_valid = true; + } + } + if (!$buffer_valid && @is_readable('/dev/urandom')) { + $f = fopen('/dev/urandom', 'r'); + $read = PasswordCompat\binary\_strlen($buffer); + while ($read < $raw_salt_len) { + $buffer .= fread($f, $raw_salt_len - $read); + $read = PasswordCompat\binary\_strlen($buffer); + } + fclose($f); + if ($read >= $raw_salt_len) { + $buffer_valid = true; + } + } + if (!$buffer_valid || PasswordCompat\binary\_strlen($buffer) < $raw_salt_len) { + $bl = PasswordCompat\binary\_strlen($buffer); + for ($i = 0; $i < $raw_salt_len; $i++) { + if ($i < $bl) { + $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255)); + } else { + $buffer .= chr(mt_rand(0, 255)); + } + } + } + $salt = $buffer; + $salt_requires_encoding = true; + } + if ($salt_requires_encoding) { + // encode string with the Base64 variant used by crypt + $base64_digits = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + $bcrypt64_digits = + './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + $base64_string = base64_encode($salt); + $salt = strtr(rtrim($base64_string, '='), $base64_digits, $bcrypt64_digits); + } + $salt = PasswordCompat\binary\_substr($salt, 0, $required_salt_len); + + $hash = $hash_format . $salt; + + $ret = crypt($password, $hash); + + if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != $resultLength) { + return false; + } + + return $ret; + } + + /** + * Get information about the password hash. Returns an array of the information + * that was used to generate the password hash. + * + * array( + * 'algo' => 1, + * 'algoName' => 'bcrypt', + * 'options' => array( + * 'cost' => PASSWORD_BCRYPT_DEFAULT_COST, + * ), + * ) + * + * @param string $hash The password hash to extract info from + * + * @return array The array of information about the hash. + */ + function password_get_info($hash) { + $return = array( + 'algo' => 0, + 'algoName' => 'unknown', + 'options' => array(), + ); + if (PasswordCompat\binary\_substr($hash, 0, 4) == '$2y$' && PasswordCompat\binary\_strlen($hash) == 60) { + $return['algo'] = PASSWORD_BCRYPT; + $return['algoName'] = 'bcrypt'; + list($cost) = sscanf($hash, "$2y$%d$"); + $return['options']['cost'] = $cost; + } + return $return; + } + + /** + * Determine if the password hash needs to be rehashed according to the options provided + * + * If the answer is true, after validating the password using password_verify, rehash it. + * + * @param string $hash The hash to test + * @param int $algo The algorithm used for new password hashes + * @param array $options The options array passed to password_hash + * + * @return boolean True if the password needs to be rehashed. + */ + function password_needs_rehash($hash, $algo, array $options = array()) { + $info = password_get_info($hash); + if ($info['algo'] != $algo) { + return true; + } + switch ($algo) { + case PASSWORD_BCRYPT: + $cost = isset($options['cost']) ? $options['cost'] : PASSWORD_BCRYPT_DEFAULT_COST; + if ($cost != $info['options']['cost']) { + return true; + } + break; + } + return false; + } + + /** + * Verify a password against a hash using a timing attack resistant approach + * + * @param string $password The password to verify + * @param string $hash The hash to verify against + * + * @return boolean If the password matches the hash + */ + function password_verify($password, $hash) { + if (!function_exists('crypt')) { + trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING); + return false; + } + $ret = crypt($password, $hash); + if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != PasswordCompat\binary\_strlen($hash) || PasswordCompat\binary\_strlen($ret) <= 13) { + return false; + } + + $status = 0; + for ($i = 0; $i < PasswordCompat\binary\_strlen($ret); $i++) { + $status |= (ord($ret[$i]) ^ ord($hash[$i])); + } + + return $status === 0; + } + } + +} + +namespace PasswordCompat\binary { + + if (!function_exists('PasswordCompat\\binary\\_strlen')) { + + /** + * Count the number of bytes in a string + * + * We cannot simply use strlen() for this, because it might be overwritten by the mbstring extension. + * In this case, strlen() will count the number of *characters* based on the internal encoding. A + * sequence of bytes might be regarded as a single multibyte character. + * + * @param string $binary_string The input string + * + * @internal + * @return int The number of bytes + */ + function _strlen($binary_string) { + if (function_exists('mb_strlen')) { + return mb_strlen($binary_string, '8bit'); + } + return strlen($binary_string); + } + + /** + * Get a substring based on byte limits + * + * @see _strlen() + * + * @param string $binary_string The input string + * @param int $start + * @param int $length + * + * @internal + * @return string The substring + */ + function _substr($binary_string, $start, $length) { + if (function_exists('mb_substr')) { + return mb_substr($binary_string, $start, $length, '8bit'); + } + return substr($binary_string, $start, $length); + } + + /** + * Check if current PHP version is compatible with the library + * + * @return boolean the check result + */ + function check() { + static $pass = NULL; + + if (is_null($pass)) { + if (function_exists('crypt')) { + $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG'; + $test = crypt("password", $hash); + $pass = $test == $hash; + } else { + $pass = false; + } + } + return $pass; + } + + } +} \ No newline at end of file diff --git a/web/vendor/ircmaxell/password-compat/version-test.php b/web/vendor/ircmaxell/password-compat/version-test.php new file mode 100644 index 000000000..96f60ca8d --- /dev/null +++ b/web/vendor/ircmaxell/password-compat/version-test.php @@ -0,0 +1,6 @@ + Date: Sun, 12 May 2019 14:48:23 -0400 Subject: [PATCH 67/90] add Password back so User object indexes don't change --- src/zm_user.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zm_user.cpp b/src/zm_user.cpp index 84536c16e..e91f0c067 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -161,7 +161,7 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { if (username != "") { char sql[ZM_SQL_MED_BUFSIZ] = ""; snprintf(sql, sizeof(sql), - "SELECT Id, Username, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds, TokenMinExpiry" + "SELECT Id, Username, Password, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds, TokenMinExpiry" " FROM Users WHERE Username = '%s' and Enabled = 1", username.c_str() ); if ( mysql_query(&dbconn, sql) ) { From cc0d23ce4ef64300efebd7a8b38fcddc33c24a68 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 12 May 2019 15:01:49 -0400 Subject: [PATCH 68/90] move token index after adding password --- src/zm_user.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/zm_user.cpp b/src/zm_user.cpp index e91f0c067..68b52e08c 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -184,8 +184,7 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { MYSQL_ROW dbrow = mysql_fetch_row(result); User *user = new User(dbrow); - Info ("DB 9=%s", dbrow[9]); - unsigned int stored_iat = strtoul(dbrow[9], NULL,0 ); + unsigned int stored_iat = strtoul(dbrow[10], NULL,0 ); if (stored_iat > iat ) { // admin revoked tokens mysql_free_result(result); From 21710b6e49697bcd8dc5e5117822e4f09f55ea8a Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 12 May 2019 15:45:39 -0400 Subject: [PATCH 69/90] demote logs --- src/zm_crypt.cpp | 10 +++++----- src/zm_user.cpp | 4 ++-- src/zms.cpp | 2 +- web/skins/classic/views/options.php | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/zm_crypt.cpp b/src/zm_crypt.cpp index c37ab25bc..0235e5c13 100644 --- a/src/zm_crypt.cpp +++ b/src/zm_crypt.cpp @@ -35,7 +35,7 @@ std::pair verifyToken(std::string jwt_token_str, std } if (decoded.has_payload_claim("user")) { username = decoded.get_payload_claim("user").as_string(); - Info ("Got %s as user claim from token", username.c_str()); + Debug (1, "Got %s as user claim from token", username.c_str()); } else { Error ("User not found in claim"); @@ -44,7 +44,7 @@ std::pair verifyToken(std::string jwt_token_str, std if (decoded.has_payload_claim("iat")) { token_issued_at = (unsigned int) (decoded.get_payload_claim("iat").as_int()); - Info ("Got IAT token=%u", token_issued_at); + Debug (1,"Got IAT token=%u", token_issued_at); } else { @@ -73,7 +73,7 @@ bool verifyPassword(const char *username, const char *input_password, const char } if (db_password_hash[0] == '*') { // MYSQL PASSWORD - Info ("%s is using an MD5 encoded password", username); + Debug (1,"%s is using an MD5 encoded password", username); SHA_CTX ctx1, ctx2; unsigned char digest_interim[SHA_DIGEST_LENGTH]; @@ -96,14 +96,14 @@ bool verifyPassword(const char *username, const char *input_password, const char sprintf(&final_hash[i*2]+1, "%02X", (unsigned int)digest_final[i]); final_hash[SHA_DIGEST_LENGTH *2 + 1]=0; - Info ("Computed password_hash:%s, stored password_hash:%s", final_hash, db_password_hash); + Debug (1,"Computed password_hash:%s, stored password_hash:%s", final_hash, db_password_hash); Debug (5, "Computed password_hash:%s, stored password_hash:%s", final_hash, db_password_hash); password_correct = (strcmp(db_password_hash, final_hash)==0); } else if ((db_password_hash[0] == '$') && (db_password_hash[1]== '2') &&(db_password_hash[3] == '$')) { // BCRYPT - Info ("%s is using a bcrypt encoded password", username); + Debug (1,"%s is using a bcrypt encoded password", username); BCrypt bcrypt; std::string input_hash = bcrypt.generateHash(std::string(input_password)); password_correct = bcrypt.validatePassword(std::string(input_password), std::string(db_password_hash)); diff --git a/src/zm_user.cpp b/src/zm_user.cpp index 68b52e08c..35f25f7c9 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -152,7 +152,7 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { key += remote_addr; } - Info ("Inside zmLoadTokenUser, formed key=%s", key.c_str()); + Debug (1,"Inside zmLoadTokenUser, formed key=%s", key.c_str()); std::pair ans = verifyToken(jwt_token_str, key); std::string username = ans.first; @@ -192,7 +192,7 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) { return NULL; } - Info ("Got stored expiry time of %u",stored_iat); + Debug (1,"Got stored expiry time of %u",stored_iat); Info ("Authenticated user '%s' via token", username.c_str()); mysql_free_result(result); return user; diff --git a/src/zms.cpp b/src/zms.cpp index 8442b6a65..64c1103db 100644 --- a/src/zms.cpp +++ b/src/zms.cpp @@ -161,7 +161,7 @@ int main( int argc, const char *argv[] ) { strncpy( auth, value, sizeof(auth)-1 ); } else if ( !strcmp( name, "token" ) ) { jwt_token_str = value; - Info("ZMS: JWT token found: %s", jwt_token_str.c_str()); + Debug(1,"ZMS: JWT token found: %s", jwt_token_str.c_str()); } else if ( !strcmp( name, "user" ) ) { username = UriDecode( value ); diff --git a/web/skins/classic/views/options.php b/web/skins/classic/views/options.php index d7b264834..c0b437855 100644 --- a/web/skins/classic/views/options.php +++ b/web/skins/classic/views/options.php @@ -345,7 +345,7 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI dbQuery('UPDATE Users SET APIEnabled=1 WHERE Id=?', array($markUid)); // echo "UPDATE Users SET APIEnabled=1"." WHERE Id=".$markUid."
"; } - echo "Updated."; + echo "Updated"; } if(array_key_exists('revokeAllTokens',$_POST)){ From 881d531fe960d35c33889128b3d258197269c626 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 12 May 2019 18:19:19 -0400 Subject: [PATCH 70/90] make old API auth optional, on by default --- .../lib/ZoneMinder/ConfigData.pm.in | 11 +++ web/api/app/Controller/HostController.php | 78 +++++++++---------- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in index c6ec697f1..ef456f085 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in @@ -396,6 +396,17 @@ our @options = ( type => $types{boolean}, category => 'system', }, + { + name => 'ZM_OPT_USE_LEGACY_API_AUTH', + default => 'yes', + description => 'Enable legacy API authentication', + help => q` + Starting version 1.34.0, ZoneMinder uses a more secure + Authentication mechanism using JWT tokens. Older versions used a less secure MD5 based auth hash. It is recommended you turn this off after you are sure you don't need it. If you are using a 3rd party app that relies on the older API auth mechanisms, you will have to update that app if you turn this off. Note that zmNinja 1.3.057 onwards supports the new token system + `, + type => $types{boolean}, + category => 'system', + }, { name => 'ZM_OPT_USE_EVENTNOTIFICATION', default => 'no', diff --git a/web/api/app/Controller/HostController.php b/web/api/app/Controller/HostController.php index 7b8c7c0ff..a296ef2bc 100644 --- a/web/api/app/Controller/HostController.php +++ b/web/api/app/Controller/HostController.php @@ -31,56 +31,52 @@ class HostController extends AppController { } function login() { - $cred_depr = $this->_getCredentialsDeprecated(); - $ver = $this->_getVersion(); - + $mUser = $this->request->query('user') ? $this->request->query('user') : $this->request->data('user'); $mPassword = $this->request->query('pass') ? $this->request->query('pass') : $this->request->data('pass'); $mToken = $this->request->query('token') ? $this->request->query('token') : $this->request->data('token'); + $ver = $this->_getVersion(); + $cred = []; + $cred_depr = []; + if ($mUser && $mPassword) { - $cred = $this->_getCredentials(true); - // if you authenticated via user/pass then generate new refresh - $this->set(array( - 'access_token'=>$cred[0], - 'access_token_expires'=>$cred[1], - 'refresh_token'=>$cred[2], - 'refresh_token_expires'=>$cred[3], - 'credentials'=>$cred_depr[0], - 'append_password'=>$cred_depr[1], - 'version' => $ver[0], - 'apiversion' => $ver[1], - '_serialize' => array( - 'access_token', - 'access_token_expires', - 'refresh_token', - 'refresh_token_expires', - 'version', - 'credentials', - 'append_password', - 'apiversion' - ))); + $cred = $this->_getCredentials(true); // generate refresh } else { - $cred = $this->_getCredentials(false); - $this->set(array( - 'access_token'=>$cred[0], - 'access_token_expires'=>$cred[1], - 'credentials'=>$cred_depr[0], - 'append_password'=>$cred_depr[1], - 'version' => $ver[0], - 'apiversion' => $ver[1], - '_serialize' => array( - 'access_token', - 'access_token_expires', - 'version', - 'credentials', - 'append_password', - 'apiversion' - ))); - + $cred = $this->_getCredentials(false); // don't generate refresh } + $login_array = array ( + 'access_token'=>$cred[0], + 'access_token_expires'=>$cred[1], + 'version' => $ver[0], + 'apiversion' => $ver[1] + ); + + $login_serialize_list = array ( + 'access_token', + 'access_token_expires', + 'version', + 'apiversion' + ); + + if ($mUser && mPassword) { + $login_array['refresh_token'] = $cred[2]; + $login_array['refresh_token_expires'] = $cred[3]; + array_push ($login_serialize_list, 'refresh_token', 'refresh_token_expires'); + } + + if (ZM_OPT_USE_LEGACY_API_AUTH) { + $cred_depr = $this->_getCredentialsDeprecated(); + $login_array ['credentials']=$cred_depr[0]; + $login_array ['append_password']=$cred_depr[1]; + array_push ($login_serialize_list, 'credentials', 'append_password'); + } + + $this->set($login_array, + '_serialize' => $login_serialize_list); + } // end function login() From ec279ccc9af525ea0c5d1d023db68520965fa519 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 12 May 2019 18:51:07 -0400 Subject: [PATCH 71/90] make old API auth mechanism optional --- web/api/app/Controller/HostController.php | 36 +++++++++++++++++------ 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/web/api/app/Controller/HostController.php b/web/api/app/Controller/HostController.php index a296ef2bc..b5629aca4 100644 --- a/web/api/app/Controller/HostController.php +++ b/web/api/app/Controller/HostController.php @@ -36,6 +36,11 @@ class HostController extends AppController { $mPassword = $this->request->query('pass') ? $this->request->query('pass') : $this->request->data('pass'); $mToken = $this->request->query('token') ? $this->request->query('token') : $this->request->data('token'); + + if ( !($mUser && $mPassword) && !$mToken ) { + throw new UnauthorizedException(__('No identity provided')); + } + $ver = $this->_getVersion(); $cred = []; $cred_depr = []; @@ -47,21 +52,28 @@ class HostController extends AppController { $cred = $this->_getCredentials(false); // don't generate refresh } + $this->set(array( + 'credentials' => $cred[0], + 'append_password'=>$cred[1], + 'version' => $ver[0], + 'apiversion' => $ver[1], + '_serialize' => array('credentials', + 'append_password', + 'version', + 'apiversion' + ))); + $login_array = array ( 'access_token'=>$cred[0], - 'access_token_expires'=>$cred[1], - 'version' => $ver[0], - 'apiversion' => $ver[1] + 'access_token_expires'=>$cred[1] ); $login_serialize_list = array ( 'access_token', - 'access_token_expires', - 'version', - 'apiversion' + 'access_token_expires' ); - if ($mUser && mPassword) { + if ($mUser && $mPassword) { $login_array['refresh_token'] = $cred[2]; $login_array['refresh_token_expires'] = $cred[3]; array_push ($login_serialize_list, 'refresh_token', 'refresh_token_expires'); @@ -74,8 +86,14 @@ class HostController extends AppController { array_push ($login_serialize_list, 'credentials', 'append_password'); } - $this->set($login_array, - '_serialize' => $login_serialize_list); + + $login_array['version'] = $ver[0]; + $login_array['apiversion'] = $ver[1]; + array_push ($login_serialize_list, 'version', 'apiversion'); + + $login_array["_serialize"] = $login_serialize_list; + + $this->set($login_array); } // end function login() From 41ae745b176e132da5ca3b649f6e372e69af34f5 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 12 May 2019 18:53:51 -0400 Subject: [PATCH 72/90] removed stale code --- web/api/app/Controller/HostController.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/web/api/app/Controller/HostController.php b/web/api/app/Controller/HostController.php index b5629aca4..9ff4e7c76 100644 --- a/web/api/app/Controller/HostController.php +++ b/web/api/app/Controller/HostController.php @@ -52,17 +52,6 @@ class HostController extends AppController { $cred = $this->_getCredentials(false); // don't generate refresh } - $this->set(array( - 'credentials' => $cred[0], - 'append_password'=>$cred[1], - 'version' => $ver[0], - 'apiversion' => $ver[1], - '_serialize' => array('credentials', - 'append_password', - 'version', - 'apiversion' - ))); - $login_array = array ( 'access_token'=>$cred[0], 'access_token_expires'=>$cred[1] From 87e407aa906ae2e4d913d4f442abb6d747b1a425 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Mon, 13 May 2019 10:31:09 -0400 Subject: [PATCH 73/90] forgot to checkin update file --- db/zm_update-1.33.9.sql | 3 +++ distros/debian/control | 4 ++-- scripts/zmupdate.pl.in | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 db/zm_update-1.33.9.sql diff --git a/db/zm_update-1.33.9.sql b/db/zm_update-1.33.9.sql new file mode 100644 index 000000000..2b298b8b9 --- /dev/null +++ b/db/zm_update-1.33.9.sql @@ -0,0 +1,3 @@ +ALTER TABLE `Users` +ADD COLUMN `TokenMinExpiry` BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER `MonitorIds`, +ADD COLUMN `APIEnabled` tinyint(3) UNSIGNED NOT NULL default 1 AFTER `TokenMinExpiry`; diff --git a/distros/debian/control b/distros/debian/control index 2b46f44e1..dd5a405cd 100644 --- a/distros/debian/control +++ b/distros/debian/control @@ -26,7 +26,7 @@ Build-Depends: debhelper (>= 9), cmake , libdata-dump-perl, libclass-std-fast-perl, libsoap-wsdl-perl, libio-socket-multicast-perl, libdigest-sha-perl , libsys-cpu-perl, libsys-meminfo-perl , libdata-uuid-perl - , libssl-dev + , libssl-dev,libdigest-bcrypt-perl Standards-Version: 3.9.4 Package: zoneminder @@ -52,7 +52,7 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} , zip , libvlccore5 | libvlccore7 | libvlccore8, libvlc5 , libpolkit-gobject-1-0, php5-gd - , libssl + , libssl,libdigest-bcrypt-perl Recommends: mysql-server | mariadb-server Description: Video camera security and surveillance solution ZoneMinder is intended for use in single or multi-camera video security diff --git a/scripts/zmupdate.pl.in b/scripts/zmupdate.pl.in index bb9dddac4..136856d64 100644 --- a/scripts/zmupdate.pl.in +++ b/scripts/zmupdate.pl.in @@ -312,6 +312,7 @@ if ( $migrateEvents ) { if ( $freshen ) { print( "\nFreshening configuration in database\n" ); migratePaths(); + migratePasswords(); ZoneMinder::Config::loadConfigFromDB(); ZoneMinder::Config::saveConfigToDB(); } @@ -999,6 +1000,20 @@ sub patchDB { } +sub migratePasswords { + + print ("****** MIGRATION"); + my $sql = "select * from Users"; + my $sth = $dbh->prepare_cached( $sql ) or die( "Can't prepare '$sql': ".$dbh->errstr() ); + my $res = $sth->execute() or die( "Can't execute: ".$sth->errstr() ); + while( my $user = $sth->fetchrow_hashref() ) { + my $scheme = substr $user->{Password}, 0, 1; + if ($scheme eq "*") { + print ("*********** HOLY GODZILLA ".$user->{Username}. " uses PASSWORD"); + } + } +} + sub migratePaths { my $customConfigFile = '@ZM_CONFIG_SUBDIR@/zmcustom.conf'; From e9f843f29707f2f3c8f5c3792357d05e3ff64612 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Mon, 13 May 2019 14:29:24 -0400 Subject: [PATCH 74/90] bulk overlay hash mysql encoded passwords --- distros/debian/control | 6 ++++-- scripts/zmupdate.pl.in | 17 +++++++++++++---- web/includes/auth.php | 31 +++++++++++++++++++++---------- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/distros/debian/control b/distros/debian/control index dd5a405cd..59afe46a8 100644 --- a/distros/debian/control +++ b/distros/debian/control @@ -26,7 +26,7 @@ Build-Depends: debhelper (>= 9), cmake , libdata-dump-perl, libclass-std-fast-perl, libsoap-wsdl-perl, libio-socket-multicast-perl, libdigest-sha-perl , libsys-cpu-perl, libsys-meminfo-perl , libdata-uuid-perl - , libssl-dev,libdigest-bcrypt-perl + , libdigest-bcrypt-perl, libdata-entropy-perl Standards-Version: 3.9.4 Package: zoneminder @@ -52,7 +52,9 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} , zip , libvlccore5 | libvlccore7 | libvlccore8, libvlc5 , libpolkit-gobject-1-0, php5-gd - , libssl,libdigest-bcrypt-perl + , libssl + ,libdigest-bcrypt-perl, libdata-entropy-perl + Recommends: mysql-server | mariadb-server Description: Video camera security and surveillance solution ZoneMinder is intended for use in single or multi-camera video security diff --git a/scripts/zmupdate.pl.in b/scripts/zmupdate.pl.in index 136856d64..8c620443b 100644 --- a/scripts/zmupdate.pl.in +++ b/scripts/zmupdate.pl.in @@ -51,6 +51,8 @@ configuring upgrades etc, including on the fly upgrades. use strict; use bytes; use version; +use Digest; +use Data::Entropy::Algorithms qw(rand_bits); # ========================================================================== # @@ -1001,15 +1003,22 @@ sub patchDB { } sub migratePasswords { - - print ("****** MIGRATION"); + print ("Migratings passwords, if any...\n"); my $sql = "select * from Users"; my $sth = $dbh->prepare_cached( $sql ) or die( "Can't prepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute() or die( "Can't execute: ".$sth->errstr() ); while( my $user = $sth->fetchrow_hashref() ) { - my $scheme = substr $user->{Password}, 0, 1; + my $scheme = substr($user->{Password}, 0, 1); if ($scheme eq "*") { - print ("*********** HOLY GODZILLA ".$user->{Username}. " uses PASSWORD"); + print ("-->".$user->{Username}. " password will be migrated\n"); + my $bcrypt = Digest->new('Bcrypt', cost=>10, salt=>rand_bits(16*8)); + my $settings = $bcrypt->settings(); + my $pass_hash = $bcrypt->add($user->{Password})->bcrypt_b64digest; + #print ("--- New pass overlay ----".$pass_hash); + my $new_pass_hash = "-ZM-".$settings.$pass_hash; + $sql = "UPDATE Users SET PASSWORD=? WHERE Username=?"; + my $sth = $dbh->prepare_cached( $sql ) or die( "Can't prepare '$sql': ".$dbh->errstr() ); + my $res = $sth->execute($new_pass_hash, $user->{Username}) or die( "Can't execute: ".$sth->errstr() ); } } } diff --git a/web/includes/auth.php b/web/includes/auth.php index 643feb952..581fba1be 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -131,20 +131,31 @@ function userLogin($username='', $password='', $passwordHashed=false, $apiLogin $password_type = 'mysql'; } - else { - // bcrypt can have multiple signatures - if (preg_match('/^\$2[ayb]\$.+$/', $saved_password)) { + elseif (preg_match('/^\$2[ayb]\$.+$/', $saved_password)) { + ZM\Logger::Debug('bcrypt signature found, assumed bcrypt password'); + $password_type='bcrypt'; + $password_correct = $passwordHashed? ($password == $saved_password) : password_verify($password, $saved_password); + } + // 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 + // for every wrong password entered. This will only be invoked for passwords zmupdate.pl has + // overlay hashed + elseif (substr($saved_password, 0,4) == '-ZM-') { + ZM\Logger::Debug("Detected bcrypt overlay hashing for $username"); + ZM\Info("Detected bcrypt overlay hashing for $username"); + $bcrypt_hash = substr ($saved_password, 4); + $mysql_encoded_password ='*'.strtoupper(sha1(sha1($password, true))); + 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_type = "mysql"; // so we can migrate later down - ZM\Logger::Debug ('bcrypt signature found, assumed bcrypt password'); - $password_type='bcrypt'; - $password_correct = $passwordHashed? ($password == $saved_password) : password_verify($password, $saved_password); - } - else { + } + else { // we really should nag the user not to use plain ZM\Warning ('assuming plain text password as signature is not known. Please do not use plain, it is very insecure'); $password_type = 'plain'; $password_correct = ($saved_password == $password); - } } } else { @@ -165,7 +176,7 @@ function userLogin($username='', $password='', $passwordHashed=false, $apiLogin ZM\Info("Login successful for user \"$username\""); $user = $saved_user_details; 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); } unset($_SESSION['loginFailed']); From 4a2ac470eedd2c4479f2d9c49686c9d6baceafe8 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Mon, 13 May 2019 14:32:48 -0400 Subject: [PATCH 75/90] add back ssl_dev, got deleted --- distros/debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/distros/debian/control b/distros/debian/control index 59afe46a8..87873f294 100644 --- a/distros/debian/control +++ b/distros/debian/control @@ -26,6 +26,7 @@ Build-Depends: debhelper (>= 9), cmake , libdata-dump-perl, libclass-std-fast-perl, libsoap-wsdl-perl, libio-socket-multicast-perl, libdigest-sha-perl , libsys-cpu-perl, libsys-meminfo-perl , libdata-uuid-perl + , libssl-dev , libdigest-bcrypt-perl, libdata-entropy-perl Standards-Version: 3.9.4 From 4027a5adf8528a48987b495e81f58e3f4d799a2a Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Mon, 13 May 2019 14:48:26 -0400 Subject: [PATCH 76/90] fix update script --- db/zm_update-1.33.9.sql | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/db/zm_update-1.33.9.sql b/db/zm_update-1.33.9.sql index 2b298b8b9..e0d289ba4 100644 --- a/db/zm_update-1.33.9.sql +++ b/db/zm_update-1.33.9.sql @@ -1,3 +1,27 @@ -ALTER TABLE `Users` -ADD COLUMN `TokenMinExpiry` BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER `MonitorIds`, -ADD COLUMN `APIEnabled` tinyint(3) UNSIGNED NOT NULL default 1 AFTER `TokenMinExpiry`; +-- +-- Add per user API enable/disable and ability to set a minimum issued time for tokens +-- + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Users' + AND column_name = 'TokenMinExpiry' + ) > 0, +"SELECT 'Column TokenMinExpiry already exists in Users'", +"ALTER TABLE Users ADD `TokenMinExpiry` BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER `MonitorIds`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Users' + AND column_name = 'APIEnabled' + ) > 0, +"SELECT 'Column APIEnabled already exists in Users'", +"ALTER TABLE Users ADD `APIEnabled` tinyint(3) UNSIGNED NOT NULL default 1 AFTER `TokenMinExpiry`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; From 95460a945ab7e5c63dc0551bdd6230a3026ceb9e Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Tue, 14 May 2019 19:22:49 -0400 Subject: [PATCH 77/90] added token support to index.php --- web/includes/auth.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/web/includes/auth.php b/web/includes/auth.php index 581fba1be..33d2b3fb6 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -386,6 +386,14 @@ if ( ZM_OPT_USE_AUTH ) { } else if ( isset($_REQUEST['username']) and isset($_REQUEST['password']) ) { userLogin($_REQUEST['username'], $_REQUEST['password'], false); } + + if ( ZM_AUTH_HASH_LOGINS && empty($user) && !empty($_REQUEST['token']) ) { + + $ret = validateToken($_REQUEST['token'], 'access'); + $user = $ret[0]; + } + + if ( !empty($user) ) { // generate it once here, while session is open. Value will be cached in session and return when called later on generateAuthHash(ZM_AUTH_HASH_IPS); From a07da01f0ca57b505d442e86bcf8847433195b86 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Wed, 15 May 2019 13:57:51 -0400 Subject: [PATCH 78/90] reworked API document for new changes in 2.0 --- docs/api.rst | 318 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 244 insertions(+), 74 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 2f90b7fdf..d60847d92 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,10 +1,12 @@ + API ==== -This document will provide an overview of ZoneMinder's API. This is work in progress. +This document will provide an overview of ZoneMinder's API. Overview ^^^^^^^^ + In an effort to further 'open up' ZoneMinder, an API was needed. This will allow quick integration with and development of ZoneMinder. @@ -12,87 +14,175 @@ The API is built in CakePHP and lives under the ``/api`` directory. It provides a RESTful service and supports CRUD (create, retrieve, update, delete) functions for Monitors, Events, Frames, Zones and Config. -Streaming Interface -^^^^^^^^^^^^^^^^^^^ -Developers working on their application often ask if there is an "API" to receive live streams, or recorded event streams. -It is possible to stream both live and recorded streams. This isn't strictly an "API" per-se (that is, it is not integrated -into the Cake PHP based API layer discussed here) and also why we've used the term "Interface" instead of an "API". +API evolution +^^^^^^^^^^^^^^^ -Live Streams -~~~~~~~~~~~~~~ -What you need to know is that if you want to display "live streams", ZoneMinder sends you streaming JPEG images (MJPEG) -which can easily be rendered in a browser using an ``img src`` tag. +The ZoneMinder API has evolved over time. Broadly speaking the iterations were as follows: -For example: +* Prior to version 1.29, there really was no API layer. Users had to use the same URLs that the web console used to 'mimic' operations, or use an XML skin +* Starting version 1.29, a v1.0 CakePHP based API was released which continues to evolve over time. From a security perspective, it still tied into ZM auth and required client cookies for many operations. Primarily, two authentication modes were offered: + * You use cookies to maintain session state (`ZM_SESS_ID`) + * You use an authentication hash to validate yourself, which included encoding personal information and time stamps which at times caused timing validation issues, especially for mobile consumers +* Starting version 1.34, ZoneMinder has introduced a new "token" based system which is based JWT. We have given it a '2.0' version ID. These tokens don't encode any personal data and can be statelessly passed around per request. It introduces concepts like access tokens, refresh tokens and per user level API revocation to manage security better. The internal components of ZoneMinder all support this new scheme now and if you are using the APIs we strongly recommend you migrate to 1.34 and use this new token system (as a side note, 1.34 also moves from MYSQL PASSWORD to Bcrypt for passwords, which is also a good reason why you should migate). +* Note that as of 1.34, both versions of API access will work (tokens and the older auth hash mechanism). -:: +.. NOTE:: + For the rest of the document, we will specifically highlight v2.0 only features. If you don't see a special mention, assume it applies for both API versions. - - -will display a live feed from monitor id 1, scaled down by 50% in quality and resized to 640x480px. - -* This assumes ``/zm/cgi-bin`` is your CGI_BIN path. Change it to what is correct in your system -* The "auth" token you see above is required if you use ZoneMinder authentication. To understand how to get the auth token, please read the "Login, Logout & API security" section below. -* The "connkey" parameter is essentially a random number which uniquely identifies a stream. If you don't specify a connkey, ZM will generate its own. It is recommended to generate a connkey because you can then use it to "control" the stream (pause/resume etc.) -* Instead of dealing with the "auth" token, you can also use ``&user=username&pass=password`` where "username" and "password" are your ZoneMinder username and password respectively. Note that this is not recommended because you are transmitting them in a URL and even if you use HTTPS, they may show up in web server logs. - - -PTZ on live streams -------------------- -PTZ commands are pretty cryptic in ZoneMinder. This is not meant to be an exhaustive guide, but just something to whet your appetite: - - -Lets assume you have a monitor, with ID=6. Let's further assume you want to pan it left. - -You'd need to send a: -``POST`` command to ``https://yourserver/zm/index.php`` with the following data payload in the command (NOT in the URL) - -``view=request&request=control&id=6&control=moveConLeft&xge=30&yge=30`` - -Obviously, if you are using authentication, you need to be logged in for this to work. - -Like I said, at this stage, this is only meant to get you started. Explore the ZoneMinder code and use "Inspect source" as you use PTZ commands in the ZoneMinder source code. -`control_functions.php `__ is a great place to start. - - -Pre-recorded (past event) streams -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Similar to live playback, if you have chosen to store events in JPEG mode, you can play it back using: - -:: - - - - -* This assumes ``/zm/cgi-bin`` is your CGI_BIN path. Change it to what is correct in your system -* This will playback event 293820, starting from frame 1 as an MJPEG stream -* Like before, you can add more parameters like ``scale`` etc. -* auth and connkey have the same meaning as before, and yes, you can replace auth by ``&user=usename&pass=password`` as before and the same security concerns cited above apply. - -If instead, you have chosen to use the MP4 (Video) storage mode for events, you can directly play back the saved video file: - -:: - - - -* This will play back the video recording for event 294690 - -What other parameters are supported? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The best way to answer this question is to play with ZoneMinder console. Open a browser, play back live or recorded feed, and do an "Inspect Source" to see what parameters -are generated. Change and observe. Enabling API -^^^^^^^^^^^^ -A default ZoneMinder installs with APIs enabled. You can explictly enable/disable the APIs -via the Options->System menu by enabling/disabling ``OPT_USE_API``. Note that if you intend -to use APIs with 3rd party apps, such as zmNinja or others that use APIs, you should also -enable ``AUTH_HASH_LOGINS``. +^^^^^^^^^^^^^ -Login, Logout & API Security -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +ZoneMinder comes with APIs enabled. To check if APIs are enabled, visit ``Options->System``. If ``OPT_USE_API`` is enabled, your APIs are active. +For v2.0 APIs, you have an additional option right below it - ``OPT_USE_LEGACY_API_AUTH`` which is enabled by default. When enabled, the `login.json` API (discussed later) will return both the old style (``auth=``) and new style (``token=``) credentials. The reason this is enabled by default is because any existing apps that use the API would break if they were not updated to use v2.0. (Note that zmNinja 1.3.057 and beyond will support tokens) + +Enabling secret key +^^^^^^^^^^^^^^^^^^^ + +* It is important that you create a "Secret Key". This needs to be a set of hard to guess characters, that only you know. ZoneMinder does not create a key for you. It is your responsibility to create it. If you haven't created one already, please do so by going to ``Options->Systems`` and populating ``AUTH_HASH_SECRET``. Don't forget to save. +* If you plan on using V2.0 token based security, **it is mandatory to populate this secret key**, as it is used to sign the token. If you don't, token authentication will fail. V1.0 did not mandate this requirement. + + +Getting API key +^^^^^^^^^^^^^^^^^^^^^^^ + +To get API key: + +:: + + curl -XPOST [-c cookies.txt] -d "user=yourusername&pass=yourpassword" https://yourserver/zm/api/host/login.json + + +The ``[-c cookies.txt]`` is optional, and will be explained in the next section. + +This returns a payload like this for API v1.0: + +:: + + { + "credentials": "auth=05f3a50e8f7063", + "append_password": 0, + "version": "1.33.9", + "apiversion": "1.0" + } + +Or for API 2.0: + +:: + + { + "access_token": "eyJ0eXAiOiJKHE", + "access_token_expires": 3600, + "refresh_token": "eyJ0eXAiOimPs", + "refresh_token_expires": 86400, + "credentials": "auth=05f3a50e8f7063", # only if OPT_USE_LEGACY_API_AUTH is enabled + "append_password": 0, # only if OPT_USE_LEGACY_API_AUTH is enabled + "version": "1.33.9", + "apiversion": "2.0" + } + +Using these keys with subsequent requests +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Once you have the keys (a.k.a credentials (v1.0, v2.0) or token (v2.0)) you should now supply that credential to subsequent API calls like this: + +:: + + # RECOMMENDED: v2.0 token based + curl -XPOST https://yourserver/zm/api/monitors.json&token= + # or + + # v1.0 or 2.0 based API access (will only work if AUTH_HASH_LOGINS is enabled) + curl -XPOST -d "auth=" https://yourserver/zm/api/monitors.json + # or + curl -XGET https://yourserver/zm/api/monitors.json&auth= + # or, if you specified -c cookies.txt in the original login request + curl -b cookies.txt -XGET https://yourserver/zm/api/monitors.json + + +.. NOTE:: + ZoneMinder's API layer allows API keys to be encoded either as a querty parameter or as a data payload. If you don't pass keys, you could use cookies (not recommended as a general approach) + + +Key lifetime (v1.0) +^^^^^^^^^^^^^^^^^^^^^ + +If you are using the old ``auth_hash`` mechanism present in v1.0, then the credentials will time out based on PHP session timeout. This is often confusing and sometime causes additional issues due to the fact that the old method also includes timestamps in its hash. + +Key lifetime (v2.0) +^^^^^^^^^^^^^^^^^^^^^^ + +In version 2.0, it is very easy to know when a key will expire. You can find that out from the ``access_token_expires`` and ``refresh_token_exipres`` values (in seconds). You should refresh the keys before the timeout occurs, or you will not be able to use the APIs. + +Understanding access/refresh tokens (v2.0) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you are using V2.0, then you need to know how to use these tokens effectively: + +* Access tokens are short lived. ZoneMinder issues access tokens that live for 3600 seconds (1 hour). +* Access tokens should be used for all subsequent API accesses. +* Refresh tokens should ONLY be used to generate new access tokens. For example, if an access token lives for 1 hour, before the hour completes, invoke the ``login.json`` API above with the refresh token to get a new access token. ZoneMinder issues refresh tokens that live for 24 hours. +* To generate a new refresh token before 24 hours are up, you will need to pass your user login and password to ``login.json`` + +**To Summarize:** + +* Pass your ``username`` and ``password`` to ``login.json`` only once in 24 hours to renew your tokens +* Pass your "refresh token" to ``login.json`` once an hour to renew your ``access token`` +* Use your ``access token`` for all API invocations. + +In fact, V2.0 will reject your request (if it is not to ``login.json``) if it comes with a refresh token instead of an access token to discourage usage of this token when it should not be used. + +This minimizes the amount of sensitive data that is sent over the wire and the lifetime durations are made so that if they get compromised, you can regenerate or invalidate them (more on this later) + +Understanding key security +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* Version 1.0 uses an MD5 hash to generate the credentials. The hash is computed over your secret key (if avaiable), username, password and some time parameters (along with remote IP if enabled). This is not a secure/recommended hashing mechanism. If your auth hash is compromised, an attacker will be able to use your hash till it expires. To avoid this, you could disable the user in ZoneMinder. + +* Version 2.0 uses a different approach. The hash is a simple base64 encoded form of "claims", but signed with your secret key. Consider for example, the following access key: + +:: + + eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJab25lTWluZGVyIiwiaWF0IjoxNTU3OTQwNzUyLCJleHAiOjE1NTc5NDQzNTIsInVzZXIiOiJhZG1pbiIsInR5cGUiOiJhY2Nlc3MifQ.-5VOcpw3cFHiSTN5zfGDSrrPyVya1M8_2Anh5u6eNlI + +If you were to use any `JWT token verifier `__ it can easily decode that token and will show: + +:: + + { + "iss": "ZoneMinder", + "iat": 1557940752, + "exp": 1557944352, + "user": "admin", + "type": "access" + } + Invalid Signature + + +Don't be surprised. JWT tokens are not meant to be encrypted. It is just an assertion of a claim. It states that the issuer of this token was ZoneMinder, +It was issued at (iat) Wednesday, 2019-05-15 17:19:12 UTC and will expire on (exp) Wednesday, 2019-05-15 18:19:12 UTC. This token claims to be owned by an admin and is an access token. If your token were to be stolen, this information is available to the person who stole it. Note that there are no sensitive details like passwords in this claim. + +However, that person will **not** have your secret key as part of this token and therefore, will NOT be able to create a new JWT token to get, say, a refresh token. They will however, be able to use your access token to access resources just like the auth hash above, till the access token expires (1hr). To revoke this token, you don't need to disable the user. Go to ``Options->API`` and tap on "Revoke All Access Tokens". This will invalidate the token immediately (this option will invalidate all tokens for all users, and new ones will need to be generated). + +Over time, we will provide you with more fine grained access to these options. + +**Summarizing good practices:** + +* Use HTTPS, not HTTP +* If possible, use free services like `LetsEncrypt `__ instead of self-signed certificates (sometimes this is not possible) +* Keep your tokens as private as possible, and use them as recommended above +* If you believe your tokens are compromised, revoke them, but also check if your attacker has compromised more than you think (example, they may also have your username/password or access to your system via other exploits, in which case they can regenerate as many tokens/credentials as they want). + + + +.. NOTE:: + Subsequent sections don't explicitly callout the key addition to APIs. We assume that you will append the correct keys as per our explanation above. + + + +Logout APIs +^^^^^^^^^^^^^^ The APIs tie into ZoneMinder's existing security model. This means if you have OPT_AUTH enabled, you need to log into ZoneMinder using the same browser you plan to use the APIs from. If you are developing an app that relies on the API, you need @@ -585,6 +675,86 @@ Returns: This only works if you have a multiserver setup in place. If you don't it will return an empty array. +Streaming Interface +^^^^^^^^^^^^^^^^^^^ +Developers working on their application often ask if there is an "API" to receive live streams, or recorded event streams. +It is possible to stream both live and recorded streams. This isn't strictly an "API" per-se (that is, it is not integrated +into the Cake PHP based API layer discussed here) and also why we've used the term "Interface" instead of an "API". + +Live Streams +~~~~~~~~~~~~~~ +What you need to know is that if you want to display "live streams", ZoneMinder sends you streaming JPEG images (MJPEG) +which can easily be rendered in a browser using an ``img src`` tag. + +For example: + +:: + + + #or + + + +will display a live feed from monitor id 1, scaled down by 50% in quality and resized to 640x480px. + +* This assumes ``/zm/cgi-bin`` is your CGI_BIN path. Change it to what is correct in your system +* The "auth" token you see above is required if you use ZoneMinder authentication. To understand how to get the auth token, please read the "Login, Logout & API security" section below. +* The "connkey" parameter is essentially a random number which uniquely identifies a stream. If you don't specify a connkey, ZM will generate its own. It is recommended to generate a connkey because you can then use it to "control" the stream (pause/resume etc.) +* Instead of dealing with the "auth" token, you can also use ``&user=username&pass=password`` where "username" and "password" are your ZoneMinder username and password respectively. Note that this is not recommended because you are transmitting them in a URL and even if you use HTTPS, they may show up in web server logs. + + +PTZ on live streams +------------------- +PTZ commands are pretty cryptic in ZoneMinder. This is not meant to be an exhaustive guide, but just something to whet your appetite: + + +Lets assume you have a monitor, with ID=6. Let's further assume you want to pan it left. + +You'd need to send a: +``POST`` command to ``https://yourserver/zm/index.php`` with the following data payload in the command (NOT in the URL) + +``view=request&request=control&id=6&control=moveConLeft&xge=30&yge=30`` + +Obviously, if you are using authentication, you need to be logged in for this to work. + +Like I said, at this stage, this is only meant to get you started. Explore the ZoneMinder code and use "Inspect source" as you use PTZ commands in the ZoneMinder source code. +`control_functions.php `__ is a great place to start. + + +Pre-recorded (past event) streams +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Similar to live playback, if you have chosen to store events in JPEG mode, you can play it back using: + +:: + + + #or + + + +* This assumes ``/zm/cgi-bin`` is your CGI_BIN path. Change it to what is correct in your system +* This will playback event 293820, starting from frame 1 as an MJPEG stream +* Like before, you can add more parameters like ``scale`` etc. +* auth and connkey have the same meaning as before, and yes, you can replace auth by ``&user=usename&pass=password`` as before and the same security concerns cited above apply. + +If instead, you have chosen to use the MP4 (Video) storage mode for events, you can directly play back the saved video file: + +:: + + + #or + + +* This will play back the video recording for event 294690 + +What other parameters are supported? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The best way to answer this question is to play with ZoneMinder console. Open a browser, play back live or recorded feed, and do an "Inspect Source" to see what parameters +are generated. Change and observe. + + + Further Reading ^^^^^^^^^^^^^^^^ As described earlier, treat this document as an "introduction" to the important parts of the API and streaming interfaces. From 0e72080c4a016cb667846760f2fd86125f03daf5 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Thu, 16 May 2019 09:37:11 -0400 Subject: [PATCH 79/90] Migrate from libdigest to crypt-eks-blowfish due to notice --- distros/debian/control | 4 ++-- scripts/zmupdate.pl.in | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/distros/debian/control b/distros/debian/control index 87873f294..3296b88c3 100644 --- a/distros/debian/control +++ b/distros/debian/control @@ -27,7 +27,7 @@ Build-Depends: debhelper (>= 9), cmake , libsys-cpu-perl, libsys-meminfo-perl , libdata-uuid-perl , libssl-dev - , libdigest-bcrypt-perl, libdata-entropy-perl + , libcrypt-eksblowfish-perl, libdata-entropy-perl Standards-Version: 3.9.4 Package: zoneminder @@ -54,7 +54,7 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} , libvlccore5 | libvlccore7 | libvlccore8, libvlc5 , libpolkit-gobject-1-0, php5-gd , libssl - ,libdigest-bcrypt-perl, libdata-entropy-perl + ,libcrypt-eksblowfish-perl, libdata-entropy-perl Recommends: mysql-server | mariadb-server Description: Video camera security and surveillance solution diff --git a/scripts/zmupdate.pl.in b/scripts/zmupdate.pl.in index 8c620443b..8dbc4d14f 100644 --- a/scripts/zmupdate.pl.in +++ b/scripts/zmupdate.pl.in @@ -51,7 +51,7 @@ configuring upgrades etc, including on the fly upgrades. use strict; use bytes; use version; -use Digest; +use Crypt::Eksblowfish::Bcrypt; use Data::Entropy::Algorithms qw(rand_bits); # ========================================================================== @@ -1011,11 +1011,10 @@ sub migratePasswords { my $scheme = substr($user->{Password}, 0, 1); if ($scheme eq "*") { print ("-->".$user->{Username}. " password will be migrated\n"); - my $bcrypt = Digest->new('Bcrypt', cost=>10, salt=>rand_bits(16*8)); - my $settings = $bcrypt->settings(); - my $pass_hash = $bcrypt->add($user->{Password})->bcrypt_b64digest; - #print ("--- New pass overlay ----".$pass_hash); - my $new_pass_hash = "-ZM-".$settings.$pass_hash; + my $salt = Crypt::Eksblowfish::Bcrypt::en_base64(rand_bits(16*8)); + my $settings = '$2a$10$'.$salt; + my $pass_hash = Crypt::Eksblowfish::Bcrypt::bcrypt($user->{Password},$settings); + my $new_pass_hash = "-ZM-".$pass_hash; $sql = "UPDATE Users SET PASSWORD=? WHERE Username=?"; my $sth = $dbh->prepare_cached( $sql ) or die( "Can't prepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute($new_pass_hash, $user->{Username}) or die( "Can't execute: ".$sth->errstr() ); From 06eb9a3bb2d61349b131856b755e773fb9a1de54 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Thu, 16 May 2019 16:15:16 -0400 Subject: [PATCH 80/90] merge typo --- src/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ba1894a6c..3628a4944 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,7 +4,6 @@ configure_file(zm_config.h.in "${CMAKE_CURRENT_BINARY_DIR}/zm_config.h" @ONLY) # Group together all the source files that are used by all the binaries (zmc, zma, zmu, zms etc) -<<<<<<< HEAD set(ZM_BIN_SRC_FILES zm_box.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp zm_config.cpp zm_coord.cpp zm_curl_camera.cpp zm.cpp zm_db.cpp zm_logger.cpp zm_event.cpp zm_frame.cpp zm_eventstream.cpp zm_exception.cpp zm_file_camera.cpp zm_ffmpeg_input.cpp zm_ffmpeg_camera.cpp zm_group.cpp zm_image.cpp zm_jpeg.cpp zm_libvlc_camera.cpp zm_local_camera.cpp zm_monitor.cpp zm_monitorstream.cpp zm_ffmpeg.cpp zm_mpeg.cpp zm_packet.cpp zm_packetqueue.cpp zm_poly.cpp zm_regexp.cpp zm_remote_camera.cpp zm_remote_camera_http.cpp zm_remote_camera_nvsocket.cpp zm_remote_camera_rtsp.cpp zm_rtp.cpp zm_rtp_ctrl.cpp zm_rtp_data.cpp zm_rtp_source.cpp zm_rtsp.cpp zm_rtsp_auth.cpp zm_sdp.cpp zm_signal.cpp zm_stream.cpp zm_swscale.cpp zm_thread.cpp zm_time.cpp zm_timer.cpp zm_user.cpp zm_utils.cpp zm_video.cpp zm_videostore.cpp zm_zone.cpp zm_storage.cpp zm_fifo.cpp zm_crypt.cpp) From 923f798e69f3fe31eb4e5c964ff774594a319309 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Fri, 17 May 2019 09:32:23 -0400 Subject: [PATCH 81/90] css classess for text that disappear --- web/lang/en_gb.php | 1 + web/skins/classic/css/base/skin.css | 43 +++++++++++++++++++++++++++++ web/skins/classic/views/options.php | 7 ++--- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index 9dfa2860c..1fdd112e0 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -102,6 +102,7 @@ $SLANG = array( 'AlarmRGBUnset' => 'You must set an alarm RGB colour', 'Alert' => 'Alert', 'All' => 'All', + 'AllTokensRevoked' => 'All Tokens Revoked', 'AnalysisFPS' => 'Analysis FPS', 'AnalysisUpdateDelay' => 'Analysis Update Delay', 'API' => 'API', diff --git a/web/skins/classic/css/base/skin.css b/web/skins/classic/css/base/skin.css index 069952d40..99692bff6 100644 --- a/web/skins/classic/css/base/skin.css +++ b/web/skins/classic/css/base/skin.css @@ -350,10 +350,53 @@ fieldset > legend { .alert, .warnText, .warning, .disabledText { color: #ffa801; } + + .alarm, .errorText, .error { color: #ff3f34; } +.timedErrorBox { + color:white; + background:#e74c3c; + border-radius:5px; + padding:5px; + -moz-animation: inAndOut 5s ease-in forwards; + -webkit-animation: inAndOut 5s ease-in forwards; + animation: inAndOut 5s ease-in forwards; +} + +/* + the timed classed auto disappear after 5s +*/ +.timedWarningBox { + color:white; + background:#e67e22; + border-radius:5px; + padding:5px; + -moz-animation: inAndOut 5s ease-in forwards; + -webkit-animation: inAndOut 5s ease-in forwards; + animation: inAndOut 5s ease-in forwards; +} + +.timedSuccessBox { + color:white; + background:#27ae60; + border-radius:5px; + padding:5px; + -moz-animation: inAndOut 5s ease-in forwards; + -webkit-animation: inAndOut 5s ease-in forwards; + animation: inAndOut 5s ease-in forwards; +} + +@keyframes inAndOut { + 0% {opacity:0;} + 10% {opacity:1;} + 90% {opacity:1;} + 100% {opacity:0;} +} + + .fakelink { color: #7f7fb2; cursor: pointer; diff --git a/web/skins/classic/views/options.php b/web/skins/classic/views/options.php index c0b437855..fa6a1fe61 100644 --- a/web/skins/classic/views/options.php +++ b/web/skins/classic/views/options.php @@ -330,7 +330,7 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI { $minTokenTime = time(); dbQuery ('UPDATE Users SET TokenMinExpiry=?', array ($minTokenTime)); - echo "All Tokens Revoked"; + echo "translate('AllTokensRevoked')"; } function updateSelected() @@ -338,14 +338,13 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI dbQuery("UPDATE Users SET APIEnabled=0"); foreach( $_REQUEST["tokenUids"] as $markUid ) { $minTime = time(); - // echo "UPDATE Users SET TokenMinExpiry=".$minTime." WHERE Id=".$markUid."
"; dbQuery('UPDATE Users SET TokenMinExpiry=? WHERE Id=?', array($minTime, $markUid)); } foreach( $_REQUEST["apiUids"] as $markUid ) { dbQuery('UPDATE Users SET APIEnabled=1 WHERE Id=?', array($markUid)); - // echo "UPDATE Users SET APIEnabled=1"." WHERE Id=".$markUid."
"; + } - echo "Updated"; + echo "translate('Updated'); } if(array_key_exists('revokeAllTokens',$_POST)){ From a4eff3e8e098acf995969f3359939bca00b368e1 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Fri, 17 May 2019 09:44:22 -0400 Subject: [PATCH 82/90] fixed html typo --- web/skins/classic/views/options.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/skins/classic/views/options.php b/web/skins/classic/views/options.php index fa6a1fe61..b0ac2af1b 100644 --- a/web/skins/classic/views/options.php +++ b/web/skins/classic/views/options.php @@ -330,7 +330,7 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI { $minTokenTime = time(); dbQuery ('UPDATE Users SET TokenMinExpiry=?', array ($minTokenTime)); - echo "translate('AllTokensRevoked')"; + echo "".translate('AllTokensRevoked').""; } function updateSelected() @@ -344,7 +344,7 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI dbQuery('UPDATE Users SET APIEnabled=1 WHERE Id=?', array($markUid)); } - echo "translate('Updated'); + echo "".translate('Updated').""; } if(array_key_exists('revokeAllTokens',$_POST)){ From e2dd11fdd4f42a0431f58f33c30396c9c53c08fa Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Fri, 17 May 2019 11:21:07 -0400 Subject: [PATCH 83/90] added deps to ubuntu control files --- distros/ubuntu1204/control | 10 ++++++++-- distros/ubuntu1604/control | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/distros/ubuntu1204/control b/distros/ubuntu1204/control index f1756c5e8..fdf9c186e 100644 --- a/distros/ubuntu1204/control +++ b/distros/ubuntu1204/control @@ -23,6 +23,8 @@ Build-Depends: debhelper (>= 9), python-sphinx | python3-sphinx, apache2-dev, dh ,libsys-mmap-perl [!hurd-any] ,libwww-perl ,libdata-uuid-perl + ,libssl-dev + ,libcrypt-eksblowfish-perl, libdata-entropy-perl # Unbundled (dh_linktree): ,libjs-jquery ,libjs-mootools @@ -63,8 +65,12 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} ,policykit-1 ,rsyslog | system-log-daemon ,zip - ,libdata-dump-perl, libclass-std-fast-perl, libsoap-wsdl-perl, libio-socket-multicast-perl, libdigest-sha-perl - , libsys-cpu-perl, libsys-meminfo-perl + ,libdata-dump-perl, libclass-std-fast-perl, libsoap-wsdl-perl + ,libio-socket-multicast-perl, libdigest-sha-perl + ,libsys-cpu-perl, libsys-meminfo-perl + ,libssl + ,libcrypt-eksblowfish-perl, libdata-entropy-perl + Recommends: ${misc:Recommends} ,libapache2-mod-php5 | php5-fpm ,mysql-server | virtual-mysql-server diff --git a/distros/ubuntu1604/control b/distros/ubuntu1604/control index 415f54c9f..bfabd3ad3 100644 --- a/distros/ubuntu1604/control +++ b/distros/ubuntu1604/control @@ -30,6 +30,8 @@ Build-Depends: debhelper (>= 9), dh-systemd, python-sphinx | python3-sphinx, apa ,libsys-mmap-perl [!hurd-any] ,libwww-perl ,libdata-uuid-perl + ,libssl-dev + ,libcrypt-eksblowfish-perl, libdata-entropy-perl # Unbundled (dh_linktree): ,libjs-jquery ,libjs-mootools @@ -76,6 +78,8 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} ,rsyslog | system-log-daemon ,zip ,libpcre3 + ,libssl + ,libcrypt-eksblowfish-perl, libdata-entropy-perl Recommends: ${misc:Recommends} ,libapache2-mod-php5 | libapache2-mod-php | php5-fpm | php-fpm ,mysql-server | mariadb-server | virtual-mysql-server From 682d95470d73e6154c25971839122d457dab5577 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Fri, 17 May 2019 11:47:20 -0400 Subject: [PATCH 84/90] spaces --- distros/ubuntu1204/control | 6 ++++-- distros/ubuntu1604/control | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/distros/ubuntu1204/control b/distros/ubuntu1204/control index fdf9c186e..d80cbc67a 100644 --- a/distros/ubuntu1204/control +++ b/distros/ubuntu1204/control @@ -24,7 +24,8 @@ Build-Depends: debhelper (>= 9), python-sphinx | python3-sphinx, apache2-dev, dh ,libwww-perl ,libdata-uuid-perl ,libssl-dev - ,libcrypt-eksblowfish-perl, libdata-entropy-perl + ,libcrypt-eksblowfish-perl + ,libdata-entropy-perl # Unbundled (dh_linktree): ,libjs-jquery ,libjs-mootools @@ -69,7 +70,8 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} ,libio-socket-multicast-perl, libdigest-sha-perl ,libsys-cpu-perl, libsys-meminfo-perl ,libssl - ,libcrypt-eksblowfish-perl, libdata-entropy-perl + ,libcrypt-eksblowfish-perl + ,libdata-entropy-perl Recommends: ${misc:Recommends} ,libapache2-mod-php5 | php5-fpm diff --git a/distros/ubuntu1604/control b/distros/ubuntu1604/control index bfabd3ad3..d07104100 100644 --- a/distros/ubuntu1604/control +++ b/distros/ubuntu1604/control @@ -31,7 +31,8 @@ Build-Depends: debhelper (>= 9), dh-systemd, python-sphinx | python3-sphinx, apa ,libwww-perl ,libdata-uuid-perl ,libssl-dev - ,libcrypt-eksblowfish-perl, libdata-entropy-perl + ,libcrypt-eksblowfish-perl + ,libdata-entropy-perl # Unbundled (dh_linktree): ,libjs-jquery ,libjs-mootools @@ -79,7 +80,8 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} ,zip ,libpcre3 ,libssl - ,libcrypt-eksblowfish-perl, libdata-entropy-perl + ,libcrypt-eksblowfish-perl + ,libdata-entropy-perl Recommends: ${misc:Recommends} ,libapache2-mod-php5 | libapache2-mod-php | php5-fpm | php-fpm ,mysql-server | mariadb-server | virtual-mysql-server From 304192472d74bfb2c280a7e340d2f0a07290b21a Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Fri, 17 May 2019 12:02:24 -0400 Subject: [PATCH 85/90] removed extra line --- distros/ubuntu1204/control | 1 - 1 file changed, 1 deletion(-) diff --git a/distros/ubuntu1204/control b/distros/ubuntu1204/control index d80cbc67a..1dc8a2b38 100644 --- a/distros/ubuntu1204/control +++ b/distros/ubuntu1204/control @@ -72,7 +72,6 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} ,libssl ,libcrypt-eksblowfish-perl ,libdata-entropy-perl - Recommends: ${misc:Recommends} ,libapache2-mod-php5 | php5-fpm ,mysql-server | virtual-mysql-server From 8e1037458ae675579c6e0b7efc6b0b1c9611530e Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sat, 18 May 2019 11:23:16 -0400 Subject: [PATCH 86/90] when regenerating using refresh tokens, username needs to be derived from the refresh token, as no session would exist --- web/api/app/Controller/HostController.php | 19 +++++++++++++++---- web/includes/auth.php | 4 ++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/web/api/app/Controller/HostController.php b/web/api/app/Controller/HostController.php index 9ff4e7c76..d6827c491 100644 --- a/web/api/app/Controller/HostController.php +++ b/web/api/app/Controller/HostController.php @@ -49,7 +49,7 @@ class HostController extends AppController { $cred = $this->_getCredentials(true); // generate refresh } else { - $cred = $this->_getCredentials(false); // don't generate refresh + $cred = $this->_getCredentials(false, $mToken); // don't generate refresh } $login_array = array ( @@ -114,7 +114,7 @@ class HostController extends AppController { } } - private function _getCredentials($generate_refresh_token=false) { + private function _getCredentials($generate_refresh_token=false, $mToken='') { $credentials = ''; $this->loadModel('Config'); @@ -127,6 +127,17 @@ class HostController extends AppController { throw new ForbiddenException(__('Please create a valid AUTH_HASH_SECRET in ZoneMinder')); } + if ($mToken) { + // If we have a token, we need to derive username from there + $ret = validateToken($mToken, 'refresh'); + $mUser = $ret[0]['Username']; + + } else { + $mUser = $_SESSION['username']; + } + + ZM\Info("Creating token for \"$mUser\""); + /* we won't support AUTH_HASH_IPS in token mode reasons: a) counter-intuitive for mobile consumers @@ -149,7 +160,7 @@ class HostController extends AppController { "iss" => "ZoneMinder", "iat" => $access_issued_at, "exp" => $access_expire_at, - "user" => $_SESSION['username'], + "user" => $mUser, "type" => "access" ); @@ -167,7 +178,7 @@ class HostController extends AppController { "iss" => "ZoneMinder", "iat" => $refresh_issued_at, "exp" => $refresh_expire_at, - "user" => $_SESSION['username'], + "user" => $mUser, "type" => "refresh" ); $jwt_refresh_token = \Firebase\JWT\JWT::encode($refresh_token, $key, 'HS256'); diff --git a/web/includes/auth.php b/web/includes/auth.php index 33d2b3fb6..3b823004c 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -244,7 +244,7 @@ function validateToken ($token, $allowed_token_type='access') { $minIssuedAt = $saved_user_details['TokenMinExpiry']; 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; unset($user); return array(false, "Token revoked. Please re-generate"); @@ -253,7 +253,7 @@ function validateToken ($token, $allowed_token_type='access') { $user = $saved_user_details; return array($user, "OK"); } else { - ZM\Error ("Could not retrieve user $username details"); + ZM\Error ("Could not retrieve user \"$username\" details"); $_SESSION['loginFailed'] = true; unset($user); return array(false, "No such user/credentials"); From 33db7e1e35944668f3a5b9db9b424d6f4fd2967a Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sat, 18 May 2019 13:47:31 -0400 Subject: [PATCH 87/90] add libssl1.0.0 for ubuntu 16/12 --- distros/ubuntu1204/control | 2 +- distros/ubuntu1604/control | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/distros/ubuntu1204/control b/distros/ubuntu1204/control index 1dc8a2b38..9e54e2aa3 100644 --- a/distros/ubuntu1204/control +++ b/distros/ubuntu1204/control @@ -69,7 +69,7 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} ,libdata-dump-perl, libclass-std-fast-perl, libsoap-wsdl-perl ,libio-socket-multicast-perl, libdigest-sha-perl ,libsys-cpu-perl, libsys-meminfo-perl - ,libssl + ,libssl | libssl1.0.0 ,libcrypt-eksblowfish-perl ,libdata-entropy-perl Recommends: ${misc:Recommends} diff --git a/distros/ubuntu1604/control b/distros/ubuntu1604/control index d07104100..30451f7e1 100644 --- a/distros/ubuntu1604/control +++ b/distros/ubuntu1604/control @@ -79,7 +79,7 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} ,rsyslog | system-log-daemon ,zip ,libpcre3 - ,libssl + ,libssl | libssl1.0.0 ,libcrypt-eksblowfish-perl ,libdata-entropy-perl Recommends: ${misc:Recommends} From 036d47545f6b22fb1c0139ff55f97080fd3d039f Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sat, 18 May 2019 19:35:13 -0400 Subject: [PATCH 88/90] small API fixes --- docs/api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index d60847d92..2b96bb397 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -85,7 +85,7 @@ Or for API 2.0: Using these keys with subsequent requests ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Once you have the keys (a.k.a credentials (v1.0, v2.0) or token (v2.0)) you should now supply that credential to subsequent API calls like this: +Once you have the keys (a.k.a credentials (v1.0, v2.0) or token (v2.0)) you should now supply that key to subsequent API calls like this: :: @@ -108,7 +108,7 @@ Once you have the keys (a.k.a credentials (v1.0, v2.0) or token (v2.0)) you shou Key lifetime (v1.0) ^^^^^^^^^^^^^^^^^^^^^ -If you are using the old ``auth_hash`` mechanism present in v1.0, then the credentials will time out based on PHP session timeout. This is often confusing and sometime causes additional issues due to the fact that the old method also includes timestamps in its hash. +If you are using the old credentials mechanism present in v1.0, then the credentials will time out based on PHP session timeout (if you are using cookies), or the value of ``AUTH_HASH_TTL`` which defaults to 2 hours. Note that there is no way to look at the hash and decipher how much time is remaining. So it is your responsibility to record the time you got the hash and assume it was generated at the time you got it and re-login before that time expires. Key lifetime (v2.0) ^^^^^^^^^^^^^^^^^^^^^^ From ddd02ec10df4b91905d8785262cf3897104b72ce Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Sun, 19 May 2019 07:29:27 -0400 Subject: [PATCH 89/90] clean up of API, remove redundant sections --- docs/api.rst | 162 ++++++++++++++++----------------------------------- 1 file changed, 49 insertions(+), 113 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 2b96bb397..177678977 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -40,14 +40,14 @@ For v2.0 APIs, you have an additional option right below it - ``OPT_USE_LEGACY_A Enabling secret key ^^^^^^^^^^^^^^^^^^^ -* It is important that you create a "Secret Key". This needs to be a set of hard to guess characters, that only you know. ZoneMinder does not create a key for you. It is your responsibility to create it. If you haven't created one already, please do so by going to ``Options->Systems`` and populating ``AUTH_HASH_SECRET``. Don't forget to save. +* It is **important** that you create a "Secret Key". This needs to be a set of hard to guess characters, that only you know. ZoneMinder does not create a key for you. It is your responsibility to create it. If you haven't created one already, please do so by going to ``Options->Systems`` and populating ``AUTH_HASH_SECRET``. Don't forget to save. * If you plan on using V2.0 token based security, **it is mandatory to populate this secret key**, as it is used to sign the token. If you don't, token authentication will fail. V1.0 did not mandate this requirement. -Getting API key +Getting an API key ^^^^^^^^^^^^^^^^^^^^^^^ -To get API key: +To get an API key: :: @@ -91,29 +91,34 @@ Once you have the keys (a.k.a credentials (v1.0, v2.0) or token (v2.0)) you shou # RECOMMENDED: v2.0 token based curl -XPOST https://yourserver/zm/api/monitors.json&token= - # or + + # or # v1.0 or 2.0 based API access (will only work if AUTH_HASH_LOGINS is enabled) curl -XPOST -d "auth=" https://yourserver/zm/api/monitors.json + # or + curl -XGET https://yourserver/zm/api/monitors.json&auth= + # or, if you specified -c cookies.txt in the original login request + curl -b cookies.txt -XGET https://yourserver/zm/api/monitors.json .. NOTE:: - ZoneMinder's API layer allows API keys to be encoded either as a querty parameter or as a data payload. If you don't pass keys, you could use cookies (not recommended as a general approach) + ZoneMinder's API layer allows API keys to be encoded either as a query parameter or as a data payload. If you don't pass keys, you could use cookies (not recommended as a general approach) Key lifetime (v1.0) ^^^^^^^^^^^^^^^^^^^^^ -If you are using the old credentials mechanism present in v1.0, then the credentials will time out based on PHP session timeout (if you are using cookies), or the value of ``AUTH_HASH_TTL`` which defaults to 2 hours. Note that there is no way to look at the hash and decipher how much time is remaining. So it is your responsibility to record the time you got the hash and assume it was generated at the time you got it and re-login before that time expires. +If you are using the old credentials mechanism present in v1.0, then the credentials will time out based on PHP session timeout (if you are using cookies), or the value of ``AUTH_HASH_TTL`` (if you are using ``auth=`` and have enabled ``AUTH_HASH_LOGINS``) which defaults to 2 hours. Note that there is no way to look at the hash and decipher how much time is remaining. So it is your responsibility to record the time you got the hash and assume it was generated at the time you got it and re-login before that time expires. Key lifetime (v2.0) ^^^^^^^^^^^^^^^^^^^^^^ -In version 2.0, it is very easy to know when a key will expire. You can find that out from the ``access_token_expires`` and ``refresh_token_exipres`` values (in seconds). You should refresh the keys before the timeout occurs, or you will not be able to use the APIs. +In version 2.0, it is easy to know when a key will expire before you use it. You can find that out from the ``access_token_expires`` and ``refresh_token_exipres`` values (in seconds) after you decode the JWT key (there are JWT decode libraries for every language you want). You should refresh the keys before the timeout occurs, or you will not be able to use the APIs. Understanding access/refresh tokens (v2.0) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -128,7 +133,7 @@ If you are using V2.0, then you need to know how to use these tokens effectively **To Summarize:** * Pass your ``username`` and ``password`` to ``login.json`` only once in 24 hours to renew your tokens -* Pass your "refresh token" to ``login.json`` once an hour to renew your ``access token`` +* Pass your "refresh token" to ``login.json`` once in two hours (or whatever you have set the value of ``AUTH_HASH_TTL`` to) to renew your ``access token`` * Use your ``access token`` for all API invocations. In fact, V2.0 will reject your request (if it is not to ``login.json``) if it comes with a refresh token instead of an access token to discourage usage of this token when it should not be used. @@ -138,7 +143,7 @@ This minimizes the amount of sensitive data that is sent over the wire and the l Understanding key security ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -* Version 1.0 uses an MD5 hash to generate the credentials. The hash is computed over your secret key (if avaiable), username, password and some time parameters (along with remote IP if enabled). This is not a secure/recommended hashing mechanism. If your auth hash is compromised, an attacker will be able to use your hash till it expires. To avoid this, you could disable the user in ZoneMinder. +* Version 1.0 uses an MD5 hash to generate the credentials. The hash is computed over your secret key (if available), username, password and some time parameters (along with remote IP if enabled). This is not a secure/recommended hashing mechanism. If your auth hash is compromised, an attacker will be able to use your hash till it expires. To avoid this, you could disable the user in ZoneMinder. Furthermore, enabling remote IP (``AUTH_HASH_REMOTE_IP``) requires that you issue future requests from the same IP that generated the tokens. While this may be considered an additional layer for security, this can cause issues with mobile devices. * Version 2.0 uses a different approach. The hash is a simple base64 encoded form of "claims", but signed with your secret key. Consider for example, the following access key: @@ -160,10 +165,10 @@ If you were to use any `JWT token verifier `__ it can easily dec Invalid Signature -Don't be surprised. JWT tokens are not meant to be encrypted. It is just an assertion of a claim. It states that the issuer of this token was ZoneMinder, +Don't be surprised. JWT tokens, by default, are `not meant to be encrypted `__. It is just an assertion of a claim. It states that the issuer of this token was ZoneMinder, It was issued at (iat) Wednesday, 2019-05-15 17:19:12 UTC and will expire on (exp) Wednesday, 2019-05-15 18:19:12 UTC. This token claims to be owned by an admin and is an access token. If your token were to be stolen, this information is available to the person who stole it. Note that there are no sensitive details like passwords in this claim. -However, that person will **not** have your secret key as part of this token and therefore, will NOT be able to create a new JWT token to get, say, a refresh token. They will however, be able to use your access token to access resources just like the auth hash above, till the access token expires (1hr). To revoke this token, you don't need to disable the user. Go to ``Options->API`` and tap on "Revoke All Access Tokens". This will invalidate the token immediately (this option will invalidate all tokens for all users, and new ones will need to be generated). +However, that person will **not** have your secret key as part of this token and therefore, will NOT be able to create a new JWT token to get, say, a refresh token. They will however, be able to use your access token to access resources just like the auth hash above, till the access token expires (2 hrs). To revoke this token, you don't need to disable the user. Go to ``Options->API`` and tap on "Revoke All Access Tokens". This will invalidate the token immediately (this option will invalidate all tokens for all users, and new ones will need to be generated). Over time, we will provide you with more fine grained access to these options. @@ -175,105 +180,12 @@ Over time, we will provide you with more fine grained access to these options. * If you believe your tokens are compromised, revoke them, but also check if your attacker has compromised more than you think (example, they may also have your username/password or access to your system via other exploits, in which case they can regenerate as many tokens/credentials as they want). - .. NOTE:: Subsequent sections don't explicitly callout the key addition to APIs. We assume that you will append the correct keys as per our explanation above. - -Logout APIs -^^^^^^^^^^^^^^ -The APIs tie into ZoneMinder's existing security model. This means if you have -OPT_AUTH enabled, you need to log into ZoneMinder using the same browser you plan to -use the APIs from. If you are developing an app that relies on the API, you need -to do a POST login from the app into ZoneMinder before you can access the API. - -Then, you need to re-use the authentication information of the login (returned as cookie states) -with subsequent APIs for the authentication information to flow through to the APIs. - -This means if you plan to use cuRL to experiment with these APIs, you first need to login: - -**Login process for ZoneMinder v1.32.0 and above** - -:: - - curl -XPOST -d "user=XXXX&pass=YYYY" -c cookies.txt http://yourzmip/zm/api/host/login.json - -Staring ZM 1.32.0, you also have a `logout` API that basically clears your session. It looks like this: - -:: - - curl -b cookies.txt http://yourzmip/zm/api/host/logout.json - - -**Login process for older versions of ZoneMinder** - -:: - - curl -d "username=XXXX&password=YYYY&action=login&view=console" -c cookies.txt http://yourzmip/zm/index.php - -The equivalent logout process for older versions of ZoneMinder is: - -:: - - curl -XPOST -d "username=XXXX&password=YYYY&action=logout&view=console" -b cookies.txt http://yourzmip/zm/index.php - -replacing *XXXX* and *YYYY* with your username and password, respectively. - -Please make sure you do this in a directory where you have write permissions, otherwise cookies.txt will not be created -and the command will silently fail. - - -What the "-c cookies.txt" does is store a cookie state reflecting that you have logged into ZM. You now need -to apply that cookie state to all subsequent APIs. You do that by using a '-b cookies.txt' to subsequent APIs if you are -using CuRL like so: - -:: - - curl -b cookies.txt http://yourzmip/zm/api/monitors.json - -This would return a list of monitors and pass on the authentication information to the ZM API layer. - -A deeper dive into the login process -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -As you might have seen above, there are two ways to login, one that uses the `login.json` API and the other that logs in using the ZM portal. If you are running ZoneMinder 1.32.0 and above, it is *strongly* recommended you use the `login.json` approach. The "old" approach will still work but is not as powerful as the API based login. Here are the reasons why: - - * The "old" approach basically uses the same login webpage (`index.php`) that a user would log into when viewing the ZM console. This is not really using an API and more importantly, if you have additional components like reCAPTCHA enabled, this will not work. Using the API approach is much cleaner and will work irrespective of reCAPTCHA - - * The new login API returns important information that you can use to stream videos as well, right after login. Consider for example, a typical response to the login API (`/login.json`): - -:: - - { - "credentials": "auth=f5b9cf48693fe8552503c8ABCD5", - "append_password": 0, - "version": "1.31.44", - "apiversion": "1.0" - } - -In this example I have `OPT_AUTH` enabled in ZoneMinder and it returns my credential key. You can then use this key to stream images like so: - -:: - - - -Where `authval` is the credentials returned to start streaming videos. - -The `append_password` field will contain 1 when it is necessary for you to append your ZM password. This is the case when you set `AUTH_RELAY` in ZM options to "plain", for example. In that case, the `credentials` field may contain something like `&user=admin&pass=` and you have to add your password to that string. - - -.. NOTE:: It is recommended you invoke the `login` API once every 60 minutes to make sure the session stays alive. The same is true if you use the old login method too. - - - -Examples (please read security notice above) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Please remember, if you are using authentication, please add a ``-b cookies.txt`` to each of the commands below if you are using -CuRL. If you are not using CuRL and writing your own app, you need to make sure you pass on cookies to subsequent requests -in your app. - +Examples +^^^^^^^^^ (In all examples, replace 'server' with IP or hostname & port where ZoneMinder is running) @@ -500,6 +412,15 @@ This returns number of events per monitor that were recorded in the last day whe +Return sorted events +^^^^^^^^^^^^^^^^^^^^^^ + +This returns a list of events within a time range and also sorts it by descending order + +:: + + curl -XGET "http://server/zm/api/events/index/StartTime%20>=:2015-05-15%2018:43:56/EndTime%20<=:208:43:56.json?sort=StartTime&direction=desc" + Configuration Apis ^^^^^^^^^^^^^^^^^^^ @@ -674,6 +595,9 @@ Returns: This only works if you have a multiserver setup in place. If you don't it will return an empty array. +Other APIs +^^^^^^^^^^ +This is not a complete list. ZM supports more parameters/APIs. A good way to dive in is to look at the `API code `__ directly. Streaming Interface ^^^^^^^^^^^^^^^^^^^ @@ -690,9 +614,13 @@ For example: :: + + + # or + - #or - + + will display a live feed from monitor id 1, scaled down by 50% in quality and resized to 640x480px. @@ -728,10 +656,13 @@ Similar to live playback, if you have chosen to store events in JPEG mode, you c :: - - #or + # or + + + + * This assumes ``/zm/cgi-bin`` is your CGI_BIN path. Change it to what is correct in your system * This will playback event 293820, starting from frame 1 as an MJPEG stream @@ -741,12 +672,16 @@ Similar to live playback, if you have chosen to store events in JPEG mode, you c If instead, you have chosen to use the MP4 (Video) storage mode for events, you can directly play back the saved video file: :: + - - #or -* This will play back the video recording for event 294690 + # or + + + + +This above will play back the video recording for event 294690 What other parameters are supported? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -757,6 +692,7 @@ are generated. Change and observe. Further Reading ^^^^^^^^^^^^^^^^ + As described earlier, treat this document as an "introduction" to the important parts of the API and streaming interfaces. There are several details that haven't yet been documented. Till they are, here are some resources: From 6dd1bbe895b47025dddc877e8e9179f3da01056b Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Thu, 23 May 2019 13:52:54 -0400 Subject: [PATCH 90/90] moved to ZM fork for bcrypt --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index f5bcf359d..b64d78997 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,7 +7,7 @@ url = https://github.com/ZoneMinder/CakePHP-Enum-Behavior.git [submodule "third_party/bcrypt"] path = third_party/bcrypt - url = https://github.com/pliablepixels/libbcrypt + url = https://github.com/ZoneMinder/libbcrypt [submodule "third_party/jwt-cpp"] path = third_party/jwt-cpp url = https://github.com/Thalhammer/jwt-cpp