Initial commit
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..e1f036e
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,6 @@
+[run]
+branch = True
+omit =
+    */tests/*
+    .tox/*
+    servers/*/config_default.py
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..68d56e7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+*.pyc
+.cache
+.coverage
+.tox
+backup.json
+backup_dta.json
+config.py
+coverage.xml
+credentials.json
+M_Pin_Backend.egg-info/
+mpin_*_storage.json
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c69698a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..76f3295
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,5 @@
+Milagro MFA Server
+Copyright 2015 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f324e2d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,176 @@
+# Milagro MFA Services
+
+## General
+
+This repository contains all the services code required to build and deploy the *Milagro MFA Server*.
+This README describes the process of building and running the services on your **Ubuntu** machine.
+For other Linux distributions the process would be similar, while some steps as installation of
+required packages, should be adjusted for the specific platform.
+
+## Building and Installation
+
+### Cloning the code from the Git repository
+
+Using standard Git client, clone the code from this repository to a local one.
+The rest of this document assumes that the repository is cloned into `<mpin-backend>`.
+Wherever `<mpin-backend>` appears, it should be replaced with the real location on your machine.
+
+### Installing Prerequisites
+
+**1** Update your package manager tool
+```
+ > sudo apt-get update
+```
+**2** Install the following packages as shown below
+```
+ > sudo apt-get install python-dev python-pip libffi-dev
+ > sudo pip install -r requirements/common.txt
+```
+
+### Building the Milagro Crypto Libraries
+
+Clone the [_milagro-crypto_ repository](https://github.com/miracl/milagro-crypto) and checkout tag `1.0.0`.
+```
+> git clone https://github.com/miracl/milagro-crypto.git
+> git checkout tags/1.0.0
+``` 
+Follow the instructions for your platform from the milagro-crypto [README file](https://github.com/miracl/milagro-crypto/blob/master/README.md#build-instructions).
+
+### Getting Credentials
+
+Before running any Milagro Services, you should obtain your *Credentials*.
+This is done with the following script:
+```
+ > cd <mpin-backend>
+ > python scripts/getCommunityCredentials.py .
+```
+*NOTE:* Make sure you don't miss the dot (.) at the end of the above command.<br/>
+**Important:** During the above process you will be asked to enter your e-mail address. While this is not mandatory, it is recommended so we can later contact you in case of any problems with the service.
+The above script will download a `credentials.json` file into the `<mpin-backend>` directory.
+
+### Configuring the Services
+
+The Milagro MFA Services consist of: *Distributed Trusted Authority (D-TA)* and *Relying Party Service (RPS)*.
+The Web Application that integrates with the MFA in order to be able to log-in users using Milagro, is called *Relying Party Application (RPA)*.
+The installation, and the source code itself include a *Demo RPA*, which should be eventually replaced by the customer specific application.
+The initial configuration allows the Demo RPA and the Milagro MFA Services to be access from any machine on the local network.
+Further details about configuration options might be found in the [Documentation](http://docs.miracl.com/m-pin-core-configuration).
+
+#### Configuring the D-TA
+
+The source includes a "default" D-TA configuration file that should serve as a template for the actual one. To configure the D-TA perform the following steps:<br/>
+**1** Go to the directory `<mpin-backend>/servers` and copy the `dta/config_default.py` to `dta/config.py`
+```
+ > cd <mpin-backend>/servers
+ > cp dta/config_default.py dta/config.py
+```
+**2** Generate a hex-encoded 8-byte (or longer) random number. You can do this with the following command:
+```
+ > python -c "import os; print os.urandom(8).encode('hex')"
+ 038b9f0756b2a2c4
+```
+**3** Using `vi` or other editor, edit the `dta/config.py` file<br/>
+**4** Change the `salt` parameter, setting its value to the random value that was just generated:
+```
+salt = "038b9f0756b2a2c4"
+```
+**5** Add/change the `credentialsFile` parameter, specifying the location for the credential file that was previously obtained:
+```
+credentialsFile = "<mpin-backend>/credentials.json"
+```
+**6** Change the value of the `backup_file` parameter to the path to file where the master secret will be backed up.<br/>
+```
+backup_file = "<mpin-backend>/backup_dta.json"
+```
+**7** Change the value of the `passphrase` parameter as well. You might generate a random string for it as well, or write some phrase of your own.<br/>
+**8** Save the file and exit the editor
+
+#### Configuring the RPS
+
+The source includes a "default" RPS configuration file that should serve as a template for the actual one. To configure the RPS perform the following steps:<br/>
+**1** Go to the directory `<mpin-backend>/servers` and copy the `rps/config_default.py` to `rps/config.py`
+```
+ > cd <mpin-backend>/servers
+ > cp rps/config_default.py rps/config.py
+```
+**2** Using `vi` or other editor, edit the `rps/config.py` file<br/>
+**3** Add/change the `credentialsFile` parameter, specifying the location for the credential file that was previously obtained:
+```
+credentialsFile = "<mpin-backend>/credentials.json"
+```
+**4** Save the file and exit the editor
+
+#### Configuring the Demo RPA
+
+The source includes a "default" Demo RPA configuration file. To configure the Demo RPA perform the following steps:<br/>
+**1** Go to the directory `<mpin-backend>/servers` and copy the `demo/config_default.py` to `demo/config.py`
+```
+ > cd <mpin-backend>/servers
+ > cp demo/config_default.py demo/config.py
+```
+**2** Generate a base64-encoded 64-byte (or longer) random number. You can do this with the following command:
+```
+ > python -c "import os; print os.urandom(64).encode('base64')"
+ nju31zsOvxg+a0U4aCVrIOXf/VH/GC7/6oWK+8eEBM3OzNbbGOaL0mtne2g68O78MDYEz8fQz4MG
+ /7irix5Gfg==
+```
+**3** Using `vi` or other editor, edit the `demo/config.py` file<br/>
+**4** Change the `cookieSecret` parameter, setting its value to the random value that was just generated:
+```
+cookieSecret = "nju31zsOvxg+a0U4aCVrIOXf/VH/GC7/6oWK+8eEBM3OzNbbGOaL0mtne2g68O78MDYEz8fQz4MG/7irix5Gfg=="
+```
+**5** Change the `mpinJSURL` to the URL where the PIN Pad is served from. For instance:
+```
+mpinJSURL = "http://mpin.miracl.com/v4/mpin.js"
+```
+**6** Save the file and exit the editor
+
+### Running and Testing the Services
+
+For development purposes you might run the services from command line. Open 3 terminals and set the following two environment variables as shown below:
+```
+export PYTHONPATH=<mpin-backend>/lib:/usr/local/lib/python2.7/site-packages
+export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
+```
+To run the services, perform the following commands, each in separate terminal:
+```
+ > python dta/dta.py
+```
+```
+ > python rps/rps.py
+```
+```
+ > python demo/mpinDemo.py
+```
+For more automated execution, you might need to write start/stop scripts in `/etc/init.d`
+
+Using the following commands, you can test whether the services are running fine
+- **D-TA**:
+```
+> curl http://127.0.0.1:8001/status
+{"service_name": "D-TA server", "message": "OK", "startTime": ["2015-03-13T15:09:41Z"]}
+```
+- **RPS**:
+```
+> curl http://127.0.0.1:8011/status
+{"message": "active", "version": "0.3"}
+> curl http://127.0.0.1:8011/rps/clientSettings
+{"requestOTP": false, "mpinAuthServerURL": "/rps", "timePermitsURL": "http://127.0.0.1:8011/rps/timePermit", "useWebSocket": false, "setDeviceName": false, "seedValue": "3cd2a085a056eb4748ed0ff9dd1b0e298a7bfbb59e96f75fb2228ab22f9b55133894d36edec166d5eb8c2941c0ddf16aea70f1fa392a41486c463c715e5b4a696c0bed2d264011852f23a33172a19636da27a5df90d49bcde5c8a36bb0c5cb1abe67345d", "accessNumberDigits": 7, "accessNumberURL": "http://127.0.0.1:8011/rps/accessnumber", "setupDoneURL": "http://127.0.0.1:8011/rps/setupDone", "timePermitsStorageURL": "https://timepermits.certivox.net", "authenticateURL": "/mpinAuthenticate", "certivoxURL": "https://community-api.certivox.net/v3/", "registerURL": "http://127.0.0.1:8011/rps/user", "appID": "a2fcdf24c98e11e4b69d02547e1fd4a1", "cSum": 1, "signatureURL": "http://127.0.0.1:8011/rps/signature", "getAccessNumberURL": "http://127.0.0.1:8011/rps/getAccessNumber", "mobileAuthenticateURL": "http://127.0.0.1:8011/rps/authenticate"}
+```
+- **Demo**:
+```
+> curl http://127.0.0.1:8005
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+	. . .
+</head>
+<body>
+</body>
+	. . .
+</html>
+```
+Finally, open a browser on any machine that has network access to the machine on which the Milagro MFA Services are running. Browse to `http://<mpin-server-ip>:8005`.
+You should see the Demo application loaded.
+
+For further details on the Milagro MFA Server Configuration, see the [Documentation](http://docs.miracl.com/m-pin-core).
diff --git a/lib/crypto.py b/lib/crypto.py
new file mode 100644
index 0000000..b26ef6e
--- /dev/null
+++ b/lib/crypto.py
@@ -0,0 +1,373 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Wrapper function for mpin library."""
+import mpin
+import datetime
+from mpin_utils.common import Time
+
+# AES-GCM Key size
+PAS = mpin.PAS
+
+
+class CryptoError(Exception):
+
+    """Exception raises by crypto module."""
+
+    pass
+
+
+def today():
+    """Return time in slots since epoch using synced time"""
+    utc_dt = datetime.datetime.utcfromtimestamp(0)
+    return int((Time.syncedNow() - utc_dt).total_seconds() / 86400)
+
+
+def get_random_generator(seed):
+    """Return random number generator."""
+    SEED = mpin.ffi.new('octet*')
+    SEED_val = mpin.ffi.new('char [%s]' % len(seed), seed)
+    SEED[0].val = SEED_val
+    SEED[0].len = len(seed)
+    SEED[0].max = len(seed)
+
+    # random number generator
+    RNG = mpin.ffi.new('csprng*')
+    mpin.libmpin.CREATE_CSPRNG(RNG, SEED)
+
+    return RNG
+
+
+def random_generate(rng, length):
+    """Generate random number with predefined length."""
+    OTT = mpin.ffi.new('octet*')
+    OTTval = mpin.ffi.new('char []', length)
+    OTT[0].val = OTTval
+    OTT[0].max = length
+    OTT[0].len = length
+
+    mpin.libmpin.generateRandom(rng, OTT)
+    return mpin.toHex(OTT)
+
+
+def generate_otp(rng):
+    """Generate One time password."""
+    return mpin.libmpin.generateOTP(rng)
+
+
+def mpin_random_generate(rng):
+    """Generate random secret."""
+    MASTER_SECRET = mpin.ffi.new('octet*')
+    MASTER_SECRET_val = mpin.ffi.new('char []', mpin.PGS)
+    MASTER_SECRET[0].val = MASTER_SECRET_val
+    MASTER_SECRET[0].max = mpin.PGS
+    MASTER_SECRET[0].len = mpin.PGS
+
+    rtn = mpin.libmpin.MPIN_RANDOM_GENERATE(rng, MASTER_SECRET)
+    if rtn != 0:
+        raise CryptoError(rtn)
+
+    master_secret_hex = mpin.toHex(MASTER_SECRET)
+    return master_secret_hex.decode('hex')
+
+
+def aes_gcm_encrypt(master_secret, aes_key, rand, header, iv=mpin.IVL):
+    """Encrypt master secret."""
+    # AES Key
+    AES_KEY = mpin.ffi.new('octet*')
+    AES_KEY_val = mpin.ffi.new('char [%s]' % len(aes_key), aes_key)
+    AES_KEY[0].val = AES_KEY_val
+    AES_KEY[0].max = len(aes_key)
+    AES_KEY[0].len = len(aes_key)
+
+    # Initialisation Vector
+    IV = mpin.ffi.new('octet*')
+    IV_val = mpin.ffi.new('char []', iv)
+    IV[0].val = IV_val
+    IV[0].max = iv
+    IV[0].len = iv
+    mpin.libmpin.generateRandom(rand, IV)
+
+    # Authentication tag
+    TAG = mpin.ffi.new('octet*')
+    TAG_val = mpin.ffi.new('char []', mpin.PAS)
+    TAG[0].val = TAG_val
+    TAG[0].max = mpin.PAS
+
+    # Header
+    HEADER = mpin.ffi.new('octet*')
+    HEADER_val = mpin.ffi.new('char [%s]' % len(header), header)
+    HEADER[0].val = HEADER_val
+    HEADER[0].max = len(header)
+    HEADER[0].len = len(header)
+
+    # Plaintext input
+    plaintext = master_secret
+    PLAINTEXT = mpin.ffi.new('octet*')
+    PLAINTEXT_val = mpin.ffi.new('char [%s]' % len(plaintext), plaintext)
+    PLAINTEXT[0].val = PLAINTEXT_val
+    PLAINTEXT[0].max = len(plaintext)
+    PLAINTEXT[0].len = len(plaintext)
+
+    # Ciphertext
+    CIPHERTEXT = mpin.ffi.new('octet*')
+    CIPHERTEXT_val = mpin.ffi.new('char []', len(plaintext))
+    CIPHERTEXT[0].val = CIPHERTEXT_val
+    CIPHERTEXT[0].max = len(plaintext)
+
+    mpin.libmpin.AES_GCM_ENCRYPT(AES_KEY, IV, HEADER, PLAINTEXT, CIPHERTEXT, TAG)
+    IV_hex = mpin.toHex(IV)
+    CIPHERTEXT_hex = mpin.toHex(CIPHERTEXT)
+    TAG_hex = mpin.toHex(TAG)
+
+    return CIPHERTEXT_hex, IV_hex, TAG_hex
+
+
+def aes_gcm_decrypt(aes_key, iv, header, ciphertext):
+    """AES GCM Decrypt."""
+    # AES Key
+    AES_KEY = mpin.ffi.new('octet*')
+    AES_KEY_val = mpin.ffi.new('char [%s]' % len(aes_key), aes_key)
+    AES_KEY[0].val = AES_KEY_val
+    AES_KEY[0].max = len(aes_key)
+    AES_KEY[0].len = len(aes_key)
+
+    # Initialization Vector
+    IV = mpin.ffi.new('octet*')
+    IV_val = mpin.ffi.new('char [%s]' % len(iv), iv)
+    IV[0].val = IV_val
+    IV[0].max = len(iv)
+    IV[0].len = len(iv)
+
+    # Header
+    HEADER = mpin.ffi.new('octet*')
+    HEADER_val = mpin.ffi.new('char [%s]' % len(header), header)
+    HEADER[0].val = HEADER_val
+    HEADER[0].max = len(header)
+    HEADER[0].len = len(header)
+
+    # Ciphertext
+    CIPHERTEXT = mpin.ffi.new('octet*')
+    CIPHERTEXT_val = mpin.ffi.new('char [%s]' % len(ciphertext), ciphertext)
+    CIPHERTEXT[0].val = CIPHERTEXT_val
+    CIPHERTEXT[0].max = len(ciphertext)
+    CIPHERTEXT[0].len = len(ciphertext)
+
+    # Plaintext
+    PLAINTEXT = mpin.ffi.new('octet*')
+    PLAINTEXT_val = mpin.ffi.new('char []', CIPHERTEXT[0].len)
+    PLAINTEXT[0].val = PLAINTEXT_val
+    PLAINTEXT[0].max = CIPHERTEXT[0].len
+    PLAINTEXT[0].len = CIPHERTEXT[0].len
+
+    # Authentication tag
+    TAG = mpin.ffi.new('octet*')
+    TAG_val = mpin.ffi.new('char []', mpin.PAS)
+    TAG[0].val = TAG_val
+    TAG[0].max = mpin.PAS
+
+    # Decrypt ciphertext
+    mpin.libmpin.AES_GCM_DECRYPT(AES_KEY, IV, HEADER, CIPHERTEXT, PLAINTEXT, TAG)
+
+    return mpin.toHex(TAG), mpin.toHex(PLAINTEXT)
+
+
+def get_server_secret(master_secret):
+    """Generate secret secret."""
+    MASTER_SECRET = mpin.ffi.new('octet*')
+    MASTER_SECRET_val = mpin.ffi.new('char [%s]' % len(master_secret), master_secret)
+    MASTER_SECRET[0].val = MASTER_SECRET_val
+    MASTER_SECRET[0].max = len(master_secret)
+    MASTER_SECRET[0].len = len(master_secret)
+
+    SERVER_SECRET = mpin.ffi.new('octet*')
+    SERVER_SECRET_val = mpin.ffi.new('char []', mpin.G2)
+    SERVER_SECRET[0].val = SERVER_SECRET_val
+    SERVER_SECRET[0].max = mpin.G2
+    SERVER_SECRET[0].len = mpin.G2
+
+    rtn = mpin.libmpin.MPIN_GET_SERVER_SECRET(MASTER_SECRET, SERVER_SECRET)
+    if rtn != 0:
+        raise CryptoError(rtn)
+    return mpin.toHex(SERVER_SECRET)
+
+
+def get_client_multiple(master_secret, mpin_id):
+    """Generate client secret."""
+    MASTER_SECRET = mpin.ffi.new('octet*')
+    MASTER_SECRET_val = mpin.ffi.new('char [%s]' % len(master_secret), master_secret)
+    MASTER_SECRET[0].val = MASTER_SECRET_val
+    MASTER_SECRET[0].max = len(master_secret)
+    MASTER_SECRET[0].len = len(master_secret)
+
+    CLIENT_SECRET = mpin.ffi.new('octet*')
+    CLIENT_SECRET_val = mpin.ffi.new('char []', mpin.G1)
+    CLIENT_SECRET[0].val = CLIENT_SECRET_val
+    CLIENT_SECRET[0].max = mpin.G1
+    CLIENT_SECRET[0].len = mpin.G1
+
+    HASH_MPIN_ID = mpin.ffi.new('octet*')
+    HASH_MPIN_ID_val = mpin.ffi.new('char [%s]' % len(mpin_id), mpin_id)
+    HASH_MPIN_ID[0].val = HASH_MPIN_ID_val
+    HASH_MPIN_ID[0].max = len(mpin_id)
+    HASH_MPIN_ID[0].len = len(mpin_id)
+
+    rtn = mpin.libmpin.MPIN_GET_CLIENT_SECRET(MASTER_SECRET, HASH_MPIN_ID, CLIENT_SECRET)
+    if rtn != 0:
+        raise CryptoError(rtn)
+    return mpin.toHex(CLIENT_SECRET)
+
+
+def get_time_permit(master_secret, mpin_id, date=None):
+    """Generate client time permit."""
+    MASTER_SECRET = mpin.ffi.new('octet*')
+    MASTER_SECRET_val = mpin.ffi.new('char [%s]' % len(master_secret), master_secret)
+    MASTER_SECRET[0].val = MASTER_SECRET_val
+    MASTER_SECRET[0].max = len(master_secret)
+    MASTER_SECRET[0].len = len(master_secret)
+
+    TIME_PERMIT = mpin.ffi.new('octet*')
+    TIME_PERMIT_val = mpin.ffi.new('char []', mpin.G1)
+    TIME_PERMIT[0].val = TIME_PERMIT_val
+    TIME_PERMIT[0].max = mpin.G1
+    TIME_PERMIT[0].len = mpin.G1
+
+    HASH_MPIN_ID = mpin.ffi.new('octet*')
+    HASH_MPIN_ID_val = mpin.ffi.new('char [%s]' % len(mpin_id), mpin_id)
+    HASH_MPIN_ID[0].val = HASH_MPIN_ID_val
+    HASH_MPIN_ID[0].max = len(mpin_id)
+    HASH_MPIN_ID[0].len = len(mpin_id)
+
+    date = date or today()
+    rtn = mpin.libmpin.MPIN_GET_CLIENT_PERMIT(date, MASTER_SECRET, HASH_MPIN_ID, TIME_PERMIT)
+    if rtn != 0:
+        raise CryptoError(rtn)
+    return mpin.toHex(TIME_PERMIT)
+
+
+def mpin_recombine_g2(certivox_server_secret, customer_server_secret):
+    """Recombine server secret."""
+    SS1 = mpin.ffi.new("octet*")
+    SS1_val = mpin.ffi.new("char [%s]" % len(certivox_server_secret), certivox_server_secret)
+    SS1[0].val = SS1_val
+    SS1[0].max = mpin.G2
+    SS1[0].len = len(certivox_server_secret)
+
+    SS2 = mpin.ffi.new("octet*")
+    SS2_val = mpin.ffi.new("char [%s]" % len(customer_server_secret), customer_server_secret)
+    SS2[0].val = SS2_val
+    SS2[0].max = mpin.G2
+    SS2[0].len = len(customer_server_secret)
+
+    SERVER_SECRET = mpin.ffi.new("octet*")
+    SERVER_SECRET_val = mpin.ffi.new("char []", mpin.G2)
+    SERVER_SECRET[0].val = SERVER_SECRET_val
+    SERVER_SECRET[0].max = mpin.G2
+    SERVER_SECRET[0].len = mpin.G2
+
+    rtn = mpin.libmpin.MPIN_RECOMBINE_G2(SS1, SS2, SERVER_SECRET)
+    if rtn != 0:
+        raise CryptoError(rtn)
+    return mpin.toHex(SERVER_SECRET)
+
+
+def mpin_server_1(mpin_id, date):
+    """Calculate HID and HTOD."""
+    HID = mpin.ffi.new("octet*")
+    HIDval = mpin.ffi.new("char []", mpin.G1)
+    HID[0].val = HIDval
+    HID[0].max = mpin.G1
+    HID[0].len = mpin.G1
+
+    # H(T|H(ID))
+    HTID = mpin.ffi.new("octet*")
+    HTIDval = mpin.ffi.new("char []", mpin.G1)
+    HTID[0].val = HTIDval
+    HTID[0].max = mpin.G1
+    HTID[0].len = mpin.G1
+
+    MPIN_ID = mpin.ffi.new("octet*")
+    MPIN_ID_val = mpin.ffi.new("char [%s]" % len(mpin_id), mpin_id)
+    MPIN_ID[0].val = MPIN_ID_val
+    MPIN_ID[0].max = len(mpin_id)
+    MPIN_ID[0].len = len(mpin_id)
+
+    mpin.libmpin.MPIN_SERVER_1(date, MPIN_ID, HID, HTID)
+
+    return mpin.toHex(HID).decode('hex'), mpin.toHex(HTID).decode('hex')
+
+
+def mpin_server_2(server_secret, v, date, hid, htid, y, u, ut):
+    """Check credentials."""
+    SERVER_SECRET = mpin.ffi.new("octet*")
+    SERVER_SECRET_val = mpin.ffi.new("char [%s]" % len(server_secret), server_secret)
+    SERVER_SECRET[0].val = SERVER_SECRET_val
+    SERVER_SECRET[0].max = mpin.G2
+    SERVER_SECRET[0].len = len(server_secret)
+
+    V = mpin.ffi.new("octet*")
+    V_val = mpin.ffi.new("char [%s]" % len(v), v)
+    V[0].val = V_val
+    V[0].max = len(v)
+    V[0].len = len(v)
+
+    lenEF = 12 * mpin.PFS
+    E = mpin.ffi.new("octet*")
+    Eval = mpin.ffi.new("char []", lenEF)
+    E[0].val = Eval
+    E[0].max = lenEF
+    E[0].len = lenEF
+
+    F = mpin.ffi.new("octet*")
+    Fval = mpin.ffi.new("char []", lenEF)
+    F[0].val = Fval
+    F[0].max = lenEF
+    F[0].len = lenEF
+
+    HID = mpin.ffi.new("octet*")
+    HIDval = mpin.ffi.new("char [%s]" % len(hid), hid)
+    HID[0].val = HIDval
+    HID[0].max = len(hid)
+    HID[0].len = len(hid)
+
+    # H(T|H(ID))
+    HTID = mpin.ffi.new("octet*")
+    HTIDval = mpin.ffi.new("char [%s]" % len(htid), htid)
+    HTID[0].val = HTIDval
+    HTID[0].max = len(htid)
+    HTID[0].len = len(htid)
+
+    # Client part
+    Y = mpin.ffi.new("octet*")
+    Yval = mpin.ffi.new("char [%s]" % len(y), y)
+    Y[0].val = Yval
+    Y[0].max = len(y)
+    Y[0].len = len(y)
+
+    U = mpin.ffi.new("octet*")
+    Uval = mpin.ffi.new("char [%s]" % len(u), u)
+    U[0].val = Uval
+    U[0].max = len(u)
+    U[0].len = len(u)
+
+    UT = mpin.ffi.new("octet*")
+    UTval = mpin.ffi.new("char [%s]" % len(ut), ut)
+    UT[0].val = UTval
+    UT[0].max = len(ut)
+    UT[0].len = len(ut)
+
+    return mpin.libmpin.MPIN_SERVER_2(date, HID, HTID, Y, SERVER_SECRET, U, UT, V, E, F), mpin.toHex(E), mpin.toHex(F)
diff --git a/lib/dynamic_options.py b/lib/dynamic_options.py
new file mode 100644
index 0000000..fc1c142
--- /dev/null
+++ b/lib/dynamic_options.py
@@ -0,0 +1,121 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import json
+
+from tornado.options import options
+from tornado.log import app_log as log
+from tornado.httpclient import AsyncHTTPClient
+from tornado.gen import coroutine
+
+
+def _extract_options(body, local_mapping):
+    try:
+        dynamic_options = json.loads(body)
+        log.debug(
+            "Recieved dynamic options: {0}"
+            .format(dynamic_options))
+        return dynamic_options
+    except Exception as e:
+        dynamic_options = {}
+        log.error(
+            "Can not parse dynamic options, exception: {0}"
+            .format(e))
+    return dynamic_options
+
+
+@coroutine
+def process_dynamic_options(name_mapping, handlers, application=None, initial=False):
+    """
+    Process dynamic updates.
+    `name_mappings` is a dict in the form `remote_option_name`: `local_option_name`
+    `handlers` is a list of callbacks to run after options update
+    `application` is the applicatiopn object, that may be needed by some handlers
+    `initial` is a boolean flag that tells if the caller is a request handler or main loop
+    Handlers signature:
+    `updated` is a list of actually updated options (ones that changed value)
+    `application` is the application object
+    `initial` is the initial flag
+    """
+    if not options.dynamicOptionsURL:
+        log.debug("Dynamic options disabled")
+        updated = {}
+    else:
+        try:
+            client = AsyncHTTPClient()
+            response = yield client.fetch(options.dynamicOptionsURL)
+            dynamic_options = _extract_options(response.body, name_mapping)
+            updated = update_dynamic_options(dynamic_options, name_mapping)
+        except Exception as e:
+            log.error("Error while getting dynamic options")
+            log.error(e)
+            updated = {}
+
+    for handler in handlers:
+        try:
+            handler(updated, application, initial)
+        except Exception as e:
+            log.error("Error while running update handlers")
+            log.error(e)
+
+
+def update_dynamic_options(dynamic_options, local_mapping):
+    updated = []
+    for remote_name, remote_value in dynamic_options.items():
+        if remote_name not in local_mapping:
+            log.debug(
+                "Recieved unsupported option for sync: {0}"
+                .format(remote_name))
+            continue
+        local_name = local_mapping[remote_name]
+        local_value = getattr(options, local_name)
+        if local_value != remote_value:
+            setattr(options, local_name, remote_value)
+            updated.append(local_name)
+    log.debug("Dynamic options for update {0}".format(updated))
+    return updated
+
+
+def generate_dynamic_options(local_mapping):
+    log.debug("Generating synamic options")
+    dynamic_options = {}
+    for remote_name, local_name in local_mapping.items():
+        dynamic_options[remote_name] = getattr(options, local_name)
+    return dynamic_options
+
+
+def _test_dynamic_options_update(
+        initial_options,
+        response_body,
+        expected_options,
+        local_mapping):
+    """
+    initial_options - dict of initial options to be set.
+    response_body - body of the recieved response (expected to be json).
+    expected_options - values to be checked in options object after processing.
+    local_mapping - local mapping for option names.
+    """
+
+    for option, value in initial_options.items():
+        setattr(options, option, value)
+
+    dynamic_options = json.loads(response_body)
+
+    update_dynamic_options(dynamic_options, local_mapping)
+
+    for option, value in expected_options.items():
+        assert getattr(options, option) == value
diff --git a/lib/entropy/__init__.py b/lib/entropy/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/lib/entropy/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/lib/entropy/base.py b/lib/entropy/base.py
new file mode 100644
index 0000000..dfff3a2
--- /dev/null
+++ b/lib/entropy/base.py
@@ -0,0 +1,44 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+class EntropySourceBase(object):
+    entropyDescription = "Entropy source base"
+
+    def __init__(self, bytesNeeded, logger=None, **kwargs):
+        self.bytesNeeded = bytesNeeded
+        self.logger = logger
+        for key, value in kwargs.items():
+            setattr(self, key, value)
+
+    def _getEntropy(self):
+        raise NotImplementedError
+
+    def getEntropy(self):
+        _bytes = b""
+        if self.logger:
+            self.logger.info("Getting entropy from {0}...".format(self.entropyDescription))
+
+        while len(_bytes) < self.bytesNeeded:
+            _bytes += self._getEntropy()
+
+        _result = _bytes[:self.bytesNeeded]
+
+        if self.logger:
+            self.logger.debug("Entropy from {0}: {1}".format(self.entropyDescription, _result.encode("hex")))
+
+        return _result
diff --git a/lib/entropy/certivox.py b/lib/entropy/certivox.py
new file mode 100644
index 0000000..0b82d50
--- /dev/null
+++ b/lib/entropy/certivox.py
@@ -0,0 +1,27 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from http import EntropySource as HttpEntropySource
+
+
+class EntropySource(HttpEntropySource):
+    entropyDescription = "CertiVox"
+    urlBase = "https://entropy.certivox.net/Generate.svc"
+
+    def __init__(self, bytesNeeded, **kwargs):
+        HttpEntropySource.__init__(self, bytesNeeded, **kwargs)
+        self.url = "{0}/{1}/hex".format(self.urlBase, bytesNeeded)
diff --git a/lib/entropy/dev_random.py b/lib/entropy/dev_random.py
new file mode 100644
index 0000000..a12536e
--- /dev/null
+++ b/lib/entropy/dev_random.py
@@ -0,0 +1,26 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from base import EntropySourceBase
+
+
+class EntropySource(EntropySourceBase):
+    entropyDescription = "/dev/random"
+    device = '/dev/random'
+
+    def _getEntropy(self):
+        return open(self.device, 'rb').read(self.bytesNeeded)
diff --git a/lib/entropy/dev_urandom.py b/lib/entropy/dev_urandom.py
new file mode 100644
index 0000000..14c5378
--- /dev/null
+++ b/lib/entropy/dev_urandom.py
@@ -0,0 +1,26 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from base import EntropySourceBase
+
+
+class EntropySource(EntropySourceBase):
+    entropyDescription = "/dev/urandom"
+    device = '/dev/urandom'
+
+    def _getEntropy(self):
+        return open(self.device, 'rb').read(self.bytesNeeded)
diff --git a/lib/entropy/http.py b/lib/entropy/http.py
new file mode 100644
index 0000000..3335be2
--- /dev/null
+++ b/lib/entropy/http.py
@@ -0,0 +1,29 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from base import EntropySourceBase
+
+import urllib2
+
+
+class EntropySource(EntropySourceBase):
+    entropyDescription = "http"
+
+    def _getEntropy(self):
+        req = urllib2.urlopen(self.url)
+        buf = req.read()
+        return buf.decode("hex")
diff --git a/lib/mpDaemon.py b/lib/mpDaemon.py
new file mode 100755
index 0000000..2bfdab7
--- /dev/null
+++ b/lib/mpDaemon.py
@@ -0,0 +1,179 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import atexit
+import os
+import platform
+import sys
+import time
+from signal import SIGTERM
+
+
+def pidExists(pid):
+    if not pid:
+        return False
+
+    if (platform.system().lower() != "linux"):
+        return True
+
+    if not os.path.exists("/proc"):
+        return True
+
+    cmdline = ""
+    try:
+        cmdline = open("/proc/{0}/cmdline".format(pid), "r").read()
+    except Exception:
+        return False
+    if cmdline.find(os.path.split(__file__)[1]):
+        return True
+    else:
+        return False
+
+
+class Daemon(object):
+    """
+    A generic daemon class.
+
+    Usage: subclass the Daemon class and override the run() method
+    """
+    def __init__(self, **kwargs):
+        self.stdin = kwargs.get('stdin', '/dev/null')
+        self.stdout = kwargs.get('stdout', '/dev/null')
+        self.stderr = kwargs.get('stderr', '/dev/null')
+        self.pidfile = kwargs['pidfile']
+
+    def daemonize(self):
+        """
+        do the UNIX double-fork magic, see Stevens' "Advanced
+        Programming in the UNIX Environment" for details (ISBN 0201563177)
+        http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
+        """
+        try:
+            pid = os.fork()
+            if pid > 0:
+                # exit first parent
+                sys.exit(0)
+        except OSError, e:
+            sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
+            sys.exit(1)
+
+        # decouple from parent environment
+        # os.chdir("/")
+        os.setsid()
+        os.umask(0)
+
+        # do second fork
+        try:
+            pid = os.fork()
+            if pid > 0:
+                # exit from second parent
+                sys.exit(0)
+        except OSError, e:
+            sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
+            sys.exit(1)
+
+        # redirect standard file descriptors
+        sys.stdout.flush()
+        sys.stderr.flush()
+        si = file(self.stdin, 'r')
+        so = file(self.stdout, 'a+')
+        se = file(self.stderr, 'a+', 0)
+
+        if (self.stdout != "/dev/null"):
+            os.chmod(self.stdout, 0600)
+        if (self.stderr != self.stdout) and (self.stderr != "/dev/null"):
+            os.chmod(self.stderr, 0600)
+
+        os.dup2(si.fileno(), sys.stdin.fileno())
+        os.dup2(so.fileno(), sys.stdout.fileno())
+        os.dup2(se.fileno(), sys.stderr.fileno())
+
+        # write pidfile
+        atexit.register(self.delpid)
+        pid = str(os.getpid())
+        file(self.pidfile, 'w+').write("%s\n" % pid)
+        os.chmod(self.pidfile, 0600)
+
+    def delpid(self):
+        sys.stdout.write("Stopping.\n")
+        sys.stdout.flush()
+        os.remove(self.pidfile)
+
+    def start(self):
+        """
+        Start the daemon
+        """
+        # Check for a pidfile to see if the daemon already runs
+        try:
+            pf = file(self.pidfile, 'r')
+            pid = int(pf.read().strip())
+            pf.close()
+        except IOError:
+            pid = None
+
+        if pidExists(pid):
+            message = "pidfile %s already exist. Daemon already running?\n"
+            sys.stderr.write(message % self.pidfile)
+            sys.exit(1)
+
+        # Start the daemon
+        self.daemonize()
+        self.run()
+
+    def stop(self):
+        """
+        Stop the daemon
+        """
+        # Get the pid from the pidfile
+        try:
+            pf = file(self.pidfile, 'r')
+            pid = int(pf.read().strip())
+            pf.close()
+        except IOError:
+            pid = None
+
+        if not pid:
+            message = "pidfile %s does not exist. Daemon not running?\n"
+            sys.stderr.write(message % self.pidfile)
+            return  # not an error in a restart
+
+        # Try killing the daemon process
+        try:
+            while 1:
+                os.kill(pid, SIGTERM)
+                time.sleep(0.1)
+        except OSError, err:
+            err = str(err)
+            if err.find("No such process") > 0:
+                if os.path.exists(self.pidfile):
+                    os.remove(self.pidfile)
+            else:
+                print str(err)
+                sys.exit(1)
+
+    def restart(self):
+        """
+        Restart the daemon
+        """
+        self.stop()
+        self.start()
+
+    def run(self):
+        """
+        You should override this method when you subclass Daemon. It will be called after the process has been
+        daemonized by start() or restart().
+        """
diff --git a/lib/mpWinService.py b/lib/mpWinService.py
new file mode 100755
index 0000000..90bad15
--- /dev/null
+++ b/lib/mpWinService.py
@@ -0,0 +1,106 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import time
+from os.path import splitext, abspath
+from sys import modules
+
+import servicemanager
+import win32api
+import win32event
+import win32service
+import win32serviceutil
+
+
+class Service(win32serviceutil.ServiceFramework):
+    _svc_name_ = '_unNamed'
+    _svc_display_name_ = '_Service Template'
+
+    def __init__(self, *args):
+        win32serviceutil.ServiceFramework.__init__(self, *args)
+        self.log('init')
+        self.stop_event = win32event.CreateEvent(None, 0, 0, None)
+
+    def log(self, msg):
+        servicemanager.LogInfoMsg(str(msg))
+
+        def sleep(self, sec):
+            win32api.Sleep(sec * 1000, True)
+
+    def SvcDoRun(self):
+        self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
+        try:
+            self.ReportServiceStatus(win32service.SERVICE_RUNNING)
+            self.log('start')
+            self.start()
+            self.log('wait')
+            win32event.WaitForSingleObject(self.stop_event, win32event.INFINITE)
+            self.log('done')
+        except Exception, x:
+            self.log('Exception : %s' % x)
+            self.SvcStop()
+
+    def SvcStop(self):
+        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
+        self.log('stopping')
+        self.stop()
+        self.log('stopped')
+        win32event.SetEvent(self.stop_event)
+        self.ReportServiceStatus(win32service.SERVICE_STOPPED)
+
+    def sleep(self, sec):
+        time.sleep(sec)
+
+    def start(self):
+        self.run()
+        self.runflag = True
+        while self.runflag:
+            self.sleep(1)
+
+    def stop(self):
+        self.runflag = False
+
+
+def installService(cls, name, display_name=None, stay_alive=True):
+    cls._svc_name_ = name
+    cls._svc_display_name_ = display_name or name
+    try:
+        module_path = modules[cls.__module__].__file__
+    except AttributeError:
+        # maybe py2exe went by
+        from sys import executable
+        module_path = executable
+    module_file = splitext(abspath(module_path))[0]
+    cls._svc_reg_class_ = '%s.%s' % (module_file, cls.__name__)
+    if stay_alive:
+        win32api.SetConsoleCtrlHandler(lambda x: True, True)
+    try:
+        win32serviceutil.InstallService(
+            cls._svc_reg_class_,
+            cls._svc_name_,
+            cls._svc_display_name_,
+            startType=win32service.SERVICE_AUTO_START
+        )
+        print 'Installing service %s... done' % name
+        win32serviceutil.StartService(
+            cls._svc_name_
+        )
+        print 'Starting service %s... done' % name
+        print '\nAll done!'
+
+    except Exception, x:
+        print str(x)
diff --git a/lib/mpin_utils/__init__.py b/lib/mpin_utils/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/lib/mpin_utils/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/lib/mpin_utils/common.py b/lib/mpin_utils/common.py
new file mode 100755
index 0000000..1acf06d
--- /dev/null
+++ b/lib/mpin_utils/common.py
@@ -0,0 +1,361 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import datetime
+import hashlib
+import hmac
+import json
+import os
+import time
+from pprint import pformat
+from urlparse import urlparse
+
+import tornado.httpclient
+from tornado.log import app_log as log
+
+proxies = {}
+
+# Time for which signatures are valid
+SIGNATURE_EXPIRES_OFFSET_SECONDS = 60
+
+
+class Keys(object):
+    app_id = "\0" * 16
+    app_key = ""
+    api_url = ""
+    timePermitsStorageURL = ""
+    managementConsoleURL = ""
+
+    @staticmethod
+    def loadFromFile(keysfile):
+        try:
+            with open(keysfile, "r") as keysfile:
+                data = json.loads(keysfile.read())
+        except IOError:
+            log.error("Cannot load keys file!")
+            return False
+        except ValueError:
+            log.error("Keys file contains invalid json!")
+            return False
+
+        try:
+            Keys.app_id = str(data["app_id"])
+            Keys.app_key = str(data["app_key"])
+            Keys.api_url = str(data["api_url"]).rstrip("/") + "/"
+        except KeyError:
+            log.error("Invalid keys file. app_id, app_key and api_url fields are required!")
+            return False
+
+        if 'certivox_server_secret' in data:
+            Keys.certivox_server_secret = data['certivox_server_secret']
+
+        return True
+
+    @staticmethod
+    def certivoxServer():
+        return Keys.api_url
+
+    @staticmethod
+    def timeServer():
+        return "{0}time".format(Keys.api_url)
+
+    @staticmethod
+    def _getAPISettings():
+
+        apiSettingsURL = "{0}apiSettings".format(Keys.api_url)
+        log.debug("Getting API settings from {0}".format(apiSettingsURL))
+
+        httpClient = tornado.httpclient.HTTPClient()
+        apiResponse = httpClient.fetch(apiSettingsURL, **fetchConfig(apiSettingsURL))
+        apiData = json.loads(apiResponse.body)
+        Keys.timePermitsStorageURL = apiData.get("timePermitsStorageURL", "")
+        Keys.managementConsoleURL = apiData.get("managementConsoleURL", "")
+        log.debug("timpePermitsStorageURL = {0}; managementConsoleURL = {1}".format(Keys.timePermitsStorageURL, Keys.managementConsoleURL))
+
+    @staticmethod
+    def getAPISettings(wait=False):
+        seconds = 5
+        while True:
+            try:
+                Keys._getAPISettings()
+                break
+            except Exception as E:
+                log.error(E)
+                log.error("Unable to get data from API server. Retrying in {0} seconds".format(seconds))
+                if not wait:
+                    break
+                time.sleep(seconds)
+
+
+class Applications(object):
+    """
+    Load the file that contains the list of applications
+    Provide ability to add and remove apps
+    Provide ability to save applications to file
+    """
+
+    def __init__(self, filename=None):
+        self.filename = 'apps.json'
+        if not filename:
+            self.appData = {}
+        else:
+            data = json.load(open(filename, "r"))
+            self.appData = data['appData']
+
+    def addApp(self, app_id, app_key, app_url):
+        '''Add applications to memory and update file'''
+        self.appData[app_id] = [app_key, app_url]
+        self.writeFile()
+
+    def deleteApp(self, app_id):
+        '''Delete application from memory and update file'''
+        del self.appData[app_id]
+        self.writeFile()
+
+    def writeFile(self):
+        '''Write data in JSON format to file'''
+        data = {'appData': self.appData}
+        json.dump(data, open(self.filename, "w"))
+
+    def __repr__(self):
+        data = {}
+        data['appData'] = self.appData
+        return str(data)
+
+
+def detectProxy():
+    # Detect proxy settings for http and https from the environment
+    # Uses http_proxy and https_proxy environment variables
+
+    httpProxy = os.environ.get("HTTP_PROXY", os.environ.get("http_proxy", ""))
+    httpsProxy = os.environ.get("HTTPS_PROXY", os.environ.get("https_proxy", ""))
+    noProxy = os.environ.get("NO_PROXY", os.environ.get("no_proxy", ""))
+
+    if httpProxy:
+        u = urlparse(httpProxy)
+        proxies["http"] = {
+            "proxy_host": u.hostname or None,
+            "proxy_port": u.port or None,
+            "proxy_username": u.username or None,
+            "proxy_password": u.password or None
+        }
+
+    if httpsProxy:
+        u = urlparse(httpsProxy)
+        proxies["https"] = {
+            "proxy_host": u.hostname or None,
+            "proxy_port": u.port or None,
+            "proxy_username": u.username or None,
+            "proxy_password": u.password or None
+        }
+
+    if httpProxy or httpsProxy:
+        tornado.httpclient.AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
+
+    if noProxy:
+        proxies["noProxy"] = noProxy.split(",")
+
+
+def fetchConfig(url):
+    # Get fetch config settings for httpclient for proxy
+    u = urlparse(url)
+    if u.scheme in proxies:
+        if u.hostname not in proxies.get("noProxy", []):
+            return proxies[u.scheme]
+
+    return {}
+
+
+class Time(object):
+    monthNames = ["Jan.", "Feb.", "Mar.", "Apr.", "May", "June", "July", "Aug.", "Sept.", "Oct.", "Nov.", "Dec."]
+
+    timeOffset = datetime.timedelta()
+
+    @staticmethod
+    def syncedNow(**kwards):
+        if kwards:
+            return datetime.datetime.utcnow() + Time.timeOffset + datetime.timedelta(**kwards)
+        else:
+            return datetime.datetime.utcnow() + Time.timeOffset
+
+    @staticmethod
+    def Now(**kwards):
+        if kwards:
+            return datetime.datetime.utcnow() + datetime.timedelta(**kwards)
+        else:
+            return datetime.datetime.utcnow()
+
+    @staticmethod
+    def DateTimeToISO(dt):
+        return dt.isoformat("T").split(".")[0] + "Z"
+
+    @staticmethod
+    def syncedISO(**kwards):
+        return Time.syncedNow(**kwards).isoformat("T").split(".")[0] + "Z"
+
+    @staticmethod
+    def ISO(**kwards):
+        return Time.Now(**kwards).isoformat("T").split(".")[0] + "Z"
+
+    @staticmethod
+    def ISOtoDateTime(isoDate):
+        isoDate = isoDate.split(".")[0].replace(" ", "T")
+        return datetime.datetime.strptime(isoDate.split("Z")[0], "%Y-%m-%dT%H:%M:%S")
+
+    @staticmethod
+    def DateTimetoHuman(d):
+        return d.strftime("%H:%M GMT, {0} %d, %Y".format(Time.monthNames[d.month - 1]))
+
+    @staticmethod
+    def _getTime(timeServer=None):
+        if not timeServer:
+            timeServer = Keys.timeServer()
+
+        log.info("Getting time from {0}".format(timeServer))
+        httpClient = tornado.httpclient.HTTPClient()
+        timeResponse = httpClient.fetch(timeServer, **fetchConfig(timeServer))
+        timeData = json.loads(timeResponse.body)
+        certivoxClock = timeData["Time"].replace(" ", "T")
+        certivoxTime = datetime.datetime.strptime(certivoxClock[:-1], '%Y-%m-%dT%H:%M:%S')
+        log.debug("CertiVox Time: %s" % certivoxTime)
+        log.debug("Local system time: %s" % datetime.datetime.utcnow())
+        Time.timeOffset = certivoxTime - datetime.datetime.utcnow()
+        log.info("Synced time: %s" % (datetime.datetime.utcnow() + Time.timeOffset))
+
+    @staticmethod
+    def getTime(wait=False, timeServer=None):
+        seconds = 5
+        while True:
+            try:
+                Time._getTime(timeServer)
+                break
+            except Exception as E:
+                log.error(E)
+                log.error("Unable to get data from the time server. Retrying in {0} seconds".format(seconds))
+                if not wait:
+                    break
+                time.sleep(seconds)
+
+    @staticmethod
+    def DateTimetoEpoch(d):
+        return int(time.mktime(d.timetuple()) * 1000)
+
+
+class Seed(object):
+    seedValue = None
+
+    @staticmethod
+    def _getSeed(entropySources=""):
+        sources = entropySources.split(",")
+
+        totalSize = 100
+        totalEntropy = ""
+        for source in sources:
+            if not source.strip():
+                continue
+            s = source.split(":")
+            moduleName = s[0]
+            eSize = len(s) > 1 and int(s[1]) or totalSize
+
+            E = __import__("entropy.{0}".format(moduleName), globals(), locals(), ["EntropySource"]).EntropySource
+            totalEntropy += E(eSize, logger=log).getEntropy()
+            if len(totalEntropy) / 2 > totalSize:
+                break
+
+        if len(totalEntropy) < totalSize:
+            log.error("Seed value size is too small. Needed: {0} bytes, Got: {1} bytes.".format(totalSize, len(totalEntropy)))
+            raise Exception("Seed value size is too small. Check your configuration.")
+
+        Seed.seedValue = totalEntropy
+        log.debug("Seed.seedValue: %s" % Seed.seedValue.encode("hex"))
+        log.debug("Seed.seedValue length: %d" % len(Seed.seedValue))
+
+    @staticmethod
+    def getSeed(entropySources=""):
+        seconds = 5
+        while True:
+            try:
+                Seed._getSeed(entropySources)
+                break
+            except Exception as E:
+                log.error(E)
+                log.error("Unable to get seed. Retrying in {0} seconds".format(seconds))
+                time.sleep(seconds)
+
+
+def signMessage(message, key):
+    return hmac.new(key, message.encode('utf-8'), hashlib.sha256).hexdigest()
+
+
+def verifySignature(message, signature, key, expiresStr=None):
+    """
+        Verify the signature and also that the timestamp has not expired. If expiresStr is None, the timestamp is not checked.
+
+        Returns:
+            Valid - bool
+
+    """
+    nowTime = Time.syncedNow()
+    try:
+        expiresTime = expiresStr and Time.ISOtoDateTime(expiresStr) or nowTime
+        hmacExpected = hmac.new(key, message.encode('utf-8'), hashlib.sha256).hexdigest()
+        hmac1 = hmac.new(key, signature, hashlib.sha256).hexdigest()
+        hmac2 = hmac.new(key, hmacExpected, hashlib.sha256).hexdigest()
+        if hmac1 != hmac2:
+            reason = "Invalid signature"
+            valid = False
+            code = 401
+        elif nowTime > expiresTime:
+            reason = "Request expired"
+            valid = False
+            code = 408
+        else:
+            reason = "Valid signature"
+            valid = True
+            code = 200
+
+    except Exception as E:
+        valid = False
+        reason = "Error verifying message. {0}".format(E)
+        code = 500
+
+    if not valid:
+        debugData = {
+            "reason": reason,
+            "message": message,
+            "key": key,
+            "signature": signature,
+            "hmacExpected": hmacExpected,
+            "expiresStr": expiresStr,
+            "expiresTime": Time.DateTimeToISO(expiresTime),
+            "nowTime": Time.DateTimeToISO(nowTime)
+        }
+
+        log.debug("verifyMessage: {0}".format(pformat(debugData)))
+
+    return valid, reason, code
+
+
+def getLogLevel(logLevel):
+    return (type(logLevel) == int) and logLevel or {
+        'CRITICAL': 50,
+        'ERROR': 40,
+        'WARN': 30,
+        'INFO': 20,
+        'DEBUG': 10,
+        'ALL': 0,
+    }.get(logLevel.upper(), 20)
diff --git a/lib/mpin_utils/secrets.py b/lib/mpin_utils/secrets.py
new file mode 100644
index 0000000..66d9c92
--- /dev/null
+++ b/lib/mpin_utils/secrets.py
@@ -0,0 +1,400 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Utility functions for working with secrets."""
+from __future__ import division, absolute_import, print_function, unicode_literals
+
+import json
+import os
+import time
+
+from pbkdf2 import PBKDF2
+
+import tornado
+from tornado.httputil import url_concat
+from tornado.log import app_log as log
+from tornado.options import define, options
+
+import crypto
+from mpin_utils.common import (
+    fetchConfig,
+    Keys,
+    SIGNATURE_EXPIRES_OFFSET_SECONDS,
+    signMessage,
+    Time,
+)
+
+
+today = crypto.today
+
+
+def generate_aes_key(passphrase, salt):
+    """Return AES key (128 bit) using a pass-phrase.
+
+    It uses the passphrase argument or in its absence asks the user for
+    a pass-phrase to derive an AES key. The algorithm used is
+    Password-Based Key Derivation Function 2 (PBKDF2)
+
+    Keyword arguments:
+        passphrase  -- A string used to generate an AES key
+        salt        -- Salt value for the PBKDF2 Algorithm. 64 bits hex encoded
+    """
+    return PBKDF2(passphrase, str(salt)).read(16)
+
+
+def backup_master_secret(master_secret, encrypt_master_secret, passphrase, salt, backup_file, time, rng):
+    """Write the master secret to file."""
+    aes_key = generate_aes_key(passphrase, salt)
+    data = {
+        'startTime': time.strftime('%Y-%m-%dT%H:%M:%SZ')
+    }
+
+    if encrypt_master_secret:
+        ciphertext_hex, iv_hex, tag_hex = crypto.aes_gcm_encrypt(
+            master_secret, aes_key, rng, time.strftime('%Y-%m-%dT%H:%M:%SZ'))
+        data.update({
+            'IV': iv_hex,
+            'ciphertext': ciphertext_hex,
+            'tag': tag_hex})
+    else:
+        data['master_secret_hex'] = master_secret.encode('hex'),
+
+    with open(backup_file, 'w') as json_file:
+        json.dump(data, json_file)
+
+
+def generate_random_number(rng, length):
+    """Return random number with predefined length."""
+    return crypto.random_generate(rng, length)
+
+
+def generate_ott(length, rng, encoding=None):
+    """Generate a one time token (OTT).
+
+    Uses the Random Number Generator to generate a value
+    of length OTTLength set in the config file. This is
+    then encoded.
+    """
+    ott_hex = generate_random_number(rng, length)
+    if encoding == 'hex':
+        return ott_hex
+
+    ott = ott_hex.decode('hex')
+    if encoding:
+        ott = ott.encode(encoding)
+    return ott
+
+
+def generate_otp(rng):
+    """Generate a one time password (OTP).
+
+    Uses the Random Number Generator to generate 6 long value
+    """
+    return crypto.generate_otp(rng)
+
+
+def get_checksum(num, length):
+    """Return checksum."""
+    sum_digits = sum([
+        int(digit) * (length + 1 - i)
+        for i, digit in enumerate(str(num).zfill(length))])
+    checksum = (11 - (sum_digits % 11)) % 11
+    return checksum if checksum != 10 else None
+
+
+def generate_random_webid(rng, use_checksum=True):
+    """Generate a web identifier for mobile login.
+
+    Generates a random six digit integer. This is
+    appended with a one digit checksum.
+    """
+    num = generate_otp(rng)
+    checksum = get_checksum(num, 6) if use_checksum else ''
+
+    if not checksum and use_checksum:
+        return None
+
+    return "{0:06d}{1}".format(num, checksum)
+
+
+def generate_auth_ott(rng):
+    """Return auth OTT."""
+    return generate_random_number(rng, crypto.PAS)
+
+
+class SecretsError(Exception):
+
+    """Exception raises by secrets module."""
+
+    pass
+
+
+class MasterSecret(object):
+
+    """Master Secret."""
+
+    master_secret = None
+    start_time = None
+
+    def __init__(self, passphrase, salt, seed, time, backup_file=None, encrypt_master_secret=True):
+        """Constructor."""
+        self.rng = crypto.get_random_generator(seed)
+        self.master_secret, self.start_time = self._get_master_secret(
+            passphrase, salt, time, backup_file, encrypt_master_secret)
+
+    def _get_master_secret(self, passphrase, salt, time, backup_file=None, encrypt_master_secret=True):
+        """Restore/generate master secret.
+
+        Restore from backup_file if such is provided, generate new otherwise.
+        Set backup_file=None for in memory master_secret.
+        """
+        if not backup_file:
+            log.info('Master Secret Share not backed up to file')
+            return self._generate_master_secret(), time
+
+        if not os.path.exists(backup_file):
+            log.info('Master Secret backup file doesn\'t exists. Generate new.')
+            master_secret = self._generate_master_secret()
+            backup_master_secret(
+                master_secret, encrypt_master_secret, passphrase, salt, backup_file, time, self.rng)
+            return master_secret, time
+
+        log.info('Restore Master Secret Share from file')
+        return self._restore_master_secret(
+            backup_file,
+            encrypt_master_secret,
+            passphrase,
+            salt)
+
+    def _generate_master_secret(self):
+        """Generate the M-Pin Master Secret."""
+        try:
+            return crypto.mpin_random_generate(self.rng)
+        except crypto.CryptoError as e:
+            log.error(e)
+            raise SecretsError('M-Pin Master Secret Generation Failed')
+
+    def _restore_master_secret(self, backup_file, encrypt_master_secret, passphrase, salt):
+        """Restore secret from file.
+
+        Decode secret if encrypted.
+        """
+        try:
+            with open(backup_file) as json_file:
+                backup = json.load(json_file)
+        except ValueError:
+            raise SecretsError('Master Secret backup file is corrupted.')
+
+        if encrypt_master_secret:
+            tag, plaintext = crypto.aes_gcm_decrypt(
+                aes_key=generate_aes_key(passphrase, salt),
+                iv=str(backup['IV'].decode('hex')),
+                header=str(backup['startTime']),
+                ciphertext=str(backup['ciphertext'].decode('hex')))
+
+            # Check authentication tag
+            if backup['tag'] != tag:
+                raise SecretsError('AES-GSM Decryption Failed. Authentication tag is not correct')
+
+            self.start_time = Time.ISOtoDateTime(str(backup['startTime']))
+            master_secret = plaintext.decode('hex')
+        else:
+            self.start_time = Time.ISOtoDateTime(backup['startTime'])
+            master_secret = backup['master_secret_hex'].decode('hex')
+
+        return master_secret, self.start_time
+
+    def get_server_secret(self):
+        """Generate server secret."""
+        try:
+            return crypto.get_server_secret(self.master_secret)
+        except crypto.CryptoError as e:
+            log.error(e)
+            raise SecretsError('Server Secret generation failed')
+
+    def get_client_secret(self, mpin_id):
+        """Generate client secret."""
+        try:
+            return crypto.get_client_multiple(self.master_secret, mpin_id)
+        except crypto.CryptoError as e:
+            log.error(e)
+            raise SecretsError('Client secret generation failed')
+
+    def get_time_permits(self, mpin_id, count):
+        """Generate client time permit."""
+        start_date = crypto.today()
+        try:
+            return dict(
+                (date, crypto.get_time_permit(self.master_secret, mpin_id, date))
+                for date in range(start_date, start_date + count))
+        except crypto.CryptoError as e:
+            log.error(e)
+            raise SecretsError('M-Pin Time Permit Generation Failed')
+
+
+define("certivoxServerSecret", default='dta', type=unicode)
+
+
+class ServerSecret(object):
+
+    """Server Secret."""
+
+    server_secret = None
+
+    def __init__(self, seed, app_id, app_key):
+        """Constructor."""
+        self.rng = crypto.get_random_generator(seed)
+        self.app_id = app_id
+        self.app_key = app_key
+        self.server_secret = self._get_server_secret()
+
+    def _get_certivox_server_secret_share_dta(self, expires):
+        path = 'serverSecret'
+        url_params = url_concat('{0}{1}'.format(Keys.certivoxServer(), path), {
+            'app_id': self.app_id,
+            'expires': expires,
+            'signature': signMessage('{0}{1}{2}'.format(path, self.app_id, expires), self.app_key)
+        })
+        log.debug('MIRACL server secret request: {0}'.format(url_params))
+        httpclient = tornado.httpclient.HTTPClient()
+        try:
+            response = httpclient.fetch(url_params, **fetchConfig(url_params))
+        except tornado.httpclient.HTTPError as e:
+            log.error(e)
+            raise SecretsError('Unable to get Server Secret from the MIRACL TA server')
+        httpclient.close()
+
+        try:
+            data = json.loads(response.body)
+        except ValueError as e:
+            log.error(e)
+            raise SecretsError('Invalid response from TA server')
+
+        if 'serverSecret' not in data:
+            raise SecretsError('serverSecret not in response from TA server')
+
+        return data["serverSecret"]
+
+    def _get_certivox_server_secret_share_credentials(self, expires):
+        if not hasattr(Keys, 'certivox_server_secret'):
+            raise SecretsError(
+                'MIRACL server secret share is not in the credentials.json. '
+                'You can get it by: \n'
+                'scripts/getServerSecretShare.py credentials.json > credentials_with_secret.json')
+        return Keys.certivox_server_secret
+
+    def _get_certivox_server_secret_share(self, expires):
+        method = options.certivoxServerSecret
+        methods = {
+            'dta': self._get_certivox_server_secret_share_dta,
+            'credentials.json': self._get_certivox_server_secret_share_credentials,
+            'manual': lambda x: raw_input('MIRACL server secret share:'),
+            'config': lambda x: options.certivoxServerSecret
+        }
+        func = methods[method if method in methods else 'config']
+        certivox_server_secret_hex = func(expires)
+
+        try:
+            return certivox_server_secret_hex.decode("hex")
+        except TypeError as e:
+            log.error(e)
+            raise SecretsError('Invalid CertiVox server secret share')
+
+    def _get_customer_server_secret_share(self, expires):
+        path = 'serverSecret'
+        url_params = url_concat(
+            '{0}/{1}'.format(options.DTALocalURL, path),
+            {
+                'app_id': self.app_id,
+                'expires': expires,
+                'signature': signMessage('{0}{1}{2}'.format(path, self.app_id, expires), self.app_key)
+            })
+        log.debug('customer server secret request: {0}'.format(url_params))
+
+        httpclient = tornado.httpclient.HTTPClient()
+
+        import socket
+        # Make at most 30 attempts to get server secret from local TA
+        for attempt in range(30):
+            try:
+                response = httpclient.fetch(url_params)
+            except (tornado.httpclient.HTTPError, socket.error) as e:
+                log.error(e)
+                log.error(
+                    'Unable to get Server Secret from the customer TA server. '
+                    'Retying...')
+                time.sleep(2)
+                continue
+
+            httpclient.close()
+            break
+        else:
+            # Max attempts reached
+            raise SecretsError(
+                'Unable to get Server Secret from the customer TA server.')
+
+        try:
+            data = json.loads(response.body)
+        except ValueError:
+            raise SecretsError('TA server response contains invalid JSON')
+
+        if 'serverSecret' not in data:
+            raise SecretsError('serverSecret not in response from TA server')
+
+        return data["serverSecret"].decode("hex")
+
+    def _get_server_secret(self):
+        expires = Time.syncedISO(seconds=SIGNATURE_EXPIRES_OFFSET_SECONDS)
+        certivox_server_secret = self._get_certivox_server_secret_share(expires)
+        customer_server_secret = self._get_customer_server_secret_share(expires)
+
+        try:
+            server_secret_hex = crypto.mpin_recombine_g2(certivox_server_secret, customer_server_secret)
+        except crypto.CryptoError as e:
+            log.error(e)
+            raise SecretsError('M-Pin Server Secret Generation Failed')
+
+        return server_secret_hex.decode("hex")
+
+    def get_pass1_value(self):
+        """Return pass1 value."""
+        try:
+            random_number = crypto.mpin_random_generate(self.rng)
+        except crypto.CryptoError as e:
+            log.error(e)
+            raise SecretsError('Pass 1 - failed to generate Y')
+
+        return random_number.encode('hex')
+
+    def validate_pass2_value(self, mpin_id, u, ut, y, v):
+        """Validate pass2 value.
+
+        y - pass 1 values
+        v - pass 2 value in question
+        """
+        date = crypto.today()
+        check_dates = [date]
+        if Time.syncedNow().hour < 1:
+            check_dates.append(date - 1)
+
+        for date in check_dates:
+            hid, htid = crypto.mpin_server_1(mpin_id, date)
+            success, _, _ = crypto.mpin_server_2(self.server_secret, v, date, hid, htid, y, u, ut)
+            if success != -19:
+                break
+
+        return success
diff --git a/lib/storage/__init__.py b/lib/storage/__init__.py
new file mode 100644
index 0000000..60493f0
--- /dev/null
+++ b/lib/storage/__init__.py
@@ -0,0 +1,56 @@
+#! /usr/bin/python
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import division, absolute_import, print_function, unicode_literals
+
+import sys
+
+from tornado.options import define, options
+from tornado.log import app_log as log
+
+
+define("storage", default="memory")
+
+define("redisHost", default="127.0.0.1")
+define("redisPort", default=6379)
+define("redisDB", default=0)
+define("redisPassword", default=None)
+define("redisPrefix", default="mpin")
+
+define("fileStorageLocation", type=unicode)
+
+
+class StorageError(Exception):
+    pass
+
+
+def get_storage_cls():
+    if options.storage == "redis":
+        from storage.backends.redis import MPinStorage
+    elif options.storage == "memory":
+        from storage.backends.memory import MPinStorage
+    elif options.storage == "json":
+        if not options.fileStorageLocation:
+            raise StorageError('File storage requires fileStorageLocation option')
+        from storage.backends.file import MPinStorage
+    else:
+        log.error("Invalid storage: {0}".format(options.storage))
+        sys.exit(1)
+
+    return MPinStorage
diff --git a/lib/storage/backends/__init__.py b/lib/storage/backends/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/lib/storage/backends/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/lib/storage/backends/base.py b/lib/storage/backends/base.py
new file mode 100644
index 0000000..1c8208b
--- /dev/null
+++ b/lib/storage/backends/base.py
@@ -0,0 +1,139 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from storage.item import Item
+
+
+class BaseIndex(dict):
+
+    def __init__(self, storage, name, fields):
+        super(BaseIndex, self).__init__()
+
+        self.storage = storage
+        self.name = name
+        self.fields = fields[:]
+
+    def _add_item(self, key, obj):
+        raise NotImplemented
+
+    def _find_item(self, key):
+        raise NotImplemented
+
+    def _delete_item(self, key):
+        raise NotImplemented
+
+    def _get_item_key(self, obj):
+        key = []
+        for field_name in self.fields:
+            if type(obj) is dict:
+                val = obj.get(field_name)
+            else:
+                val = getattr(obj, field_name, None)
+            # update index only if all the fields have values
+            if not val:
+                return None
+            key.append(str(val))
+        return "_".join(key)
+
+    def add(self, obj):
+        key = self._get_item_key(obj)
+        if key:
+            self._add_item(key, obj)
+
+    def find(self, **kwargs):
+        key = self._get_item_key(kwargs)
+        if key:
+            return self._find_item(key)
+        else:
+            return None
+
+    def delete(self, obj):
+        key = self._get_item_key(obj)
+        if key:
+            self._delete_item(key)
+
+
+class BaseStorage(object):
+    index_cls = BaseIndex
+
+    def __init__(self, ioloop, *indexes):
+        '''
+        Initialize the in-memory storage and create indexes defined as a list of parameters.
+        The indexed fields must be coma-separated.
+
+        Example:
+            M = MPinStorage("mpinid", "mpinid,wid") #Will create two indexes: by mpinid and by mpinid and wid
+        '''
+
+        self.ioloop = ioloop
+        self.indexes = {}
+
+        # Create indexes
+        for index in indexes:
+            index_fields = map(lambda x: x.strip(), index.split(","))
+            index_name = ",".join(index_fields)
+
+            self.indexes[index_name] = self.index_cls(
+                storage=self,
+                name=index_name,
+                fields=index_fields,
+            )
+
+    def _add_item(self, item):
+        raise NotImplemented
+
+    def _find_item(self, index, **kwargs):
+        raise NotImplemented
+
+    def _delete_item(self, item):
+        raise NotImplemented
+
+    def _delete_from_indexes(self, item):
+        for index in self.indexes.itervalues():
+            index.delete(item)
+
+    def _storage_change(self):
+        pass
+
+    def update_item(self, item):
+        self.update_index(item)
+        self._add_item(item)
+
+    def add(self, expire_time=None, **kwargs):
+        item = Item(self, expire_time, **kwargs)
+        self._add_item(item)
+        return item
+
+    def find(self, **kwargs):
+        correct_index = None
+        for index in self.indexes.itervalues():
+            if sorted(kwargs.keys()) == sorted(index.fields):
+                correct_index = index
+                break
+
+        if correct_index is not None:
+            return self._find_item(correct_index, **kwargs)
+        else:
+            return None
+
+    def delete(self, item):
+        self._delete_item(item)
+        self._delete_from_indexes(item)
+
+    def update_index(self, item):
+        for index in self.indexes.itervalues():
+            index.add(item)
diff --git a/lib/storage/backends/file.py b/lib/storage/backends/file.py
new file mode 100644
index 0000000..1615621
--- /dev/null
+++ b/lib/storage/backends/file.py
@@ -0,0 +1,100 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import datetime
+import dateutil.parser
+import json
+import os.path
+
+from tornado.options import options
+
+from storage.backends.memory import (
+    Index as MemoryIndex,
+    MPinStorage as MemoryStorage,
+)
+from storage.item import Item
+
+
+class MyEncoder(json.JSONEncoder):
+
+    def default(self, obj):
+        if isinstance(obj, datetime.datetime):
+            return obj.isoformat()
+
+        elif isinstance(obj, Item):
+            return obj.dict
+
+        return json.JSONEncoder.default(self, obj)
+
+
+class Index(MemoryIndex):
+    pass
+
+
+class MPinStorage(MemoryStorage):
+    def __init__(self, *args, **kwargs):
+        super(MPinStorage, self).__init__(*args, **kwargs)
+        self._deserialize()
+
+    def _storage_change(self):
+        self._serialize()
+
+    def _serialize(self):
+        data = {
+            'expires_list': self._expires_list,
+            'items': self._items,
+            'indexes': self.indexes,
+        }
+        json_data = json.dumps(data, cls=MyEncoder)
+        with open(options.fileStorageLocation, 'w') as json_storage:
+            json_storage.write(json_data)
+
+    def _deserialize_expires_list(self, data):
+        return [
+            (dateutil.parser.parse(expiration), _id)
+            for expiration, _id in data.get('expires_list', [])
+        ]
+
+    def _deserialize_items(self, data):
+        return dict((
+            (_id, Item(self, None, **item_data))
+            for _id, item_data in data.get('items', {}).iteritems()
+        ))
+
+    def _deserialize_index(self, data):
+        indexes = {}
+        for index_name, index_data in data.get('indexes', {}).iteritems():
+            index = Index(self, index_name, index_name.split(','))
+            for key, value in index_data.iteritems():
+                index[key] = value
+
+            indexes[index_name] = index
+
+        return indexes
+
+    def _deserialize(self):
+        if not os.path.isfile(options.fileStorageLocation):
+            return
+
+        with open(options.fileStorageLocation, 'r') as json_storage:
+            data = json.load(json_storage)
+
+        self._expires_list = self._deserialize_expires_list(data)
+        self._items = self._deserialize_items(data)
+        self.indexes = self._deserialize_index(data)
+
+        self._schedule_expiration_check()
diff --git a/lib/storage/backends/memory.py b/lib/storage/backends/memory.py
new file mode 100644
index 0000000..5cec7a6
--- /dev/null
+++ b/lib/storage/backends/memory.py
@@ -0,0 +1,101 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import division, absolute_import, print_function, unicode_literals
+
+import bisect
+import datetime
+
+from mpin_utils.common import Time
+
+from storage.backends.base import BaseIndex, BaseStorage
+
+
+class Index(BaseIndex):
+
+    def _add_item(self, key, obj):
+        self[key] = obj._id
+
+    def _find_item(self, key):
+        return self.get(key)
+
+    def _delete_item(self, key):
+        if key in self:
+            del self[key]
+
+
+class MPinStorage(BaseStorage):
+    index_cls = Index
+
+    def __init__(self, *args, **kwargs):
+        super(MPinStorage, self).__init__(*args, **kwargs)
+        self._expires_list = []
+        self._timeout = None
+        self._items = {}
+
+    def __len__(self):
+        return len(self._items)
+
+    def _add_item(self, item):
+        item._active = True
+        self._items[item._id] = item
+        if item._expiration_datetime:
+            bisect.insort_left(self._expires_list, (item._expiration_datetime, item._id))
+            self._schedule_expiration_check()
+
+        self._storage_change()
+
+    def _find_item(self, index, **kwargs):
+        _id = index.find(**kwargs)
+        item = self._items.get(_id)
+        if item and item._active:
+            return item
+        else:
+            return None
+
+    def _delete_item(self, item):
+        item._active = False
+
+    def _schedule_expiration_check(self):
+        if self._timeout:
+            self.ioloop.remove_timeout(self._timeout)
+            self._timeout = None
+
+        while len(self._expires_list) > 0:
+            item_expiration, item_id = self._expires_list[0]
+
+            try:
+                item = self._items[item_id]
+            except KeyError:
+                del self._expires_list[0]
+                continue
+
+            now = Time.syncedNow()
+            if not item._active or item_expiration < now:
+                del self._expires_list[0]
+                del self._items[item_id]
+                self._delete_from_indexes(item)
+                continue
+
+            # No more expired items, schedule next check
+            self._timeout = self.ioloop.add_timeout(
+                item_expiration - now + datetime.timedelta(milliseconds=100),
+                self._schedule_expiration_check
+            )
+            break
+
+        self._storage_change()
diff --git a/lib/storage/backends/redis.py b/lib/storage/backends/redis.py
new file mode 100644
index 0000000..ccd80e3
--- /dev/null
+++ b/lib/storage/backends/redis.py
@@ -0,0 +1,105 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import division, absolute_import, print_function, unicode_literals
+
+import json
+
+import redis
+
+from tornado.options import options
+
+from mpin_utils.common import Time
+
+from storage.item import Item
+from storage.backends.base import BaseIndex, BaseStorage
+
+
+def get_redis_id(key):
+    return '_'.join([options.redisPrefix, key])
+
+
+class RedisConnection(object):
+
+    def __init__(self, host, port, password, db):
+        self.redis = redis.StrictRedis(host, port, password, db)
+
+    def _execute(self, command, *args, **kwargs):
+        method = getattr(self.redis, command)
+        return method(*args, **kwargs)
+
+    def add(self, key, expires, value):
+        if expires:
+            self._execute("setex", key, (expires - Time.syncedNow()), value)
+        else:
+            self._execute("set", key, value)
+
+    def get(self, key):
+        return self._execute("get", key)
+
+    def delete(self, key):
+        return self._execute("delete", key)
+
+
+class Index(BaseIndex):
+
+    def _get_item_key(self, obj):
+        key = super(Index, self)._get_item_key(obj)
+        if key:
+            return get_redis_id(key)
+        else:
+            return None
+
+    def _add_item(self, key, obj):
+        self.storage.redis.add(key, obj._expiration_datetime, obj._id)
+
+    def _find_item(self, key):
+        return self.storage.redis.get(key)
+
+    def _delete_item(self, key):
+        self.storage.redis.delete(key)
+
+
+class MPinStorage(BaseStorage):
+    index_cls = Index
+
+    def __init__(self, *args, **kwargs):
+        super(MPinStorage, self).__init__(*args, **kwargs)
+        self.redis = RedisConnection(
+            host=options.redisHost,
+            port=options.redisPort,
+            password=options.redisPassword,
+            db=options.redisDB
+        )
+
+    def _add_item(self, item):
+        self.redis.add(
+            get_redis_id(item._id),
+            item._expiration_datetime,
+            item.json
+        )
+
+    def _find_item(self, index, **kwargs):
+        _id = index.find(**kwargs)
+        if not _id:
+            return None
+
+        data = self.redis.get(get_redis_id(_id))
+        return Item(self, None, **json.loads(data))
+
+    def _delete_item(self, item):
+        self.redis.delete(get_redis_id(item._id))
diff --git a/lib/storage/item.py b/lib/storage/item.py
new file mode 100644
index 0000000..6a38327
--- /dev/null
+++ b/lib/storage/item.py
@@ -0,0 +1,79 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import division, absolute_import, print_function, unicode_literals
+
+import datetime
+import json
+import uuid
+
+from mpin_utils.common import Time
+
+
+class Item(object):
+
+    def __init__(self, storage, expire_time, **kwargs):
+        '''expireTime should be in ISO format'''
+        self.__fields = ["_id", "_active", "_expires"]
+        self.__storage = storage
+
+        self._id = uuid.uuid1().hex
+
+        if isinstance(expire_time, datetime.datetime):
+            self._expires = expire_time.isoformat()
+        else:
+            self._expires = expire_time
+
+        self._update_item(**kwargs)
+
+        self._expiration_datetime = None
+        if self._expires:
+            self._expiration_datetime = Time.ISOtoDateTime(self._expires)
+
+        self.__storage.update_index(self)
+
+    def __getattr__(self, name):
+        return None
+
+    def __str__(self):
+        return "\n".join([
+            "{0}: {1}".format(k, getattr(self, k)) for k in self.__fields
+        ])
+
+    def _update_item(self, **kwargs):
+        for key, value in kwargs.items():
+            setattr(self, key, value)
+            if key in self.__fields:
+                if not value:
+                    self.__fields.remove(key)
+            else:
+                self.__fields.append(key)
+
+    @property
+    def dict(self):
+        return dict([(k, getattr(self, k)) for k in self.__fields])
+
+    @property
+    def json(self):
+        return json.dumps(self.dict)
+
+    def update(self, **kwargs):
+        self._update_item(**kwargs)
+        self.__storage.update_item(self)
+
+    def delete(self):
+        self.__storage.delete(self)
diff --git a/lib/storage/tests/__init__.py b/lib/storage/tests/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/lib/storage/tests/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/lib/storage/tests/cases.py b/lib/storage/tests/cases.py
new file mode 100644
index 0000000..633898d
--- /dev/null
+++ b/lib/storage/tests/cases.py
@@ -0,0 +1,318 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Common test cases for storage."""
+from __future__ import division, absolute_import, print_function, unicode_literals
+
+from datetime import datetime, timedelta
+
+import pytest
+
+
+def simplekey_index_case(storage):
+    """
+    indexes = 'id1'
+    """
+
+    storage.add(
+        datetime.now() + timedelta(days=1),
+        id1=1.1,
+        id2=1.2,
+        key='value1'
+    )
+
+    storage.add(
+        datetime.now() + timedelta(days=1),
+        id1=2.1,
+        id2=2.2,
+        key='value2'
+    )
+
+    item1 = storage.find(id1=1.1)
+    item2 = storage.find(id1=2.1)
+
+    assert item1.key == 'value1'
+    assert item2.key == 'value2'
+    assert not storage.find(id2=1.2)  # Log warning at this point
+
+    storage.delete(item1)
+    assert not storage.find(id1=1.1)
+    assert storage.find(id1=2.1)
+
+
+def multi_indexes_case(storage):
+    """
+    indexes = 'id1', 'id2'
+    """
+
+    storage.add(
+        datetime.now() + timedelta(days=1),
+        id1=1.1,
+        id2=1.2,
+        key='value1'
+    )
+
+    storage.add(
+        datetime.now() + timedelta(days=1),
+        id1=2.1,
+        id2=2.2,
+        key='value2'
+    )
+
+    assert storage.find(id1=1.1).key == 'value1'
+    assert storage.find(id2=2.2).key == 'value2'
+
+    assert not storage.find(id1=2.1, id2=2.2)
+    assert not storage.find(id1=1.1, id2=2.2)
+    assert not storage.find(id1=1.1, id2=2.2)
+    assert not storage.find(id1=2.1, key='value2')
+
+
+def multikey_indexes(storage):
+    """
+    indexes = 'id1,id2'
+    """
+
+    storage.add(
+        datetime.now() + timedelta(days=1),
+        id1=1.1,
+        id2=1.2,
+        key='value1'
+    )
+
+    storage.add(
+        datetime.now() + timedelta(days=1),
+        id1=2.1,
+        id2=2.2,
+        key='value2'
+    )
+
+    assert storage.find(id1=2.1, id2=2.2).key == 'value2'
+    assert not storage.find(id1=1.1)
+    assert not storage.find(id1=1.1, id2=2.2)
+    assert not storage.find(id1=2.1, key='value2')
+
+
+def mixkey_indexes_case(storage):
+    """
+    indexes = 'id1', 'id1,id2'
+    """
+
+    storage.add(
+        datetime.now() + timedelta(days=1),
+        id1=1.1,
+        id2=1.2,
+        key='value1'
+    )
+
+    storage.add(
+        datetime.now() + timedelta(days=1),
+        id1=2.1,
+        id2=2.2,
+        key='value2'
+    )
+
+    assert storage.find(id1=2.1, id2=2.2).key == 'value2'
+    assert storage.find(id1=1.1).key == 'value1'
+    assert not storage.find(id2=2.2)
+    assert not storage.find(id1=1.1, id2=2.2)
+    assert not storage.find(id1=2.1, key='value2')
+
+
+def missing_key_indexes_case(storage):
+    """
+    indexes = 'id1', 'id2'
+    """
+
+    storage.add(
+        datetime.now() + timedelta(days=1),
+        id1=1.1,
+        id2=1.2,
+        key='value1'
+    )
+
+    storage.add(
+        datetime.now() + timedelta(days=1),
+        id1=2.1,
+        key='value2'
+    )
+
+    assert storage.find(id1=1.1).key == 'value1'
+    assert storage.find(id2=1.2).key == 'value1'
+    assert storage.find(id1=2.1).key == 'value2'
+
+    assert not storage.find(id2=2.2)
+
+    item2 = storage.find(id1=2.1)
+    storage.delete(item2)
+    assert not storage.find(id1=2.1)
+
+
+def valid_string_date_case(storage):
+    """
+    indexes = 'id1', 'id2'
+    """
+
+    storage.add(
+        (datetime.now() + timedelta(days=1)).isoformat(),
+        id1=1.1,
+        id2=1.2,
+        key='value1'
+    )
+
+    storage.add(
+        (datetime.now() + timedelta(days=1)).isoformat(),
+        id1=2.1,
+        id2=2.2,
+        key='value2'
+    )
+
+    assert storage.find(id1=2.1).key == 'value2'
+    assert storage.find(id2=1.2).key == 'value1'
+
+
+def invalid_string_date_case(storage):
+    """
+    indexes = 'id1', 'id2'
+    """
+
+    with pytest.raises(ValueError):
+        storage.add(
+            'invalid date',
+            id1=1.1,
+            id2=1.2,
+            key='value1'
+        )
+
+    assert not storage.find(id1=1.1)
+
+
+def no_date_case(storage):
+    """
+    indexes = 'id1', 'id2'
+    """
+
+    storage.add(
+        id1=1.1,
+        id2=1.2,
+        key='value1'
+    )
+
+    storage.add(
+        id1=2.1,
+        id2=2.2,
+        key='value2'
+    )
+
+    assert storage.find(id1=2.1).key == 'value2'
+    assert storage.find(id2=1.2).key == 'value1'
+
+
+def item_deletion_case(storage):
+    """
+    indexes = 'id1', 'id2'
+    """
+
+    storage.add(
+        datetime.now() + timedelta(days=1),
+        id1=1.1,
+        id2=1.2,
+        key='value1'
+    )
+
+    storage.add(
+        datetime.now() + timedelta(days=1),
+        id1=2.1,
+        key='value2'
+    )
+
+    assert storage.find(id1=1.1).key == 'value1'
+    assert storage.find(id2=1.2).key == 'value1'
+    assert storage.find(id1=2.1).key == 'value2'
+    assert not storage.find(id2=2.2)
+
+    item1 = storage.find(id1=1.1)
+    storage.delete(item1)
+
+    # try to delete item that does not exist in index
+    storage.delete(item1)
+
+    assert not storage.find(id1=1.1)
+    assert not storage.find(id2=1.2)
+    assert storage.find(id1=2.1).key == 'value2'
+    assert not storage.find(id2=2.2)
+
+
+def keys_with_underscore_case(storage):
+    """
+    indexes = 'id_1', 'id_1,id_2'
+    """
+
+    storage.add(
+        datetime.now() + timedelta(days=1),
+        id_1=1.1,
+        id_2=1.2,
+        key='value1'
+    )
+
+    storage.add(
+        datetime.now() + timedelta(days=1),
+        id_1=2.1,
+        id_2=2.2,
+        key='value2'
+    )
+
+    assert storage.find(id_1=2.1, id_2=2.2).key == 'value2'
+    assert storage.find(id_1=1.1).key == 'value1'
+    assert not storage.find(id_2=2.2)
+    assert not storage.find(id_1=1.1, id_2=2.2)
+    assert not storage.find(id_1=2.1, key='value2')
+
+
+def update_item_case(storage):
+    """
+    indexes = 'id1'
+    """
+    storage.add(
+        datetime.now() + timedelta(days=1),
+        id1=1.1,
+        id2=1.2,
+        key='value'
+    )
+
+    item = storage.find(id1=1.1)
+
+    item.update(key='value_new')
+    item = storage.find(id1=1.1)
+    assert item.key == 'value_new'
+
+    item.update(id1=1.11)
+    item = storage.find(id1=1.11)
+    assert item.key == 'value_new'
+
+    item.update(key=None)
+    item = storage.find(id1=1.11)
+    assert not item.key
+
+    item.update(key='value', key1='value1')
+    item = storage.find(id1=1.11)
+    assert item.key == 'value'
+    assert item.key1 == 'value1'
+
+    item.delete()
+    item = storage.find(id1=1.11)
+    assert not item
diff --git a/lib/storage/tests/test_file_storage.py b/lib/storage/tests/test_file_storage.py
new file mode 100644
index 0000000..7651e94
--- /dev/null
+++ b/lib/storage/tests/test_file_storage.py
@@ -0,0 +1,155 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import division, absolute_import, print_function, unicode_literals
+
+import uuid
+import os
+
+from tornado.options import options
+
+from storage.backends.file import Index, MPinStorage
+from storage.tests.cases import (
+    simplekey_index_case,
+    multi_indexes_case,
+    multikey_indexes,
+    mixkey_indexes_case,
+    missing_key_indexes_case,
+    valid_string_date_case,
+    invalid_string_date_case,
+    no_date_case,
+    item_deletion_case,
+    keys_with_underscore_case,
+    update_item_case,
+)
+
+STORAGE_FILENAME = '/tmp/{0}.json'.format(uuid.uuid4().hex)
+
+
+def get_storage(*args, **kwargs):
+    options.fileStorageLocation = STORAGE_FILENAME
+    if os.path.isfile(STORAGE_FILENAME):
+        os.remove(STORAGE_FILENAME)
+    return MPinStorage(*args, **kwargs)
+
+
+def test_index():
+    index = Index(None, 'a_b', ['a', 'b'])
+
+    assert index.name == 'a_b'
+    assert index.fields == ['a', 'b']
+
+    obj = {
+        'a': 1,
+        'b': 2,
+        'c': 3,
+    }
+
+    key = index._get_item_key(obj)
+    assert key == '1_2'
+
+    class Obj(object):
+        a = 1
+        b = 2
+        c = 3
+
+    key = index._get_item_key(Obj())
+    assert key == '1_2'
+
+    obj = {
+        'b': 2,
+        'c': 3,
+    }
+
+    key = index._get_item_key(obj)
+    assert not key
+
+    class Obj(object):
+        b = 2
+        c = 3
+
+    key = index._get_item_key(Obj())
+    assert not key
+
+
+def test_simplekey_index(io_loop):
+    storage = get_storage(io_loop, 'id1')
+    simplekey_index_case(storage)
+
+
+def test_multi_indexes(io_loop):
+    storage = get_storage(io_loop, 'id1', 'id2')
+    multi_indexes_case(storage)
+
+
+def test_multikey_indexes(io_loop):
+    storage = get_storage(io_loop, 'id1,id2')
+    multikey_indexes(storage)
+
+
+def test_mixkey_indexes(io_loop):
+    storage = get_storage(io_loop, 'id1', 'id1,id2')
+    mixkey_indexes_case(storage)
+
+
+def test_missing_key_indexes(io_loop):
+    storage = get_storage(io_loop, 'id1', 'id2')
+    missing_key_indexes_case(storage)
+
+
+def test_valid_string_date(io_loop):
+    storage = get_storage(io_loop, 'id1', 'id2')
+    valid_string_date_case(storage)
+
+
+def test_invalid_string_date(io_loop):
+    storage = get_storage(io_loop, 'id1', 'id2')
+    invalid_string_date_case(storage)
+
+
+def test_no_date(io_loop):
+    storage = get_storage(io_loop, 'id1', 'id2')
+    no_date_case(storage)
+
+
+def test_item_deletion(io_loop):
+    storage = get_storage(io_loop, 'id1', 'id2')
+    item_deletion_case(storage)
+
+
+def test_keys_with_underscore(io_loop):
+    storage = get_storage(io_loop, 'id_1', 'id_1,id_2')
+    keys_with_underscore_case(storage)
+
+
+def test_update_item(io_loop):
+    storage = get_storage(io_loop, 'id1')
+    update_item_case(storage)
+
+
+def test_reinitialization(io_loop):
+    storage = get_storage(io_loop, 'id1')
+    simplekey_index_case(storage)
+
+    del storage
+    storage = MPinStorage(io_loop, 'id1')
+
+    item1 = storage.find(id1=1.1)
+    item2 = storage.find(id1=2.1)
+
+    assert item1.key == 'value1'
+    assert item2.key == 'value2'
diff --git a/lib/storage/tests/test_get_storage.py b/lib/storage/tests/test_get_storage.py
new file mode 100644
index 0000000..2d05dfb
--- /dev/null
+++ b/lib/storage/tests/test_get_storage.py
@@ -0,0 +1,61 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import division, absolute_import, print_function, unicode_literals
+
+import pytest
+
+from tornado.options import options
+
+from storage import get_storage_cls, StorageError
+from storage.backends.memory import MPinStorage as MemoryStorage
+from storage.backends.redis import MPinStorage as RedisStorage
+from storage.backends.file import MPinStorage as FileStorage
+
+
+def test_defauld(io_loop):
+    storage = get_storage_cls()(io_loop, 'key')
+    assert isinstance(storage, MemoryStorage)
+
+
+def test_memory_defauld(io_loop):
+    options.storage = 'memory'
+    storage = get_storage_cls()(io_loop, 'key')
+    assert isinstance(storage, MemoryStorage)
+
+
+def test_redis_defauld(io_loop):
+    options.storage = 'redis'
+    storage = get_storage_cls()(io_loop, 'key')
+    assert isinstance(storage, RedisStorage)
+
+
+def test_file_defauld(io_loop):
+    options.storage = 'json'
+    options.fileStorageLocation = None
+    with pytest.raises(StorageError):
+        storage = get_storage_cls()(io_loop, 'key')
+
+    options.fileStorageLocation = 'file.json'
+    storage = get_storage_cls()(io_loop, 'key')
+    assert isinstance(storage, FileStorage)
+
+
+def test_invalid_defauld(io_loop):
+    options.storage = 'invalid'
+    with pytest.raises(SystemExit):
+        get_storage_cls()(io_loop, 'key')
diff --git a/lib/storage/tests/test_memory_storage.py b/lib/storage/tests/test_memory_storage.py
new file mode 100644
index 0000000..326189e
--- /dev/null
+++ b/lib/storage/tests/test_memory_storage.py
@@ -0,0 +1,127 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import division, absolute_import, print_function, unicode_literals
+
+from storage.backends.memory import Index, MPinStorage
+from storage.tests.cases import (
+    simplekey_index_case,
+    multi_indexes_case,
+    multikey_indexes,
+    mixkey_indexes_case,
+    missing_key_indexes_case,
+    valid_string_date_case,
+    invalid_string_date_case,
+    no_date_case,
+    item_deletion_case,
+    keys_with_underscore_case,
+    update_item_case,
+)
+
+
+def test_index():
+    index = Index(None, 'a_b', ['a', 'b'])
+
+    assert index.name == 'a_b'
+    assert index.fields == ['a', 'b']
+
+    obj = {
+        'a': 1,
+        'b': 2,
+        'c': 3,
+    }
+
+    key = index._get_item_key(obj)
+    assert key == '1_2'
+
+    class Obj(object):
+        a = 1
+        b = 2
+        c = 3
+
+    key = index._get_item_key(Obj())
+    assert key == '1_2'
+
+    obj = {
+        'b': 2,
+        'c': 3,
+    }
+
+    key = index._get_item_key(obj)
+    assert not key
+
+    class Obj(object):
+        b = 2
+        c = 3
+
+    key = index._get_item_key(Obj())
+    assert not key
+
+
+def test_simplekey_index(io_loop):
+    storage = MPinStorage(io_loop, 'id1')
+    simplekey_index_case(storage)
+
+
+def test_multi_indexes(io_loop):
+    storage = MPinStorage(io_loop, 'id1', 'id2')
+    multi_indexes_case(storage)
+
+
+def test_multikey_indexes(io_loop):
+    storage = MPinStorage(io_loop, 'id1,id2')
+    multikey_indexes(storage)
+
+
+def test_mixkey_indexes(io_loop):
+    storage = MPinStorage(io_loop, 'id1', 'id1,id2')
+    mixkey_indexes_case(storage)
+
+
+def test_missing_key_indexes(io_loop):
+    storage = MPinStorage(io_loop, 'id1', 'id2')
+    missing_key_indexes_case(storage)
+
+
+def test_valid_string_date(io_loop):
+    storage = MPinStorage(io_loop, 'id1', 'id2')
+    valid_string_date_case(storage)
+
+
+def test_invalid_string_date(io_loop):
+    storage = MPinStorage(io_loop, 'id1', 'id2')
+    invalid_string_date_case(storage)
+
+
+def test_no_date(io_loop):
+    storage = MPinStorage(io_loop, 'id1', 'id2')
+    no_date_case(storage)
+
+
+def test_item_deletion(io_loop):
+    storage = MPinStorage(io_loop, 'id1', 'id2')
+    item_deletion_case(storage)
+
+
+def test_keys_with_underscore(io_loop):
+    storage = MPinStorage(io_loop, 'id_1', 'id_1,id_2')
+    keys_with_underscore_case(storage)
+
+
+def test_update_item(io_loop):
+    storage = MPinStorage(io_loop, 'id1')
+    update_item_case(storage)
diff --git a/lib/storage/tests/test_redis_storage.py b/lib/storage/tests/test_redis_storage.py
new file mode 100644
index 0000000..416abbf
--- /dev/null
+++ b/lib/storage/tests/test_redis_storage.py
@@ -0,0 +1,86 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import division, absolute_import, print_function, unicode_literals
+
+import fakeredis
+
+from storage.backends.redis import MPinStorage
+from storage.tests.cases import (
+    simplekey_index_case,
+    multi_indexes_case,
+    multikey_indexes,
+    mixkey_indexes_case,
+    missing_key_indexes_case,
+    valid_string_date_case,
+    invalid_string_date_case,
+    no_date_case,
+    item_deletion_case,
+    keys_with_underscore_case,
+    update_item_case,
+)
+
+
+def get_storage(*args, **kwargs):
+    storage = MPinStorage(*args, **kwargs)
+    storage.redis.redis = fakeredis.FakeStrictRedis()
+    storage.redis.redis.flushdb()
+    return storage
+
+
+def test_simplekey_index(io_loop):
+    simplekey_index_case(get_storage(io_loop, 'id1'))
+
+
+def test_multi_indexes(io_loop):
+    multi_indexes_case(get_storage(io_loop, 'id1', 'id2'))
+
+
+def test_multikey_indexes(io_loop):
+    multikey_indexes(get_storage(io_loop, 'id1,id2'))
+
+
+def test_mixkey_indexes(io_loop):
+    mixkey_indexes_case(get_storage(io_loop, 'id1', 'id1,id2'))
+
+
+def test_missing_key_indexes(io_loop):
+    missing_key_indexes_case(get_storage(io_loop, 'id1', 'id2'))
+
+
+def test_valid_string_date(io_loop):
+    valid_string_date_case(get_storage(io_loop, 'id1', 'id2'))
+
+
+def test_invalid_string_date(io_loop):
+    invalid_string_date_case(get_storage(io_loop, 'id1', 'id2'))
+
+
+def test_no_date(io_loop):
+    no_date_case(get_storage(io_loop, 'id1', 'id2'))
+
+
+def test_item_deletion(io_loop):
+    item_deletion_case(get_storage(io_loop, 'id1', 'id2'))
+
+
+def test_keys_with_underscore(io_loop):
+    keys_with_underscore_case(get_storage(io_loop, 'id_1', 'id_1,id_2'))
+
+
+def test_update_item(io_loop):
+    update_item_case(get_storage(io_loop, 'id1'))
diff --git a/requirements/common.txt b/requirements/common.txt
new file mode 100644
index 0000000..b9a0df3
--- /dev/null
+++ b/requirements/common.txt
@@ -0,0 +1,5 @@
+cffi==0.8.6
+pbkdf2==1.3
+python-dateutil==2.4.2
+redis==2.10.3
+tornado==4.1
diff --git a/requirements/dev.txt b/requirements/dev.txt
new file mode 100644
index 0000000..4f4c6b7
--- /dev/null
+++ b/requirements/dev.txt
@@ -0,0 +1,11 @@
+-r common.txt
+
+diff-cover==0.8.6
+fakeredis==0.6.2
+freezegun==0.3.5
+pytest-cov==2.2.0
+pytest-flakes==1.0.1
+pytest-pep8==1.0.6
+pytest-tornado==0.4.4
+pytest==2.8.5
+tox==2.3.1
diff --git a/runtests.sh b/runtests.sh
new file mode 100755
index 0000000..3096f5c
--- /dev/null
+++ b/runtests.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+DIRECTORY="${1:-.}"
+
+py.test \
+    --cov lib \
+    --cov servers \
+    --cov-report term-missing \
+    --cov-report xml \
+    --no-cov-on-fail \
+    --flakes \
+    --pep8 \
+    "$DIRECTORY" || exit 1
+
+diff-cover --compare-branch master coverage.xml
diff --git a/scripts/change_ms_backup_passphrase.py b/scripts/change_ms_backup_passphrase.py
new file mode 100755
index 0000000..6cea99f
--- /dev/null
+++ b/scripts/change_ms_backup_passphrase.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Decrypt master secret backup and crypt it back with new passphrase."""
+from __future__ import division, absolute_import, print_function, unicode_literals
+
+import os
+from datetime import datetime
+from getpass import getpass
+
+from mpin_utils import secrets
+from mpin_utils.common import Seed
+
+DEFAULT_BACKUP_FILE = './backup.json'
+DEFAULT_ENTROPY_SOURCES = 'dev_urandom:100'
+
+
+def main():
+    """Main function."""
+    while True:
+        backup_file = raw_input('Path to M-Pin master secret backup file (Default: {0}): '.format(DEFAULT_BACKUP_FILE)) or DEFAULT_BACKUP_FILE
+        if not os.path.exists(backup_file):
+            print('No such file {0}'.format(backup_file))
+        else:
+            break
+
+    old_passphrase = getpass('Passphrase for {0}: '.format(backup_file))
+    salt = raw_input('SALT used for old encryption:') or ''
+    entropy_sources = raw_input('Entropy sources (Default: {0}): '.format(DEFAULT_ENTROPY_SOURCES)) or DEFAULT_ENTROPY_SOURCES
+
+    Seed.getSeed(entropy_sources)
+    seed = Seed.seedValue
+
+    secrets_obj = secrets.Secrets(
+        old_passphrase, salt, seed, datetime.now(), backup_file, True)
+
+    while True:
+        new_passphrase1 = getpass("Please enter passphrase: ")
+        new_passphrase2 = getpass("Please enter passphrase (again): ")
+
+        if new_passphrase1 == new_passphrase2:
+            new_passphrase = new_passphrase1
+            break
+        else:
+            print('Passphrases don\'t match')
+
+    secrets.backup_master_secret(
+        secrets_obj.master_secret, True, new_passphrase, salt, backup_file,
+        secrets_obj.start_time, secrets_obj.rng)
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/getCommunityCredentials.py b/scripts/getCommunityCredentials.py
new file mode 100755
index 0000000..6d80a8e
--- /dev/null
+++ b/scripts/getCommunityCredentials.py
@@ -0,0 +1,185 @@
+#! /usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import datetime
+import hashlib
+import hmac
+import json
+import os
+import sys
+import urllib2
+
+KEYS_ENDPOINT = "https://register.certivox.net/api/v3/platform/core"
+INSTALLER_KEY = "b4e62b5ae100dd51dfc0d910579b3311"
+DEFAULT_INSTALL_PATH = "/opt/mpin"
+INSTALLATION_TYPE = "mpincore"
+
+
+class Console(object):
+    BLUE = '\033[94m'
+    GREEN = '\033[92m'
+    YELLOW = '\033[93m'
+    RED = '\033[91m'
+    CLEAR = '\033[0m'
+
+    @staticmethod
+    def initConsole():
+        # Check for console colors support
+        if not sys.stdout.isatty():
+            Console.BLUE, Console.GREEN, Console.YELLOW, Console.RED, Console.CLEAR = ["" for _ in range(5)]
+
+    @staticmethod
+    def out(rawtext):
+        sys.stdout.write(rawtext)
+        sys.stdout.flush()
+
+    @staticmethod
+    def text(text, newLine=True):
+        sys.stdout.write("{0}{1}{2}".format(Console.CLEAR, text, newLine and "\n" or " "))
+        sys.stdout.flush()
+
+    @staticmethod
+    def info(text, newLine=True):
+        sys.stdout.write("{0}{1}{2}{3}".format(Console.GREEN, text, Console.CLEAR, newLine and "\n" or " "))
+        sys.stdout.flush()
+
+    @staticmethod
+    def blue(text, newLine=True):
+        sys.stdout.write("{0}{1}{2}{3}".format(Console.BLUE, text, Console.CLEAR, newLine and "\n" or " "))
+        sys.stdout.flush()
+
+    @staticmethod
+    def error(text="ERROR", newLine=True):
+        sys.stdout.write("{0}{1}{2}{3}".format(Console.RED, text, Console.CLEAR, newLine and "\n" or " "))
+        sys.stdout.flush()
+
+    @staticmethod
+    def fatal(text, prevLineStrip=False):
+        if prevLineStrip:
+            Console.error()
+        Console.error("*** {0}\n".format(text))
+        sys.exit(1)
+
+    @staticmethod
+    def done(text="Done", newLine=True):
+        sys.stdout.write("{0}{1}{2}".format(Console.CLEAR, text, newLine and "\n" or " "))
+        sys.stdout.flush()
+
+    @staticmethod
+    def ok(text="Ok", newLine=True):
+        Console.done(text)
+        sys.stdout.flush()
+
+    @staticmethod
+    def getInput(text, validateFunc=None, errorMessage=None):
+        while True:
+            try:
+                r_input = raw_input
+            except NameError:
+                r_input = input
+
+            try:
+                res = r_input("{0}{1}: ".format(Console.CLEAR, text))
+            except KeyboardInterrupt:
+                Console.fatal("Installation interrupted.")
+
+            if not validateFunc:
+                break
+            else:
+                v = validateFunc(res)
+                if v:
+                    break
+                else:
+                    if errorMessage:
+                        Console.error(errorMessage)
+
+        return res
+
+
+def signMessage(message, key):
+    return hmac.new(key, message.encode('utf-8'), hashlib.sha256).hexdigest()
+
+
+def requestFreeKeys(user_contact):
+    method = "POST"
+    path = "getCommunityCredentials"
+    installation_type = INSTALLATION_TYPE
+    timestamp = datetime.datetime.utcnow().isoformat()
+    user_contact_hex = user_contact.encode("hex")
+    M = "{0}{1}{2}{3}{4}".format(method, path, installation_type, timestamp, user_contact_hex)
+    signature = signMessage(M, INSTALLER_KEY)
+
+    params = {
+        "method": method,
+        "path": path,
+        "installation_type": installation_type,
+        "timestamp": timestamp,
+        "user_contact_hex": user_contact_hex,
+        "signature": signature
+    }
+
+    try:
+        req = urllib2.Request(KEYS_ENDPOINT, data=json.dumps(params), headers={"Content-Type": "application/json"})
+        resp = urllib2.urlopen(req)
+        return True, json.dumps(json.loads(resp.read()), indent=4)
+    except urllib2.URLError, e:
+        try:
+            code = e.code
+        except:
+            code = ""
+        return False, "{0} {1}".format(code, e.reason)
+
+
+def getFreeKeys(installPath):
+
+    Console.text(
+        'You can enter an email address or a Twitter account to be contacted on by our support team for '
+        'full info on out to get the most value from M-Pin Core. ',
+        False
+    )
+    Console.text('To skip this step just hit enter, you can still contact us on support@miracl.com or on Twitter on @miraclhq\n')
+    user_contact = Console.getInput('Your email or Twitter account')
+
+    Console.text("\nGetting Community D-TA keys...", False)
+
+    ok, result = requestFreeKeys(user_contact)
+
+    if ok:
+        credentialsFile = os.path.join(installPath, "credentials.json")
+        try:
+            open(credentialsFile, "w").write(result)
+            Console.info("Done\n")
+        except Exception, e:
+            Console.error("Fail")
+            Console.error("Unable to write Community D-TA credentials file: {0}\n".format(e))
+            Console.text("Your credentials.json:")
+            Console.text(result)
+
+    else:
+        Console.error("Fail")
+        Console.error("Unable to get Community D-TA keys: {0}".format(result))
+
+
+if __name__ == "__main__":
+    if len(sys.argv) > 1:
+        installPath = sys.argv[1]
+    else:
+        installPath = DEFAULT_INSTALL_PATH
+
+    getFreeKeys(installPath)
diff --git a/scripts/getServerSecretShare.py b/scripts/getServerSecretShare.py
new file mode 100755
index 0000000..f50dc51
--- /dev/null
+++ b/scripts/getServerSecretShare.py
@@ -0,0 +1,127 @@
+#! /usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import unicode_literals
+
+import datetime
+import hashlib
+import hmac
+import json
+import sys
+import traceback
+import urllib
+import urllib2
+
+SIGNATURE_EXPIRES_SECONDS = 60
+
+
+class ScriptException(Exception):
+    pass
+
+
+def sign_message(message, key):
+    return hmac.new(key, message.encode('utf-8'), hashlib.sha256).hexdigest()
+
+
+def get_arguments():
+    if len(sys.argv) < 2:
+        raise ScriptException('credentials.json required')
+
+    if len(sys.argv) > 2:
+        raise ScriptException('Unexpected number of arguments')
+
+    return sys.argv[1]
+
+
+def get_credentials(credentials_json):
+    """ Parses the content of the file.
+
+    Will raise exception if any of the required keys are missing.
+    """
+    try:
+        with open(credentials_json, 'r') as credentials_file:
+            credentials = json.loads(credentials_file.read())
+    except IOError:
+        raise ScriptException('Invalid filename')
+    except ValueError:
+        print credentials_file.read()
+        raise ScriptException('Invalid json (invalid formating)')
+
+    for key in ['app_id', 'app_key', 'api_url']:
+        if key not in credentials:
+            raise ScriptException('Invalid json ({} key missing)'.format(key))
+
+    return credentials
+
+
+def get_expiration_time():
+    expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=SIGNATURE_EXPIRES_SECONDS)
+    expires_str = expires.isoformat().split(".")[0] + "Z"
+    return expires_str
+
+
+def get_server_secret(credentials, expires):
+    """ Fetch server secret from CertiVox server """
+    path = 'serverSecret'
+    params = urllib.urlencode({
+        'app_id': credentials['app_id'],
+        'expires': expires,
+        'signature': sign_message(
+            '{}{}{}'.format(path, credentials['app_id'], expires),
+            str(credentials['app_key'])
+        )
+    })
+
+    try:
+        response = urllib2.urlopen('{api_url}{end_point}?{params}'.format(
+            api_url=credentials['api_url'],
+            end_point=path,
+            params=params,
+        ))
+    except urllib2.HTTPError as e:
+        if e.code == 408:
+            print "Make sure your time it correct!"
+        raise ScriptException('Response code: {} - {}'.format(e.code, e.read()))
+
+    data = json.loads(response.read())
+    return data['serverSecret']
+
+
+def main():
+    credentials_filename = get_arguments()
+    credentials = get_credentials(credentials_filename)
+
+    # Make a request for CertiVox server secret share
+    server_secret = get_server_secret(credentials, get_expiration_time())
+
+    # Add to initial credential json and print to stdout
+    credentials['certivox_server_secret'] = server_secret
+    print json.dumps(credentials, indent=2)
+
+
+if __name__ == '__main__':
+    try:
+        main()
+    except ScriptException as e:
+        print(e)
+    except KeyboardInterrupt:
+        print "Shutdown requested...exiting"
+    except Exception:
+        traceback.print_exc(file=sys.stdout)
+    sys.exit(0)
diff --git a/servers/demo/__init__.py b/servers/demo/__init__.py
new file mode 100644
index 0000000..a6b3acf
--- /dev/null
+++ b/servers/demo/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License./
diff --git a/servers/demo/config_default.py b/servers/demo/config_default.py
new file mode 100644
index 0000000..0a7e4b0
--- /dev/null
+++ b/servers/demo/config_default.py
@@ -0,0 +1,69 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import unicode_literals
+
+"""HTTP server settings"""
+address = '0.0.0.0'
+port = 8005
+
+cookieSecret = '%COOKIESECRET%'
+
+"""CDN for mpin.js"""
+mpinJSURL = '%MPINJSURL%'
+
+"""RPS discovery options"""
+RPSURL = 'http://127.0.0.1:8011'
+# rpsPrefix = 'rps'  # Default
+clientSettingsURL = '/rps/clientSettings'
+verifyIdentityURL = '/mpinActivate'
+
+"""Key value storage options"""
+# storage = 'memory'
+
+# storage = 'redis'
+# redisHost = '127.0.0.1'  # Default
+# redisPort = 6379  # Default
+# redisDB = 0  # Default
+# redisPassword = None  # Default
+# redisPrefix = 'mpin'  # Default
+
+storage = 'json'
+fileStorageLocation = './mpin_demo_storage.json'
+
+"""Verification emails settings
+
+If forceActivate is True the demo site will activate new users without verifying them
+with email.
+"""
+forceActivate = True
+
+"""Email options"""
+emailSubject = 'New user activation'
+emailSender = ''
+smtpServer = ''
+smtpPort = 25
+smtpUser = ''
+smtpPassword = ''
+# smtpUseTLS = True
+
+"""Mobile app"""
+mobileAppPath = '%MOBILEAPPPATH%'
+mobileAppFullURL = '/m'
+
+"""Debug options"""
+# logLevel = "INFO"
diff --git a/servers/demo/demoservice.py b/servers/demo/demoservice.py
new file mode 100755
index 0000000..a4df34f
--- /dev/null
+++ b/servers/demo/demoservice.py
@@ -0,0 +1,29 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import os
+import sys
+
+if (os.name != "nt"):
+    print "This file can be run on Windows."
+    sys.exit(1)
+
+import mpinDemo
+from mpWinService import installService
+
+
+installService(mpinDemo.ServiceDaemon, 'mpinDemo', 'M-Pin Demo Application Service')
diff --git a/servers/demo/mailer.py b/servers/demo/mailer.py
new file mode 100755
index 0000000..1647c31
--- /dev/null
+++ b/servers/demo/mailer.py
@@ -0,0 +1,83 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Mailer module, send mail from a separate thread
+import smtplib
+import os
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+from threading import Thread
+from tornado import template
+
+smtpServer = ""
+smtpPort = 0
+senderAddress = ""
+useTLS = False
+
+
+# Render templates
+def render_template(template_name, **kwargs):
+    loader = template.Loader(os.path.dirname(os.path.realpath(__file__)) + "/templates")
+    t = loader.load(template_name)
+    return t.generate(**kwargs)
+
+
+# Initialize the mailer parameters
+def setup(smtpserver, smtpport, senderaddress, usetls):
+    global smtpServer, smtpPort, senderAddress, useTLS
+    smtpServer = smtpserver
+    smtpPort = smtpport
+    senderAddress = senderaddress
+    useTLS = usetls
+
+
+# The actual mail sending routine (which should be ran from sendActivationEmail() in a separate thread so that Tornado is not blocked)
+def mailerThread(recipientAddress, subject, deviceName, validationURL, user=None, password=None):
+    if not smtpServer:
+        return
+    msg = MIMEMultipart('alternative')
+    msg['Subject'] = subject
+    msg['From'] = senderAddress
+    msg['To'] = recipientAddress
+
+    # Text version (in case the mail client does not like the HTML part).
+    mailText = render_template('activation_email.txt', validationURL=validationURL)
+
+    # HTML version
+    mailHTML = render_template('activation_email.html', validationURL=validationURL)
+
+    mailPartText = MIMEText(mailText, 'plain')
+    mailPartHTML = MIMEText(mailHTML, 'html')
+    msg.attach(mailPartText)
+    msg.attach(mailPartHTML)
+
+    sender = smtplib.SMTP(smtpServer, smtpPort)
+
+    if useTLS:
+        sender.starttls()
+        sender.ehlo()
+
+    if user:
+        sender.login(user, password)
+
+    sender.sendmail(senderAddress, recipientAddress, msg.as_string())
+    sender.quit()
+
+
+def sendActivationEmail(recipientAddress, subject, deviceName, validationURL, user=None, password=None):
+    thread = Thread(target=mailerThread, args=(recipientAddress, subject, deviceName, validationURL, user, password))
+    thread.start()
diff --git a/servers/demo/mpinDemo.py b/servers/demo/mpinDemo.py
new file mode 100755
index 0000000..c3a3439
--- /dev/null
+++ b/servers/demo/mpinDemo.py
@@ -0,0 +1,702 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import division, absolute_import, print_function, unicode_literals
+
+import datetime
+import json
+import os
+import sys
+import time
+import uuid
+from urlparse import urlparse
+
+import tornado.autoreload
+import tornado.escape
+import tornado.gen
+import tornado.httpclient
+import tornado.httpserver
+import tornado.ioloop
+import tornado.options
+import tornado.web
+from tornado.log import app_log as log
+from tornado.options import define, options
+
+import mailer
+from mpin_utils.common import detectProxy, getLogLevel, Time
+from storage import get_storage_cls
+
+if os.name == "posix":
+    from mpDaemon import Daemon
+elif os.name == "nt":
+    from mpWinService import Service as Daemon
+else:
+    raise Exception("Unsupported platform: {0}".format(os.name))
+
+BASE_DIR = os.path.dirname(__file__)
+CONFIG_FILE = os.path.join(BASE_DIR, "config.py")
+
+
+# OPTIONS
+
+# general options
+define("configFile", default=os.path.join(BASE_DIR, "config.py"), type=unicode)
+define("address", default="127.0.0.1", type=unicode)
+define("port", default=8005, type=int)
+define("cookieSecret", type=unicode)
+define("resourcesBasePath", default=BASE_DIR, type=unicode)
+define("mpinJSURL", default="", type=unicode)
+
+# debugging options
+define("autoReload", default=False, type=bool)
+define("logLevel", default="ERROR", type=unicode)
+define("forceActivate", default=False, type=bool)
+
+# RPS service discovery options
+define("RPSURL", default="", type=unicode)
+define("rpsPrefix", default="rps", type=unicode)
+define("clientSettingsURL", default="", type=unicode)
+define("verifyIdentityURL", default="", type=unicode)
+
+# authentication options
+define("requestOTP", default=False, type=bool)
+
+# email options
+define("emailSubject", type=unicode)
+define("emailSender", type=unicode)
+define("smtpServer", type=unicode)
+define("smtpPort", default=25, type=int)
+define("smtpUser", type=unicode)
+define("smtpPassword", type=unicode)
+define("smtpUseTLS", default=False, type=bool)
+
+# mobile
+define("mobileOnly", default=False, type=bool)
+define("mobileSupport", default=True, type=bool)
+define("mobileAppPath", default="", type=unicode)
+define("mobileAppFullURL", default="", type=unicode)
+
+define("mobileUseNative", default=False, type=bool)
+define("mobileConfigURL", default=None, type=unicode)
+
+
+# UTILITIES
+class MobileLoginHandler(object):
+    def __init__(self):
+        self.waiters = dict()
+
+    def waitForLogin(self, callback, sessionId, logout=False):
+        sid = logout and ("logout;%s" % sessionId) or sessionId
+        log.debug("Adding callback for session {0}".format(sid))
+        self.waiters[sid] = callback
+
+    def cancelWait(self, sessionId, logout=False):
+        sid = logout and ("logout;%s" % sessionId) or sessionId
+        if self.waiters.get(sid):
+            del self.waiters[sid]
+
+    def userLogged(self, sessionId, logout=False):
+        sid = logout and ("logout;%s" % sessionId) or sessionId
+        log.debug("WAITING CALLBACK FOR: {0}".format(sid))
+
+        callback = self.waiters.get(sid)
+        if callback:
+            callback()
+            if self.waiters.get(sid):
+                del self.waiters[sid]
+            return True
+        else:
+            log.debug("No Callback!")
+
+            return False
+mobileLoginHandler = MobileLoginHandler()
+
+
+def generateSessionID():
+    return uuid.uuid4().hex + uuid.uuid1().hex
+
+
+# BASE HANDLERS
+class BaseHandler(tornado.web.RequestHandler):
+    def set_default_headers(self):
+        self.set_header("Access-Control-Allow-Origin", "*")
+        self.set_header("Access-Control-Allow-Credentials", "true")
+        self.set_header("Access-Control-Allow-Methods", "GET,POST,HEAD,OPTIONS")
+        self.set_header("Access-Control-Allow-Headers", "Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, X-Requested-By, If-Modified-Since, X-File-Name, Cache-Control, Pragma, Expires, WWW-Authenticate")
+        # self.set_header("Connection", "keep-alive")
+
+        self.set_header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate")
+        self.set_header("Pragma", "no-cache")
+        self.set_header("Expires", "Sat, 26 Jul 1997 05:00:00 GMT")
+
+    def prepare(self):
+        self.sessionId = self.get_secure_cookie("mpindemo_session")
+
+        if not self.sessionId:
+            self.sessionId = generateSessionID()
+            log.debug("Generated new sessionId: {0}".format(self.sessionId))
+            self.set_secure_cookie("mpindemo_session", self.sessionId)
+            self.loggedUser = ""
+        else:
+            item = self.storage.find(key="s;{0}".format(self.sessionId))
+            self.loggedUser = item.value if item else None
+
+    def get_flash(self):
+        flash = self.get_secure_cookie("flash")
+        if flash:
+            self.clear_cookie("flash")
+        return flash
+
+    def set_flash(self, message):
+        self.set_secure_cookie("flash", message)
+
+    @property
+    def storage(self):
+        return self.application.storage
+
+    def finish(self, *args, **kwargs):
+        if self._status_code == 401:
+            log.error('asdasdasd')
+            self.set_header("WWW-Authenticate", "Authenticate")
+        super(BaseHandler, self).finish(*args, **kwargs)
+
+
+# APPLICATION HANDLERS
+class RPSRedirectHandler(BaseHandler):
+    SUPPORTED_METHODS = ['GET', 'POST', 'PUT', 'OPTIONS']
+
+    @tornado.web.asynchronous
+    @tornado.gen.engine
+    def _processRequest(self, path):
+
+        url = "{0}/{1}".format(options.RPSURL.rstrip("/"), self.request.uri.strip("/"))
+        method = self.request.method
+        headers = self.request.headers
+        data = self.request.body
+
+        if (not data) and (method in ["GET", "OPTIONS"]):
+            data = None
+
+        client = tornado.httpclient.AsyncHTTPClient()
+
+        response = yield tornado.gen.Task(client.fetch, url, method=method, headers=headers, body=data)
+
+        for h in response.headers:
+            hV = response.headers.get(h)
+            if hV:
+                self.set_header(h, hV)
+
+        if (response.error):
+            if response.code > 500:
+                errorCode = 500
+            else:
+                errorCode = response.code
+            self.set_status(errorCode)
+        else:
+            self.set_status(response.code)
+
+        if response.body:
+            self.write(response.body)
+        self.finish()
+
+    def get(self, path):
+        return self._processRequest(path)
+
+    def post(self, path):
+        return self._processRequest(path)
+
+    def put(self, path):
+        return self._processRequest(path)
+
+    def options(self, path):
+        return self._processRequest(path)
+
+
+class IndexHandler(BaseHandler):
+
+    def get(self):
+        mobileAppURL = options.mobileAppFullURL
+
+        if not mobileAppURL.startswith("http"):
+            pr = urlparse(self.request.full_url())
+            mobileAppURL = "{0}://{1}{2}".format(pr.scheme, pr.netloc, mobileAppURL or "/m")
+
+        params = {
+            "clientSettingsURL": options.clientSettingsURL,
+            "mobileAppFullURL": mobileAppURL,
+            "mpinJSURL": options.mpinJSURL,
+            "logoutWaitURL": "/logoutWait",
+            "user": self.loggedUser,
+            "mobileOnly": options.mobileOnly and "true" or "false",
+            "mobileSupport": options.mobileSupport and "true" or "false",
+            "mobileUseNative": options.mobileUseNative,
+            "mobileConfigURL": options.mobileConfigURL
+            # "emailCheckRegex": "[0-9a-zA-Z]+"
+        }
+
+        templateName = self.request.path == "/login" and "login.html" or "index.html"
+
+        self.render(templateName, flash=self.get_flash(), **params)
+
+
+class VerifyUserHandler(BaseHandler):
+
+    def _generateValidationURL(self, base_url, identity, signature, expires):
+        return "{0}?i={1}&e={2}&s={3}".format(base_url, identity, expires, signature)
+
+    def post(self):
+        self.content_type = 'application/json'
+
+        try:
+            data = json.loads(self.request.body)
+            identity = data["mpinId"]
+            userid = data["userId"]
+            expireTime = data["expireTime"]
+            activateKey = data["activateKey"]
+            mobile = data["mobile"]
+        except ValueError:
+            log.error("Cannot decode body as JSON.")
+            log.debug(self.request.body)
+            self.set_status(400, reason="BAD REQUEST. INVALID JSON")
+            self.finish()
+            return
+
+        userId = data.get("userId")
+        if not userId:
+            log.error("Missing userId")
+            log.debug(self.request.body)
+            self.set_status(400, reason="BAD REQUEST. INVALID USER ID")
+            self.finish()
+            return
+
+        if options.verifyIdentityURL.startswith("/"):  # relative path
+            base_url = "{0}/{1}".format(
+                self.request.headers.get("RPS-BASE-URL").rstrip("/"),
+                options.verifyIdentityURL.lstrip("/")
+            )
+        else:
+            base_url = options.verifyIdentityURL
+
+        validateURL = self._generateValidationURL(base_url, identity, activateKey, expireTime)
+        log.info("Sending activation email for user {0}: {1}".format(userid.encode("utf-8"), validateURL))
+
+        deviceName = mobile and "Mobile" or "PC"
+
+        if options.forceActivate:
+            log.warning("forceActivate option set! User activated without verification!")
+        else:
+            mailer.sendActivationEmail(userid.encode("utf-8"), options.emailSubject, deviceName, validateURL, options.smtpUser, options.smtpPassword)
+            log.warning("Sending Mail!")
+
+        responseData = {
+            "forceActivate": options.forceActivate
+        }
+        self.write(json.dumps(responseData))
+
+        self.set_status(200)
+        self.finish()
+
+
+class mpinPermitUserHandler(BaseHandler):
+
+    def get(self):
+
+        # The revocation handler
+        # When the RPS option RPAPermitUserURL is set
+        # It will make a request for validating the identity
+        # Before giving the time permit share to the client
+
+        self.content_type = 'application/json'
+
+        # If you return 403 it will show Unauthoirized message inside the PinPad
+        # self.set_status(403)
+
+        self.set_status(200)  # We give permissions to everyone
+        self.finish()
+
+
+class mpinActivateHandler(BaseHandler):
+    def _verifySignature(self):
+        identity = self.get_argument("i", default="")
+        expires = self.get_argument("e", default="")
+        signature = self.get_argument("s", default="")
+
+        log.debug("/mpinActivate request for identity: {0}".format(identity))
+
+        try:
+            data = json.loads(identity.decode("hex"))
+            userid = data["userID"]
+            issued = data["issued"]
+            sIssued = Time.DateTimetoHuman(Time.ISOtoDateTime(issued))
+
+            mobile = int(data.get("mobile") or 0)
+
+        except Exception as E:
+            log.error("Error parsing the verification email: {0}".format(E))
+            userid, issued, sIssued = None, None, None
+
+        if userid:
+            if expires < datetime.datetime.utcnow().isoformat(b"T").split(".")[0] + "Z":
+                isValid = False
+                info = "Link expired"
+            else:
+                isValid = True
+                info = ""
+
+            deviceName = mobile and "Mobile" or "PC"
+
+        else:
+            log.error("/mpinActivate: Invalid IDENTITY: {0}".format(identity))
+            isValid, info = False, "Invalid identity"
+            deviceName, issued = "", ""
+
+        params = {
+            "isValid": isValid,
+            "identity": identity,
+            "errorMessage": info,
+            "userid": userid,
+            "issued": issued,
+            "humanIssued": sIssued,
+            "activated": False,
+            "deviceName": deviceName,
+            "activateKey": signature
+        }
+
+        return params
+
+    def get(self):
+        params = self._verifySignature()
+        self.render("activate.html", **params)
+
+    @tornado.web.asynchronous
+    @tornado.gen.engine
+    def post(self):
+        params = self._verifySignature()
+
+        if params['isValid']:
+            mpinId = params["identity"]
+
+            url = "{0}/user/{1}".format(options.RPSURL.rstrip("/"), mpinId)
+
+            data = json.dumps({"activateKey": params["activateKey"]})
+            client = tornado.httpclient.AsyncHTTPClient()
+            response = yield tornado.gen.Task(client.fetch, url, method="POST", body=data)
+
+            if (response.error):
+                log.error("URL: {0}: Error: {1}".format(url, response.error))
+
+                params["isValid"] = False
+                params["errorMessage"] = "This verification link has already been used or has expired!"
+            else:
+                params["activated"] = True
+
+        self.render("activate.html", **params)
+
+
+class AuthenticateUserHandler(BaseHandler):
+
+    @tornado.web.asynchronous
+    @tornado.gen.engine
+    def post(self):
+        self.content_type = 'application/json'
+
+        try:
+            data = json.loads(self.request.body)
+            authOTT = data["mpinResponse"]["authOTT"]
+        except ValueError:
+            log.error("Cannot decode body as JSON.")
+            log.debug(self.request.body)
+            self.set_status(400, reason="BAD REQUEST. INVALID JSON")
+            self.finish()
+            return
+        except KeyError:
+            log.error("Invalid JSON structure.")
+            log.debug(self.request.body)
+            self.set_status(400, reason="BAD REQUEST. INVALID JSON")
+            self.finish()
+            return
+
+        url = "{0}/authenticate".format(options.RPSURL.rstrip("/"))
+        client = tornado.httpclient.AsyncHTTPClient()
+        reqData = {
+            "authOTT": authOTT,
+
+            "logoutData": {"sessionToken": self.sessionId}
+        }
+        response = yield tornado.gen.Task(client.fetch, url, method="POST", body=json.dumps(reqData))
+
+        try:
+            data = json.loads(response.body)
+            status = data["status"]
+            userId = data["userId"]
+            message = data["message"]
+        except:
+            log.error("Invalid data from RPS: {1}: {0}".format(url, response.body))
+            message = "Server error"
+            status = 500
+
+        if (status == 200):
+            # The Revocation check based on userId or mpinId can be performed here
+            # The new status can be
+            # 200 - Login successfull
+            # 401 - Invalid PIN
+            # 403 - User not authorized. Login denied without deleting the client's token.
+            # 408 - The authentication has been expired.
+            # 410 - Login denied permanently. Will delete the client's token.
+
+            # If the RPS waitLoginResult option is set, /loginResult request must be made
+            # It can contain logoutData and logoutURL for mobile Logout functionality
+
+            url = "{0}/loginResult".format(options.RPSURL.rstrip("/"))
+            client = tornado.httpclient.AsyncHTTPClient()
+            reqData = {
+                "authOTT": authOTT,
+                "status": status,
+                "message": message,
+                "logoutData": {"sessionToken": self.sessionId, "userId": userId}
+            }
+            response = yield tornado.gen.Task(client.fetch, url, method="POST", body=json.dumps(reqData))
+
+            if status == 200:
+                # Login user. Update session information
+                session = self.sessionId
+                if session:
+                    key = "s;{0}".format(session)
+                    item = self.storage.find(key=key)
+                    if item:
+                        item.update(value=userId)
+                    else:
+                        self.storage.add(
+                            key=key,
+                            value=userId,
+                            expire_time=datetime.datetime.now() + datetime.timedelta(seconds=3600)
+                        )
+
+        self.set_status(status, message)
+        # Optional data can be send to the client's javascript handler.
+        if options.requestOTP:
+            ttlSeconds = 60
+            nowtm = int(time.mktime(datetime.datetime.utcnow().timetuple()) * 1000)
+            returnData = {
+                "expireTime": nowtm + ttlSeconds * 1000,
+                "ttlSeconds": ttlSeconds,
+                "nowTime": nowtm
+            }
+        else:
+            returnData = {"someUserData": "This will be handled by onSuccessLogin handler."}
+
+        self.write(returnData)
+        self.finish()
+
+
+class AboutHandler(BaseHandler):
+    def get(self):
+        self.redirect("http://www.miracl.com/miracl-product-m-pin-core")
+
+
+class ProtectedHandler(BaseHandler):
+    def get(self, protected_page=None):
+        if not self.loggedUser:
+            self.set_flash("protected")
+            self.redirect("/")
+        else:
+            template_name = "protected_{}.html".format(protected_page) if protected_page else "protected.html"
+            self.render(template_name, welcome=(self.get_flash() == "login"), user=self.loggedUser, logoutWaitURL="/logoutWait")
+
+
+class LogoutHandler(BaseHandler):
+    def get(self):
+        item = self.storage.find(key="s;{0}".format(self.sessionId))
+        if item:
+            item.delete()
+        self.clear_cookie("mpindemo_session")
+        self.redirect("/")
+
+    def post(self):
+        data = tornado.escape.json_decode(self.request.body)
+        sessionId = data.get("sessionToken")
+
+        log.debug("Logout request. Session token: {0}".format(sessionId))
+
+        item = self.storage.find(key="s;{0}".format(sessionId))
+        loggedUser = item.value if item else None
+
+        if (data.get("userId") != loggedUser):
+            log.debug(" The logged user {0} does not match the requested user {1}".format(loggedUser, data.get("userId")))
+            self.set_status(400, "Logout failed")
+            return
+
+        mobileLoginHandler.userLogged(sessionId, True)
+        item = self.storage.find(key="s;{0}".format(sessionId))
+        if item:
+            item.delete()
+
+    def options(self):
+        self.set_status(200)
+        self.finish()
+
+
+class LogoutWaitHandler(BaseHandler):
+    @tornado.web.asynchronous
+    def get(self):
+        '''Wait for user logout'''
+        mobileLoginHandler.waitForLogin(self.onUserLoggedOut, self.sessionId, True)
+
+    def onUserLoggedOut(self):
+        if self.request.connection.stream.closed():
+            return
+        self.set_flash("forced_logout")
+        self.write("OK")
+        self.finish()
+
+    def on_connection_close(self):
+        mobileLoginHandler.cancelWait(self.sessionId, True)
+
+
+class ServeMobileFileHandler(tornado.web.StaticFileHandler):
+    def set_extra_headers(self, path):
+        if path == "mpin.appcache":
+            self.set_header("Content-Type", "text/cache-manifest")
+
+
+class NotFoundHandler(BaseHandler):
+    def get(self, *args):
+        self.render("404.html")
+
+
+# MAIN
+class Application(tornado.web.Application):
+    def __init__(self):
+        staticPath = os.path.join(options.resourcesBasePath, "public")
+        templatesPath = os.path.join(options.resourcesBasePath, "templates")
+        mobilePath = options.mobileAppPath
+
+        if mobilePath:
+            handlers = [
+                (r'/m', tornado.web.RedirectHandler, {"url": "/m/index.html"}),
+                (r'/m/', tornado.web.RedirectHandler, {"url": "/m/index.html"}),
+                (r'/m/(.*)', ServeMobileFileHandler, {'path': mobilePath}),
+            ]
+        else:
+            handlers = []
+
+        handlers.extend([
+            (r"/", IndexHandler),
+            (r"/login", IndexHandler),
+
+            # M-PIN handlers
+            (r"/{0}/(.*)".format(options.rpsPrefix), RPSRedirectHandler),
+            (r"/mpinVerify", VerifyUserHandler),
+            (r"/mpinAuthenticate", AuthenticateUserHandler),
+            (r"/mpinActivate", mpinActivateHandler),
+            (r"/mpinPermitUser", mpinPermitUserHandler),
+
+            # Application handlers
+            (r"/protected/(.*)", ProtectedHandler),
+            (r"/protected", ProtectedHandler),
+            (r"/about", AboutHandler),
+            (r"/logout", LogoutHandler),
+            (r"/logoutWait", LogoutWaitHandler),
+
+        ])
+
+        if os.path.exists(os.path.join(templatesPath, "404.html")):
+            handlers.extend([(r"/(.*)", NotFoundHandler)])
+
+        settings = {
+            "template_path": templatesPath,
+            "static_path": staticPath,
+            "static_url_prefix": "/public/",
+            "cookie_secret": options.cookieSecret,
+            "xsrf_cookies": False
+        }
+
+        super(Application, self).__init__(handlers, **settings)
+
+        storage_cls = get_storage_cls()
+        self.storage = storage_cls(tornado.ioloop.IOLoop.instance(), 'key')
+
+
+def main():
+    options.parse_command_line()
+
+    if os.path.exists(options.configFile):
+        try:
+            options.parse_config_file(options.configFile)
+            options.parse_command_line()
+        except Exception, E:
+            print("Invalid config file {0}".format(options.configFile))
+            print(E)
+            sys.exit(1)
+
+    # Set Log level
+    log.setLevel(getLogLevel(options.logLevel))
+
+    if not options.cookieSecret:
+        log.error("cookieSecret option required")
+        sys.exit(1)
+
+    detectProxy()
+    mailer.setup(options.smtpServer, options.smtpPort, options.emailSender, options.smtpUseTLS)
+
+    log.info("Server starting on {0}:{1}...".format(options.address, options.port))
+
+    http_server = Application()
+    http_server.listen(options.port, options.address, xheaders=True)
+    io_loop = tornado.ioloop.IOLoop.instance()
+
+    if options.autoReload:
+        log.debug("Starting autoreloader")
+
+        tornado.autoreload.watch(CONFIG_FILE)
+        for f in os.listdir(http_server.settings["template_path"]):
+            fn = os.path.join(http_server.settings["template_path"], f)
+            if os.path.isfile(fn):
+                tornado.autoreload.watch(fn)
+        tornado.autoreload.start(io_loop)
+
+    log.info("Server started. Listening on {0}:{1}".format(options.address, options.port))
+    io_loop.start()
+
+
+class ServiceDaemon(Daemon):
+    def run(self):
+        main()
+
+
+if __name__ == "__main__":
+    if len(sys.argv) > 1 and sys.argv[1].lower() in ("start", "stop"):
+        action = sys.argv.pop(1)
+        logFile = os.path.join(BASE_DIR, "mpinDemo.log")
+        pidFile = os.path.join(BASE_DIR, "mpinDemo.pid")
+
+        daemon = ServiceDaemon(pidfile=pidFile, stdout=logFile, stderr=logFile)
+        if action == "start":
+            log.info("Starting as daemon. Log file: {0}".format(logFile))
+            daemon.start()
+        elif action == "stop":
+            log.info("Stopping daemon...")
+            daemon.stop()
+            sys.exit()
+    else:
+        try:
+            main()
+        except Exception as e:
+            log.error(e)
+            sys.exit(1)
diff --git a/servers/demo/public/css/reset.css b/servers/demo/public/css/reset.css
new file mode 100644
index 0000000..905a8ce
--- /dev/null
+++ b/servers/demo/public/css/reset.css
@@ -0,0 +1,49 @@
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+margin: 0;
+padding: 0;
+border: 0;
+font-size: 100%;
+font: inherit;
+vertical-align: baseline;
+}
+
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+display: block;
+}
+
+body {
+line-height: 1;
+}
+
+ol, ul {
+list-style: none;
+}
+
+blockquote, q {
+quotes: none;
+}
+
+blockquote:before, blockquote:after,
+q:before, q:after {
+content: '';
+content: none;
+}
+
+table {
+border-collapse: collapse;
+border-spacing: 0;
+}
diff --git a/servers/demo/public/css/styles.css b/servers/demo/public/css/styles.css
new file mode 100644
index 0000000..663490c
--- /dev/null
+++ b/servers/demo/public/css/styles.css
@@ -0,0 +1,478 @@
+body {
+    font-family: 'Open Sans', Arial, Helvetica, sans-serif;
+    color: #36424a;
+    font-size: 1em;
+    overflow-y: scroll;
+}
+
+a {
+    color: #4891dc;
+    text-decoration: underline;
+}
+
+a:hover {
+    color: #f68428;
+}
+
+span.support {
+    width: 33.33%;
+    color: #3F4A52;
+    padding: 0px;
+    margin: 0px;
+    float: left;
+    font-size: 0.88em;
+    text-decoration: none;
+    padding: 10px 0px
+}
+
+.nav ul li span.support a {
+    width: auto;
+    float: none;
+    padding-left: 5px;
+    font-size: 1em;
+}
+
+.nav ul li span.support a:hover {
+    background-color: #ffffff
+}
+
+input {
+    height: 24px;
+    width: 200px;
+    margin: 0px 10px;
+    font-size: 16px;
+    vertical-align: middle;
+}
+
+#header {
+    padding: 10px 0px 10px;
+    border-bottom: 1px solid #CFCFCF;
+}
+
+.container {
+    max-width: 980px;
+    margin-left: auto;
+    margin-right: auto;
+}
+
+.logo1 {
+    width: 49.9%;
+    float: left;
+}
+
+.logo1 img {
+    margin: 5px 0px 0px;
+}
+
+.logo2 {
+    width: 49.9%;
+    text-align: right;
+    float: left;
+}
+
+.clear {
+    clear: both;
+}
+
+.nav {
+    border-bottom: 1px solid #CFCFCF;
+    text-align: center;
+}
+
+.nav ul {
+    padding: 0px;
+    margin: 0px;
+    width: 100%
+}
+
+.nav ul li {
+    display: inline;
+}
+
+.nav ul li a {
+    width: 33.33%;
+    color: #4891dc;
+    padding: 0px;
+    margin: 0px;
+    float: left;
+    font-size: 0.88em;
+    text-decoration: none;
+    padding: 10px 0px
+}
+
+.nav ul li a:hover {
+    color: #f68428;
+    background-color: #edf0ee;
+}
+
+.nav ul li a.active {
+    color: #f68428;
+}
+
+.content h1 {
+    color: #4891dc;
+    text-align: center;
+    font-size: 2.5em;
+}
+
+.content h2 {
+    color: #363C74;
+    margin-top: 0px
+}
+
+.content p {}
+
+.column {
+    float: left;
+    padding: 20px;
+}
+
+.one {
+    width: 93.5%;
+    margin: 10px;
+}
+
+.two {
+    width: 43%;
+    margin: 10px;
+}
+
+.imageList {
+    margin-top: 0px;
+    padding-bottom: 0px;
+    padding-top: 0px;
+}
+
+.imageList li {
+    text-align: left
+}
+
+.center {
+    text-align: center
+}
+
+.leftAlign {
+    text-align: left;
+}
+
+.marBot10 {
+    margin-bottom: 10px;
+}
+
+.marBot20 {
+    margin-bottom: 20px;
+}
+
+.secret {
+    background-color: #FFFFFF;
+    color: grey;
+    font-size: 10px;
+    padding: 3px;
+    width: 80%;
+    word-wrap: break-word;
+}
+
+.grey {
+    background-color: ##edf0ee;
+}
+
+.dkRed {
+    color: #900;
+}
+
+.dkPurple {
+    color: #63C;
+}
+
+.dkBlue {
+    color: #039;
+}
+
+.dkGreen {
+    color: #063;
+}
+
+.error {
+    color: red;
+    font-weight: bold;
+}
+
+.grey p {
+    color: #767676;
+    font-size: 0.88em;
+}
+
+.grey p label {
+    color: #36424a;
+}
+
+section {
+    margin-bottom: 10px;
+    text-align: left;
+}
+
+#footer {
+    text-align: center;
+    font-size: 0.6em;
+    color: #36424a;
+    border-top: 1px solid #CFCFCF;
+    padding-top: 10px;
+    margin-top: 15px;
+}
+
+.instruction {
+    border: 1px solid #767676;
+    float: left;
+    display: block;
+    background-color: white;
+    width: 230px;
+    margin: 60px 0px 0px 20px;
+    padding: 0px 15px 15px;
+}
+
+
+/*#Buttons */
+
+.button,
+button,
+input[type="submit"],
+input[type="reset"],
+input[type="button"] {
+    margin: 0 5px;
+    color: #fff;
+    background: #4891dc;
+    border: 0 none;
+    -moz-border-radius: 3px;
+    -webkit-border-radius: 3px;
+    border-radius: 3px;
+    color: #fff display: inline-block;
+    font-size: 11px;
+    font-weight: bold;
+    text-decoration: none;
+    cursor: pointer;
+    margin-bottom: 0px;
+    line-height: normal;
+    padding: 8px 10px;
+    font-family: "Open Sans", Helvetica, Arial, sans-serif;
+}
+
+.button:hover,
+button:hover,
+input[type="submit"]:hover,
+input[type="reset"]:hover,
+input[type="button"]:hover {
+    background: #f68428;
+    border: 0 none;
+}
+
+.button:active,
+button:active,
+input[type="submit"]:active,
+input[type="reset"]:active,
+input[type="button"]:active {
+    background: #f68428;
+    border: 0 none;
+}
+
+.button.full-width,
+button.full-width,
+input[type="submit"].full-width,
+input[type="reset"].full-width,
+input[type="button"].full-width {
+    width: 100%;
+    padding-left: 0 !important;
+    padding-right: 0 !important;
+    text-align: center;
+}
+
+
+/* Fix for odd Mozilla border & padding issues */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+    border: 0;
+    padding: 0;
+}
+
+.larger {
+    font-size: 1.5em;
+    padding: 15px 20px;
+    -moz-border-radius: 10px;
+    -webkit-border-radius: 10px;
+    border-radius: 10px;
+}
+
+
+/*css for hubspot js form */
+
+.content .page-header {
+    color: #2F2F2F;
+    float: none;
+    font-family: 'Open Sans', Tahoma, Geneva, sans-serif;
+    font-size: 1.5em;
+    letter-spacing: 0.1em;
+    margin: 0px auto 20px;
+    text-align: center;
+    text-transform: uppercase;
+    width: 980px;
+    border-top: 1px solid #CFCFCF;
+    padding-top: 20px;
+}
+
+.content .page-header h1 {
+    font-size: 44px;
+    line-height: 1.3em;
+    margin-bottom: 80px;
+    margin-top: 80px;
+    width: 470px;
+    color: #36424a;
+    font-family: 'Open Sans', Trebuchet MS, sans-serif;
+    font-weight: 300;
+    float: left;
+}
+
+.section-subheader {
+    padding-bottom: 20px;
+    padding-top: 20px;
+    font-size: 18px;
+    font-weight: 300;
+    line-height: 24px;
+    display: block;
+    text-align: center;
+    color: #36424a;
+    font-family: 'Open Sans', Trebuchet MS, sans-serif;
+}
+
+.blue-box-holder {
+    background-color: #6e8878;
+    color: #FFFFFF;
+    display: block;
+    float: left;
+    padding: 0px 23px 10px;
+}
+
+.blue-box-holder .hs-form label {
+    color: #FFFFFF;
+    float: left;
+    font-size: 0.9em;
+    font-weight: bold;
+    letter-spacing: normal;
+    margin: 20px 20px 5px;
+    text-align: left;
+    text-transform: none;
+    width: 280px;
+}
+
+.blue-box-holder .hs-form .input {
+    width: 300px;
+    float: left;
+    margin-left: 0px;
+    margin-top: 20px;
+}
+
+.blue-box-holder .hs-form .hs-button {
+    border: 1px solid #0F4059 !important;
+    padding: 8px 14px !important;
+    background-color: #6e8878;
+    color: #FFFFFF;
+    -moz-user-select: none !important;
+    background-color: #6e8878 !important;
+    background-image: -moz-linear-gradient(center top, #6e8878, #166086) !important;
+    border: 2px solid #0F4059 !important;
+    border-radius: 4px 4px 4px 4px !important;
+    box-shadow: 0 1px #25A0DF inset !important;
+    color: #FFFFFF !important;
+    cursor: pointer !important;
+    display: inline-block !important;
+    font-family: 'Open Sans', Trebuchet MS, sans-serif !important;
+    font-size: 16px !important;
+    font-weight: bold !important;
+    height: auto !important;
+    line-height: 24px !important;
+    padding: 4px 12px !important;
+    text-align: center !important;
+    text-decoration: none !important;
+    text-shadow: 0 -1px #092635 !important;
+    width: auto !important;
+    font-size: 20px !important;
+    padding: 15px 25px !important;
+    float: left;
+}
+
+.blue-box-holder .hs-form .hs-button:hover {
+    border: 1px solid #124C6B !important;
+    background-color: #218CC4 !important;
+    background-image: -moz-linear-gradient(center top, #218CC4, #166086) !important;
+    border: 2px solid #124C6B !important;
+    box-shadow: 0 1px #28ACF1 inset, 0 1px 8px rgba(0, 0, 0, 0.3) !important;
+    color: #FFFFFF !important;
+}
+
+.blue-box-holder .hs-form .hs_submit {
+    float: left;
+    margin: 0px;
+    padding: 0px;
+}
+
+.blue-box-holder .hs-form .smart-field {
+    display: block;
+    width: 640px;
+    float: left;
+}
+
+.blue-box-holder .hs-form .actions {
+    margin: 7px 0px;
+    padding: 0px;
+    float: left;
+    clear: both;
+}
+
+.blue-box-holder .hs-form label {
+    color: #FFFFFF;
+    float: left;
+    font-size: 0.8em;
+}
+
+
+/* max-width */
+
+@media screen and (max-width: 480px) {
+    .logo1 {
+        display: none;
+    }
+    .logo2 {
+        text-align: center;
+        width: 100%
+    }
+    .nav ul li a {
+        width: 100%;
+        border-bottom: 1px solid #CFCFCF;
+    }
+    .nav {
+        border-bottom: 0px
+    }
+    .column {
+        float: none;
+    }
+    .two {
+        width: 95%;
+        padding: 10px 0px;
+    }
+}
+
+#loggedInHolder {
+    color: #36424a;
+    background-color: #edf0ee;
+    border-bottom: 1px solid #CFCFCF;
+    margin: 0px;
+    padding: 0px;
+}
+
+#loggedInHolder .loggedInStatus {
+    width: 980px;
+    margin-left: auto;
+    margin-right: auto;
+    text-align: right;
+    font-size: 0.75em;
+    padding: 7px 2px
+}
diff --git a/servers/demo/public/images/blank-logo.svg b/servers/demo/public/images/blank-logo.svg
new file mode 100644
index 0000000..7e6d2cb
--- /dev/null
+++ b/servers/demo/public/images/blank-logo.svg
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+<svg
+   xmlns="http://www.w3.org/2000/svg"
+   width="1000"
+   height="600"
+   viewBox="0 0 5 3">
+
+  <defs>
+    <clipPath id="a">
+      <rect
+         width="5"
+         height="3" />
+    </clipPath>
+  </defs>
+  
+  <rect
+     width="5"
+     height="3"
+     fill="#fff" />
+  <path
+     d="M 0,0 L 5,3 M 0,3 L 5,0"
+     stroke="#ccc"
+     stroke-width=".3"
+     clip-path="url(#a)" />
+</svg>
diff --git a/servers/demo/public/images/favicon.ico b/servers/demo/public/images/favicon.ico
new file mode 100644
index 0000000..3f5c556
--- /dev/null
+++ b/servers/demo/public/images/favicon.ico
Binary files differ
diff --git a/servers/demo/public/images/landing-page.png b/servers/demo/public/images/landing-page.png
new file mode 100644
index 0000000..216748b
--- /dev/null
+++ b/servers/demo/public/images/landing-page.png
Binary files differ
diff --git a/servers/demo/public/images/milagro-logo.svg b/servers/demo/public/images/milagro-logo.svg
new file mode 100644
index 0000000..2e5fded
--- /dev/null
+++ b/servers/demo/public/images/milagro-logo.svg
@@ -0,0 +1,40 @@
+<svg version="1.1"
+	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
+	 x="0px" y="0px" width="581.8px" height="117px" viewBox="0 0 581.8 117" style="enable-background:new 0 0 581.8 117;"
+	 xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#3E454C;}
+	.st1{fill:#C2FF17;}
+	.st2{fill:#31BCA4;}
+	.st3{fill:#40D37B;}
+</style>
+<defs>
+</defs>
+<g>
+	<path class="st0" d="M179.1,95h-6.9L148,34.1V95h-9.9V22.9h15.7l21.9,56.3L198,22.9h15.7V95h-9.9V34.1L179.1,95z"/>
+	<path class="st0" d="M230.6,95V22.9h9.9V95H230.6z"/>
+	<path class="st0" d="M257.5,95V22.9h9.9v63.2h31.3V95H257.5z"/>
+	<path class="st0" d="M354,95l-5.7-15.3h-30.7L312.1,95h-10.8l27.9-72.1h8.7L365.4,95H354z M321,70.8h24.2l-12.1-32.9H333L321,70.8z
+		"/>
+	<path class="st0" d="M408.6,63.9V55h28.2v27.4c-6.9,8.8-17.7,13.8-29.6,13.8c-22,0-38.7-14.9-38.7-37.3c0-21.3,17.4-37.3,38.7-37.3
+		c11.1,0,21,4.9,27.9,12.8l-7,5.8c-5-5.7-12.7-9.7-20.9-9.7C391,30.5,379,42.5,379,58.9c0,17.5,11.7,28.4,28.2,28.4
+		c7.6,0,14.6-2.7,19.7-7.5v-16H408.6z"/>
+	<path class="st0" d="M491.2,95l-21.6-30.5h-7.4V95h-9.9V22.9h21.4c11,0,24.6,5.8,24.6,20.7c0,12.3-8.8,18.4-17.6,20.3L503.6,95
+		H491.2z M473.2,31.8h-11v23.9h9c9.6,0,16.7-3.5,16.7-11.7C487.8,36.6,481.9,31.8,473.2,31.8z"/>
+	<path class="st0" d="M544.8,96.2c-21,0-37.1-16.7-37.1-37.3c0-20.6,16.1-37.3,37.1-37.3c21,0,37.1,16.7,37.1,37.3
+		C581.8,79.5,565.8,96.2,544.8,96.2z M544.8,30.5c-15.3,0-26.6,12.5-26.6,28.4s11.2,28.4,26.6,28.4s26.6-12.5,26.6-28.4
+		S560.1,30.5,544.8,30.5z"/>
+</g>
+<g>
+	<path class="st1" d="M85.9,105.4L1.8,66.2C0.7,65.7,0,64.6,0,63.4v-49c0-2.3,2.4-3.8,4.4-2.8l84.1,39.2c1.1,0.5,1.8,1.6,1.8,2.8v49
+		C90.3,104.9,88,106.4,85.9,105.4z"/>
+	<g>
+		<path class="st2" d="M88.8,52.3L0,93.7l0,20.2c0,2.3,2.4,3.8,4.4,2.8l84.1-39.2c1.1-0.5,1.8-1.6,1.8-2.8V53.6
+			c0-0.7-0.2-1.3-0.5-1.8l0,0l0,0c0,0,0,0,0,0l0,0c-0.2-0.4-0.6-0.6-1-0.9c0.2,0.2,0.3,0.4,0.3,0.7C89.1,51.9,89,52.1,88.8,52.3z"/>
+	</g>
+	<g>
+		<path class="st3" d="M1.6,64.7l88.8-41.4V3.1c0-2.3-2.4-3.8-4.4-2.8L1.8,39.5C0.7,40,0,41.1,0,42.3v21.1c0,0.7,0.2,1.3,0.5,1.8
+			l0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c0.2,0.4,0.6,0.6,1,0.9c-0.2-0.2-0.3-0.4-0.3-0.7C1.2,65.1,1.3,64.9,1.6,64.7z"/>
+	</g>
+</g>
+</svg>
diff --git a/servers/demo/templates/activate.html b/servers/demo/templates/activate.html
new file mode 100644
index 0000000..b19dfd2
--- /dev/null
+++ b/servers/demo/templates/activate.html
@@ -0,0 +1,33 @@
+{% extends "base.html" %}
+{% block extra-css %}
+    <style>
+        .button, button, input[type="submit"], input[type="reset"], input[type="button"] { font-size: 15px;}
+    </style>
+{% end %}
+
+{% block content %}
+    <h1>Identity activation</h1>
+
+    {% if not isValid %}
+        <p class="center">Cannot validate your identity! Reason: <label class="error">{{errorMessage}}</label></p>
+    {% elif activated %}
+        <div class="one column center">
+            <p>Your identity has been activated</p>
+            <p>You can now close this window and go back to complete your sign in procedure</p>
+        </div>
+    {% else %}
+        <p class="center">Please confirm your Milagro MFA activation:</p>
+        <!--action box start-->
+        <div class="one column center grey marBot20 marTop20">
+            <p>Email address: <label> {{userid}}</label></p>
+            <p>Requested on: <label id="issued">{{humanIssued}}</label></p>
+            <p>From: <label>{{deviceName}}</label></p>
+            <form method="POST">
+                <button type="submit">Confirm and activate</button>
+                <button onclick="window.location = '/'; return false;">Cancel activation</button>
+            </form>
+        </div>
+        <!--action box end-->
+    {% end %}
+    <div class="clear"></div>
+{% end %}
diff --git a/servers/demo/templates/activation_email.html b/servers/demo/templates/activation_email.html
new file mode 100644
index 0000000..83730e9
--- /dev/null
+++ b/servers/demo/templates/activation_email.html
@@ -0,0 +1,11 @@
+<html>
+<head></head>
+<body>
+<p><b>Milagro MFA Platform</b></p>
+<p>Your identity is now ready to activate:</p>
+<p><a href="{{ validationURL }}" target="_blank">Click this activation link and follow the instructions</a></p>
+<p>Regards,<br/>
+The Milagro MFA Team</p>
+</body>
+</html>
+
diff --git a/servers/demo/templates/activation_email.txt b/servers/demo/templates/activation_email.txt
new file mode 100644
index 0000000..0a6cdd4
--- /dev/null
+++ b/servers/demo/templates/activation_email.txt
@@ -0,0 +1,7 @@
+Milagro MFA Platform.
+Your identity is now ready to activate:
+Click this activation link and follow the instructions:
+{{ validationURL }}
+
+Regards,
+The Milagro MFA Team
diff --git a/servers/demo/templates/base.html b/servers/demo/templates/base.html
new file mode 100644
index 0000000..9801a4d
--- /dev/null
+++ b/servers/demo/templates/base.html
@@ -0,0 +1,52 @@
+{% import time %}
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+    <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+        <title>Milagro MFA demo</title>
+
+        <link href="{{ static_url("css/styles.css") }}" rel="stylesheet" type="text/css" />
+
+        <link rel="shortcut icon" href="{{ static_url("images/favicon.ico") }}">
+        <link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Open+Sans" />
+
+        {% block extra-javascript %}
+        {% end %}
+
+        {% block extra-css %}
+        {% end %}
+    </head>
+
+    <body>
+        <div id="header">
+            <div class="container">
+                <a href="http://milagro.incubator.apache.org/" target="_blank" class="logo1"><img src="{{ static_url("images/milagro-logo.svg") }}" alt="Milagro Logo" width="179" height="57" title="Milagro Logo" style="border-style: none"></a>
+            </div>
+            <div class="clear"></div>
+        </div>
+
+        {% try %}
+            {% if user %}
+                <div id="loggedInHolder"><div class="loggedInStatus">You are logged in as: {{ user }}   |   <a href="/logout"> Log Out </a></div></div>
+            {% end %}
+        {% except %}
+        {% end %}
+
+        <div id="content">
+            <div class="container">
+                <div class="nav">
+                    <ul>
+                        <li><a href="http://milagro.incubator.apache.org/" target="_blank">Milagro Web Site</a></li>
+                        <li><a href="http://www.milagro.io/" target="_blank">Milagro Documentation</a></li>
+                        <li><span class="support">Technical support: <a href="mailto:dev-subscribe@milagro.incubator.apache.org">Milagro mailing list</a></span></li>
+                    </ul>
+                    <div class="clear"></div>
+                </div>
+                <div class="content">
+                    {% block content %}
+                    {% end %}
+                </div>
+            </div>
+        </div>
+    </body>
+</html>
diff --git a/servers/demo/templates/index.html b/servers/demo/templates/index.html
new file mode 100644
index 0000000..26b2b6e
--- /dev/null
+++ b/servers/demo/templates/index.html
@@ -0,0 +1,110 @@
+{% extends "base.html" %}
+{% block extra-javascript %}
+    <script type="text/javascript" src="{{ mpinJSURL }}?r={{int(time.time()*100)}}"></script>
+
+    <script type="text/javascript">
+        new mpin({
+            targetElement: "pinHolder",
+
+            clientSettingsURL: "{{ clientSettingsURL }}",
+            mobileAppFullURL: "{{ mobileAppFullURL }}",
+
+            successLoginURL: "/protected",
+
+            {% if mobileUseNative %}
+            mobileConfigURL: "{{ mobileConfigURL }}",
+            mobileNativeApp: true,
+            {% else %}
+            mobileNativeApp: false,
+            {% end %}
+
+            onSuccessSetup: function(authData, onSuccess) {
+                console.log("Setup PIN successful")
+                console.log(authData)
+                onSuccess()
+            },
+
+            onSuccessLogin: function(authData) {
+                window.location = "/protected"
+            },
+
+            onReactivate: function(userId){
+                window.location = "/new?userId=" + userId;
+            },
+
+            onUnsupportedBrowser: function(){
+                window.location = "http://www.miracl.com/browser-compatibility-page"
+            },
+
+            onVerifySuccess: function(data){
+            },
+        });
+
+        function createAjax(){
+            if (typeof XMLHttpRequest != "undefined")
+            {
+                return new XMLHttpRequest();
+            }
+            else if (window.ActiveXObject)
+            {
+                var aVersions = ["MSXML2.XMLHttp.5.0","MSXML2.XMLHttp.4.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp","Microsoft.XMLHttp"];
+
+                for (var i = 0; i < aVersions.length; i++)
+                {
+                    try
+                    {
+                        var oXmlHttp = new ActiveXObject(aVersions[i]);
+                        return oXmlHttp;
+                    }
+
+                    catch(oError)
+                    {
+                        throw new Error("XMLHttp object could be created.");
+                    }
+                }
+            }
+            throw new Error("XMLHttp object could be created.");
+        }
+
+        {% if user %}
+            (function waitLogout() {
+                xhr = createAjax();
+
+                xhr.onreadystatechange=function(evtXHR) {
+                    if (xhr.readyState == 4)
+                    {
+                        if (xhr.status == 200)
+                        {
+                            window.location.reload();
+                        }
+                    }
+                };
+
+                xhr.open("GET", "{{ logoutWaitURL }}", true);
+                xhr.timeout = 30000;
+                xhr.ontimeout = function() {
+                    waitLogout()
+                }
+                xhr.setRequestHeader("Content-Type", "application/json");
+                xhr.send()
+            })()
+        {% end %}
+    </script>
+{% end %}
+
+{% block content %}
+    {% if flash == "protected" %}
+        <div class="center" style="margin-top:10px;"><b class="dkRed" style="text-align:center">You are not authenticated! Please complete the following steps</b></div>
+    {% end %}
+    {% if flash == "forced_logout" %}
+        <div class="center" style="margin-top:10px;"><b class="dkRed" style="text-align:center">You have been logged out!</b></div>
+    {% end %}
+
+    <h1>Welcome to the Milagro MFA System Demo</h1>
+    <div class="one column center">
+        <div id="pinHolder" style="margin:auto; width:260px;">
+            Loading PinPad...
+        </div>
+    </div>
+    <div class="clear"></div>
+{% end %}
diff --git a/servers/demo/templates/protected.html b/servers/demo/templates/protected.html
new file mode 100644
index 0000000..73623c9
--- /dev/null
+++ b/servers/demo/templates/protected.html
@@ -0,0 +1,73 @@
+{% extends "base.html" %}
+{% block extra-javascript %}
+    <script type="text/javascript">
+        function createAjax()
+        {
+            if (typeof XMLHttpRequest != "undefined")
+            {
+                return new XMLHttpRequest();
+            }
+            else if (window.ActiveXObject)
+            {
+                var aVersions = ["MSXML2.XMLHttp.5.0","MSXML2.XMLHttp.4.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp","Microsoft.XMLHttp"];
+
+                for (var i = 0; i < aVersions.length; i++)
+                {
+                    try
+                    {
+                        var oXmlHttp = new ActiveXObject(aVersions[i]);
+                        return oXmlHttp;
+                    }
+                    catch(oError)
+                    {
+                        throw new Error("XMLHttp object could be created.");
+                    }
+                }
+            }
+            throw new Error("XMLHttp object could be created.");
+        }
+
+        {% if user %}
+            (function waitLogout(){
+                xhr = createAjax();
+
+                xhr.onreadystatechange=function(evtXHR) {
+                    if (xhr.readyState == 4)
+                    {
+                        if (xhr.status == 200)
+                        {
+                            window.location = "/";
+                        }
+                    }
+                };
+
+                xhr.open("GET", "{{ logoutWaitURL }}", true);
+                xhr.timeout = 30000;
+                xhr.ontimeout = function() {
+                    waitLogout()
+                }
+                xhr.setRequestHeader("Content-Type", "application/json");
+                xhr.send()
+            })()
+        {% end %}
+    </script>
+{% end %}
+
+{% block content %}
+    {% if welcome %}
+        <h1>{{ user }}, you are now logged in!</h1>
+    {% else %}
+        <h1>{{ user }}</h1>
+        <section class="center"><p>You see this page because you are logged in. <a href="/logout">Log out</a></p></section>
+    {% end %}
+
+    <section>
+        <div class="page-header section-header">
+            <h1>Usernames and Passwords are history</h1>
+            <div class="clear"></div>
+            <p class="secondary-header"><span class="section-subheader" id="hs_cos_wrapper_subheader">Security has evolved; the Future of Strong Authentication is here.</span></p>
+            <div class="clear"></div>
+        </div>
+    </section>
+    <div class="clear"></div>
+{% end %}
diff --git a/servers/dta/__init__.py b/servers/dta/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/servers/dta/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/servers/dta/config_default.py b/servers/dta/config_default.py
new file mode 100644
index 0000000..f7ff160
--- /dev/null
+++ b/servers/dta/config_default.py
@@ -0,0 +1,65 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import unicode_literals
+
+"""HTTP server settings"""
+address = "127.0.0.1"
+port = 8001
+
+"""Time synchronization
+
+To be able to perform time based verification, by default D-TA syncs its time
+with MIRACL servers. If you set it to False, you should still sync the server
+using an accurate NTP time server!
+"""
+# syncTime = False
+
+"""The location of your keys file (relative to mpin-backend/servers/dta)."""
+credentialsFile = '%CREDENTIALSFILE%'
+
+"""Entropy sources
+
+D-TA supports multiple ways to gather entropy random, urandom, certivox or
+combination of those.
+"""
+# EntropySources = 'dev_urandom:100'  # Default
+# EntropySources = 'certivox:100'
+# EntropySources = 'dev_urandom:60,certivox:40'
+
+"""Backup master secret
+
+D-TA supports storing the master secret in a file rather than generating it every
+time on startup. It is enabled by default, set to False to disable. Master secret
+will be encrypted by default unless disabled by settingencrypt_master_secret to
+False. Master secret will be encoded with passphrase and salt to be provided
+- salt in the config file
+- passphrase - supplied on startup or in the config (not encouraged)
+
+Passphrase can be changed by running the service with changePassphrase option.
+
+To change the location of the backup file change backup_file option (relative to
+mpin-backend/servers/dta).
+"""
+# backup = False
+backup_file = '%BACKUP_FILE%'
+# encrypt_master_secret = False
+passphrase = '%PASSPHRASE%'
+salt = '%SALT%'
+
+"""Debug options"""
+# logLevel = "INFO"
diff --git a/servers/dta/dta.py b/servers/dta/dta.py
new file mode 100755
index 0000000..8a42654
--- /dev/null
+++ b/servers/dta/dta.py
@@ -0,0 +1,678 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import division, absolute_import, print_function, unicode_literals
+
+import getpass
+import hashlib
+import hmac
+import os
+import sys
+
+import tornado.autoreload
+import tornado.escape
+import tornado.gen
+import tornado.httpclient
+import tornado.httpserver
+import tornado.ioloop
+import tornado.options
+import tornado.web
+from tornado.log import app_log as log
+from tornado.options import define, options
+from tornado.web import HTTPError
+
+from mpin_utils.common import (
+    detectProxy,
+    getLogLevel,
+    Keys,
+    Seed,
+    Time,
+    verifySignature,
+)
+from mpin_utils import secrets
+
+
+if os.name == "posix":
+    from mpDaemon import Daemon
+elif os.name == "nt":
+    from mpWinService import Service as Daemon
+else:
+    raise Exception("Unsupported platform: {0}".format(os.name))
+
+BASE_DIR = os.path.dirname(__file__)
+CONFIG_FILE = os.path.join(BASE_DIR, "config.py")
+DEFAULT_BACKUP_FILE = os.path.join(BASE_DIR, "backup.json")
+
+
+# OPTIONS
+
+# general options
+define("configFile", default=os.path.join(BASE_DIR, "config.py"), type=unicode)
+define("address", default="127.0.0.1", type=unicode)
+define("port", default=8001, type=int)
+
+# debugging options
+define("autoReload", default=False, type=bool)
+define("logLevel", default="ERROR", type=unicode)
+
+# time synchronization options
+define("timePeriod", default=86400000, type=int)
+define("syncTime", default=True, type=bool)
+
+# security options
+define("credentialsFile", default=os.path.join(BASE_DIR, "credentials.json"), type=unicode)
+define("EntropySources", default="dev_urandom:100", type=unicode)
+
+# backup master secret options
+define("backup", default=True, type=bool)
+define("backup_file", default=DEFAULT_BACKUP_FILE, type=unicode)
+define("encrypt_master_secret", default=True, type=bool)
+define("passphrase", type=unicode)
+define("salt", type=unicode)
+
+
+# BASE HANDLERS
+class BaseHandler(tornado.web.RequestHandler):
+    def set_default_headers(self):
+        self.set_header("Access-Control-Allow-Origin", "*")
+        self.set_header("Access-Control-Allow-Credentials", "true")
+        self.set_header("Access-Control-Allow-Methods", "GET, OPTIONS")
+        self.set_header("Access-Control-Allow-Headers", "Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, X-Requested-By, If-Modified-Since, X-File-Name, Cache-Control")
+
+    def write_error(self, status_code, **kwargs):
+        self.set_status(status_code, reason=self._reason.upper())
+        self.content_type = 'application/json'
+        self.write({'service_name': 'D-TA server', 'message': self._reason.upper()})
+
+    def options(self):
+        self.set_status(200, reason="OK")
+        self.content_type = 'application/json'
+        self.write({'service_name': 'D-TA server', 'message': "options request"})
+        self.finish()
+        return
+
+    def finish(self, *args, **kwargs):
+        if self._status_code == 401:
+            self.set_header("WWW-Authenticate", "Authenticate")
+        super(BaseHandler, self).finish(*args, **kwargs)
+
+
+# HANDLERS
+class ServerSecretHandler(BaseHandler):
+    """
+    ..  apiTextStart
+
+    *Description*
+
+      Retrieves the M-Pin server secret
+
+    *URL structure*
+
+      ``/serverSecret?app_id=<app_id>&expires=<UTC Timestamp>&signature=<signature>``
+
+    *HTTP Request Method*
+
+      GET
+
+    *Parameters*
+
+    - app_id: <identity of the Application>
+
+    - expires: <time at which request expires>
+
+    - signature: <signature>
+
+    *Signature*
+
+      The signature is generated for this message
+
+       message = <serverSecret><app_id><expires>
+
+    *Returns*
+
+      Calculates the MPIN Server secret which is returned in this JSON object::
+    n
+        JSON response.
+        {
+           "message" : "OK",
+           "serverSecret" : "<serverSecret>"
+        }
+
+    *Status-Codes and Response-Phrases*
+
+      ::
+
+        Status-Code          Response-Phrase
+
+        200                  OK
+        401                  Invalid signature
+        403                  Missing argument [value]
+        408                  Request expired
+        500                  M-Pin Server Secret Generation
+
+    .. apiTextEnd
+
+    """
+    def get(self):
+        # Remote request information
+        if 'User-Agent' in self.request.headers.keys():
+            UA = self.request.headers['User-Agent']
+        else:
+            UA = 'unknown'
+        request_info = '%s %s %s %s ' % (self.request.method, self.request.path, self.request.remote_ip, UA)
+
+        # Get arguments
+        try:
+            app_id = str(self.get_argument('app_id'))
+            expires = self.get_argument('expires')
+            signature = self.get_argument('signature')
+        except tornado.web.MissingArgumentError as ex:
+            reason = ex.log_message
+            log.error("%s %s" % (request_info, reason))
+            self.set_status(403, reason=reason)
+            self.content_type = 'application/json'
+            self.write({'message': reason})
+            self.finish()
+            return
+        request_info = request_info + app_id
+
+        # Get path used for signature
+        path = self.request.path
+        path = path.replace("/", "")
+
+        # Check signature is valid and that timestamp has not expired
+        M = str("%s%s%s" % (path, Keys.app_id, expires))
+        valid, reason, code = verifySignature(M, signature, Keys.app_key, expires)
+        if not valid:
+            return_data = {
+                'code': code,
+                'message': reason
+            }
+            log.error("%s %s" % (request_info, reason))
+            self.set_status(status_code=code, reason=reason)
+            self.content_type = 'application/json'
+            self.write(return_data)
+            self.finish()
+            return
+
+        try:
+            server_secret_hex = self.application.master_secret.get_server_secret()
+        except secrets.SecretsError as e:
+            log.error('M-Pin Server Secret Generation Failed: {0}. Request info: {1}'.format(e, request_info))
+            return_data = {
+                'errorCode': e.message,
+                'reason': 'M-Pin Server Secret Generation Failed',
+            }
+            self.set_status(500, reason=reason)
+            self.content_type = 'application/json'
+            self.write(return_data)
+            self.finish()
+            return
+
+        # Hash server secret share
+        server_secret = server_secret_hex.decode("hex")
+        hash_server_secret_hex = hashlib.sha256(server_secret).hexdigest()
+        log.info("%s hash_server_secret_hex: %s" % (request_info, hash_server_secret_hex))
+
+        # Returned data
+        reason = "OK"
+        self.set_status(200, reason=reason)
+        self.content_type = 'application/json'
+        return_data = {
+            'serverSecret': server_secret_hex,
+            'startTime': Time.DateTimeToISO(self.application.master_secret.start_time),
+            'message': reason
+        }
+        self.write(return_data)
+        log.debug("%s %s" % (request_info, return_data))
+        self.finish()
+        return
+
+
+class ClientSecretHandler(BaseHandler):
+    """
+    ..  apiTextStart
+
+    *Description*
+
+      Retrieves the M-Pin client secret
+
+    *URL structure*
+
+      ``/clientSecret?app_id=<app_id>&expires=<UTC Timestamp>&hash_mpin_id=<hash_mpin_id>&signature=<signature>&mobile=<0||1>``
+
+    *HTTP Request Method*
+
+      GET
+
+    *Parameters*
+
+
+    - app_id: identity of the Application
+
+    - hash_mpin_id:  hex encoded hash of the M-Pin identity for which client secret is requested
+
+    - expires: time at which request expires
+
+    - signature: signature
+
+    - mobile: 1 means mobile request || 0 means desktop request
+
+    *Signature*
+
+      The signature is generated for this message using a hmac
+
+      message = <clientSecret><app_id><hash_mpin_id><expires>
+
+    *Returns*
+
+      Calculates the MPIN Client secret which is returned in this JSON object::
+
+        JSON response.
+        {
+           "message" : "OK",
+           "clientSecret" : "<clientSecret>"
+        }
+
+    *Status-Codes and Response-Phrases*
+
+      ::
+
+        Status-Code          Response-Phrase
+
+        200                  OK
+        401                  Invalid signature
+        403                  Missing argument [value]
+        403                  Invalid data received. Hex object could be decoded
+        403                  Invalid data received. hash_mpin_id null
+        403                  Invalid data received. hash_mpin_id should be 64 bytes
+        408                  Request expired
+        500                  M-Pin Client Secret Generation Failed
+
+    .. apiTextEnd
+
+    """
+    def get(self):
+        # Remote request information
+        if 'User-Agent' in self.request.headers.keys():
+            UA = self.request.headers['User-Agent']
+        else:
+            UA = 'unknown'
+        request_info = '%s %s %s %s ' % (self.request.method, self.request.path, self.request.remote_ip, UA)
+
+        # Get arguments
+        try:
+            app_id = str(self.get_argument('app_id'))
+            expires = self.get_argument('expires')
+            signature = self.get_argument('signature')
+            hash_mpin_id_hex = self.get_argument('hash_mpin_id')
+            hash_mpin_id = hash_mpin_id_hex.decode("hex")
+            hash_user_id = self.get_argument('hash_user_id')
+        except tornado.web.MissingArgumentError as ex:
+            reason = ex.log_message
+            log.error("%s %s" % (request_info, reason))
+            self.set_status(403, reason=reason)
+            self.content_type = 'application/json'
+            self.write({'message': reason})
+            self.finish()
+            return
+        except TypeError as ex:
+            reason = "Invalid data received. Hex object could be decoded"
+            log.error("%s %s" % (request_info, reason))
+            self.set_status(403, reason=reason)
+            self.content_type = 'application/json'
+            self.write({'message': reason})
+            self.finish()
+            return
+        if len(hash_mpin_id_hex) != 64:
+            reason = "Invalid data received. hash_mpin_id should be 64 bytes"
+            log.error("%s %s" % (request_info, reason))
+            self.set_status(403, reason=reason)
+            self.content_type = 'application/json'
+            self.write({'message': reason})
+            self.finish()
+            return
+        request_info = request_info + app_id + " " + hash_mpin_id_hex
+
+        # Get path used for signature
+        path = self.request.path
+        path = path.replace("/", "")
+
+        # Check signature is valid and that timestamp has not expired
+        M = str("%s%s%s%s%s" % (path, Keys.app_id, hash_mpin_id_hex, hash_user_id, expires))
+        valid, reason, code = verifySignature(M, signature, Keys.app_key, expires)
+        if not valid:
+            return_data = {
+                'code': code,
+                'message': reason
+            }
+            log.error("%s %s" % (request_info, reason))
+            self.set_status(status_code=code, reason=reason)
+            self.content_type = 'application/json'
+            self.write(return_data)
+            self.finish()
+            return
+
+        try:
+            client_secret_hex = self.application.master_secret.get_client_secret(hash_mpin_id)
+        except secrets.SecretsError as e:
+            return_data = {
+                'errorCode': e.message,
+                'message': 'M-Pin Client Secret Generation Failed'
+            }
+            log.error("%s %s" % (request_info, reason))
+            self.set_status(500, reason=reason)
+            self.content_type = 'application/json'
+            self.write(return_data)
+            self.finish()
+            return
+        # Hash client secret share
+        client_secret = client_secret_hex.decode("hex")
+        hash_client_secret_hex = hashlib.sha256(client_secret).hexdigest()
+        log.info("%s hash_client_secret_hex: %s" % (request_info, hash_client_secret_hex))
+
+        reason = "OK"
+        self.set_status(200, reason=reason)
+        self.content_type = 'application/json'
+        return_data = {
+            'clientSecret': client_secret_hex,
+            'message': reason
+        }
+        self.write(return_data)
+        log.debug("%s %s" % (request_info, return_data))
+        self.finish()
+        return
+
+
+class TimePermitsHandler(BaseHandler):
+
+    def get_hash_mpin_id_hex(self):
+        try:
+            hash_mpin_id_hex = self.get_argument('hash_mpin_id')
+        except tornado.web.MissingArgumentError as e:
+            log.debug(e)
+            raise HTTPError(403, e.log_message)
+
+        if len(hash_mpin_id_hex) != 64:
+            reason = "Invalid data received. hash_mpin_id should be 64 bytes"
+            log.debug(reason)
+            raise HTTPError(403, reason)
+
+        return hash_mpin_id_hex
+
+    def get_hash_mpin_id(self, hash_mpin_id_hex):
+        hash_mpin_id_hex = self.get_hash_mpin_id_hex()
+        try:
+            return hash_mpin_id_hex.decode("hex")
+        except TypeError:
+            reason = "Invalid data received. Hex object could be decoded"
+            log.debug(reason)
+            raise HTTPError(403, reason)
+
+    def get_signature(self):
+        try:
+            return self.get_argument('signature')
+        except tornado.web.MissingArgumentError as e:
+            raise HTTPError(403, e.log_message)
+
+    def get_count(self):
+        try:
+            count = self.get_argument('count')
+        except tornado.web.MissingArgumentError as e:
+            raise HTTPError(403, e.log_message)
+
+        try:
+            count = int(count)
+        except ValueError:
+            raise HTTPError(403, 'Count invalid format integer')
+
+        return count
+
+    def verify_signature(self, signature, hash_mpin_id_hex):
+        hmacExpected = hmac.new(Keys.app_key, hash_mpin_id_hex.encode('utf-8'), hashlib.sha256).hexdigest()
+        hmac1 = hmac.new(Keys.app_key, signature, hashlib.sha256).hexdigest()
+        hmac2 = hmac.new(Keys.app_key, hmacExpected, hashlib.sha256).hexdigest()
+        return hmac1 == hmac2
+
+    def get_timepermits(self, hash_mpin_id, count):
+        try:
+            time_permits = self.application.master_secret.get_time_permits(
+                hash_mpin_id, count=count)
+        except secrets.SecretsError as e:
+            raise HTTPError(500, e.message)
+
+        return time_permits
+
+    def get(self):
+        hash_mpin_id_hex = self.get_hash_mpin_id_hex()
+        hash_mpin_id = self.get_hash_mpin_id(hash_mpin_id_hex)
+        signature = self.get_signature()
+        count = self.get_count()
+
+        if not self.verify_signature(signature, hash_mpin_id_hex):
+            reason = "Invalid signature"
+            log.debug(reason)
+            raise HTTPError(401, reason)
+
+        self.finish({
+            'timePermits': self.get_timepermits(hash_mpin_id, count),
+            'message': 'OK'
+        })
+
+
+class TimePermitHandler(TimePermitsHandler):
+
+    """Kept for backwards compatibility."""
+
+    def get_timepermit(self, hash_mpin_id):
+        return self.get_timepermits(hash_mpin_id, 1).values()[0]
+
+    def get(self):
+        hash_mpin_id_hex = self.get_hash_mpin_id_hex()
+        hash_mpin_id = self.get_hash_mpin_id(hash_mpin_id_hex)
+        signature = self.get_signature()
+
+        if not self.verify_signature(signature, hash_mpin_id_hex):
+            reason = "Invalid signature"
+            log.debug(reason)
+            raise HTTPError(401, reason)
+
+        self.finish({
+            'timePermit': self.get_timepermit(hash_mpin_id),
+            'message': 'OK'
+        })
+
+
+class StatusHandler(BaseHandler):
+    """
+    ..  apiTextStart
+
+    *Description*
+
+      Retrieves status of the D-TA Proxy.
+
+    *URL structure*
+
+      ``/status``
+
+    *HTTP Request Method*
+
+      GET
+
+    *Returns*
+
+      JSON response::
+
+        {
+           'message' : 'OK',
+           'startTime': <DateTime>
+           'service_name': 'D-TA server'
+        }
+
+    *Status-Codes and Response-Phrases*
+
+      ::
+
+        Status-Code          Response-Phrase
+
+        200                  OK
+
+    ..  apiTextEnd
+    """
+
+    def get(self):
+        reason = "OK"
+        self.set_status(200, reason=reason)
+        start_time_str = Time.DateTimeToISO(self.application.master_secret.start_time),
+        self.write({'startTime': start_time_str, 'service_name': 'D-TA server', 'message': reason})
+        return
+
+
+class DefaultHandler(BaseHandler):
+    def get(self, input):
+        reason = "NOT FOUND"
+        self.set_status(404, reason=reason)
+        self.write({'service_name': 'D-TA server', 'message': reason})
+        return
+
+    def post(self, input):
+        reason = "URI NOT FOUND"
+        self.set_status(404, reason=reason)
+        self.write({'service_name': 'D-TA server', 'message': reason})
+        return
+
+    def put(self, input):
+        reason = "URI NOT FOUND"
+        self.set_status(404, reason=reason)
+        self.write({'service_name': 'D-TA server', 'message': reason})
+        return
+
+    def delete(self, input):
+        reason = "URI NOT FOUND"
+        self.set_status(404, reason=reason)
+        self.write({'service_name': 'D-TA server', 'message': reason})
+        return
+
+
+# MAIN
+class Application(tornado.web.Application):
+    def __init__(self):
+        handlers = [
+            (r"/clientSecret", ClientSecretHandler),
+            (r"/serverSecret", ServerSecretHandler),
+            (r"/timePermit", TimePermitHandler),
+            (r"/timePermits", TimePermitsHandler),
+            (r"/status", StatusHandler),
+            (r"/(.*)", DefaultHandler),
+        ]
+        settings = dict(
+            xsrf_cookies=False
+        )
+        super(Application, self).__init__(handlers, **settings)
+
+        Seed.getSeed(options.EntropySources)  # Get seed value for random number generator
+        self.master_secret = secrets.MasterSecret(
+            passphrase=options.passphrase,
+            salt=options.salt,
+            seed=Seed.seedValue,
+            backup_file=options.backup_file,
+            encrypt_master_secret=options.encrypt_master_secret,
+            time=Time.syncedNow())
+
+
+def main():
+    options.parse_command_line()
+
+    if os.path.exists(options.configFile):
+        try:
+            options.parse_config_file(options.configFile)
+            options.parse_command_line()
+        except Exception, E:
+            print("Invalid config file {0}".format(options.configFile))
+            print(E)
+            sys.exit(1)
+
+    # Set Log level
+    log.setLevel(getLogLevel(options.logLevel))
+
+    detectProxy()
+
+    # Load the credentials from file
+    log.info("Loading credentials")
+    try:
+        credentialsFile = options.credentialsFile
+        Keys.loadFromFile(credentialsFile)
+    except Exception as E:
+        log.error("Error opening the credentials file: {0}".format(credentialsFile))
+        log.error(E)
+        sys.exit(1)
+
+    # TMP fix for 'ValueError: I/O operation on closed epoll fd'
+    # Fixed in Tornado 4.2
+    tornado.ioloop.IOLoop.instance()
+
+    # Sync time to CertiVox time server
+    if options.syncTime:
+        Time.getTime(wait=True)
+
+    if options.backup and options.encrypt_master_secret and not options.passphrase:
+        options.passphrase = getpass.getpass("Please enter passphrase:")
+
+    http_server = Application()
+    http_server.listen(options.port, options.address, xheaders=True)
+    io_loop = tornado.ioloop.IOLoop.instance()
+
+    if options.autoReload:
+        log.debug("Starting autoreloader")
+        tornado.autoreload.watch(CONFIG_FILE)
+        tornado.autoreload.start(io_loop)
+
+    if options.syncTime and (options.timePeriod > 0):
+        scheduler = tornado.ioloop.PeriodicCallback(Time.getTime, options.timePeriod, io_loop=io_loop)
+        scheduler.start()
+
+    log.info("Server started. Listening on {0}:{1}".format(options.address, options.port))
+    io_loop.start()
+
+
+class ServiceDaemon(Daemon):
+    def run(self):
+        main()
+
+
+if __name__ == "__main__":
+    if len(sys.argv) > 1 and sys.argv[1].lower() in ("start", "stop"):
+        action = sys.argv.pop(1)
+        logFile = os.path.join(BASE_DIR, "dta.log")
+        pidFile = os.path.join(BASE_DIR, "dta.pid")
+
+        daemon = ServiceDaemon(pidfile=pidFile, stdout=logFile, stderr=logFile)
+        if action == "start":
+            log.info("Starting as daemon. Log file: {0}".format(logFile))
+            daemon.start()
+        elif action == "stop":
+            log.info("Stopping daemon...")
+            daemon.stop()
+            sys.exit()
+    else:
+        try:
+            main()
+        except Exception as e:
+            log.error(e)
+            sys.exit(1)
diff --git a/servers/dta/dtaservice.py b/servers/dta/dtaservice.py
new file mode 100755
index 0000000..a13ebd4
--- /dev/null
+++ b/servers/dta/dtaservice.py
@@ -0,0 +1,29 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import os
+import sys
+
+if (os.name != "nt"):
+    print "This file can be run on Windows."
+    sys.exit(1)
+
+import dta
+from mpWinService import installService
+
+
+installService(dta.ServiceDaemon, 'mpinDTA', 'M-Pin DTA Service')
diff --git a/servers/rps/__init__.py b/servers/rps/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/servers/rps/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/servers/rps/config_default.py b/servers/rps/config_default.py
new file mode 100644
index 0000000..915efee
--- /dev/null
+++ b/servers/rps/config_default.py
@@ -0,0 +1,133 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import unicode_literals
+
+"""HTTP server settings"""
+address = '127.0.0.1'
+port = 8011
+
+"""Set Access-Control-Allow-Origin header"""
+# allowOrigin = ['*']
+
+"""Time synchronization
+
+To be able to perform time based verification, by default RPS syncs its time
+with MIRACL servers. If you set it to False, you should still sync the server
+using an accurate NTP time server!
+"""
+# syncTime = False
+
+"""
+Dynamic options url
+
+Location to be queried for dynamically (runtime) changeable options.
+'None' mean dynamic options are disabled and it is default value.
+"""
+# dynamicOptionsURL = None  # Default
+
+"""The location of your keys file (relative to mpin-backend/servers/dta)."""
+credentialsFile = '%CREDENTIALSFILE%'
+
+"""Entropy sources
+
+D-TA supports multiple ways to gather entropy random, urandom, certivox or
+combination of those.
+"""
+# EntropySources = 'dev_urandom:100'  # Default
+# EntropySources = 'certivox:100'
+# EntropySources = 'dev_urandom:60,certivox:40'
+
+"""MIRACL server secret share acquisition
+
+- dta - get server secret from MIRACL dta automatically on start
+- credentials.json - get server secret from credentials.json (key: certivox_server_secret)
+- manual - service will prompt for it
+- the secret itself
+
+You can get your MIRACL server secret by:
+    ./scripts/getServerSecretShare.py credentials.json
+which will output your credentials json including certivox_server_secret.
+NOTE: Don't pipe it directly to the same file - you'll lose your original
+      credentials file.
+Alternatively you can copy only your certivox_server_secret value and supply it
+either manually or via config.py setting the certivoxServerSecret to the
+corresponding value.
+"""
+# certivoxServerSecret = 'dta'  # Default
+
+"""Local DTA address."""
+DTALocalURL = 'http://127.0.0.1:8001'
+
+"""Access number options
+
+- enable access number
+- accessNumberExpireSeconds - The default time client will show the access number
+- accessNumberExtendValiditySeconds - Validity of the access number (on top of accessNumberExpireSeconds)
+- accessNumberUseCheckSum - Should access number have checksum
+"""
+# requestOTP = True
+# accessNumberExpireSeconds = 60  # Default
+# accessNumberExtendValiditySeconds = 5  # Default
+# accessNumberUseCheckSum = True  # Default
+
+"""Authentication options
+
+- waitForLoginResult -For the mobile flow. Wait the browser login before showing the Done/Logout button.
+"""
+waitForLoginResult = True
+# VerifyUserExpireSeconds = 3600  # Default
+# maxInvalidLoginAttempts = 3  # Default
+# cacheTimePermits = True   #Default
+
+"""RPA options
+
+- RPAPermitUserURL - RPA Revocation endpoint
+- RegisterForwardUserHeaders - Coma separated list of headers
+    - '' - do not forward headers
+    - * - forward all headers
+- LogoutURL - RPA Logout url. For logout using the mobile client.
+"""
+RPAVerifyUserURL = 'http://127.0.0.1:8005/mpinVerify'
+# RPAPermitUserURL = 'http://127.0.0.1:8005/mpinPermitUser'
+RPAAuthenticateUserURL = '/mpinAuthenticate'
+RegisterForwardUserHeaders = ''
+LogoutURL = '/logout'
+
+"""PIN pad client options"""
+# rpsBaseURL = ''
+# rpsPrefix = 'rps'  # Default
+# setDeviceName = True
+
+"""Key value storage options"""
+storage = 'memory'
+
+# storage = 'redis'
+# redisHost = '127.0.0.1'  # Default
+# redisPort = 6379  # Default
+# redisDB = 0  # Default
+# redisPassword = None  # Default
+# redisPrefix = 'mpin'  # Default
+
+# storage = 'json'
+# fileStorageLocation = './mpin_rps_storage.json'
+
+"""Debug options"""
+# logLevel = "INFO"
+
+"""Use NFC flag for mobile clients"""
+useNFC = False
diff --git a/servers/rps/rps.py b/servers/rps/rps.py
new file mode 100755
index 0000000..4dd9c6f
--- /dev/null
+++ b/servers/rps/rps.py
@@ -0,0 +1,1630 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import division, absolute_import, print_function, unicode_literals
+
+import datetime
+import hashlib
+import json
+import os
+import random
+import sys
+import time
+import urllib
+from urlparse import urlparse
+
+import tornado.autoreload
+import tornado.gen
+import tornado.httpclient
+import tornado.httpserver
+import tornado.web
+import tornado.websocket
+from tornado.httputil import url_concat
+from tornado.log import app_log as log
+from tornado.options import define, options
+from tornado.web import HTTPError
+
+from mpin_utils.common import (
+    detectProxy,
+    getLogLevel,
+    Keys,
+    Seed,
+    SIGNATURE_EXPIRES_OFFSET_SECONDS,
+    signMessage,
+    Time,
+)
+from mpin_utils import secrets
+from storage import get_storage_cls
+from dynamic_options import (
+    generate_dynamic_options,
+    process_dynamic_options,
+)
+
+if os.name == "posix":
+    from mpDaemon import Daemon
+elif os.name == "nt":
+    from mpWinService import Service as Daemon
+else:
+    raise Exception("Unsupported platform: {0}".format(os.name))
+
+
+VERSION = '0.3'
+BASE_DIR = os.path.dirname(__file__)
+CONFIG_FILE = os.path.join(BASE_DIR, "config.py")
+MOBILE_LOGIN_AUTHENTICATION_TIMEOUT_SECONDS = 10
+
+PASS1_EXPIRES_TIME = 15
+PERMITS_MIN, PERMITS_MAX = 7, 13
+
+
+# OPTIONS
+
+# general options
+define("configFile", default=os.path.join(BASE_DIR, "config.py"), type=unicode)
+define("address", default="127.0.0.1", type=unicode)
+define("port", default=8011, type=int)
+define("allowOrigin", default="*")
+define("dynamicOptionsURL", default=None, type=unicode)
+
+# debugging options
+define("autoReload", default=False, type=bool)
+define("logLevel", default="ERROR", type=unicode)
+
+# time synchronization options
+define("timePeriod", default=86400000, type=int)
+define("syncTime", default=True, type=bool)
+
+# security options
+define("credentialsFile", default=os.path.join(BASE_DIR, "credentials.json"), type=unicode)
+define("EntropySources", default="certivox:100", type=unicode)
+define("seedValueLength", default=100, type=int)
+
+# customer DTA service discovery options
+define("DTALocalURL", default="", type=unicode)
+
+# access number options
+define("accessNumberExpireSeconds", default=60, type=int)
+define("accessNumberExtendValiditySeconds", default=5, type=int)
+define("accessNumberUseCheckSum", default=True, type=bool)
+
+# authentication options
+define("waitForLoginResult", default=False, type=bool)
+define("VerifyUserExpireSeconds", default=3600, type=int)
+define("maxInvalidLoginAttempts", default=3, type=int)
+define("cacheTimePermits", default=True, type=bool)
+
+# OTP options
+define("requestOTP", default=False, type=bool)
+define("OTTLength", default=16, type=int)
+
+# RPA options
+define("RPAVerifyUserURL", default="", type=unicode)
+define("RPAPermitUserURL", default="", type=unicode)
+define("RPAAuthenticateUserURL", default="", type=unicode)
+define("RegisterForwardUserHeaders", default="", type=unicode)
+define("LogoutURL", default="", type=unicode)
+
+# PIN pad client options
+define("rpsBaseURL", default="")
+define("rpsPrefix", default="rps")
+define("setDeviceName", default=False, type=bool)
+
+# mobile client config
+define("mobileUseNative", default=False, type=bool)
+define("mobileConfig", default=None, type=list)
+define("useNFC", default=False, type=bool)
+
+
+# Mapping between local names of dynamic options and names from json
+# in the form `remote_name`: `local_name`
+# Only options that have mapping are processed
+DYNAMIC_OPTION_MAPPING = {
+    'time_synchronization': 'syncTime',
+    'time_synchronization_period': 'timePeriod',
+    'mobile_use_native': 'mobileUseNative',
+    'mobile_client_config': 'mobileConfig',
+}
+
+
+# Dynamic options handlers
+def handle_time_synchronization_update(updated, application, initial):
+
+    log.debug("Handling time synchronization")
+    if not any(x in updated for x in ('syncTime', 'timePeriod')) and not initial:
+        log.debug("Nothing to do on time synchronization")
+        return
+
+    def _stop_scheduler():
+            try:
+                application.time_sync_scheduler.stop()
+                log.debug("Stopped time sync schduler")
+            except:
+                pass
+    if options.syncTime and (options.timePeriod > 0):
+        _stop_scheduler()
+        application.time_sync_scheduler \
+            = tornado.ioloop.PeriodicCallback(
+                Time.getTime,
+                options.timePeriod,
+                io_loop=application.io_loop)
+        application.time_sync_scheduler.start()
+        log.debug(
+            "Started time sync schduler with period {0}"
+            .format(options.timePeriod))
+    else:
+        _stop_scheduler()
+
+
+# Convenience variable - list of dynamic options update handlers
+DYNAMIC_OPTION_HANDLERS = [
+    handle_time_synchronization_update,
+]
+
+
+# UTILITIES
+def makeMPinID(userId, isMobile):
+    endUserData = {
+        "issued": str(Time.syncedNow()),
+        "userID": userId,
+        "mobile": int(isMobile or 0),
+        "salt": os.urandom(16).encode("hex")
+    }
+
+    mpin_id = json.dumps(endUserData)
+
+    return mpin_id.encode("hex")
+
+
+def verifyToken(token):
+    """A method for verifying the authentication token.
+
+       n.b. The message variable should not be returned in a deployed application
+    """
+    successCode = int(token["successCode"])
+    pinError = int(token["pinError"])
+
+    # Get current time and token expired time
+    expiresStr = token["expires"].replace(" ", "T").replace("Z", "")
+    expiresTime = datetime.datetime.strptime(expiresStr, '%Y-%m-%dT%H:%M:%S')
+    syncedTime = Time.syncedNow()
+
+    # Check if token has expired.
+    if syncedTime > expiresTime:
+        fail = 1
+        status = 401
+        reason = "Authentication Failed. Token Expired."
+
+    elif successCode != 0:
+        if pinError == 0:  # No token.
+            fail = 1
+            status = 401
+            reason = "Authentication Failed. Invalid Token."
+        else:
+            # Entering wrong PIN.
+            fail = 1
+            status = 401
+            reason = "Authentication Failed. Invalid PIN."
+    else:
+        # Successful authentication
+        fail = 0
+        status = 200
+        reason = "OK"
+
+    return (fail, status, reason)
+
+
+# BASE HANDLERS
+class BaseHandler(tornado.web.RequestHandler):
+
+    def set_default_headers(self):
+        try:
+            log.debug("Origin Header %s" % self.request.headers['Origin'])
+            if self.request.headers['Origin'] in options.allowOrigin:
+                self.set_header("Access-Control-Allow-Origin", self.request.headers['Origin'])
+            elif "*" in options.allowOrigin:
+                self.set_header("Access-Control-Allow-Origin", "*")
+        except:
+            log.debug("Origin header not defined")
+        self.set_header("Access-Control-Allow-Credentials", "true")
+        self.set_header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS")
+        self.set_header("Access-Control-Allow-Headers", "Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, X-Requested-By, If-Modified-Since, X-File-Name, Cache-Control, WWW-Authenticate")
+
+    def write_error(self, status_code, **kwargs):
+        self.set_status(status_code, reason=self._reason)
+        self.content_type = 'application/json'
+        self.write({'version': VERSION, 'message': self._reason})
+
+    def options(self, *args, **kwargs):
+        self.set_status(200, reason="OK")
+        self.content_type = 'application/json'
+        self.write({'version': VERSION, 'message': "options request"})
+        self.finish()
+        return
+
+    def finish(self, *args, **kwargs):
+        if self._status_code == 401:
+            self.set_header("WWW-Authenticate", "Authenticate")
+        super(BaseHandler, self).finish(*args, **kwargs)
+
+    @property
+    def storage(self):
+        return self.application.storage
+
+
+class PrivateBaseHandler(BaseHandler):
+
+    def prepare(self):
+        # TODO: Check the remoteIP option
+        # allow connections from whitelisted IP's
+        # print self.request.remote_ip
+        # self.set_status(404)
+        # self.finish()
+        pass
+
+
+# PUBLIC HANDLERS
+class ClientSettingsHandler(BaseHandler):
+    def get(self):
+        self.set_header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate")
+        self.set_header("Pragma", "no-cache")
+        self.set_header("Expires", "Sat, 26 Jul 1997 05:00:00 GMT")
+
+        baseURL = "{0}/{1}".format(options.rpsBaseURL, options.rpsPrefix.strip("/"))
+        params = {
+            "certivoxURL": Keys.certivoxServer(),
+            "signatureURL": "{0}/signature".format(baseURL),
+            "registerURL": "{0}/user".format(baseURL),
+            "timePermitsURL": "{0}/timePermit".format(baseURL),
+            "timePermitsStorageURL": "{0}".format(Keys.timePermitsStorageURL),
+            "setupDoneURL": "{0}/setupDone".format(baseURL),
+            "mpinAuthServerURL": baseURL,
+            "authenticateURL": options.RPAAuthenticateUserURL,
+            "mobileAuthenticateURL": "{0}/authenticate".format(baseURL),
+            "setDeviceName": options.setDeviceName,
+            "accessNumberUseCheckSum": options.accessNumberUseCheckSum,
+
+            "appID": Keys.app_id,
+            "requestOTP": options.requestOTP,
+            "seedValue": secrets.generate_random_number(
+                self.application.server_secret.rng, options.seedValueLength),
+
+            "useWebSocket": False,
+
+            "accessNumberDigits": 7 if options.accessNumberUseCheckSum else 6,
+            "cSum": 1,
+            "useNFC": options.useNFC,
+        }
+
+        if not options.requestOTP:
+            params["accessNumberURL"] = "{0}/accessnumber".format(baseURL)
+            params["getAccessNumberURL"] = "{0}/getAccessNumber".format(baseURL)
+
+        self.write(params)
+        self.finish()
+
+
+class RPSUserHandler(BaseHandler):
+
+    @tornado.web.asynchronous
+    @tornado.gen.engine
+    def put(self, mpinId):
+
+        try:
+            data = json.loads(self.request.body)
+            mobile = int(data.get("mobile", "0"))
+            userId = data.get("userId")
+            deviceName = data.get("deviceName", "")
+            oldRegOTT = data.get("regOTT")
+
+            if not userId:
+                log.error("Missing userId")
+                log.debug(self.request.body)
+                self.set_status(400, reason="BAD REQUEST. INVALID USERID")
+                self.finish()
+                return
+
+        except ValueError:
+            log.error("Cannot decode body as JSON.")
+            log.debug(self.request.body)
+            self.set_status(400, reason="BAD REQUEST. INVALID JSON")
+            self.finish()
+            return
+
+        if mpinId.strip("/"):
+            mpinId = mpinId.strip("/")
+            log.debug("Reactivation request for mpinId: {0}".format(mpinId))
+
+            updateItem = self.storage.find(stage="register", mpinId=mpinId)
+            if not updateItem:
+                mpinId = None
+                oldRegOTT = None
+                log.error("Missing or invalid mpinID. Will generate a new mpinID")
+
+            elif updateItem.regOTT != oldRegOTT:
+                log.error("Missing or invalid regOTT")
+                log.debug(self.request.body)
+                self.set_status(400, reason="BAD REQUEST. INVALID REGOTT")
+                self.finish()
+                return
+
+        if not mpinId:
+            # Generate new mpinID
+            updateItem = None
+            mpinId = makeMPinID(userId, mobile)
+            log.debug("New mpinID generated for user {0}: {1}".format(userId, mpinId))
+
+        userData = data.get("userData")
+
+        # Verify user >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+
+        # Generate activateKey
+        regOTT = oldRegOTT or secrets.generate_ott(options.OTTLength, self.application.server_secret.rng, "hex")
+        activateKey = signMessage("{0}{1}".format(mpinId, regOTT), Keys.app_key)
+        nowTime = Time.syncedNow()
+        expireTime = nowTime + datetime.timedelta(seconds=options.VerifyUserExpireSeconds)
+
+        requestBody = json.dumps({
+            "userId": userId,
+            "mpinId": mpinId,
+            "mobile": mobile,
+            "activateKey": activateKey,
+            "expireTime": Time.DateTimeToISO(expireTime),
+            "resend": bool(updateItem),
+            "deviceName": deviceName,
+            "userData": userData or ""
+        })
+
+        if updateItem:
+            updateItem.delete()
+
+        client = tornado.httpclient.AsyncHTTPClient()
+
+        pr = urlparse(self.request.full_url())
+        base_url = "{0}://{1}".format(pr.scheme, pr.netloc)
+
+        headers = {
+            "RPS-BASE-URL": base_url
+        }
+
+        # Forward headers to the RPA
+        if options.RegisterForwardUserHeaders:
+            allHeaders = options.RegisterForwardUserHeaders == "*"
+            rHeaders = map(lambda x: x.strip().lower(), options.RegisterForwardUserHeaders.split(","))
+            for h in self.request.headers:
+                if allHeaders or (h.lower() in rHeaders):
+                    headers[h] = self.request.headers[h]
+
+        RPAVerifyUserURL = options.RPAVerifyUserURL
+
+        if not RPAVerifyUserURL:
+            log.error("RPAVerifyUserURL option not set! Unable to make Verify request")
+            self.set_status(400, "RPAVerifyUserURL option not set.")
+            self.finish()
+            return
+
+        # Make the verify request to the RPA
+        response = yield tornado.gen.Task(client.fetch, RPAVerifyUserURL, method="POST", headers=headers, body=requestBody)
+
+        if response.error:
+            log.error("RPA verify request error: {0}. Code: {1}, Reason: {2}".format(response.error, response.code, response.reason))
+            error = response.code
+            if error >= 500:
+                error = 500
+
+            self.set_status(error)
+            self.finish()
+            return
+
+        forceActivate = False
+        if response.body:
+            try:
+                responseData = json.loads(response.body)
+                forceActivate = responseData.get("forceActivate", forceActivate)
+            except:
+                log.error("RPA verify request: Invalid JSON response: {0}".format(response.body))
+                self.set_status(500)
+                self.finish()
+                return
+
+        if forceActivate:
+            log.debug("RPA response: force_activate. Activating UserID: {0}".format(userId))
+
+        if forceActivate:
+            active = activateKey
+        else:
+            active = 0
+
+        log.debug("New regOTT generated: {0}. ForceActivate: {1}".format(regOTT, forceActivate))
+
+        self.storage.add(
+            expire_time=expireTime,
+            stage="register",
+            mpinId=mpinId,
+            regOTT=regOTT,
+            active=active
+        )
+
+        # Response to the client
+        responseData = {
+            "mpinId": mpinId,
+            "regOTT": regOTT,
+            "expireTime": expireTime.isoformat(),
+            "nowTime": nowTime.isoformat(),
+            "active": forceActivate
+        }
+
+        self.write(responseData)
+        self.finish()
+
+
+class RPSSignatureHandler(BaseHandler):
+    @tornado.web.asynchronous
+    @tornado.gen.engine
+    def get(self, mpinId):
+
+        regOTT = self.get_argument("regOTT")
+
+        log.debug("ClientSecret request for mpinID: {0}".format(mpinId))
+
+        mpinData = json.loads(mpinId.decode("hex"))
+        mobile = mpinData.get("mobile", 0)
+
+        I = self.storage.find(stage="register", mpinId=mpinId)
+        if not I:
+            log.debug("MpinID {0} not found.".format(mpinId))
+            self.set_status(400, "M-Pin ID not found.")
+            self.finish()
+            return
+
+        # Verify regOTT
+        if I.regOTT != regOTT:
+            log.error("MpinID {0} regOTT does not match!".format(mpinId))
+            I.delete()
+            self.set_status(400, "M-Pin ID not found.")
+            self.finish()
+            return
+
+        # Verify Activated
+        if I.active != signMessage("{0}{1}".format(mpinId, regOTT), Keys.app_key):
+            log.debug("MpinID {0} is not activated!".format(mpinId))
+            self.set_status(401, "M-PinID not active.")
+            self.finish()
+            return
+
+        # Get hash of M-PIN ID
+        hash_mpin_id_hex = hashlib.sha256(mpinId.decode("hex")).hexdigest()
+
+        # Get hash of UserID ID
+        hash_user_id = hashlib.sha256('{user_id}{salt}'.format(
+            user_id=mpinData['userID'],
+            salt=hashlib.sha256(self.application.server_secret.server_secret).hexdigest(),
+        )).hexdigest()
+
+        # Generate signed params
+        path = "clientSecret"
+        expires = Time.syncedISO(seconds=SIGNATURE_EXPIRES_OFFSET_SECONDS)
+        hash_user_id = ""
+        M = str("%s%s%s%s%s" % (path, Keys.app_id, hash_mpin_id_hex, hash_user_id, expires))
+        signature_hex = signMessage(M, Keys.app_key)
+
+        param_values = {
+            'app_id': Keys.app_id,
+            'expires': expires,
+            'hash_mpin_id': hash_mpin_id_hex,
+            'hash_user_id': hash_user_id,
+            'mobile': mobile,
+            'signature': signature_hex,
+        }
+
+        url = "{0}/{1}".format(options.DTALocalURL.rstrip("/"), path)
+        urlParams = url_concat(url, param_values)
+
+        client = tornado.httpclient.AsyncHTTPClient()
+        response = yield tornado.gen.Task(client.fetch, urlParams, method="GET")
+
+        if response.error:
+            log.error("DTA clientSecret failed, URL: {0}. Code: {1}, Reason: {2}".format(urlParams, response.code, response.reason))
+            self.set_status(500)
+            self.finish()
+            return
+
+        if response.body:
+            try:
+                responseData = json.loads(response.body)
+                clientSecretShare = responseData["clientSecret"]
+            except:
+                log.error("DTA /clientSecret Failed. Invalid JSON response".format(response.body))
+                self.set_status(500)
+                self.finish()
+                return
+
+        I.delete()
+
+        params = urllib.urlencode(param_values)
+        data = {
+            "params": params,
+            "clientSecretShare": clientSecretShare
+        }
+
+        self.write(data)
+        self.finish()
+
+
+class RPSTimePermitHandler(BaseHandler):
+
+    def __init__(self, *args, **kwargs):
+        super(RPSTimePermitHandler, self).__init__(*args, **kwargs)
+        self.http_client = tornado.httpclient.AsyncHTTPClient()
+
+    @tornado.gen.coroutine
+    def get_time_permits(self, hash_mpin_id_hex, signature):
+        # Get time permit from the local D-TA
+        url = url_concat(
+            "{0}/{1}".format(options.DTALocalURL.rstrip("/"), "timePermits"), {
+                'hash_mpin_id': hash_mpin_id_hex,
+                'signature': signature,
+                'count': random.randint(PERMITS_MIN, PERMITS_MAX) if options.cacheTimePermits else 1})
+        response = yield self.http_client.fetch(url)
+
+        if response.error:
+            log.error("DTA timePermit failed, URL: {0}. Code: {1}, Reason: {2}".format(url, response.code, response.reason))
+            raise HTTPError(500)
+
+        if response.body:
+            try:
+                response_data = json.loads(response.body)
+                raise tornado.gen.Return(response_data["timePermits"])
+            except (ValueError, KeyError):
+                log.error("DTA /timePermit Failed. Invalid JSON response".format(
+                    response.body))
+                raise HTTPError(500)
+
+    def cache_time_permits(self, time_permits, hash_mpin_id_hex):
+        # Cache them in storage
+        for date_epoch, time_permit in time_permits.iteritems():
+            try:
+                date_epoch = int(date_epoch)
+            except ValueError:
+                log.error("DTA /timePermit Failed. Date invalid integer")
+                raise HTTPError(500)
+
+            self.storage.add(
+                expire_time=datetime.datetime.fromtimestamp(date_epoch * 60 * 1440) + datetime.timedelta(days=1),
+                time_permit_id=hash_mpin_id_hex,
+                time_permit_date=date_epoch,
+                time_permit=time_permit)
+
+    @tornado.gen.coroutine
+    def get_time_permit(self, hash_mpin_id_hex, date_epoch, signature):
+        """Get time permit from cache or request new."""
+        if options.cacheTimePermits:
+            time_permit_item = self.storage.find(time_permit_id=hash_mpin_id_hex, time_permit_date=date_epoch)
+            if time_permit_item:
+                # Get time permit from cache
+                raise tornado.gen.Return(time_permit_item.time_permit)
+
+        # No cached time permits for this mpin id, request new from D-TA
+        time_permits = yield self.get_time_permits(hash_mpin_id_hex, signature)
+        if options.cacheTimePermits:
+            self.cache_time_permits(time_permits, hash_mpin_id_hex)
+
+        # Return the one for today
+        if str(date_epoch) not in time_permits:
+            log.error("DTA /timePermit Failed. No time permit for today")
+            raise HTTPError(500)
+        raise tornado.gen.Return(time_permits[str(date_epoch)])
+
+    @tornado.gen.coroutine
+    def get(self, mpin_id):
+        # Check revocation status of mpin id.
+        if options.RPAPermitUserURL:
+            response = yield self.http_client.fetch(
+                url_concat(options.RPAPermitUserURL, {"mpin_id": mpin_id}),
+                raise_error=False)
+
+            if response.code != 200:
+                # RPA rejects this mpin id
+                raise HTTPError(response.code)
+
+        hash_mpin_id_hex = hashlib.sha256(mpin_id.decode("hex")).hexdigest()
+        today_epoch = secrets.today()
+        signature = signMessage(hash_mpin_id_hex, Keys.app_key)
+        time_permit = yield self.get_time_permit(hash_mpin_id_hex, today_epoch, signature)
+
+        self.set_header("Cache-Control", "no-cache")
+        self.finish({
+            "date": today_epoch,
+            "signature": signature,
+            "storageId": hash_mpin_id_hex,
+            'message': "M-Pin Time Permit Generated",
+            'timePermit': time_permit,
+            'version': VERSION,
+        })
+
+
+class RPSSetupDoneHandler(BaseHandler):
+    @tornado.web.asynchronous
+    @tornado.gen.engine
+    def post(self, mpinId):
+        log.debug("Setup done for mpinId: {0}".format(mpinId))
+        self.set_status(200)
+        self.finish()
+
+
+class RPSGetAccessNumberHandler(BaseHandler):
+    @tornado.web.asynchronous
+    @tornado.gen.engine
+    def post(self):
+        # Generate request for MPinWIDServer for WID
+        wId = secrets.generate_random_webid(self.application.server_secret.rng, options.accessNumberUseCheckSum)
+
+        while wId is None or (self.storage.find(stage="auth", webID=wId)):
+            if wId is None:
+                log.debug("WebId is None".format(wId))
+            else:
+                log.debug("WebId {0} already exists. Generating a new one".format(wId))
+            wId = secrets.generate_random_webid(self.application.server_secret.rng, options.accessNumberUseCheckSum)
+
+        log.debug("New webId generated: {0}." .format(wId))
+
+        webOTT = secrets.generate_ott(options.OTTLength, self.application.server_secret.rng, "hex")
+
+        nowTime = Time.syncedNow()
+        expirePinPadTime = nowTime + datetime.timedelta(seconds=options.accessNumberExpireSeconds)
+        expireTime = expirePinPadTime + datetime.timedelta(seconds=options.accessNumberExtendValiditySeconds)
+
+        self.storage.add(stage="auth", expire_time=expireTime, webOTT=webOTT, wid=wId)
+
+        params = {
+            "ttlSeconds": options.accessNumberExpireSeconds,
+            "accessNumber": wId,
+            "webOTT": webOTT,
+            "localTimeStart": Time.DateTimetoEpoch(nowTime),
+            "localTimeEnd": Time.DateTimetoEpoch(expirePinPadTime)
+        }
+
+        self.write(params)
+        self.finish()
+
+
+class RPSAccessNumberHandler(BaseHandler):
+    @tornado.web.asynchronous
+    @tornado.gen.engine
+    def post(self):
+        try:
+            data = json.loads(self.request.body)
+            webOTT = data["webOTT"]
+        except ValueError:
+            log.error("Cannot decode body as JSON.")
+            log.debug(self.request.body)
+            self.set_status(400, reason="BAD REQUEST. INVALID JSON")
+            self.finish()
+            return
+
+        I = self.storage.find(stage="auth", webOTT=webOTT)
+        if not I:
+            log.debug("Cannot find webOTT: {0}".format(webOTT))
+
+            self.set_status(404)
+            self.finish()
+            return
+
+        authOTT = I.authOTT
+        if authOTT and (str(I.status) == "200"):
+            self.write({"authOTT": authOTT})
+            self.finish()
+        else:
+            if not authOTT:
+                log.debug("authOTT not set for webOTT: {0}".format(webOTT))
+            else:
+                log.debug("Auth status for webOTT: {0}: {1}".format(webOTT, I.status))
+            self.set_status(401)
+            self.finish()
+
+
+class RPSAuthenticateHandler(BaseHandler):
+
+    @tornado.web.asynchronous
+    @tornado.gen.engine
+    def post(self):
+        try:
+            data = json.loads(self.request.body)
+            data = data["mpinResponse"]
+            authOTT = data["authOTT"]
+        except ValueError:
+            log.error("Cannot decode body as JSON.")
+            log.debug(self.request.body)
+            self.set_status(400, reason="BAD REQUEST. INVALID JSON")
+            self.finish()
+            return
+        except KeyError:
+            log.error("Invalid JSON data structure")
+            log.debug(data)
+            self.set_status(400, reason="BAD REQUEST. INVALID DATA")
+            self.finish()
+            return
+
+        I = self.storage.find(stage="auth", authOTT=authOTT)
+
+        if not I:
+            log.error("Invalid or expired authOTT")
+            status = 412
+            message = "Invalid or expired access number"
+            userId = ""
+            mpinId = ""
+            response = {"message": message}
+
+        else:
+            authToken = I.authToken
+            mpinId = authToken["mpin_id"].encode("hex")
+            identity = json.loads(authToken["mpin_id"])
+            userId = identity["userID"]
+
+            (fail, status, reason) = verifyToken(authToken)
+
+            aI = self.storage.find(stage="attempts", mpinId=mpinId)
+            attemptsCount = aI and aI.attemptsCount or 0
+            if attemptsCount >= options.maxInvalidLoginAttempts:
+                fail = 1
+
+            if fail == 0:
+                # Delete invalid login attempts if any
+                if aI:
+                    aI.delete()
+
+                # Authentication successful
+                message = "Authentication successful"
+
+                # Wait for browser authentication and get Logout information
+                I.update(status=status, message=message)
+
+                main_loop = tornado.ioloop.IOLoop.instance()
+
+                # Wait until the browser is ready or timeout occurs
+                timeOut = Time.syncedNow(seconds=MOBILE_LOGIN_AUTHENTICATION_TIMEOUT_SECONDS)
+                while (not I.browserReady) and (Time.syncedNow() < timeOut):
+                    yield tornado.gen.Task(main_loop.add_timeout, time.time() + 1)
+
+                # Get the new status. Status can be changed with the /loginResult request
+                if status != I.status:
+                    status = I.status
+                    message = I.message
+                    response = {"message": message}
+
+                elif I.browserReady:
+                    logoutURL = I.logoutURL or ""
+                    logoutData = I.logoutData or ""
+                    response = {"logoutURL": logoutURL, "logoutData": logoutData}
+                else:
+                    # Timeout occured
+                    status = 408
+                    message = "Authentication timed out"
+                    response = {"message": message}
+
+            else:
+                attemptsCount += 1
+                if aI:
+                    aI.update(attemptsCount=attemptsCount)
+                else:
+                    self.storage.add(stage="attempts", mpinId=mpinId, attemptsCount=attemptsCount)
+
+                if attemptsCount >= options.maxInvalidLoginAttempts:
+                    status = 410
+
+                log.debug("Wrong PIN for user {0}.".format(userId))
+                message = "Wrong PIN."
+                response = {"message": message}
+                I.update(status=status, message=message)
+
+        self.set_status(status, message)
+        self.write(response)
+        self.finish()
+
+
+class StatusHandler(BaseHandler):
+
+    @tornado.web.asynchronous
+    @tornado.gen.engine
+    def get(self):
+        reason = "active"
+        self.set_status(200, reason=reason)
+        self.write({'version': VERSION, 'message': reason})
+
+        self.finish()
+
+
+class DefaultHandler(BaseHandler):
+    def get(self, input):
+        reason = "URI NOT FOUND"
+        self.set_status(404, reason=reason)
+        self.write({'version': VERSION, 'service_name': 'M-Pin authentication server', 'message': reason})
+        return
+
+    def post(self, input):
+        reason = "URI NOT FOUND"
+        self.set_status(404, reason=reason)
+        self.write({'version': VERSION, 'service_name': 'M-Pin authentication server', 'message': reason})
+        return
+
+    def put(self, input):
+        reason = "URI NOT FOUND"
+        self.set_status(404, reason=reason)
+        self.write({'version': VERSION, 'service_name': 'M-Pin authentication server', 'message': reason})
+        return
+
+    def delete(self, input):
+        reason = "URI NOT FOUND"
+        self.set_status(404, reason=reason)
+        self.write({'version': VERSION, 'service_name': 'M-Pin authentication server', 'message': reason})
+        return
+
+
+# AUTHENTICATION HANDLER
+class Pass1Handler(BaseHandler):
+    """
+    ..  apiTextStart
+
+    *Description*
+
+      Implements the first pass of the M-Pin Protocol
+
+    *URL structure*
+
+      ``/pass1``
+
+    *Version*
+
+      0.3
+
+    *HTTP Request Method*
+
+      POST
+
+    *Request Data*
+
+      JSON request::
+
+        {
+          "mpin_id":  "7b22...227d",
+          "U":    "0409...3d9c",
+          "UT":   "0402...d1d1",
+          "pass" : 1
+        }
+
+      mpin_id is the hex encoded M-Pin ID, U is x.hash(mpin_id) and UT is
+      x.(hash(mpin_id) + hash(data||hash(mpin_id)))
+
+    *Returns*
+
+      JSON response::
+
+        {
+          "y" : "212a...8d08",
+          "version" : "0.3",
+          "message" : "OK",
+          "pass" : 1
+        }
+
+      y  is a 256 bit random value.
+
+    *Status-Codes and Response-Phrases*
+
+      ::
+
+        Status-Code          Response-Phrase
+
+        200                  OK
+        403                  Invalid data received. <argument> argument missing
+        403                  Invalid data received. No JSON object could be decoded
+        403                  Invalid data received. Non-hexadecimal digit found
+        500                  Failed to generate y
+        500                  Failed to add pass one to memory
+
+    ..  apiTextEnd
+
+    """
+    def post(self):
+        # Remote request information
+        if 'User-Agent' in self.request.headers.keys():
+            UA = self.request.headers['User-Agent']
+        else:
+            UA = 'unknown'
+        request_info = '%s %s %s %s ' % (self.request.path, self.request.remote_ip, UA, Time.syncedISO())
+
+        try:
+            receive_data = tornado.escape.json_decode(self.request.body)
+            mpin_id = receive_data['mpin_id'].decode("hex")
+            ut_hex = receive_data['UT']
+            u_hex = receive_data['U']
+        except KeyError as ex:
+            reason = "Invalid data received. %s argument missing" % ex.message
+            log.error("%s %s" % (request_info, reason))
+            self.set_status(403, reason=reason)
+            self.content_type = 'application/json'
+            self.write({'version': VERSION, 'message': reason})
+            self.finish()
+            return
+        except (ValueError, TypeError) as ex:
+            reason = "Invalid data received. %s" % ex.message
+            log.error("%s %s" % (request_info, reason))
+            self.set_status(403, reason=reason)
+            self.content_type = 'application/json'
+            self.write({'version': VERSION, 'message': reason})
+            self.finish()
+            return
+        log.debug("%s %s" % (request_info, receive_data))
+
+        # Server generates Random number Y and sends it to Client
+        try:
+            y_hex = self.application.server_secret.get_pass1_value()
+        except secrets.SecretsError as e:
+            log.error(e.message)
+            self.set_status(500, reason=e.message)
+            self.content_type = 'application/json'
+            self.write({'version': VERSION, 'message': e.message})
+            self.finish()
+            return
+
+        # Store Pass1 values
+        self.storage.add(
+            expire_time=Time.syncedISO(seconds=PASS1_EXPIRES_TIME),
+            stage="pass1",
+            mpinId=mpin_id.encode('hex'),
+            ut=ut_hex,
+            u=u_hex,
+            y=y_hex,
+        )
+
+        log.info("%s Stored Pass1 values" % request_info)
+
+        reason = "OK"
+        self.set_status(200, reason=reason)
+        self.content_type = 'application/json'
+        return_data = {
+            'version': VERSION,
+            'y': y_hex,
+            'pass': 1,
+            'message': reason
+        }
+        log.debug("%s %s" % (request_info, return_data))
+        self.write(return_data)
+        self.finish()
+        return
+
+
+class Pass2Handler(BaseHandler):
+    """
+    ..  apiTextStart
+
+    *Description*
+
+      Implements the second pass of the M-Pin Protocol. The result will be the authOTP.
+      At this point the authentication token has also been written to the RPS.
+      An authOTT will always be returned even if authentication fails.
+
+    *URL structure*
+
+      ``/pass2``
+
+    *Version*
+
+      0.3
+
+    *HTTP Request Method*
+
+      POST
+
+    *Request Data*
+
+      JSON request::
+
+        {
+          "WID" : "123456"
+          "V" : "0411...05f6a",
+          "pass" : 2,
+          "OTP" : <1||0>
+        }
+
+      WID is web identifier used for mobile authentication
+      When OTP is set to one this indicates that the radius OTP should be
+      generated.  V is a parameter used to perform the final step of the M-Pin
+      algorithm.
+
+    *Returns*
+
+      JSON response::
+
+        {
+          "OTP": "155317",
+          "authOTT": "31ba0ed5efb75d91ef69a2b7eb1d3a26",
+          "pass": 2,
+          "version": "0.3"
+        }
+
+      OTP is the radius one time password. authOTT is the password used to log into the
+      Customer's website.
+
+    *Status-Codes and Response-Phrases*
+
+      ::
+
+        Status-Code          Response-Phrase
+
+        200                  OK
+        403                  Invalid data received. <argument> argument missing
+        403                  Invalid data received. No JSON object could be decoded
+        403                  Invalid data received. Non-hexadecimal digit found
+        500                  Pass one data is not in memory
+
+    ..  apiTextEnd
+
+    """
+    @tornado.gen.coroutine
+    def post(self):
+        # Remote request information
+        if 'User-Agent' in self.request.headers.keys():
+            UA = self.request.headers['User-Agent']
+        else:
+            UA = 'unknown'
+        request_info = '%s %s %s %s ' % (self.request.path, self.request.remote_ip, UA, Time.syncedISO())
+
+        try:
+            receive_data = tornado.escape.json_decode(self.request.body)
+            mpin_id_hex = receive_data['mpin_id']
+            mpin_id = mpin_id_hex.decode('hex')
+            WID = receive_data['WID']
+            OTPEn = receive_data['OTP']
+            v_data = receive_data['V'].decode("hex")
+        except KeyError as ex:
+            reason = "Invalid data received. %s argument missing" % ex.message
+            log.error("%s %s" % (request_info, reason))
+            self.set_status(403, reason=reason)
+            self.content_type = 'application/json'
+            self.write({'version': VERSION, 'message': reason})
+            self.finish()
+            return
+        except (ValueError, TypeError) as ex:
+            reason = "Invalid data received. %s" % ex.message
+            log.error("%s %s" % (request_info, reason))
+            self.set_status(403, reason=reason)
+            self.content_type = 'application/json'
+            self.write({'version': VERSION, 'message': reason})
+            self.finish()
+            return
+        log.debug("%s %s" % (request_info, receive_data))
+
+        # Get pass one values
+        pass1Value = self.storage.find(stage="pass1", mpinId=mpin_id_hex)
+
+        if pass1Value:
+            u = pass1Value.u.decode("hex")
+            ut = pass1Value.ut.decode("hex")
+            y = pass1Value.y.decode("hex")
+        else:
+            reason = "Invalid pass one data"
+            log.error("%s %s" % (request_info, reason))
+            self.set_status(500, reason=reason)
+            self.content_type = 'application/json'
+            self.write({'version': VERSION, 'message': reason})
+            self.finish()
+            return
+        log.info("%s loaded Pass1 values" % request_info)
+
+        # Generate OTP value
+        if int(OTPEn) == 1:
+            OTP = "{0:06d}".format(
+                secrets.generate_otp(self.application.server_secret.rng))
+        else:
+            OTP = '0'
+
+        log.info("%s generate OTP" % request_info)
+
+        successCode = self.application.server_secret.validate_pass2_value(
+            mpin_id, u, ut, y, v_data)
+
+        pinError = 0
+        pinErrorCost = 0
+
+        # Authentication Token expiry
+        expires = Time.syncedISO(seconds=SIGNATURE_EXPIRES_OFFSET_SECONDS)
+
+        # Form Authentication token
+        token = {
+            "mpin_id": mpin_id,
+            "mpin_id_hex": mpin_id_hex,
+            "successCode": successCode,
+            "pinError": pinError,
+            "pinErrorCost": pinErrorCost,
+            "expires": expires,
+            "WID": WID,
+            "OTP": OTP
+        }
+        log.debug("%s M-Pin Auth token: %s" % (request_info, token))
+
+        # Form authentication 128 hex encoded One Time Password
+        authOTT = secrets.generate_auth_ott(self.application.server_secret.rng)
+
+        # Form message to return to client #
+        return_data = {
+            'version': VERSION,
+            'pass': 2,
+            'authOTT': authOTT
+        }
+
+        if int(OTPEn) == 1:
+            return_data['OTP'] = OTP
+
+        if WID != "0":
+            # Login with mobile
+            I = self.storage.find(stage="auth", wid=WID)
+
+            wid_flow = "wid"
+            flow = "mobile"
+
+            # if not I:
+            #     log.error("Invalid or expired access number: {0} for mpinid: {1}".format(WID, mpinId))
+            #     self.set_status(412, reason="INVALID OR EXPIRED ACCESS NUMBER")
+            #     self.finish()
+            #     return
+
+            if I:
+                I.update(authOTT=authOTT, mpinid=mpin_id, authToken=token)
+
+        else:
+            wid_flow = "browser"
+
+            if int(token.get("OTP", "0")) != 0:
+                flow = "OTP"
+            else:
+                flow = "Browser"
+
+            self.storage.add(
+                expire_time=Time.ISOtoDateTime(expires),
+                stage="auth",
+                authOTT=authOTT,
+                mpinId=mpin_id,
+                wid="",
+                webOTT=0,
+                authToken=token
+            )
+
+        log.debug("New M-Pin Authentication token / {0}. Flow: {1}".format(wid_flow, flow))
+
+        # Always send 200 to PIN Pad even if the user is not authenticated
+        reason = "OK"
+        log.debug("%s %s" % (request_info, return_data))
+        self.set_status(200, reason=reason)
+        self.content_type = 'application/json'
+        self.write(return_data)
+        self.finish()
+        return
+
+
+# PRIVATE HANDLERS
+class ManageGetStackInfoHandler(PrivateBaseHandler):
+
+    @tornado.web.asynchronous
+    @tornado.gen.engine
+    def get(self):
+        # Get signed API settings
+
+        path = "apiSettings"
+        expires = Time.syncedISO(seconds=SIGNATURE_EXPIRES_OFFSET_SECONDS)
+        M = "{0}{1}{2}" .format(path, Keys.app_id, expires)
+        signature_hex = signMessage(M, Keys.app_key)
+
+        param_values = {
+            'app_id': Keys.app_id,
+            'expires': expires,
+            'signature': signature_hex,
+        }
+
+        url = "{0}/{1}".format(Keys.api_url.rstrip("/"), path)
+        urlParams = url_concat(url, param_values)
+
+        client = tornado.httpclient.AsyncHTTPClient()
+        response = yield tornado.gen.Task(client.fetch, urlParams, method="GET")
+
+        status = 200
+
+        if (response.error):
+            log.error("API Request Error URL: {0}: {1}: {2}".format(url, response.error, response.body))
+            status = 500
+            self.set_status(status)
+            self.finish()
+            return
+
+        resp_data = None
+        try:
+            resp_data = json.loads(response.body)
+        except ValueError:
+            log.error("Cannot decode JSON response from API {0}: {1}".format(url, response.body))
+            status = 500
+            self.set_status(status)
+            self.finish()
+            return
+
+        expires = Time.syncedISO(hours=1)
+        M = "{0}{1}" .format(Keys.app_id, expires)
+        signature = signMessage(M, Keys.app_key)
+
+        manage_auth_params = {
+            "signature": signature,
+            "expires": expires,
+            "app_id": Keys.app_id
+        }
+
+        result = {
+            "managementConsoleURL": resp_data.get("managementConsoleURL", ""),
+            "license_info": resp_data.get("license_info", {}),
+            "manage_auth": manage_auth_params
+        }
+
+        self.write(result)
+        self.finish()
+
+
+class UserHandler(PrivateBaseHandler):
+    def post(self, mpinId):
+        log.debug("Request for activationg mpinid: {0}".format(mpinId))
+
+        try:
+            data = json.loads(self.request.body)
+            activateKey = data["activateKey"]
+        except:
+            log.error("Invalid JSON request: {0}".format(self.request.body))
+            log.debug(self.request.body)
+            self.set_status(400, reason="BAD REQUEST. INVALID JSON")
+            self.finish()
+            return
+
+        I = self.storage.find(stage="register", mpinId=mpinId)
+        if not I:
+            log.debug("MpinID {0} not found.".format(mpinId))
+            self.set_status(401, "M-Pin ID not found.")
+            self.finish()
+            return
+
+        I.update(active=activateKey)
+        self.set_status(200)
+        self.finish()
+
+
+class AuthenticateHandler(PrivateBaseHandler):
+
+    @tornado.web.asynchronous
+    @tornado.gen.engine
+    def post(self):
+        OTP = ""
+
+        try:
+            data = json.loads(self.request.body)
+            authOTT = data["authOTT"]
+        except ValueError:
+            log.error("Cannot decode body as JSON.")
+            log.debug(self.request.body)
+            self.set_status(400, reason="BAD REQUEST. INVALID JSON")
+            self.finish()
+            return
+        except KeyError:
+            log.error("Invalid JSON data structure")
+            log.debug(data)
+            self.set_status(400, reason="BAD REQUEST. INVALID DATA")
+            self.finish()
+            return
+
+        I = self.storage.find(stage="auth", authOTT=authOTT)
+
+        if not I:
+            log.error("Invalid or expired authOTT: {0}".format(authOTT))
+            status = 408
+            message = "Expired authentication request"
+            userId = ""
+            mpinId = ""
+        else:
+            authToken = I.authToken
+            mpinId = authToken["mpin_id"].encode("hex")
+            identity = json.loads(authToken["mpin_id"])
+            userId = identity["userID"]
+
+            if authToken.get("OTP", "0") != "0":
+                OTP = authToken["OTP"]
+
+            log.debug("authToken: {0}".format(authToken))
+
+            if I.status:
+                # Mobile authentication, status already set
+                status = I.status
+                message = I.message
+
+                # get logout data
+                logoutURL = data.get("logoutURL") or options.LogoutURL
+                logoutData = data.get("logoutData")
+                # If logoutURL is set, the mobile app will make a request to that URL
+                # If logoutData is set, the request method will be POST otherwise it will be GET
+
+                # If option waitForLoginResult is set, browserReady flag will be set on /loginResult request
+                # from the RPA
+                browserReady = (not options.waitForLoginResult)
+
+                I.update(logoutData=logoutData, logoutURL=logoutURL, browserReady=browserReady)
+
+                if not options.waitForLoginResult:
+                    I.delete()
+            else:
+                (fail, status, reason) = verifyToken(authToken)
+
+                aI = self.storage.find(stage="attempts", mpinId=mpinId)
+                log.debug("aI: {0}".format(aI))
+                attemptsCount = aI and aI.attemptsCount or 0
+                log.debug("attemptsCount: {0}".format(attemptsCount))
+                if attemptsCount >= options.maxInvalidLoginAttempts:
+                    fail = 1
+
+                if fail == 0:
+                    # Authentication successful
+                    message = "Authentication successful"
+                    if aI:
+                        aI.delete()
+                else:
+                    attemptsCount += 1
+                    if aI:
+                        aI.update(attemptsCount=attemptsCount)
+                    else:
+                        self.storage.add(stage="attempts", mpinId=mpinId, attemptsCount=attemptsCount)
+
+                    if attemptsCount >= options.maxInvalidLoginAttempts:
+                        status = 410
+
+                    log.debug("Wrong PIN for user {0}.".format(userId))
+                    message = "Wrong PIN."
+
+                if not options.waitForLoginResult:
+                    I.delete()
+
+        returnData = {
+            "userId": userId,
+            "mpinId": mpinId,
+            "status": status,
+            "message": message
+        }
+
+        if OTP:
+            returnData["OTP"] = OTP
+
+        self.set_status(status, message)
+        self.write(returnData)
+        self.finish()
+
+
+class LoginResultHandler(PrivateBaseHandler):
+
+    def post(self):
+        if not options.waitForLoginResult:
+            self.set_status(404)
+            self.finish()
+            return
+
+        try:
+            data = json.loads(self.request.body)
+            authOTT = data["authOTT"]
+            status = data["status"]
+        except ValueError:
+            log.error("Cannot decode body as JSON.")
+            log.debug(self.request.body)
+            self.set_status(400, reason="BAD REQUEST. INVALID JSON")
+            self.finish()
+            return
+        except KeyError:
+            log.error("Invalid JSON data structure")
+            log.debug(data)
+            self.set_status(400, reason="BAD REQUEST. INVALID DATA")
+            self.finish()
+            return
+
+        I = self.storage.find(stage="auth", authOTT=authOTT)
+
+        if not I:
+            log.error("Invalid or expired authOTT")
+            self.set_status(408, reason="Invalid or expired authOTT")
+            self.finish()
+            return
+
+        if int(status) != 200:
+            I.update(status=status, message=data.get("message", I.message), browserReady=True)
+        else:
+            # Get the logout data
+            # Logout data can be set on the previous /authenticate request as well.
+            logoutURL = data.get("logoutURL") or I.logoutURL or options.LogoutURL
+            logoutData = data.get("logoutData") or I.logoutData
+            # If logoutURL is set, the mobile app will make a request to that URL
+            # If logoutData is set, the request method will be POST otherwise it will be GET
+
+            I.update(logoutData=logoutData, logoutURL=logoutURL, browserReady=True)
+
+        I.delete()
+
+
+class DynamicOptionsHandler(PrivateBaseHandler):
+    @tornado.web.asynchronous
+    def post(self):
+        if options.dynamicOptionsURL:
+            process_dynamic_options(
+                DYNAMIC_OPTION_MAPPING,
+                DYNAMIC_OPTION_HANDLERS,
+                application=self.application)
+            self.set_status(200, 'OK')
+        else:
+            self.set_status(403, 'Dynamic options are disabled')
+        self.finish()
+
+    def get(self):
+        if options.dynamicOptionsURL:
+            self.set_status(200, 'OK')
+            self.write(generate_dynamic_options(DYNAMIC_OPTION_MAPPING))
+        else:
+            self.set_status(403, 'Dynamic options are disabled')
+
+
+class MobileConfigHandler(BaseHandler):
+    def get(self):
+        if not options.mobileConfig:
+            self.set_status(403, 'No config is available')
+        elif not options.mobileUseNative:
+            self.set_status(404, 'Native client is disabled')
+        else:
+            self.set_status(200, 'OK')
+            self.write(json.dumps(options.mobileConfig))
+
+
+# MAIN
+class Application(tornado.web.Application):
+    def __init__(self):
+        rpsPrefix = options.rpsPrefix.strip("/")
+        handlers = [
+            (r"/user/([0-9A-Fa-f]+)", UserHandler),  # POST
+            (r"/{0}/user(/?[0-9A-Fa-f]*)".format(rpsPrefix), RPSUserHandler),  # PUT
+            (r"/{0}/signature/([0-9A-Fa-f]+)".format(rpsPrefix), RPSSignatureHandler),  # GET
+            (r"/{0}/timePermit/([0-9A-Fa-f]+)".format(rpsPrefix), RPSTimePermitHandler),  # GET
+            (r"/{0}/setupDone/([0-9A-Fa-f]+)".format(rpsPrefix), RPSSetupDoneHandler),  # POST
+            (r"/{0}/accessnumber".format(rpsPrefix), RPSAccessNumberHandler),  # POST
+            (r"/{0}/getAccessNumber".format(rpsPrefix), RPSGetAccessNumberHandler),  # POST
+            (r"/{0}/clientSettings".format(rpsPrefix), ClientSettingsHandler),
+            (r"/{0}/authenticate".format(rpsPrefix), RPSAuthenticateHandler),  # POST, for mobile login
+            # Authentication
+            (r"/{0}/pass1".format(rpsPrefix), Pass1Handler),
+            (r"/{0}/pass2".format(rpsPrefix), Pass2Handler),
+
+            (r"/authenticate", AuthenticateHandler),  # POST
+
+            (r"/manage/getStackInfo", ManageGetStackInfoHandler),  # GET
+
+            (r"/loginResult", LoginResultHandler),  # POST
+
+            (r"/status", StatusHandler),
+            (r"/dynamicOptions", DynamicOptionsHandler),  # POST, GET
+            (r"/{0}/mobileConfig".format(rpsPrefix), MobileConfigHandler),  # GET
+            (r"/(.*)", DefaultHandler),
+        ]
+        settings = {}
+        super(Application, self).__init__(handlers, **settings)
+
+        Seed.getSeed(options.EntropySources)  # Get seed value for random number generator
+        self.server_secret = secrets.ServerSecret(
+            Seed.seedValue,
+            Keys.app_id,
+            Keys.app_key)
+
+        log.debug("Using storage: {0}".format(options.storage))
+        storage_cls = get_storage_cls()
+        self.storage = storage_cls(
+            tornado.ioloop.IOLoop.instance(),
+            "stage,mpinId",
+            "stage,authOTT",
+            "stage,wid",
+            "stage,webOTT",
+            "time_permit_id,time_permit_date"
+        )
+
+
+def main():
+    options.parse_command_line()
+
+    if os.path.exists(options.configFile):
+        try:
+            options.parse_config_file(options.configFile)
+            options.parse_command_line()
+        except Exception, E:
+            print("Invalid config file {0}".format(options.configFile))
+            print(E)
+            sys.exit(1)
+
+    # Set Log level
+    log.setLevel(getLogLevel(options.logLevel))
+
+    detectProxy()
+
+    # Load the credentials from file
+    log.info("Loading credentials")
+    try:
+        credentialsFile = options.credentialsFile
+        Keys.loadFromFile(credentialsFile)
+    except Exception as E:
+        log.error("Error opening the credentials file: {0}".format(credentialsFile))
+        log.error(E)
+        sys.exit(1)
+
+    # TMP fix for 'ValueError: I/O operation on closed epoll fd'
+    # Fixed in Tornado 4.2
+    tornado.ioloop.IOLoop.instance()
+
+    # Sync time to CertiVox time server
+    if options.syncTime:
+        Time.getTime(wait=True)
+
+    Keys.getAPISettings(wait=True)
+
+    log.info("Server starting on {0}:{1}...".format(options.address, options.port))
+
+    http_server = Application()
+    http_server.listen(options.port, options.address, xheaders=True)
+    main_loop = tornado.ioloop.IOLoop.instance()
+    http_server.io_loop = main_loop
+
+    if options.autoReload:
+        log.debug("Starting autoreloader")
+
+        tornado.autoreload.watch(CONFIG_FILE)
+        tornado.autoreload.start(main_loop)
+
+    process_dynamic_options(
+        DYNAMIC_OPTION_MAPPING,
+        DYNAMIC_OPTION_HANDLERS,
+        application=http_server,
+        initial=True)
+
+    log.info("Server started. Listening on {0}:{1}".format(options.address, options.port))
+    main_loop.start()
+
+
+class ServiceDaemon(Daemon):
+    def run(self):
+        main()
+
+
+if __name__ == "__main__":
+    if len(sys.argv) > 1 and sys.argv[1].lower() in ("start", "stop"):
+        action = sys.argv.pop(1)
+        logFile = os.path.join(BASE_DIR, "rps.log")
+        pidFile = os.path.join(BASE_DIR, "rps.pid")
+
+        daemon = ServiceDaemon(pidfile=pidFile, stdout=logFile, stderr=logFile)
+        if action == "start":
+            log.info("Starting as daemon. Log file: {0}".format(logFile))
+            daemon.start()
+        elif action == "stop":
+            log.info("Stopping daemon...")
+            daemon.stop()
+            sys.exit()
+    else:
+        try:
+            main()
+        except Exception as e:
+            log.error(str(e))
+            sys.exit(1)
diff --git a/servers/rps/rpsservice.py b/servers/rps/rpsservice.py
new file mode 100755
index 0000000..43c18b4
--- /dev/null
+++ b/servers/rps/rpsservice.py
@@ -0,0 +1,29 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import os
+import sys
+
+if (os.name != "nt"):
+    print "This file can be run on Windows."
+    sys.exit(1)
+
+from mpWinService import installService
+import rps
+
+
+installService(rps.ServiceDaemon, 'mpinRPS', 'M-Pin RPS Service')
diff --git a/servers/rps/test_dynamic_options.py b/servers/rps/test_dynamic_options.py
new file mode 100644
index 0000000..7a035ed
--- /dev/null
+++ b/servers/rps/test_dynamic_options.py
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import print_function, unicode_literals
+from dynamic_options import _test_dynamic_options_update
+from rps import DYNAMIC_OPTION_MAPPING
+
+
+def test_enable_sync():
+
+    _test_dynamic_options_update(
+        initial_options={'syncTime': False, 'timePeriod': 1000},
+        response_body='{"time_synchronization": true}',
+        expected_options={'syncTime': True, 'timePeriod': 1000},
+        local_mapping=DYNAMIC_OPTION_MAPPING)
+
+
+def test_change_period():
+    _test_dynamic_options_update(
+        initial_options={'syncTime': True, 'timePeriod': 1000},
+        response_body='{"time_synchronization_period": 2000}',
+        expected_options={'syncTime': True, 'timePeriod': 2000},
+        local_mapping=DYNAMIC_OPTION_MAPPING)
+
+
+def test_disable_sync():
+    _test_dynamic_options_update(
+        initial_options={'syncTime': True, 'timePeriod': 1000},
+        response_body='{"time_synchronization": false}',
+        expected_options={'syncTime': False, 'timePeriod': 1000},
+        local_mapping=DYNAMIC_OPTION_MAPPING)
+
+
+def test_mobile_change():
+    _test_dynamic_options_update(
+        initial_options={
+            "mobileUseNative": True,
+            "mobileConfig": [{
+                "mobile_otp_name": "Foo",
+                "mobile_otp_url": "foo",
+                "mobile_otp_type": "otpa",
+                "mobile_online_name": "Spam",
+                "mobile_online_url": "spam",
+                "mobile_online_type": "onlinea",
+            }],
+        },
+        response_body="""
+        {
+            "mobile_use_native": false,
+            "mobile_client_config": [{
+                "mobile_otp_name": "Bar",
+                "mobile_otp_url": "bar",
+                "mobile_otp_type": "otpb",
+                "mobile_online_name": "Eggs",
+                "mobile_online_url": "eggs",
+                "mobile_online_type": "onlineb"
+            }]
+        }
+        """,
+        expected_options={
+            "mobileUseNative": False,
+            "mobileConfig": [{
+                "mobile_otp_name": "Bar",
+                "mobile_otp_url": "bar",
+                "mobile_otp_type": 'otpb',
+                "mobile_online_name": "Eggs",
+                "mobile_online_url": "eggs",
+                "mobile_online_type": "onlineb",
+            }],
+        },
+        local_mapping=DYNAMIC_OPTION_MAPPING)
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..ca130b7
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from setuptools import setup, find_packages
+
+setup(
+    name="M-Pin Backend",
+    version="0.1",
+    packages=find_packages(),
+    install_requires=[
+        'cffi==0.9.0',
+        'pbkdf2==1.3',
+        'python-dateutil==2.4.2',
+        'redis==2.10.3',
+        'tornado==4.1',
+    ],
+    author="CertiVox",
+    author_email="support@miracl.com",
+    description="M-Pin Backend services",
+    url="https://github.com/CertiVox/mpin-backend",
+)
diff --git a/tests/test_crypto.py b/tests/test_crypto.py
new file mode 100644
index 0000000..a26c7c6
--- /dev/null
+++ b/tests/test_crypto.py
@@ -0,0 +1,138 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Unit tests for crypto mapping functions."""
+import calendar
+import datetime
+import time
+
+from pbkdf2 import PBKDF2
+
+import mpin
+import crypto
+
+
+def test_today():
+    """Get today."""
+    today = crypto.today()
+    assert type(today) == int
+    assert today == calendar.timegm(time.gmtime()) / (60 * 1440)
+
+
+def test_auth_success():
+    """Test the basic authentication flow."""
+    seed = open('/dev/urandom', 'rb').read(100)
+    RNG = crypto.get_random_generator(seed)
+
+    y = crypto.mpin_random_generate(RNG)
+
+    date = 16238
+    PIN = 1234
+    mpin_id = '{"mobile": 1, "issued": "2013-10-19T06:12:28Z", "userID": "testUser0@certivox.com", "salt": "e0842acc8cc38fc4"}'
+
+    token = "040d73e9b7c746525edbb90e042a6b8f6e41e2417a0c0c1600f8b693c0b6c0bbce0c61b73100798c7505b3eb12c2393187355145090333f904fd896684ec4990d3".decode("hex")
+    timePermit = "0408ba13483e817626a45be598b1b89296aa805a6aa31e98503321c30d7177711a1c98a77cbf8edf2497d0f5f7593c72457b36cd3e4f19e1f3c5636c0ca7a1817d".decode("hex")
+    serverSecret = "0d5a0bb4621c4eec9ee28b0339047e7afdaae87c5f0f972253f2e90f55dbda4a16efc98cb3b925d4237a14527b1db361f460dae271f115c28ff5f3ef5fb5dae20a8ccc12bea3fc3f5911853eff3642e649140fcf0892a13ec8b22e94a750a2930c64f5792a22bc01580cbd041c7a8c21659abacead12fd4460f17b27f5940d4d".decode("hex")
+
+    # Client part
+    Y = mpin.ffi.new("octet*")
+    Yval = mpin.ffi.new("char [%s]" % len(y), y)
+    Y[0].val = Yval
+    Y[0].max = len(y)
+    Y[0].len = len(y)
+
+    MPIN_ID = mpin.ffi.new("octet*")
+    MPIN_IDval = mpin.ffi.new("char [%s]" % len(mpin_id), mpin_id)
+    MPIN_ID[0].val = MPIN_IDval
+    MPIN_ID[0].max = len(mpin_id)
+    MPIN_ID[0].len = len(mpin_id)
+
+    TOKEN = mpin.ffi.new("octet*")
+    TOKENval = mpin.ffi.new("char [%s]" % len(token), token)
+    TOKEN[0].val = TOKENval
+    TOKEN[0].len = len(token)
+    TOKEN[0].max = len(token)
+
+    X = mpin.ffi.new("octet*")
+    Xval = mpin.ffi.new("char []", mpin.PGS)
+    X[0].val = Xval
+    X[0].max = mpin.PGS
+    X[0].len = mpin.PGS
+
+    CLIENT_SECRET = mpin.ffi.new("octet*")
+    CLIENT_SECRETval = mpin.ffi.new("char []", mpin.G1)
+    CLIENT_SECRET[0].val = CLIENT_SECRETval
+    CLIENT_SECRET[0].max = mpin.G1
+    CLIENT_SECRET[0].len = mpin.G1
+
+    U = mpin.ffi.new("octet*")
+    Uval = mpin.ffi.new("char []", mpin.G1)
+    U[0].val = Uval
+    U[0].max = mpin.G1
+    U[0].len = mpin.G1
+
+    UT = mpin.ffi.new("octet*")
+    UTval = mpin.ffi.new("char []", mpin.G1)
+    UT[0].val = UTval
+    UT[0].max = mpin.G1
+    UT[0].len = mpin.G1
+
+    TIMEPERMIT = mpin.ffi.new("octet*")
+    TIMEPERMITval = mpin.ffi.new("char [%s]" % len(timePermit), timePermit)
+    TIMEPERMIT[0].val = TIMEPERMITval
+    TIMEPERMIT[0].len = len(timePermit)
+    TIMEPERMIT[0].max = len(timePermit)
+
+    # Client first pass
+    rtn = mpin.libmpin.MPIN_CLIENT_1(date, MPIN_ID, RNG, X, PIN, TOKEN, CLIENT_SECRET, U, UT, TIMEPERMIT)
+    assert rtn == 0
+
+    # Client second pass
+    rtn = mpin.libmpin.MPIN_CLIENT_2(X, Y, CLIENT_SECRET)
+    assert rtn == 0
+
+    # Server second pass
+    hid, htid = crypto.mpin_server_1(mpin_id, date)
+    success_code, _, _ = crypto.mpin_server_2(
+        serverSecret,
+        mpin.toHex(CLIENT_SECRET).decode('hex'),
+        date, hid, htid,
+        mpin.toHex(Y).decode('hex'),
+        mpin.toHex(U).decode('hex'),
+        mpin.toHex(UT).decode('hex'))
+    assert success_code == 0
+
+
+def test_encrypt_decrypt_master_secret():
+    """Test encryption/decription of master secret."""
+    seed = open('/dev/urandom', 'rb').read(100)
+
+    rng = crypto.get_random_generator(seed)
+    now = datetime.datetime.now()
+
+    aes_key = PBKDF2('passphrase', 'salt').read(16)
+
+    ciphertext_hex, iv_hex, tag_hex = crypto.aes_gcm_encrypt('master_secret', aes_key, rng, now.strftime('%Y-%m-%dT%H:%M:%SZ'))
+
+    tag, plaintext = crypto.aes_gcm_decrypt(
+        aes_key=aes_key,
+        iv=str(iv_hex.decode('hex')),
+        header=str(now.strftime('%Y-%m-%dT%H:%M:%SZ')),
+        ciphertext=str(ciphertext_hex.decode('hex')))
+
+    assert tag == tag_hex
+    assert plaintext.decode('hex') == 'master_secret'
diff --git a/tests/test_signing.py b/tests/test_signing.py
new file mode 100644
index 0000000..261cd8b
--- /dev/null
+++ b/tests/test_signing.py
@@ -0,0 +1,46 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from mpin_utils.common import signMessage, verifySignature
+
+
+def test_signing_valid():
+    message = 'Hello world!'
+    key = 'super secret'
+    expected_signature = 'f577954ea54f8e8cc1b7d5d238dde635a783a3a37a4ba44877e9f63269cd4b53'
+
+    signature = signMessage(message, key)
+
+    assert signature == expected_signature
+
+    valid, reason, code = verifySignature(message, signature, key)
+
+    assert valid
+    assert reason == 'Valid signature'
+    assert code == 200
+
+
+def test_signing_invalid():
+    message = 'Hello world!'
+    key = 'super secret'
+    signature = 'invalid signature'
+
+    valid, reason, code = verifySignature(message, signature, key)
+
+    assert not valid
+    assert reason == 'Invalid signature'
+    assert code == 401
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..c3a113b
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,23 @@
+[tox]
+envlist = py26, py27
+
+[testenv]
+commands = ./runtests.sh
+deps = -rrequirements/dev.txt
+passenv = PYTHONPATH LD_LIBRARY_PATH
+
+[pytest]
+addopts =
+    -rsxX
+    -q
+    --ff
+    --strict
+norecursedirs =
+    .tox
+pep8ignore =
+    *.py E501
+    servers/rps/rpsservice.py E402
+    servers/dta/dtaservice.py E402
+    servers/demo/demoservice.py E402
+flakes-ignore =
+    config.py ImportStarUsed