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
This commit is contained in:
Pliable Pixels 2019-05-24 13:48:40 -04:00 committed by Isaac Connor
parent 34370e0060
commit fc27393a96
53 changed files with 3000 additions and 245 deletions

6
.gitmodules vendored
View File

@ -5,3 +5,9 @@
[submodule "web/api/app/Plugin/CakePHP-Enum-Behavior"] [submodule "web/api/app/Plugin/CakePHP-Enum-Behavior"]
path = web/api/app/Plugin/CakePHP-Enum-Behavior path = web/api/app/Plugin/CakePHP-Enum-Behavior
url = https://github.com/ZoneMinder/CakePHP-Enum-Behavior.git 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

View File

@ -870,6 +870,13 @@ include(Pod2Man)
ADD_MANPAGE_TARGET() ADD_MANPAGE_TARGET()
# Process subdirectories # 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(src)
add_subdirectory(scripts) add_subdirectory(scripts)
add_subdirectory(db) add_subdirectory(db)

View File

@ -640,6 +640,8 @@ CREATE TABLE `Users` (
`System` enum('None','View','Edit') NOT NULL default 'None', `System` enum('None','View','Edit') NOT NULL default 'None',
`MaxBandwidth` varchar(16), `MaxBandwidth` varchar(16),
`MonitorIds` text, `MonitorIds` text,
`TokenMinExpiry` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`APIEnabled` tinyint(3) UNSIGNED NOT NULL default 1,
PRIMARY KEY (`Id`), PRIMARY KEY (`Id`),
UNIQUE KEY `UC_Username` (`Username`) UNIQUE KEY `UC_Username` (`Username`)
) ENGINE=@ZM_MYSQL_ENGINE@; ) ENGINE=@ZM_MYSQL_ENGINE@;

27
db/zm_update-1.33.9.sql Normal file
View File

@ -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;

View File

@ -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 , libdata-dump-perl, libclass-std-fast-perl, libsoap-wsdl-perl, libio-socket-multicast-perl, libdigest-sha-perl
, libsys-cpu-perl, libsys-meminfo-perl , libsys-cpu-perl, libsys-meminfo-perl
, libdata-uuid-perl , libdata-uuid-perl
, libssl-dev
, libcrypt-eksblowfish-perl, libdata-entropy-perl
Standards-Version: 3.9.4 Standards-Version: 3.9.4
Package: zoneminder Package: zoneminder
@ -51,6 +53,9 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends}
, zip , zip
, libvlccore5 | libvlccore7 | libvlccore8, libvlc5 , libvlccore5 | libvlccore7 | libvlccore8, libvlc5
, libpolkit-gobject-1-0, php5-gd , libpolkit-gobject-1-0, php5-gd
, libssl
,libcrypt-eksblowfish-perl, libdata-entropy-perl
Recommends: mysql-server | mariadb-server Recommends: mysql-server | mariadb-server
Description: Video camera security and surveillance solution Description: Video camera security and surveillance solution
ZoneMinder is intended for use in single or multi-camera video security ZoneMinder is intended for use in single or multi-camera video security

View File

@ -23,6 +23,9 @@ Build-Depends: debhelper (>= 9), python-sphinx | python3-sphinx, apache2-dev, dh
,libsys-mmap-perl [!hurd-any] ,libsys-mmap-perl [!hurd-any]
,libwww-perl ,libwww-perl
,libdata-uuid-perl ,libdata-uuid-perl
,libssl-dev
,libcrypt-eksblowfish-perl
,libdata-entropy-perl
# Unbundled (dh_linktree): # Unbundled (dh_linktree):
,libjs-jquery ,libjs-jquery
,libjs-mootools ,libjs-mootools
@ -63,8 +66,12 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends}
,policykit-1 ,policykit-1
,rsyslog | system-log-daemon ,rsyslog | system-log-daemon
,zip ,zip
,libdata-dump-perl, libclass-std-fast-perl, libsoap-wsdl-perl, libio-socket-multicast-perl, libdigest-sha-perl ,libdata-dump-perl, libclass-std-fast-perl, libsoap-wsdl-perl
, libsys-cpu-perl, libsys-meminfo-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} Recommends: ${misc:Recommends}
,libapache2-mod-php5 | php5-fpm ,libapache2-mod-php5 | php5-fpm
,mysql-server | virtual-mysql-server ,mysql-server | virtual-mysql-server

View File

@ -30,6 +30,9 @@ Build-Depends: debhelper (>= 9), dh-systemd, python-sphinx | python3-sphinx, apa
,libsys-mmap-perl [!hurd-any] ,libsys-mmap-perl [!hurd-any]
,libwww-perl ,libwww-perl
,libdata-uuid-perl ,libdata-uuid-perl
,libssl-dev
,libcrypt-eksblowfish-perl
,libdata-entropy-perl
# Unbundled (dh_linktree): # Unbundled (dh_linktree):
,libjs-jquery ,libjs-jquery
,libjs-mootools ,libjs-mootools
@ -76,6 +79,9 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends}
,rsyslog | system-log-daemon ,rsyslog | system-log-daemon
,zip ,zip
,libpcre3 ,libpcre3
,libssl | libssl1.0.0
,libcrypt-eksblowfish-perl
,libdata-entropy-perl
Recommends: ${misc:Recommends} Recommends: ${misc:Recommends}
,libapache2-mod-php5 | libapache2-mod-php | php5-fpm | php-fpm ,libapache2-mod-php5 | libapache2-mod-php | php5-fpm | php-fpm
,mysql-server | mariadb-server | virtual-mysql-server ,mysql-server | mariadb-server | virtual-mysql-server

View File

@ -1,10 +1,12 @@
API 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 Overview
^^^^^^^^ ^^^^^^^^
In an effort to further 'open up' ZoneMinder, an API was needed. This will In an effort to further 'open up' ZoneMinder, an API was needed. This will
allow quick integration with and development of ZoneMinder. 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) provides a RESTful service and supports CRUD (create, retrieve, update, delete)
functions for Monitors, Events, Frames, Zones and Config. functions for Monitors, Events, Frames, Zones and Config.
Streaming Interface API evolution
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
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 The ZoneMinder API has evolved over time. Broadly speaking the iterations were as follows:
~~~~~~~~~~~~~~
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: * 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.
<img src="https://yourserver/zm/cgi-bin/nph-zms?scale=50&width=640p&height=480px&mode=jpeg&maxfps=5&buffer=1000&&monitor=1&auth=b54a589e09f330498f4ae2203&connkey=36139" />
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 <https://github.com/ZoneMinder/zoneminder/blob/10531df54312f52f0f32adec3d4720c063897b62/web/skins/classic/includes/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:
::
<img src="https://yourserver/zm/cgi-bin/nph-zms?mode=jpeg&frame=1&replay=none&source=event&event=293820&connkey=77493&auth=b54a58f5f4ae2203" />
* 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:
::
<video src="https://yourserver/zm/index.php?view=view_video&eid=294690&auth=33f3d558af84cf08" type="video/mp4"></video>
* 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 Enabling API
^^^^^^^^^^^^ ^^^^^^^^^^^^^
A default ZoneMinder installs with APIs enabled. You can explictly enable/disable the APIs
via the Options->System menu by enabling/disabling ``OPT_USE_API``. Note that if you intend
to use APIs with 3rd party apps, such as zmNinja or others that use APIs, you should also
enable ``AUTH_HASH_LOGINS``.
Login, Logout & API Security ZoneMinder comes with APIs enabled. To check if APIs are enabled, visit ``Options->System``. If ``OPT_USE_API`` is enabled, your APIs are active.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For v2.0 APIs, you have an additional option right below it - ``OPT_USE_LEGACY_API_AUTH`` which is enabled by default. When enabled, the `login.json` API (discussed later) will return both the old style (``auth=``) and new style (``token=``) credentials. The reason this is enabled by default is because any existing apps that use the API would break if they were not updated to use v2.0. (Note that zmNinja 1.3.057 and beyond will support tokens)
The APIs tie into ZoneMinder's existing security model. This means if you have
OPT_AUTH enabled, you need to log into ZoneMinder using the same browser you plan to
use the APIs from. If you are developing an app that relies on the API, you need
to do a POST login from the app into ZoneMinder before you can access the API.
Then, you need to re-use the authentication information of the login (returned as cookie states) Enabling secret key
with subsequent APIs for the authentication information to flow through to the APIs. ^^^^^^^^^^^^^^^^^^^
This means if you plan to use cuRL to experiment with these APIs, you first need to login: * 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=05f3a50e8f7<deleted>063",
"append_password": 0,
"version": "1.33.9",
"apiversion": "1.0"
}
Or for API 2.0:
**Login process for older versions of ZoneMinder**
:: ::
curl -d "username=XXXX&password=YYYY&action=login&view=console" -c cookies.txt http://yourzmip/zm/index.php {
"access_token": "eyJ0eXAiOiJK<deleted>HE",
"access_token_expires": 3600,
"refresh_token": "eyJ0eXAiOi<deleted>mPs",
"refresh_token_expires": 86400,
"credentials": "auth=05f3a50e8f7<deleted>063", # 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=<access_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 # v1.0 or 2.0 based API access (will only work if AUTH_HASH_LOGINS is enabled)
and the command will silently fail. curl -XPOST -d "auth=<hex digits from 'credentials'>" https://yourserver/zm/api/monitors.json
# or
curl -XGET https://yourserver/zm/api/monitors.json&auth=<hex digits from 'credentials'>
# 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 .. NOTE::
to apply that cookie state to all subsequent APIs. You do that by using a '-b cookies.txt' to subsequent APIs if you are 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)
using CuRL like so:
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. If you were to use any `JWT token verifier <https://jwt.io>`__ it can easily decode that token and will show:
A deeper dive into the login process
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
As you might have seen above, there are two ways to login, one that uses the `login.json` API and the other that logs in using the ZM portal. If you are running ZoneMinder 1.32.0 and above, it is *strongly* recommended you use the `login.json` approach. The "old" approach will still work but is not as powerful as the API based login. Here are the reasons why:
* The "old" approach basically uses the same login webpage (`index.php`) that a user would log into when viewing the ZM console. This is not really using an API and more importantly, if you have additional components like reCAPTCHA enabled, this will not work. Using the API approach is much cleaner and will work irrespective of reCAPTCHA
* The new login API returns important information that you can use to stream videos as well, right after login. Consider for example, a typical response to the login API (`/login.json`):
:: ::
{ {
"credentials": "auth=f5b9cf48693fe8552503c8ABCD5", "iss": "ZoneMinder",
"append_password": 0, "iat": 1557940752,
"version": "1.31.44", "exp": 1557944352,
"apiversion": "1.0" "user": "admin",
} "type": "access"
}
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: Invalid Signature
::
<img src="https://server/zm/cgi-bin/nph-zms?monitor=1&auth=<authval>" />
Where `authval` is the credentials returned to start streaming videos.
The `append_password` field will contain 1 when it is necessary for you to append your ZM password. This is the case when you set `AUTH_RELAY` in ZM options to "plain", for example. In that case, the `credentials` field may contain something like `&user=admin&pass=` and you have to add your password to that string.
.. NOTE:: It is recommended you invoke the `login` API once every 60 minutes to make sure the session stays alive. The same is true if you use the old login method too. Don't be surprised. JWT tokens, by default, are `not meant to be encrypted <https://softwareengineering.stackexchange.com/questions/280257/json-web-token-why-is-the-payload-public>`__. 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 <https://letsencrypt.org>`__ 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) (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 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. 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 <https://github.com/ZoneMinder/zoneminder/tree/master/web/api/app/Controller>`__ 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:
::
<img src="https://yourserver/zm/cgi-bin/nph-zms?scale=50&width=640p&height=480px&mode=jpeg&maxfps=5&buffer=1000&&monitor=1&token=eW<deleted>03&connkey=36139" />
# or
<img src="https://yourserver/zm/cgi-bin/nph-zms?scale=50&width=640p&height=480px&mode=jpeg&maxfps=5&buffer=1000&&monitor=1&auth=b5<deleted>03&connkey=36139" />
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 <https://github.com/ZoneMinder/zoneminder/blob/10531df54312f52f0f32adec3d4720c063897b62/web/skins/classic/includes/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:
::
<img src="https://yourserver/zm/cgi-bin/nph-zms?mode=jpeg&frame=1&replay=none&source=event&event=293820&connkey=77493&token=ew<deleted>" />
# or
<img src="https://yourserver/zm/cgi-bin/nph-zms?mode=jpeg&frame=1&replay=none&source=event&event=293820&connkey=77493&auth=b5<deleted>" />
* 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:
::
<video src="https://yourserver/zm/index.php?view=view_video&eid=294690&token=eW<deleted>" type="video/mp4"></video>
# or
<video src="https://yourserver/zm/index.php?view=view_video&eid=294690&auth=33<deleted>" type="video/mp4"></video>
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 Further Reading
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
As described earlier, treat this document as an "introduction" to the important parts of the API and streaming interfaces. 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: There are several details that haven't yet been documented. Till they are, here are some resources:

View File

@ -396,6 +396,17 @@ our @options = (
type => $types{boolean}, type => $types{boolean},
category => 'system', 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', name => 'ZM_OPT_USE_EVENTNOTIFICATION',
default => 'no', default => 'no',

View File

@ -51,6 +51,8 @@ configuring upgrades etc, including on the fly upgrades.
use strict; use strict;
use bytes; use bytes;
use version; use version;
use Crypt::Eksblowfish::Bcrypt;
use Data::Entropy::Algorithms qw(rand_bits);
# ========================================================================== # ==========================================================================
# #
@ -312,6 +314,7 @@ if ( $migrateEvents ) {
if ( $freshen ) { if ( $freshen ) {
print( "\nFreshening configuration in database\n" ); print( "\nFreshening configuration in database\n" );
migratePaths(); migratePaths();
migratePasswords();
ZoneMinder::Config::loadConfigFromDB(); ZoneMinder::Config::loadConfigFromDB();
ZoneMinder::Config::saveConfigToDB(); 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 { sub migratePaths {
my $customConfigFile = '@ZM_CONFIG_SUBDIR@/zmcustom.conf'; my $customConfigFile = '@ZM_CONFIG_SUBDIR@/zmcustom.conf';

View File

@ -4,20 +4,26 @@
configure_file(zm_config.h.in "${CMAKE_CURRENT_BINARY_DIR}/zm_config.h" @ONLY) 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) # 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. # A fix for cmake recompiling the source files for every target.
add_library(zm STATIC ${ZM_BIN_SRC_FILES}) add_library(zm STATIC ${ZM_BIN_SRC_FILES})
link_directories(../third_party/bcrypt)
add_executable(zmc zmc.cpp) add_executable(zmc zmc.cpp)
add_executable(zma zma.cpp) add_executable(zma zma.cpp)
add_executable(zmu zmu.cpp) add_executable(zmu zmu.cpp)
add_executable(zms zms.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(zmc zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS})
target_link_libraries(zma 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(zmu zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS} bcrypt)
target_link_libraries(zms zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS}) target_link_libraries(zms zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS} bcrypt)
# Generate man files for the binaries destined for the bin folder # Generate man files for the binaries destined for the bin folder
FOREACH(CBINARY zma zmc zmu) FOREACH(CBINARY zma zmc zmu)

117
src/zm_crypt.cpp Normal file
View File

@ -0,0 +1,117 @@
#include "zm.h"
# include "zm_crypt.h"
#include "BCrypt.hpp"
#include "jwt.h"
#include <algorithm>
#include <openssl/sha.h>
// returns username if valid, "" if not
std::pair <std::string, unsigned int> 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;
}

31
src/zm_crypt.h Normal file
View File

@ -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 <string.h>
bool verifyPassword( const char *username, const char *input_password, const char *db_password_hash);
std::pair <std::string, unsigned int> verifyToken(std::string token, std::string key);
#endif // ZM_CRYPT_H

View File

@ -27,7 +27,9 @@
#include <string.h> #include <string.h>
#include <time.h> #include <time.h>
#include "zm_utils.h" #include "zm_utils.h"
#include "zm_crypt.h"
User::User() { User::User() {
id = 0; 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. // 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 ); mysql_real_escape_string(&dbconn, safer_username, username, username_length );
if ( password ) {
int password_length = strlen(password); snprintf(sql, sizeof(sql),
char *safer_password = new char[(password_length * 2) + 1]; "SELECT Id, Username, Password, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds"
mysql_real_escape_string(&dbconn, safer_password, password, password_length); " 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 Password = password('%s') AND Enabled = 1",
safer_username, safer_password );
delete safer_password;
} else {
snprintf(sql, sizeof(sql),
"SELECT Id, Username, Password, Enabled, Stream+0, Events+0, Control+0, Monitors+0, System+0, MonitorIds"
" FROM Users where Username = '%s' and Enabled = 1", safer_username );
}
if ( mysql_query(&dbconn, sql) ) { if ( mysql_query(&dbconn, sql) ) {
Error("Can't run query: %s", mysql_error(&dbconn)); Error("Can't run query: %s", mysql_error(&dbconn));
exit(mysql_errno(&dbconn)); exit(mysql_errno(&dbconn));
} }
MYSQL_RES *result = mysql_store_result(&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); MYSQL_ROW dbrow = mysql_fetch_row(result);
User *user = new User(dbrow); User *user = new User(dbrow);
Info("Authenticated user '%s'", user->getUsername());
if (verifyPassword(username, password, user->getPassword())) {
mysql_free_result(result); Info("Authenticated user '%s'", user->getUsername());
delete safer_username; mysql_free_result(result);
delete safer_username;
return user; 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<std::string, unsigned int> 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 // Function to validate an authentication string
User *zmLoadAuthUser( const char *auth, bool use_remote_addr ) { User *zmLoadAuthUser( const char *auth, bool use_remote_addr ) {
#if HAVE_DECL_MD5 || HAVE_DECL_GNUTLS_FINGERPRINT #if HAVE_DECL_MD5 || HAVE_DECL_GNUTLS_FINGERPRINT

View File

@ -77,6 +77,7 @@ public:
User *zmLoadUser( const char *username, const char *password=0 ); User *zmLoadUser( const char *username, const char *password=0 );
User *zmLoadAuthUser( const char *auth, bool use_remote_addr ); User *zmLoadAuthUser( const char *auth, bool use_remote_addr );
User *zmLoadTokenUser( std::string jwt, bool use_remote_addr);
bool checkUser ( const char *username); bool checkUser ( const char *username);
bool checkPass (const char *password); bool checkPass (const char *password);

View File

@ -71,6 +71,7 @@ int main( int argc, const char *argv[] ) {
std::string username; std::string username;
std::string password; std::string password;
char auth[64] = ""; char auth[64] = "";
std::string jwt_token_str = "";
unsigned int connkey = 0; unsigned int connkey = 0;
unsigned int playback_buffer = 0; unsigned int playback_buffer = 0;
@ -161,6 +162,10 @@ int main( int argc, const char *argv[] ) {
playback_buffer = atoi(value); playback_buffer = atoi(value);
} else if ( !strcmp( name, "auth" ) ) { } else if ( !strcmp( name, "auth" ) ) {
strncpy( auth, value, sizeof(auth)-1 ); 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" ) ) { } else if ( !strcmp( name, "user" ) ) {
username = UriDecode( value ); username = UriDecode( value );
} else if ( !strcmp( name, "pass" ) ) { } else if ( !strcmp( name, "pass" ) ) {
@ -184,11 +189,16 @@ int main( int argc, const char *argv[] ) {
if ( config.opt_use_auth ) { if ( config.opt_use_auth ) {
User *user = 0; 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()) ) { if ( checkUser(username.c_str()) ) {
user = zmLoadUser(username.c_str()); user = zmLoadUser(username.c_str());
} else { } else {
Error("") Error("Bad username");
} }
} else { } else {

View File

@ -138,6 +138,7 @@ void Usage(int status=-1) {
" -U, --username <username> : When running in authenticated mode the username and\n" " -U, --username <username> : When running in authenticated mode the username and\n"
" -P, --password <password> : password combination of the given user\n" " -P, --password <password> : password combination of the given user\n"
" -A, --auth <authentication> : Pass authentication hash string instead of user details\n" " -A, --auth <authentication> : Pass authentication hash string instead of user details\n"
" -T, --token <token> : Pass JWT token string instead of user details\n"
"", stderr ); "", stderr );
exit(status); exit(status);
@ -242,6 +243,7 @@ int main(int argc, char *argv[]) {
{"username", 1, 0, 'U'}, {"username", 1, 0, 'U'},
{"password", 1, 0, 'P'}, {"password", 1, 0, 'P'},
{"auth", 1, 0, 'A'}, {"auth", 1, 0, 'A'},
{"token", 1, 0, 'T'},
{"version", 1, 0, 'V'}, {"version", 1, 0, 'V'},
{"help", 0, 0, 'h'}, {"help", 0, 0, 'h'},
{"list", 0, 0, 'l'}, {"list", 0, 0, 'l'},
@ -263,6 +265,7 @@ int main(int argc, char *argv[]) {
char *username = 0; char *username = 0;
char *password = 0; char *password = 0;
char *auth = 0; char *auth = 0;
std::string jwt_token_str = "";
#if ZM_HAS_V4L #if ZM_HAS_V4L
#if ZM_HAS_V4L2 #if ZM_HAS_V4L2
int v4lVersion = 2; int v4lVersion = 2;
@ -273,7 +276,7 @@ int main(int argc, char *argv[]) {
while (1) { while (1) {
int option_index = 0; 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 ) { if ( c == -1 ) {
break; break;
} }
@ -378,6 +381,9 @@ int main(int argc, char *argv[]) {
case 'A': case 'A':
auth = optarg; auth = optarg;
break; break;
case 'T':
jwt_token_str = std::string(optarg);
break;
#if ZM_HAS_V4L #if ZM_HAS_V4L
case 'V': case 'V':
v4lVersion = (atoi(optarg)==1)?1:2; v4lVersion = (atoi(optarg)==1)?1:2;
@ -438,10 +444,13 @@ int main(int argc, char *argv[]) {
user = zmLoadUser(username); user = zmLoadUser(username);
} else { } else {
if ( !(username && password) && !auth ) { if ( !(username && password) && !auth && (jwt_token_str=="")) {
Error("Username and password or auth string must be supplied"); Error("Username and password or auth/token string must be supplied");
exit_zmu(-1); exit_zmu(-1);
} }
if (jwt_token_str != "") {
user = zmLoadTokenUser(jwt_token_str, false);
}
if ( auth ) { if ( auth ) {
user = zmLoadAuthUser(auth, false); user = zmLoadAuthUser(auth, false);
} }

1
third_party/bcrypt vendored Submodule

@ -0,0 +1 @@
Subproject commit be171cd75dd65e06315a67c7dcdb8e1bbc1dabd4

1
third_party/jwt-cpp vendored Submodule

@ -0,0 +1 @@
Subproject commit bfca4f6a87bfd9d9a259939d0524169827a3a862

View File

@ -1 +1 @@
1.33.8 1.33.9

2
web/.gitignore vendored
View File

@ -4,8 +4,8 @@
/app/tmp /app/tmp
/lib/Cake/Console/Templates/skel/tmp/ /lib/Cake/Console/Templates/skel/tmp/
/plugins /plugins
/vendors
/build /build
/vendors
/dist /dist
/tags /tags
/app/webroot/events /app/webroot/events

View File

@ -9,7 +9,7 @@ add_subdirectory(tools/mootools)
configure_file(includes/config.php.in "${CMAKE_CURRENT_BINARY_DIR}/includes/config.php" @ONLY) configure_file(includes/config.php.in "${CMAKE_CURRENT_BINARY_DIR}/includes/config.php" @ONLY)
# Install the web files # 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 index.php robots.txt DESTINATION "${ZM_WEBDIR}")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/includes/config.php" DESTINATION "${ZM_WEBDIR}/includes") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/includes/config.php" DESTINATION "${ZM_WEBDIR}/includes")

View File

@ -68,25 +68,46 @@ class AppController extends Controller {
# For use throughout the app. If not logged in, this will be null. # For use throughout the app. If not logged in, this will be null.
global $user; global $user;
if ( ZM_OPT_USE_AUTH ) { if ( ZM_OPT_USE_AUTH ) {
require_once __DIR__ .'/../../../includes/auth.php'; require_once __DIR__ .'/../../../includes/auth.php';
$mUser = $this->request->query('user') ? $this->request->query('user') : $this->request->data('user'); $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'); $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 ) { 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 ) { if ( !$user ) {
throw new UnauthorizedException(__('User not found or incorrect password')); throw new UnauthorizedException(__('Incorrect credentials or API disabled'));
return; 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 ) { } else if ( $mAuth ) {
$user = getAuthUser($mAuth); $user = getAuthUser($mAuth, true);
if ( !$user ) { if ( !$user ) {
throw new UnauthorizedException(__('Invalid Auth Key')); throw new UnauthorizedException(__('Invalid Auth Key'));
return; return;
} }
} }
// We need to reject methods that are not authenticated // We need to reject methods that are not authenticated
// besides login and logout // besides login and logout
@ -100,6 +121,10 @@ class AppController extends Controller {
} }
} # end if ! login or logout } # end if ! login or logout
} # end if ZM_OPT_AUTH } # 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() } # end function beforeFilter()
} }

View File

@ -31,19 +31,60 @@ class HostController extends AppController {
} }
function login() { 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(); $ver = $this->_getVersion();
$this->set(array( $cred = [];
'credentials' => $cred[0], $cred_depr = [];
'append_password'=>$cred[1],
'version' => $ver[0], if ($mUser && $mPassword) {
'apiversion' => $ver[1], $cred = $this->_getCredentials(true); // generate refresh
'_serialize' => array('credentials', }
'append_password', else {
'version', $cred = $this->_getCredentials(false, $mToken); // don't generate refresh
'apiversion' }
)));
$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() } // end function login()
// clears out session // clears out session
@ -56,40 +97,95 @@ class HostController extends AppController {
)); ));
} // end function logout() } // end function logout()
private function _getCredentials() { private function _getCredentialsDeprecated() {
$credentials = ''; $credentials = '';
$appendPassword = 0; $appendPassword = 0;
$this->loadModel('Config'); if (ZM_OPT_USE_AUTH) {
$isZmAuth = $this->Config->find('first',array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_OPT_USE_AUTH')))['Config']['Value']; require_once __DIR__ .'/../../../includes/auth.php';
if (ZM_AUTH_RELAY=='hashed') {
if ( $isZmAuth ) { $credentials = 'auth='.generateAuthHash(ZM_AUTH_HASH_IPS,true);
// 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 else {
$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
$credentials = 'user='.$this->Session->read('Username').'&pass='; $credentials = 'user='.$this->Session->read('Username').'&pass=';
$appendPassword = 1; $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() { if ( ZM_OPT_USE_AUTH ) {
// ignore debug warnings from other functions require_once __DIR__ .'/../../../includes/auth.php';
$this->view='Json'; require_once __DIR__.'/../../../vendor/autoload.php';
$val = $this->_getCredentials();
$this->set(array( $key = ZM_AUTH_HASH_SECRET;
'credentials'=> $val[0], if (!$key) {
'append_password'=>$val[1], throw new ForbiddenException(__('Please create a valid AUTH_HASH_SECRET in ZoneMinder'));
'_serialize' => array('credentials', 'append_password') }
) );
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 // If $mid is set, only return disk usage for that monitor
@ -169,7 +265,7 @@ class HostController extends AppController {
private function _getVersion() { private function _getVersion() {
$version = Configure::read('ZM_VERSION'); $version = Configure::read('ZM_VERSION');
$apiversion = '1.0'; $apiversion = '2.0';
return array($version, $apiversion); return array($version, $apiversion);
} }

6
web/composer.json Normal file
View File

@ -0,0 +1,6 @@
{
"require": {
"firebase/php-jwt": "^5.0",
"ircmaxell/password-compat": "^1.0"
}
}

106
web/composer.lock generated Normal file
View File

@ -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": []
}

View File

@ -75,6 +75,7 @@ if ( $action == 'delete' ) {
case 'config' : case 'config' :
$restartWarning = true; $restartWarning = true;
break; break;
case 'API':
case 'web' : case 'web' :
case 'tools' : case 'tools' :
break; break;

View File

@ -28,8 +28,18 @@ if ( $action == 'user' ) {
$types = array(); $types = array();
$changes = getFormChanges($dbUser, $_REQUEST['newUser'], $types); $changes = getFormChanges($dbUser, $_REQUEST['newUser'], $types);
if ( $_REQUEST['newUser']['Password'] ) if (function_exists ('password_hash')) {
$changes['Password'] = 'Password = password('.dbEscape($_REQUEST['newUser']['Password']).')'; $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 else
unset($changes['Password']); unset($changes['Password']);
@ -53,8 +63,19 @@ if ( $action == 'user' ) {
$types = array(); $types = array();
$changes = getFormChanges($dbUser, $_REQUEST['newUser'], $types); $changes = getFormChanges($dbUser, $_REQUEST['newUser'], $types);
if ( !empty($_REQUEST['newUser']['Password']) ) if (function_exists ('password_hash')) {
$changes['Password'] = 'Password = password('.dbEscape($_REQUEST['newUser']['Password']).')'; $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 else
unset($changes['Password']); unset($changes['Password']);
if ( count($changes) ) { if ( count($changes) ) {

View File

@ -19,8 +19,33 @@
// //
// //
require_once('session.php'); 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; global $user;
if ( !$username and isset($_REQUEST['username']) ) if ( !$username and isset($_REQUEST['username']) )
@ -29,8 +54,10 @@ function userLogin($username='', $password='', $passwordHashed=false) {
$password = $_REQUEST['password']; $password = $_REQUEST['password'];
// if true, a popup will display after login // if true, a popup will display after login
// PP - lets validate reCaptcha if it exists // lets validate reCaptcha if it exists
if ( defined('ZM_OPT_USE_GOOG_RECAPTCHA') // 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_SECRETKEY')
&& defined('ZM_OPT_GOOG_RECAPTCHA_SITEKEY') && defined('ZM_OPT_GOOG_RECAPTCHA_SITEKEY')
&& ZM_OPT_USE_GOOG_RECAPTCHA && ZM_OPT_USE_GOOG_RECAPTCHA
@ -44,17 +71,17 @@ function userLogin($username='', $password='', $passwordHashed=false) {
'remoteip' => $_SERVER['REMOTE_ADDR'] 'remoteip' => $_SERVER['REMOTE_ADDR']
); );
$res = do_post_request($url, http_build_query($fields)); $res = do_post_request($url, http_build_query($fields));
$responseData = json_decode($res,true); $responseData = json_decode($res, true);
// PP - credit: https://github.com/google/recaptcha/blob/master/src/ReCaptcha/Response.php // credit: https://github.com/google/recaptcha/blob/master/src/ReCaptcha/Response.php
// if recaptcha resulted in error, we might have to deny login // if recaptcha resulted in error, we might have to deny login
if ( isset($responseData['success']) && $responseData['success'] == false ) { if ( isset($responseData['success']) && ($responseData['success'] == false) ) {
// PP - before we deny auth, let's make sure the error was not 'invalid secret' // PP - before we deny auth, let's make sure the error was not 'invalid secret'
// because that means the user did not configure the secret key correctly // because that means the user did not configure the secret key correctly
// in this case, we prefer to let him login in and display a message to correct // in this case, we prefer to let him login in and display a message to correct
// the key. Unfortunately, there is no way to check for invalid site key in code // the key. Unfortunately, there is no way to check for invalid site key in code
// as it produces the same error as when you don't answer a recaptcha // as it produces the same error as when you don't answer a recaptcha
if ( isset($responseData['error-codes']) && is_array($responseData['error-codes']) ) { if ( isset($responseData['error-codes']) && is_array($responseData['error-codes']) ) {
if ( !in_array('invalid-input-secret',$responseData['error-codes']) ) { if ( !in_array('invalid-input-secret', $responseData['error-codes']) ) {
Error('reCaptcha authentication failed'); Error('reCaptcha authentication failed');
return null; return null;
} else { } else {
@ -64,28 +91,86 @@ function userLogin($username='', $password='', $passwordHashed=false) {
} // end if success==false } // end if success==false
} // end if using reCaptcha } // end if using reCaptcha
$sql = 'SELECT * FROM Users WHERE Enabled=1'; // coming here means we need to authenticate the user
$sql_values = NULL; // if captcha existed, it was passed
if ( ZM_AUTH_TYPE == 'builtin' ) {
if ( $passwordHashed ) { $sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username = ?';
$sql .= ' AND Username=? AND Password=?'; $sql_values = array($username);
} else {
$sql .= ' AND Username=? AND Password=password(?)'; // 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 { } else {
$sql .= ' AND Username=?'; ZM\Error("Could not retrieve user $username details");
$sql_values = array($username); $_SESSION['loginFailed'] = true;
unset($user);
return false;
} }
$close_session = 0; $close_session = 0;
if ( !is_session_started() ) { if ( !is_session_started() ) {
session_start(); session_start();
$close_session = 1; $close_session = 1;
} }
$_SESSION['remoteAddr'] = $_SERVER['REMOTE_ADDR']; // To help prevent session hijacking $_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\""); 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']); unset($_SESSION['loginFailed']);
if ( ZM_AUTH_TYPE == 'builtin' ) { if ( ZM_AUTH_TYPE == 'builtin' ) {
$_SESSION['passwordHash'] = $user['Password']; $_SESSION['passwordHash'] = $user['Password'];
@ -113,7 +198,72 @@ function userLogout() {
zm_session_clear(); 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) ) { if ( ZM_OPT_USE_AUTH && ZM_AUTH_RELAY == 'hashed' && !empty($auth) ) {
$remoteAddr = ''; $remoteAddr = '';
if ( ZM_AUTH_HASH_IPS ) { if ( ZM_AUTH_HASH_IPS ) {
@ -134,7 +284,8 @@ function getAuthUser($auth) {
$sql = 'SELECT * FROM Users WHERE Enabled = 1'; $sql = 'SELECT * FROM Users WHERE Enabled = 1';
} }
foreach ( dbFetchAll($sql, NULL, $values) as $user ) { foreach ( dbFetchAll($sql, NULL, $values) as $user )
{
$now = time(); $now = time();
for ( $i = 0; $i < ZM_AUTH_HASH_TTL; $i++, $now -= ZM_AUTH_HASH_TTL * 1800 ) { // Try for last two hours for ( $i = 0; $i < ZM_AUTH_HASH_TTL; $i++, $now -= ZM_AUTH_HASH_TTL * 1800 ) { // Try for last two hours
$time = localtime($now); $time = localtime($now);
@ -142,7 +293,18 @@ function getAuthUser($auth) {
$authHash = md5($authKey); $authHash = md5($authKey);
if ( $auth == $authHash ) { 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 hour
} // end foreach user } // end foreach user
@ -153,8 +315,9 @@ function getAuthUser($auth) {
function generateAuthHash($useRemoteAddr, $force=false) { function generateAuthHash($useRemoteAddr, $force=false) {
if ( ZM_OPT_USE_AUTH and ZM_AUTH_RELAY == 'hashed' and isset($_SESSION['username']) and $_SESSION['passwordHash'] ) { if ( ZM_OPT_USE_AUTH and ZM_AUTH_RELAY == 'hashed' and isset($_SESSION['username']) and $_SESSION['passwordHash'] ) {
# regenerate a hash at half the liftetime of a hash, an hour is 3600 so half is 1800
$time = time(); $time = time();
$mintime = $time - ( ZM_AUTH_HASH_TTL * 1800 ); $mintime = $time - ( ZM_AUTH_HASH_TTL * 1800 );
if ( $force or ( !isset($_SESSION['AuthHash'.$_SESSION['remoteAddr']]) ) or ( $_SESSION['AuthHashGeneratedAt'] < $mintime ) ) { 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']) ) { if ( isset($_SESSION['username']) ) {
# Need to refresh permissions and validate that the user still exists if ( ZM_AUTH_HASH_LOGINS ) {
$sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username=?'; # Extra validation, if logged in, then the auth hash will be set in the session, so we can validate it.
$user = dbFetchOne($sql, NULL, array($_SESSION['username'])); # This prevent session modification to switch users
$user = getAuthUser($_SESSION['AuthHash'.$_SESSION['remoteAddr']]);
} else {
# Need to refresh permissions and validate that the user still exists
$sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username=?';
$user = dbFetchOne($sql, NULL, array($_SESSION['username']));
}
} }
if ( ZM_AUTH_RELAY == 'plain' ) { if ( ZM_AUTH_RELAY == 'plain' ) {
@ -234,6 +403,14 @@ if ( ZM_OPT_USE_AUTH ) {
} else if ( isset($_REQUEST['username']) and isset($_REQUEST['password']) ) { } else if ( isset($_REQUEST['username']) and isset($_REQUEST['password']) ) {
userLogin($_REQUEST['username'], $_REQUEST['password'], false); userLogin($_REQUEST['username'], $_REQUEST['password'], false);
} }
if (empty($user) && !empty($_REQUEST['token']) ) {
$ret = validateToken($_REQUEST['token'], 'access');
$user = $ret[0];
}
if ( !empty($user) ) { if ( !empty($user) ) {
// generate it once here, while session is open. Value will be cached in session and return when called later on // 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); generateAuthHash(ZM_AUTH_HASH_IPS);

View File

@ -102,8 +102,10 @@ $SLANG = array(
'AlarmRGBUnset' => 'You must set an alarm RGB colour', 'AlarmRGBUnset' => 'You must set an alarm RGB colour',
'Alert' => 'Alert', 'Alert' => 'Alert',
'All' => 'All', 'All' => 'All',
'AllTokensRevoked' => 'All Tokens Revoked',
'AnalysisFPS' => 'Analysis FPS', 'AnalysisFPS' => 'Analysis FPS',
'AnalysisUpdateDelay' => 'Analysis Update Delay', 'AnalysisUpdateDelay' => 'Analysis Update Delay',
'API' => 'API',
'Apply' => 'Apply', 'Apply' => 'Apply',
'ApplyingStateChange' => 'Applying State Change', 'ApplyingStateChange' => 'Applying State Change',
'ArchArchived' => 'Archived Only', 'ArchArchived' => 'Archived Only',
@ -420,6 +422,7 @@ $SLANG = array(
'Images' => 'Images', 'Images' => 'Images',
'Include' => 'Include', 'Include' => 'Include',
'In' => 'In', 'In' => 'In',
'InvalidateTokens' => 'Invalidate all generated tokens',
'Inverted' => 'Inverted', 'Inverted' => 'Inverted',
'Iris' => 'Iris', 'Iris' => 'Iris',
'KeyString' => 'Key String', 'KeyString' => 'Key String',
@ -658,6 +661,7 @@ $SLANG = array(
'RestrictedMonitors' => 'Restricted Monitors', 'RestrictedMonitors' => 'Restricted Monitors',
'ReturnDelay' => 'Return Delay', 'ReturnDelay' => 'Return Delay',
'ReturnLocation' => 'Return Location', 'ReturnLocation' => 'Return Location',
'RevokeAllTokens' => 'Revoke All Tokens',
'Rewind' => 'Rewind', 'Rewind' => 'Rewind',
'RotateLeft' => 'Rotate Left', 'RotateLeft' => 'Rotate Left',
'RotateRight' => 'Rotate Right', 'RotateRight' => 'Rotate Right',

View File

@ -350,10 +350,53 @@ fieldset > legend {
.alert, .warnText, .warning, .disabledText { .alert, .warnText, .warning, .disabledText {
color: #ffa801; color: #ffa801;
} }
.alarm, .errorText, .error { .alarm, .errorText, .error {
color: #ff3f34; 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 { .fakelink {
color: #7f7fb2; color: #7f7fb2;
cursor: pointer; cursor: pointer;

View File

@ -29,6 +29,7 @@ $tabs = array();
$tabs['skins'] = translate('Display'); $tabs['skins'] = translate('Display');
$tabs['system'] = translate('System'); $tabs['system'] = translate('System');
$tabs['config'] = translate('Config'); $tabs['config'] = translate('Config');
$tabs['API'] = translate('API');
$tabs['servers'] = translate('Servers'); $tabs['servers'] = translate('Servers');
$tabs['storage'] = translate('Storage'); $tabs['storage'] = translate('Storage');
$tabs['web'] = translate('Web'); $tabs['web'] = translate('Web');
@ -133,7 +134,8 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI
</div> </div>
</form> </form>
<?php
<?php
} else if ( $tab == 'users' ) { } else if ( $tab == 'users' ) {
?> ?>
<form name="userForm" method="post" action="?"> <form name="userForm" method="post" action="?">
@ -309,8 +311,87 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI
<button type="submit" class="btn-danger" name="deleteBtn" value="Delete" disabled="disabled"><?php echo translate('Delete') ?></button> <button type="submit" class="btn-danger" name="deleteBtn" value="Delete" disabled="disabled"><?php echo translate('Delete') ?></button>
</div> </div>
</form> </form>
<?php
} else { <?php
} else if ($tab == 'API') {
$apiEnabled = dbFetchOne("SELECT Value FROM Config WHERE Name='ZM_OPT_USE_API'");
if ($apiEnabled['Value']!='1') {
echo "<div class='errorText'>APIs are disabled. To enable, please turn on OPT_USE_API in Options->System</div>";
}
else {
?>
<form name="userForm" method="post" action="?">
<button class="pull-left" type="submit" name="updateSelected" id="updateSelected"><?php echo translate("Update")?> </button><button class="btn-danger pull-right" type="submit" name="revokeAllTokens" id="revokeAllTokens"> <?php echo translate("RevokeAllTokens")?></button><br/>
<?php
function revokeAllTokens()
{
$minTokenTime = time();
dbQuery ('UPDATE Users SET TokenMinExpiry=?', array ($minTokenTime));
echo "<span class='timedSuccessBox'>".translate('AllTokensRevoked')."</span>";
}
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 "<span class='timedSuccessBox'>".translate('Updated')."</span>";
}
if(array_key_exists('revokeAllTokens',$_POST)){
revokeAllTokens();
}
if(array_key_exists('updateSelected',$_POST)){
updateSelected();
}
?>
<br/><br/>
<input type="hidden" name="view" value="<?php echo $view ?>"/>
<input type="hidden" name="tab" value="<?php echo $tab ?>"/>
<input type="hidden" name="action" value="delete"/>
<table id="contentTable" class="table table-striped">
<thead class="thead-highlight">
<tr>
<th class="colUsername"><?php echo translate('Username') ?></th>
<th class="colMark"><?php echo translate('Revoke Token') ?></th>
<th class="colMark"><?php echo translate('API Enabled') ?></th>
</tr>
</thead>
<tbody>
<?php
$sql = 'SELECT * FROM Users ORDER BY Username';
foreach( dbFetchAll($sql) as $row ) {
?>
<tr>
<td class="colUsername"><?php echo validHtmlStr($row['Username']) ?></td>
<td class="colMark"><input type="checkbox" name="tokenUids[]" value="<?php echo $row['Id'] ?>" /></td>
<td class="colMark"><input type="checkbox" name="apiUids[]" value="<?php echo $row['Id']?>" <?php echo $row['APIEnabled']?'checked':''?> /></td>
</tr>
<?php
}
?>
</tbody>
</table>
</form>
<?php
} // API enabled
} // $tab == API
else {
$config = array(); $config = array();
$configCat = array(); $configCat = array();
$configCats = array(); $configCats = array();
@ -423,6 +504,7 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI
<?php <?php
} }
?> ?>
<div id="contentButtons"> <div id="contentButtons">
<button type="submit" value="Save"<?php echo $canEdit?'':' disabled="disabled"' ?>><?php echo translate('Save') ?></button> <button type="submit" value="Save"<?php echo $canEdit?'':' disabled="disabled"' ?>><?php echo translate('Save') ?></button>
</div> </div>
@ -431,6 +513,8 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI
} }
?> ?>
</div><!-- end #options --> </div><!-- end #options -->
</div> </div>
</div> <!-- end row --> </div> <!-- end row -->

7
web/vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit254e25e69fe049d603f41f5fd853ef2b::getLoader();

445
web/vendor/composer/ClassLoader.php vendored Normal file
View File

@ -0,0 +1,445 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* 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 <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @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;
}

56
web/vendor/composer/LICENSE vendored Normal file
View File

@ -0,0 +1,56 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: Composer
Upstream-Contact: Jordi Boggiano <j.boggiano@seld.be>
Source: https://github.com/composer/composer
Files: *
Copyright: 2016, Nils Adermann <naderman@naderman.de>
2016, Jordi Boggiano <j.boggiano@seld.be>
License: Expat
Files: src/Composer/Util/TlsHelper.php
Copyright: 2016, Nils Adermann <naderman@naderman.de>
2016, Jordi Boggiano <j.boggiano@seld.be>
2013, Evan Coury <me@evancoury.com>
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.

View File

@ -0,0 +1,9 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);

10
web/vendor/composer/autoload_files.php vendored Normal file
View File

@ -0,0 +1,10 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'e40631d46120a9c38ea139981f8dab26' => $vendorDir . '/ircmaxell/password-compat/lib/password.php',
);

View File

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);

10
web/vendor/composer/autoload_psr4.php vendored Normal file
View File

@ -0,0 +1,10 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Firebase\\JWT\\' => array($vendorDir . '/firebase/php-jwt/src'),
);

70
web/vendor/composer/autoload_real.php vendored Normal file
View File

@ -0,0 +1,70 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit254e25e69fe049d603f41f5fd853ef2b
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit254e25e69fe049d603f41f5fd853ef2b', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit254e25e69fe049d603f41f5fd853ef2b', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 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;
}
}

35
web/vendor/composer/autoload_static.php vendored Normal file
View File

@ -0,0 +1,35 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit254e25e69fe049d603f41f5fd853ef2b
{
public static $files = array (
'e40631d46120a9c38ea139981f8dab26' => __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);
}
}

94
web/vendor/composer/installed.json vendored Normal file
View File

@ -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"
]
}
]

30
web/vendor/firebase/php-jwt/LICENSE vendored Normal file
View File

@ -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.

200
web/vendor/firebase/php-jwt/README.md vendored Normal file
View File

@ -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
<?php
use \Firebase\JWT\JWT;
$key = "example_key";
$token = array(
"iss" => "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
<?php
use \Firebase\JWT\JWT;
$privateKey = <<<EOD
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQC8kGa1pSjbSYZVebtTRBLxBz5H4i2p/llLCrEeQhta5kaQu/Rn
vuER4W8oDH3+3iuIYW4VQAzyqFpwuzjkDI+17t5t0tyazyZ8JXw+KgXTxldMPEL9
5+qVhgXvwtihXC1c5oGbRlEDvDF6Sa53rcFVsYJ4ehde/zUxo6UvS7UrBQIDAQAB
AoGAb/MXV46XxCFRxNuB8LyAtmLDgi/xRnTAlMHjSACddwkyKem8//8eZtw9fzxz
bWZ/1/doQOuHBGYZU8aDzzj59FZ78dyzNFoF91hbvZKkg+6wGyd/LrGVEB+Xre0J
Nil0GReM2AHDNZUYRv+HYJPIOrB0CRczLQsgFJ8K6aAD6F0CQQDzbpjYdx10qgK1
cP59UHiHjPZYC0loEsk7s+hUmT3QHerAQJMZWC11Qrn2N+ybwwNblDKv+s5qgMQ5
5tNoQ9IfAkEAxkyffU6ythpg/H0Ixe1I2rd0GbF05biIzO/i77Det3n4YsJVlDck
ZkcvY3SK2iRIL4c9yY6hlIhs+K9wXTtGWwJBAO9Dskl48mO7woPR9uD22jDpNSwe
k90OMepTjzSvlhjbfuPN1IdhqvSJTDychRwn1kIJ7LQZgQ8fVz9OCFZ/6qMCQGOb
qaGwHmUK6xzpUbbacnYrIM6nLSkXgOAwv7XXCojvY614ILTK3iXiLBOxPu5Eu13k
eUz9sHyD6vkgZzjtxXECQAkp4Xerf5TGfQXGXhxIX52yH+N2LtujCdkQZjXAsGdm
B2zNzvrlgRmgBrklMTrMYgm1NPcW+bRLGcwgW2PTvNM=
-----END RSA PRIVATE KEY-----
EOD;
$publicKey = <<<EOD
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8kGa1pSjbSYZVebtTRBLxBz5H
4i2p/llLCrEeQhta5kaQu/RnvuER4W8oDH3+3iuIYW4VQAzyqFpwuzjkDI+17t5t
0tyazyZ8JXw+KgXTxldMPEL95+qVhgXvwtihXC1c5oGbRlEDvDF6Sa53rcFVsYJ4
ehde/zUxo6UvS7UrBQIDAQAB
-----END PUBLIC KEY-----
EOD;
$token = array(
"iss" => "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).

View File

@ -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"
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Firebase\JWT;
class BeforeValidException extends \UnexpectedValueException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Firebase\JWT;
class ExpiredException extends \UnexpectedValueException
{
}

379
web/vendor/firebase/php-jwt/src/JWT.php vendored Normal file
View File

@ -0,0 +1,379 @@
<?php
namespace Firebase\JWT;
use \DomainException;
use \InvalidArgumentException;
use \UnexpectedValueException;
use \DateTime;
/**
* JSON Web Token implementation, based on this spec:
* https://tools.ietf.org/html/rfc7519
*
* PHP version 5
*
* @category Authentication
* @package Authentication_JWT
* @author Neuman Vong <neuman@twilio.com>
* @author Anant Narayanan <anant@php.net>
* @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);
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Firebase\JWT;
class SignatureInvalidException extends \UnexpectedValueException
{
}

View File

@ -0,0 +1,7 @@
Copyright (c) 2012 Anthony Ferrara
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,20 @@
{
"name": "ircmaxell/password-compat",
"description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash",
"keywords": ["password", "hashing"],
"homepage": "https://github.com/ircmaxell/password_compat",
"license": "MIT",
"authors": [
{
"name": "Anthony Ferrara",
"email": "ircmaxell@php.net",
"homepage": "http://blog.ircmaxell.com"
}
],
"require-dev": {
"phpunit/phpunit": "4.*"
},
"autoload": {
"files": ["lib/password.php"]
}
}

View File

@ -0,0 +1,314 @@
<?php
/**
* A Compatibility library with PHP 5.5's simplified password hashing API.
*
* @author Anthony Ferrara <ircmaxell@php.net>
* @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;
}
}
}

View File

@ -0,0 +1,6 @@
<?php
require "lib/password.php";
echo "Test for functionality of compat library: " . (PasswordCompat\binary\check() ? "Pass" : "Fail");
echo "\n";