From 34370e0060476eb565112c1c42f2e749a28d70b0 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Fri, 24 May 2019 13:47:07 -0400 Subject: [PATCH 1/3] test for error code from db creation and if there is an error, die with an error code. (#2611) --- distros/debian/postinst | 4 ++++ distros/ubuntu1204/zoneminder.postinst | 4 ++++ distros/ubuntu1604/zoneminder.postinst | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/distros/debian/postinst b/distros/debian/postinst index 3cd3fd277..36472436a 100644 --- a/distros/debian/postinst +++ b/distros/debian/postinst @@ -31,6 +31,10 @@ if [ "$1" = "configure" ]; then # test if database if already present... if ! $(echo quit | mysql --defaults-file=/etc/mysql/debian.cnf zm > /dev/null 2> /dev/null) ; then cat /usr/share/zoneminder/db/zm_create.sql | mysql --defaults-file=/etc/mysql/debian.cnf + if [ $? -ne 0 ]; then + echo "Error creating db." + exit 1; + fi # This creates the user. echo "grant lock tables, alter,select,insert,update,delete,create,index on ${ZM_DB_NAME}.* to '${ZM_DB_USER}'@localhost identified by \"${ZM_DB_PASS}\";" | mysql --defaults-file=/etc/mysql/debian.cnf mysql else diff --git a/distros/ubuntu1204/zoneminder.postinst b/distros/ubuntu1204/zoneminder.postinst index ef715375b..603786ff6 100644 --- a/distros/ubuntu1204/zoneminder.postinst +++ b/distros/ubuntu1204/zoneminder.postinst @@ -34,6 +34,10 @@ if [ "$1" = "configure" ]; then # test if database if already present... if ! $(echo quit | mysql --defaults-file=/etc/mysql/debian.cnf zm > /dev/null 2> /dev/null) ; then cat /usr/share/zoneminder/db/zm_create.sql | mysql --defaults-file=/etc/mysql/debian.cnf + if [ $? -ne 0 ]; then + echo "Error creating db." + exit 1; + fi # This creates the user. echo "grant lock tables, alter,drop,select,insert,update,delete,create,index,alter routine,create routine, trigger,execute on ${ZM_DB_NAME}.* to '${ZM_DB_USER}'@localhost identified by \"${ZM_DB_PASS}\";" | mysql --defaults-file=/etc/mysql/debian.cnf mysql else diff --git a/distros/ubuntu1604/zoneminder.postinst b/distros/ubuntu1604/zoneminder.postinst index ffde50283..d3983950b 100644 --- a/distros/ubuntu1604/zoneminder.postinst +++ b/distros/ubuntu1604/zoneminder.postinst @@ -56,6 +56,10 @@ if [ "$1" = "configure" ]; then if ! $(echo quit | mysql --defaults-file=/etc/mysql/debian.cnf zm > /dev/null 2> /dev/null) ; then echo "Creating zm db" cat /usr/share/zoneminder/db/zm_create.sql | mysql --defaults-file=/etc/mysql/debian.cnf + if [ $? -ne 0 ]; then + echo "Error creating db." + exit 1; + fi # This creates the user. echo "grant lock tables,alter,drop,select,insert,update,delete,create,index,alter routine,create routine, trigger,execute on ${ZM_DB_NAME}.* to '${ZM_DB_USER}'@localhost identified by \"${ZM_DB_PASS}\";" | mysql --defaults-file=/etc/mysql/debian.cnf mysql else From fc27393a96ac4ae3e622c0a664df9d7dbe5d25fb Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Fri, 24 May 2019 13:48:40 -0400 Subject: [PATCH 2/3] Replace MySQL Password() with bcrypt, allow for alternate JWT tokens (#2598) * added sha1 and bcrypt submodules * added bcrypt and sha to src build process * added test sha1 and bcrypt code to validate working * bcrypt auth migration in PHP land * added include path * add sha source * added bcrypt to others * put link_dir ahead of add_executable * fixed typo * try add_library instead * absolute path * absolute path * build bcrypt as static * move to wrapper * move to fork * logs tweak * added lib-ssl/dev for JWT signing * Moved to openSSL SHA1, initial JWT plugin * removed vog * fixed SHA1 algo * typo * use php-jwt, use proper way to add PHP modules, via composer * fixed module path * first attempt to fix cast error * own fork * own fork * add composer vendor directory * go back to jwt-cpp as PR merged * moved to jwt-cpp after PR merge * New token= query for JWT * Add JWT token creation, move old code to a different function for future deprecation, simplified code for ZM_XX parameter reading * JWT integration, validate JWT token via validateToken * added token validation to zms/zmu/zmuser * add token to command line for zmu * move decode inside try/catch * exception handling for try/catch * fix db read, forgot to exec query * remove allowing auth_hash_ip for token * support refresh tokens as well for increased security * remove auth_hash_ip * Error out if used did not create an AUTH_HASH_SECRET * fixed type conversion * make sure refresh token login doesn't generate another refresh token * fix absolute path * move JWT/Bcrypt inside zm_crypt * move sha headers out * move out sha header * handle case when supplied password is hashed, fix wrong params in AppController * initial baby step for api tab * initial plumbing to introduce token expiry and API bans per user * remove M typo * display user table in api * added revoke all tokens code, removed test code * use strtoul for conversion * use strtoul for conversion * use strtoul for conversion * more fixes * more fixes * add mintokenexpiry to DB seek * typo * add ability to revoke tokens and enable/disable APIs per user * moved API enable back to system * comma * enable API options only if API enabled * move user creation to bcrypt * added password_compat for PHP >=5.3 <5.5 * add Password back so User object indexes don't change * move token index after adding password * demote logs * make old API auth optional, on by default * make old API auth mechanism optional * removed stale code * forgot to checkin update file * bulk overlay hash mysql encoded passwords * add back ssl_dev, got deleted * fix update script * added token support to index.php * reworked API document for new changes in 2.0 * Migrate from libdigest to crypt-eks-blowfish due to notice * merge typo * css classess for text that disappear * fixed html typo * added deps to ubuntu control files * spaces * removed extra line * when regenerating using refresh tokens, username needs to be derived from the refresh token, as no session would exist * add libssl1.0.0 for ubuntu 16/12 * small API fixes * clean up of API, remove redundant sections * moved to ZM fork for bcrypt * whitespace and google code style * regenerate auth hash if doing password migration * dont need AUTH HASH LOGIN to be on * Add auth hash verification to the user logged in already case * fix missing ] * reject requests if per user API disabled --- .gitmodules | 6 + CMakeLists.txt | 7 + db/zm_create.sql.in | 2 + db/zm_update-1.33.9.sql | 27 ++ distros/debian/control | 5 + distros/ubuntu1204/control | 11 +- distros/ubuntu1604/control | 6 + docs/api.rst | 366 +++++++++----- .../lib/ZoneMinder/ConfigData.pm.in | 11 + scripts/zmupdate.pl.in | 23 + src/CMakeLists.txt | 12 +- src/zm_crypt.cpp | 117 +++++ src/zm_crypt.h | 31 ++ src/zm_user.cpp | 107 ++++- src/zm_user.h | 1 + src/zms.cpp | 14 +- src/zmu.cpp | 15 +- third_party/bcrypt | 1 + third_party/jwt-cpp | 1 + version | 2 +- web/.gitignore | 2 +- web/CMakeLists.txt | 2 +- web/api/app/Controller/AppController.php | 43 +- web/api/app/Controller/HostController.php | 174 +++++-- web/composer.json | 6 + web/composer.lock | 106 +++++ web/includes/actions/options.php | 1 + web/includes/actions/user.php | 29 +- web/includes/auth.php | 229 ++++++++- web/lang/en_gb.php | 4 + web/skins/classic/css/base/skin.css | 43 ++ web/skins/classic/views/options.php | 90 +++- web/vendor/autoload.php | 7 + web/vendor/composer/ClassLoader.php | 445 ++++++++++++++++++ web/vendor/composer/LICENSE | 56 +++ web/vendor/composer/autoload_classmap.php | 9 + web/vendor/composer/autoload_files.php | 10 + web/vendor/composer/autoload_namespaces.php | 9 + web/vendor/composer/autoload_psr4.php | 10 + web/vendor/composer/autoload_real.php | 70 +++ web/vendor/composer/autoload_static.php | 35 ++ web/vendor/composer/installed.json | 94 ++++ 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 + .../ircmaxell/password-compat/LICENSE.md | 7 + .../ircmaxell/password-compat/composer.json | 20 + .../password-compat/lib/password.php | 314 ++++++++++++ .../password-compat/version-test.php | 6 + 53 files changed, 3000 insertions(+), 245 deletions(-) create mode 100644 db/zm_update-1.33.9.sql create mode 100644 src/zm_crypt.cpp create mode 100644 src/zm_crypt.h create mode 160000 third_party/bcrypt create mode 160000 third_party/jwt-cpp 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_files.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 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/.gitmodules b/.gitmodules index eb0e282a2..b64d78997 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/ZoneMinder/libbcrypt +[submodule "third_party/jwt-cpp"] + path = third_party/jwt-cpp + url = https://github.com/Thalhammer/jwt-cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9646ffc3e..0973f8726 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -870,6 +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) diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index a5f5cb70c..557745ab9 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -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/db/zm_update-1.33.9.sql b/db/zm_update-1.33.9.sql new file mode 100644 index 000000000..e0d289ba4 --- /dev/null +++ b/db/zm_update-1.33.9.sql @@ -0,0 +1,27 @@ +-- +-- 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; diff --git a/distros/debian/control b/distros/debian/control index 4c23ab367..3296b88c3 100644 --- a/distros/debian/control +++ b/distros/debian/control @@ -26,6 +26,8 @@ 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 + , libcrypt-eksblowfish-perl, libdata-entropy-perl Standards-Version: 3.9.4 Package: zoneminder @@ -51,6 +53,9 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} , zip , libvlccore5 | libvlccore7 | libvlccore8, libvlc5 , libpolkit-gobject-1-0, php5-gd + , libssl + ,libcrypt-eksblowfish-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/distros/ubuntu1204/control b/distros/ubuntu1204/control index f1756c5e8..9e54e2aa3 100644 --- a/distros/ubuntu1204/control +++ b/distros/ubuntu1204/control @@ -23,6 +23,9 @@ 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 +66,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 | libssl1.0.0 + ,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..30451f7e1 100644 --- a/distros/ubuntu1604/control +++ b/distros/ubuntu1604/control @@ -30,6 +30,9 @@ 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 +79,9 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} ,rsyslog | system-log-daemon ,zip ,libpcre3 + ,libssl | libssl1.0.0 + ,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 diff --git a/docs/api.rst b/docs/api.rst index 2f90b7fdf..177678977 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,178 +14,178 @@ 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 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -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. +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) -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. +Enabling secret key +^^^^^^^^^^^^^^^^^^^ -This means if you plan to use cuRL to experiment with these APIs, you first need to login: +* 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. -**Login process for ZoneMinder v1.32.0 and above** + +Getting an API key +^^^^^^^^^^^^^^^^^^^^^^^ + +To get an API key: :: - curl -XPOST -d "user=XXXX&pass=YYYY" -c cookies.txt http://yourzmip/zm/api/host/login.json + curl -XPOST [-c cookies.txt] -d "user=yourusername&pass=yourpassword" https://yourserver/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: + +The ``[-c cookies.txt]`` is optional, and will be explained in the next section. + +This returns a payload like this for API v1.0: :: - curl -b cookies.txt http://yourzmip/zm/api/host/logout.json + { + "credentials": "auth=05f3a50e8f7063", + "append_password": 0, + "version": "1.33.9", + "apiversion": "1.0" + } - -**Login process for older versions of ZoneMinder** +Or for API 2.0: :: - curl -d "username=XXXX&password=YYYY&action=login&view=console" -c cookies.txt http://yourzmip/zm/index.php + { + "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" + } -The equivalent logout process for older versions of ZoneMinder is: +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 key to subsequent API calls like this: :: - curl -XPOST -d "username=XXXX&password=YYYY&action=logout&view=console" -b cookies.txt http://yourzmip/zm/index.php + # RECOMMENDED: v2.0 token based + curl -XPOST https://yourserver/zm/api/monitors.json&token= -replacing *XXXX* and *YYYY* with your username and password, respectively. + # or -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. + # 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 -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: +.. NOTE:: + 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`` (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 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) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +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 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. + +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 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: :: - curl -b cookies.txt http://yourzmip/zm/api/monitors.json + eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJab25lTWluZGVyIiwiaWF0IjoxNTU3OTQwNzUyLCJleHAiOjE1NTc5NDQzNTIsInVzZXIiOiJhZG1pbiIsInR5cGUiOiJhY2Nlc3MifQ.-5VOcpw3cFHiSTN5zfGDSrrPyVya1M8_2Anh5u6eNlI -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`): +If you were to use any `JWT token verifier `__ it can easily decode that token and will show: :: - { - "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. + { + "iss": "ZoneMinder", + "iat": 1557940752, + "exp": 1557944352, + "user": "admin", + "type": "access" + } + Invalid Signature -.. 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. +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 (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. + +**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. -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) @@ -410,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 ^^^^^^^^^^^^^^^^^^^ @@ -584,9 +595,104 @@ 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 +^^^^^^^^^^^^^^^^^^^ +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 above 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. There are several details that haven't yet been documented. Till they are, here are some resources: diff --git a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in index 495e53d29..d133b3eaf 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/scripts/zmupdate.pl.in b/scripts/zmupdate.pl.in index bb9dddac4..8dbc4d14f 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 Crypt::Eksblowfish::Bcrypt; +use Data::Entropy::Algorithms qw(rand_bits); # ========================================================================== # @@ -312,6 +314,7 @@ if ( $migrateEvents ) { if ( $freshen ) { print( "\nFreshening configuration in database\n" ); migratePaths(); + migratePasswords(); ZoneMinder::Config::loadConfigFromDB(); ZoneMinder::Config::saveConfigToDB(); } @@ -999,6 +1002,26 @@ sub patchDB { } +sub migratePasswords { + 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); + if ($scheme eq "*") { + print ("-->".$user->{Username}. " password will be migrated\n"); + 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() ); + } + } +} + sub migratePaths { my $customConfigFile = '@ZM_CONFIG_SUBDIR@/zmcustom.conf'; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e27c36d6a..3628a4944 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,20 +4,26 @@ 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_fifo.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_fifo.cpp zm_crypt.cpp) + # A fix for cmake recompiling the source files for every target. add_library(zm STATIC ${ZM_BIN_SRC_FILES}) +link_directories(../third_party/bcrypt) add_executable(zmc zmc.cpp) add_executable(zma zma.cpp) add_executable(zmu zmu.cpp) add_executable(zms zms.cpp) +# 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}) -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) diff --git a/src/zm_crypt.cpp b/src/zm_crypt.cpp new file mode 100644 index 000000000..0235e5c13 --- /dev/null +++ b/src/zm_crypt.cpp @@ -0,0 +1,117 @@ +#include "zm.h" +# include "zm_crypt.h" +#include "BCrypt.hpp" +#include "jwt.h" +#include +#include + + +// returns username if valid, "" if not +std::pair verifyToken(std::string jwt_token_str, std::string key) { + std::string username = ""; + unsigned int token_issued_at = 0; + 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 std::make_pair("",0); + } + } + else { + // something is wrong. All ZM tokens have type + Error ("Missing token type. This should not happen"); + return std::make_pair("",0); + } + if (decoded.has_payload_claim("user")) { + username = decoded.get_payload_claim("user").as_string(); + Debug (1, "Got %s as user claim from token", username.c_str()); + } + else { + Error ("User not found in claim"); + return std::make_pair("",0); + } + + if (decoded.has_payload_claim("iat")) { + token_issued_at = (unsigned int) (decoded.get_payload_claim("iat").as_int()); + Debug (1,"Got IAT token=%u", token_issued_at); + + } + 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 std::make_pair("",0); + } + catch (...) { + Error ("unknown exception"); + return std::make_pair("",0); + + } + return std::make_pair(username,token_issued_at); +} + +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 + Error ("DB Password is too short or invalid to check"); + return false; + } + if (db_password_hash[0] == '*') { + // MYSQL PASSWORD + Debug (1,"%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]; + + //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]+1, "%02X", (unsigned int)digest_final[i]); + final_hash[SHA_DIGEST_LENGTH *2 + 1]=0; + + 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 + 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)); + } + 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; +} \ No newline at end of file diff --git a/src/zm_crypt.h b/src/zm_crypt.h new file mode 100644 index 000000000..340abc36c --- /dev/null +++ b/src/zm_crypt.h @@ -0,0 +1,31 @@ +// +// 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 + + + +bool verifyPassword( const char *username, const char *input_password, const char *db_password_hash); + +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 46ee2cdf1..35f25f7c9 100644 --- a/src/zm_user.cpp +++ b/src/zm_user.cpp @@ -27,7 +27,9 @@ #include #include + #include "zm_utils.h" +#include "zm_crypt.h" User::User() { id = 0; @@ -95,24 +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 ); - 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 ); - } + + 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); @@ -131,14 +124,86 @@ 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()); - - mysql_free_result(result); - delete safer_username; - - return user; + + if (verifyPassword(username, 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; + } + } +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; + } + + Debug (1,"Inside zmLoadTokenUser, formed key=%s", key.c_str()); + + 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), + "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) ) { + 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); + 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[10], NULL,0 ); + + if (stored_iat > iat ) { // admin revoked tokens + mysql_free_result(result); + Error("Token was revoked for %s", username.c_str()); + return NULL; + } + + 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; + + } + 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 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 6042dbef3..5e6e4c2d6 100644 --- a/src/zms.cpp +++ b/src/zms.cpp @@ -71,6 +71,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; @@ -161,6 +162,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; + Debug(1,"ZMS: JWT token found: %s", jwt_token_str.c_str()); + } else if ( !strcmp( name, "user" ) ) { username = UriDecode( value ); } else if ( !strcmp( name, "pass" ) ) { @@ -184,11 +189,16 @@ 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); + user = zmLoadTokenUser(jwt_token_str, false); + + } + 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..07f9ae8aa 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); @@ -242,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'}, @@ -263,6 +265,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; @@ -273,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; } @@ -378,6 +381,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 +444,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/third_party/bcrypt b/third_party/bcrypt new file mode 160000 index 000000000..be171cd75 --- /dev/null +++ b/third_party/bcrypt @@ -0,0 +1 @@ +Subproject commit be171cd75dd65e06315a67c7dcdb8e1bbc1dabd4 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 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/.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/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") diff --git a/web/api/app/Controller/AppController.php b/web/api/app/Controller/AppController.php index 51575f055..eeda4b105 100644 --- a/web/api/app/Controller/AppController.php +++ b/web/api/app/Controller/AppController.php @@ -68,25 +68,46 @@ class AppController extends Controller { # For use throughout the app. If not logged in, this will be null. global $user; + if ( ZM_OPT_USE_AUTH ) { require_once __DIR__ .'/../../../includes/auth.php'; $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); + // 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 ) { + // 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, true); + $user = $ret[0]; + $retstatus = $ret[1]; + if ( !$user ) { + throw new UnauthorizedException(__($retstatus)); + return; + } } else if ( $mAuth ) { - $user = getAuthUser($mAuth); - if ( !$user ) { - throw new UnauthorizedException(__('Invalid Auth Key')); - return; - } + $user = getAuthUser($mAuth, true); + if ( !$user ) { + throw new UnauthorizedException(__('Invalid Auth Key')); + return; + } } // We need to reject methods that are not authenticated // besides login and logout @@ -100,6 +121,10 @@ class AppController extends Controller { } } # end if ! login or logout } # end if ZM_OPT_AUTH - + // make sure populated user object has APIs enabled + if ($user['APIEnabled'] == 0 ) { + throw new UnauthorizedException(__('API Disabled')); + return; + } } # end function beforeFilter() } diff --git a/web/api/app/Controller/HostController.php b/web/api/app/Controller/HostController.php index 05b2ed3fa..f0bae277e 100644 --- a/web/api/app/Controller/HostController.php +++ b/web/api/app/Controller/HostController.php @@ -31,19 +31,60 @@ class HostController extends AppController { } function login() { + + $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) && !$mToken ) { + throw new UnauthorizedException(__('No identity provided')); + } - $cred = $this->_getCredentials(); $ver = $this->_getVersion(); - $this->set(array( - 'credentials' => $cred[0], - 'append_password'=>$cred[1], - 'version' => $ver[0], - 'apiversion' => $ver[1], - '_serialize' => array('credentials', - 'append_password', - 'version', - 'apiversion' - ))); + $cred = []; + $cred_depr = []; + + if ($mUser && $mPassword) { + $cred = $this->_getCredentials(true); // generate refresh + } + else { + $cred = $this->_getCredentials(false, $mToken); // don't generate refresh + } + + $login_array = array ( + 'access_token'=>$cred[0], + 'access_token_expires'=>$cred[1] + ); + + $login_serialize_list = array ( + 'access_token', + 'access_token_expires' + ); + + 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'); + } + + + $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() // clears out session @@ -56,40 +97,95 @@ 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 + } + + private function _getCredentials($generate_refresh_token=false, $mToken='') { + $credentials = ''; + $this->loadModel('Config'); - 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') - ) ); + if ( ZM_OPT_USE_AUTH ) { + 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')); + } + + if ($mToken) { + // If we have a token, we need to derive username from there + $ret = validateToken($mToken, 'refresh', true); + $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 + 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']; + }*/ + + $access_issued_at = time(); + $access_ttl = (ZM_AUTH_HASH_TTL || 2) * 3600; + + // 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_token = array( + "iss" => "ZoneMinder", + "iat" => $access_issued_at, + "exp" => $access_expire_at, + "user" => $mUser, + "type" => "access" + ); + + $jwt_access_token = \Firebase\JWT\JWT::encode($access_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" => $mUser, + "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); } // If $mid is set, only return disk usage for that monitor @@ -169,7 +265,7 @@ class HostController extends AppController { private function _getVersion() { $version = Configure::read('ZM_VERSION'); - $apiversion = '1.0'; + $apiversion = '2.0'; return array($version, $apiversion); } diff --git a/web/composer.json b/web/composer.json new file mode 100644 index 000000000..968d1d4cb --- /dev/null +++ b/web/composer.json @@ -0,0 +1,6 @@ +{ + "require": { + "firebase/php-jwt": "^5.0", + "ircmaxell/password-compat": "^1.0" + } +} diff --git a/web/composer.lock b/web/composer.lock new file mode 100644 index 000000000..b260d2e5a --- /dev/null +++ b/web/composer.lock @@ -0,0 +1,106 @@ +{ + "_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#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "5759823f1f047089a354efaa25903378", + "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" + }, + { + "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": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} 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/includes/actions/user.php b/web/includes/actions/user.php index af569627f..2b520cd10 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.3'); + } + + + if ( !empty($_REQUEST['newUser']['Password']) ) { + ZM\Info ("PASS CMD=".$changes['Password']); + $changes['Password'] = 'Password = '.$pass_hash; + } + else unset($changes['Password']); if ( count($changes) ) { diff --git a/web/includes/auth.php b/web/includes/auth.php index 6b061f7fc..12199d878 100644 --- a/web/includes/auth.php +++ b/web/includes/auth.php @@ -19,8 +19,33 @@ // // require_once('session.php'); +require_once(__DIR__.'/../vendor/autoload.php'); -function userLogin($username='', $password='', $passwordHashed=false) { +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 +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); + # Since password field has changed, existing auth_hash is no longer valid + generateAuthHash(ZM_AUTH_HASH_IPS, true); + } else { + ZM\Info('Cannot migrate password scheme to bcrypt, as you are using PHP < 5.3'); + 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, $from_api_layer = false) { + global $user; if ( !$username and isset($_REQUEST['username']) ) @@ -29,8 +54,10 @@ function userLogin($username='', $password='', $passwordHashed=false) { $password = $_REQUEST['password']; // if true, a popup will display after login - // PP - lets validate reCaptcha if it exists - if ( defined('ZM_OPT_USE_GOOG_RECAPTCHA') + // lets validate reCaptcha if it exists + // this only applies if it userLogin was not called from API layer + if ( !$from_api_layer + && defined('ZM_OPT_USE_GOOG_RECAPTCHA') && defined('ZM_OPT_GOOG_RECAPTCHA_SECRETKEY') && defined('ZM_OPT_GOOG_RECAPTCHA_SITEKEY') && ZM_OPT_USE_GOOG_RECAPTCHA @@ -44,17 +71,17 @@ function userLogin($username='', $password='', $passwordHashed=false) { 'remoteip' => $_SERVER['REMOTE_ADDR'] ); $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 + $responseData = json_decode($res, true); + // 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 ) { + if ( isset($responseData['success']) && ($responseData['success'] == false) ) { // PP - before we deny auth, let's make sure the error was not 'invalid secret' // because that means the user did not configure the secret key correctly // in this case, we prefer to let him login in and display a message to correct // the key. Unfortunately, there is no way to check for invalid site key in code // as it produces the same error as when you don't answer a recaptcha if ( isset($responseData['error-codes']) && is_array($responseData['error-codes']) ) { - if ( !in_array('invalid-input-secret',$responseData['error-codes']) ) { + if ( !in_array('invalid-input-secret', $responseData['error-codes']) ) { Error('reCaptcha authentication failed'); return null; } else { @@ -64,28 +91,86 @@ 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 ) { + + // if the API layer asked us to login, make sure the user + // has API enabled (admin may have banned API for this user) + + if ( $from_api_layer ) { + 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 + // 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 if ( 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 + else if ( substr($saved_password, 0,4) == '-ZM-' ) { + ZM\Logger::Debug("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"); + $password_correct = password_verify($mysql_encoded_password, $bcrypt_hash); + $password_type = 'mysql'; // so we can migrate later down + } 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 false; } + $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']; @@ -113,7 +198,72 @@ function userLogout() { zm_session_clear(); } -function getAuthUser($auth) { + +function validateToken ($token, $allowed_token_type='access', $from_api_layer=false) { + + + 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); + + $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); + + $saved_user_details = dbFetchOne($sql, NULL, $sql_values); + + if ( $saved_user_details ) { + + if ($from_api_layer && $saved_user_details['APIEnabled'] == 0) { + // if from_api_layer is true, an additional check will be done + // to make sure APIs are enabled for this user. This is a good place + // to do it, since we are doing a DB dip here. + ZM\Error ("API is disabled for \"$username\""); + unset($user); + return array(false, 'API is disabled for user'); + + } + + $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 { + ZM\Error("Could not retrieve user $username details"); + $_SESSION['loginFailed'] = true; + unset($user); + return array(false, 'No such user/credentials'); + } +} // end function validateToken($token, $allowed_token_type='access') + +function getAuthUser($auth, $from_api_layer = false) { if ( ZM_OPT_USE_AUTH && ZM_AUTH_RELAY == 'hashed' && !empty($auth) ) { $remoteAddr = ''; if ( ZM_AUTH_HASH_IPS ) { @@ -134,7 +284,8 @@ function getAuthUser($auth) { $sql = 'SELECT * FROM Users WHERE Enabled = 1'; } - foreach ( dbFetchAll($sql, NULL, $values) as $user ) { + foreach ( dbFetchAll($sql, NULL, $values) as $user ) + { $now = time(); for ( $i = 0; $i < ZM_AUTH_HASH_TTL; $i++, $now -= ZM_AUTH_HASH_TTL * 1800 ) { // Try for last two hours $time = localtime($now); @@ -142,7 +293,18 @@ function getAuthUser($auth) { $authHash = md5($authKey); if ( $auth == $authHash ) { - return $user; + if ($from_api_layer && $user['APIEnabled'] == 0) { + // if from_api_layer is true, an additional check will be done + // to make sure APIs are enabled for this user. This is a good place + // to do it, since we are doing a DB dip here. + ZM\Error ("API is disabled for \"".$user['Username']."\""); + unset($user); + return array(false, 'API is disabled for user'); + + } + else { + return $user; + } } } // end foreach hour } // end foreach user @@ -153,8 +315,9 @@ 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(); + + $mintime = $time - ( ZM_AUTH_HASH_TTL * 1800 ); if ( $force or ( !isset($_SESSION['AuthHash'.$_SESSION['remoteAddr']]) ) or ( $_SESSION['AuthHashGeneratedAt'] < $mintime ) ) { @@ -216,9 +379,15 @@ if ( ZM_OPT_USE_AUTH ) { } if ( isset($_SESSION['username']) ) { - # Need to refresh permissions and validate that the user still exists - $sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username=?'; - $user = dbFetchOne($sql, NULL, array($_SESSION['username'])); + if ( ZM_AUTH_HASH_LOGINS ) { + # Extra validation, if logged in, then the auth hash will be set in the session, so we can validate it. + # This prevent session modification to switch users + $user = getAuthUser($_SESSION['AuthHash'.$_SESSION['remoteAddr']]); + } else { + # Need to refresh permissions and validate that the user still exists + $sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username=?'; + $user = dbFetchOne($sql, NULL, array($_SESSION['username'])); + } } if ( ZM_AUTH_RELAY == 'plain' ) { @@ -234,6 +403,14 @@ if ( ZM_OPT_USE_AUTH ) { } else if ( isset($_REQUEST['username']) and isset($_REQUEST['password']) ) { userLogin($_REQUEST['username'], $_REQUEST['password'], false); } + + if (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); diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index 72d6de8ab..1fdd112e0 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -102,8 +102,10 @@ $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', 'Apply' => 'Apply', 'ApplyingStateChange' => 'Applying State Change', 'ArchArchived' => 'Archived Only', @@ -420,6 +422,7 @@ $SLANG = array( 'Images' => 'Images', 'Include' => 'Include', 'In' => 'In', + 'InvalidateTokens' => 'Invalidate all generated tokens', 'Inverted' => 'Inverted', 'Iris' => 'Iris', 'KeyString' => 'Key String', @@ -658,6 +661,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/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 f7440d92e..b0ac2af1b 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['API'] = translate('API'); $tabs['servers'] = translate('Servers'); $tabs['storage'] = translate('Storage'); $tabs['web'] = translate('Web'); @@ -133,7 +134,8 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI -
@@ -309,8 +311,87 @@ 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 { + ?> + +
+
+ + ".translate('AllTokensRevoked').""; + } + + function updateSelected() + { + dbQuery("UPDATE Users SET APIEnabled=0"); + foreach( $_REQUEST["tokenUids"] as $markUid ) { + $minTime = time(); + 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 "".translate('Updated').""; + } + + if(array_key_exists('revokeAllTokens',$_POST)){ + revokeAllTokens(); + } + + if(array_key_exists('updateSelected',$_POST)){ + updateSelected(); + } + ?> + + +

+ + + + + + + + + + + + + + + + + + + + +
/>
+
+ + + +
@@ -431,6 +513,8 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI } ?> + + 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') && ini_get('apc.enabled') ? $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..f0157a6ed --- /dev/null +++ b/web/vendor/composer/LICENSE @@ -0,0 +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 + +Files: * +Copyright: 2016, Nils Adermann + 2016, Jordi Boggiano +License: Expat + +Files: src/Composer/Util/TlsHelper.php +Copyright: 2016, Nils Adermann + 2016, Jordi Boggiano + 2013, Evan Coury +License: Expat and BSD-2-Clause + +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_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 @@ + $vendorDir . '/ircmaxell/password-compat/lib/password.php', +); diff --git a/web/vendor/composer/autoload_namespaces.php b/web/vendor/composer/autoload_namespaces.php new file mode 100644 index 000000000..b7fc0125d --- /dev/null +++ b/web/vendor/composer/autoload_namespaces.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..6d63dc4f7 --- /dev/null +++ b/web/vendor/composer/autoload_real.php @@ -0,0 +1,70 @@ += 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); + + 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 new file mode 100644 index 000000000..980a5a0d7 --- /dev/null +++ b/web/vendor/composer/autoload_static.php @@ -0,0 +1,35 @@ + __DIR__ . '/..' . '/ircmaxell/password-compat/lib/password.php', + ); + + public static $prefixLengthsPsr4 = array ( + 'F' => + 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..0e2ed23cf --- /dev/null +++ b/web/vendor/composer/installed.json @@ -0,0 +1,94 @@ +[ + { + "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" + }, + { + "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/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 @@ + + * @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: Fri, 24 May 2019 13:53:24 -0400 Subject: [PATCH 3/3] Add shutdown capability (#2575) * Add Config for showing a system shutdown/restart option * Add a translation for Shutdown * add a shutdown power button to the navbar * but the shutdown icon in a navbar-txt * set width and height of shutdown window * Add instructions for enabling the web user to run shutdown * add the shutdown view and actions --- .../lib/ZoneMinder/ConfigData.pm.in | 12 ++++ web/includes/actions/shutdown.php | 44 ++++++++++++ web/lang/en_gb.php | 1 + web/skins/classic/includes/functions.php | 7 +- web/skins/classic/js/base.js | 1 + web/skins/classic/views/shutdown.php | 69 +++++++++++++++++++ 6 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 web/includes/actions/shutdown.php create mode 100644 web/skins/classic/views/shutdown.php diff --git a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in index d133b3eaf..c848441e8 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in @@ -473,6 +473,18 @@ our @options = ( type => $types{string}, category => 'system', }, + { + name => 'ZM_SYSTEM_SHUTDOWN', + default => 'true', + description => 'Allow Admin users to power off or restart the system from the ZoneMinder UI.', + help => 'The system will need to have sudo installed and the following added to /etc/sudoers~~ + ~~ + @ZM_WEB_USER@ ALL=NOPASSWD: /sbin/shutdown~~ + ~~ + to perform the shutdown or reboot', + type => $types{boolean}, + category => 'system', + }, { name => 'ZM_USE_DEEP_STORAGE', default => 'yes', diff --git a/web/includes/actions/shutdown.php b/web/includes/actions/shutdown.php new file mode 100644 index 000000000..f6d31b4f4 --- /dev/null +++ b/web/includes/actions/shutdown.php @@ -0,0 +1,44 @@ +&1", $output, $rc); + #exec('sudo -n /bin/systemctl poweroff -i 2>&1', $output, $rc); + ZM\Logger::Debug("Shutdown output $rc " . implode("\n",$output)); + #ZM\Logger::Debug("Shutdown output " . shell_exec('/bin/systemctl poweroff -i 2>&1')); + } else if ( $action == 'restart' ) { + $output = array(); + exec("sudo -n /sbin/shutdown -r $when 2>&1", $output); + #exec('sudo -n /bin/systemctl reboot -i 2>&1', $output); + ZM\Logger::Debug("Shutdown output " . implode("\n",$output)); + } else if ( $action == 'cancel' ) { + $output = array(); + exec('sudo /sbin/shutdown -c 2>&1', $output); + } +} # end if action +?> diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index 1fdd112e0..cb5037045 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -693,6 +693,7 @@ $SLANG = array( 'Settings' => 'Settings', 'ShowFilterWindow' => 'Show Filter Window', 'ShowTimeline' => 'Show Timeline', + 'Shutdown' => 'Shutdown', 'SignalCheckColour' => 'Signal Check Colour', 'SignalCheckPoints' => 'Signal Check Points', 'Size' => 'Size', diff --git a/web/skins/classic/includes/functions.php b/web/skins/classic/includes/functions.php index 40bf18b30..d0ce75abb 100644 --- a/web/skins/classic/includes/functions.php +++ b/web/skins/classic/includes/functions.php @@ -328,10 +328,13 @@ if (isset($_REQUEST['filter']['Query']['terms']['attr'])) { - - + + + diff --git a/web/skins/classic/js/base.js b/web/skins/classic/js/base.js index b0a28dc96..5d044f229 100644 --- a/web/skins/classic/js/base.js +++ b/web/skins/classic/js/base.js @@ -60,6 +60,7 @@ var popupSizes = { 'preset': {'width': 300, 'height': 220}, 'server': {'width': 600, 'height': 405}, 'settings': {'width': 220, 'height': 235}, + 'shutdown': {'width': 400, 'height': 400}, 'state': {'width': 400, 'height': 170}, 'stats': {'width': 840, 'height': 200}, 'storage': {'width': 600, 'height': 405}, diff --git a/web/skins/classic/views/shutdown.php b/web/skins/classic/views/shutdown.php new file mode 100644 index 000000000..5e032cd1c --- /dev/null +++ b/web/skins/classic/views/shutdown.php @@ -0,0 +1,69 @@ + + +
+ +
+
+ +'.implode('
', $output).'

'; + } + if ( isset($_POST['when']) and ($_POST['when'] != 'NOW') and ($action != 'cancel') ) { + echo '

You may cancel this shutdown by clicking '.translate('Cancel').'

'; + } +?> +

Warning

+ This command will either shutdown or restart all ZoneMinder Servers
+

+

+ + +

+
+ + + + + + +
+
+
+
+ +