add eMpin Authentication logic
diff --git a/.gitignore b/.gitignore
index 68d56e7..7fe4105 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@
credentials.json
M_Pin_Backend.egg-info/
mpin_*_storage.json
+*~
diff --git a/lib/mpin_utils/secrets.py b/lib/mpin_utils/secrets.py
index 66d9c92..12f987e 100644
--- a/lib/mpin_utils/secrets.py
+++ b/lib/mpin_utils/secrets.py
@@ -21,6 +21,7 @@
import json
import os
import time
+import math
from pbkdf2 import PBKDF2
@@ -136,6 +137,43 @@
return generate_random_number(rng, crypto.PAS)
+def get_random_bytes(rng, byte_length = 4):
+ """Return random byte-array."""
+ if type(byte_length) is not int:
+ return None
+ if byte_length <= 0:
+ return None
+
+ r_hex = crypto.random_generate(rng, byte_length)
+
+ bytes = []
+ for idx in range(len(r_hex) - 2, -1, -2):
+ bytes.append(int(r_hex[idx:idx + 2], 16))
+
+ return bytes
+
+
+def get_random_integer(rng, max_digit = 4):
+ """Return random integer."""
+ if type(max_digit) is not int:
+ return None
+ if max_digit <= 0:
+ return None
+
+ mod_val = 10 ** max_digit
+ byte_length = int(math.ceil(max_digit * math.log(10, 2)/8))
+
+ bytes = get_random_bytes(rng, byte_length)
+
+ r = 0
+ for idx in range(byte_length - 1, -1, -1):
+ r = (r << 8) + bytes[idx]
+ r = r % mod_val
+
+ return r
+
+
+
class SecretsError(Exception):
"""Exception raises by secrets module."""
diff --git a/servers/demo/mailer.py b/servers/demo/mailer.py
index 1647c31..9fc2b77 100755
--- a/servers/demo/mailer.py
+++ b/servers/demo/mailer.py
@@ -46,7 +46,7 @@
# 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):
+def mailerThread(recipientAddress, subject, deviceName, replacementText, emailTemplate, user=None, password=None):
if not smtpServer:
return
msg = MIMEMultipart('alternative')
@@ -55,10 +55,10 @@
msg['To'] = recipientAddress
# Text version (in case the mail client does not like the HTML part).
- mailText = render_template('activation_email.txt', validationURL=validationURL)
+ mailText = render_template(emailTemplate + '.txt', validationURL=replacementText, activationCodeStr=replacementText)
# HTML version
- mailHTML = render_template('activation_email.html', validationURL=validationURL)
+ mailHTML = render_template(emailTemplate + '.html', validationURL=replacementText, activationCodeStr=replacementText)
mailPartText = MIMEText(mailText, 'plain')
mailPartHTML = MIMEText(mailHTML, 'html')
@@ -79,5 +79,15 @@
def sendActivationEmail(recipientAddress, subject, deviceName, validationURL, user=None, password=None):
- thread = Thread(target=mailerThread, args=(recipientAddress, subject, deviceName, validationURL, user, password))
+ thread = Thread(target=mailerThread, args=(recipientAddress, subject, deviceName, validationURL, 'activation_email', user, password))
+ thread.start()
+
+
+def sendEMpinActivationEmail(recipientAddress, subject, deviceName, activationCode, user=None, password=None):
+ ac3 = activationCode % 10000
+ ac2 = activationCode / 10000 % 10000
+ ac1 = activationCode / (10000 * 10000) % 10000
+ activationCodeStr = '%04d-%04d-%04d' % (ac1, ac2, ac3)
+
+ thread = Thread(target=mailerThread, args=(recipientAddress, subject, deviceName, activationCodeStr, 'empin_activation_email', user, password))
thread.start()
diff --git a/servers/demo/mpinDemo.py b/servers/demo/mpinDemo.py
index c3a3439..4688c8f 100755
--- a/servers/demo/mpinDemo.py
+++ b/servers/demo/mpinDemo.py
@@ -268,8 +268,11 @@
identity = data["mpinId"]
userid = data["userId"]
expireTime = data["expireTime"]
- activateKey = data["activateKey"]
mobile = data["mobile"]
+
+ activateKey = data.get("activateKey", "")
+ activationCode = int(data.get("activationCode", 0))
+
except ValueError:
log.error("Cannot decode body as JSON.")
log.debug(self.request.body)
@@ -285,23 +288,32 @@
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)
+ ## for ActivateKey
+ if ((type(activateKey) is str) or (type(activateKey) is unicode)) and (activateKey != ''):
+ 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))
+
+ mailer.sendActivationEmail(userid.encode("utf-8"), options.emailSubject, deviceName, validateURL, options.smtpUser, options.smtpPassword)
+
+ ## for ActivationCode
+ if (type(activationCode) is int) and (activationCode != 0):
+ log.info("Sending activation email for user {0}, activationCode: {1}".format(userid.encode("utf-8"), activationCode))
+
+ mailer.sendEMpinActivationEmail(userid.encode("utf-8"), options.emailSubject, deviceName, activationCode, options.smtpUser, options.smtpPassword)
+
log.warning("Sending Mail!")
responseData = {
diff --git a/servers/demo/templates/empin_activation_email.html b/servers/demo/templates/empin_activation_email.html
new file mode 100644
index 0000000..4fd2bf3
--- /dev/null
+++ b/servers/demo/templates/empin_activation_email.html
@@ -0,0 +1,9 @@
+<html>
+<head></head>
+<body>
+<p>Your Activation code is:</p>
+<p>{{ activationCodeStr }}</p>
+<p>Regards,<br/>
+The Milagro MFA Team</p>
+</body>
+</html>
diff --git a/servers/demo/templates/empin_activation_email.txt b/servers/demo/templates/empin_activation_email.txt
new file mode 100644
index 0000000..56dc145
--- /dev/null
+++ b/servers/demo/templates/empin_activation_email.txt
@@ -0,0 +1,6 @@
+Your Activation code is
+{{ activationCodeStr }}
+
+Regards,
+The Milagro MFA Team
+
diff --git a/servers/rps/rps.py b/servers/rps/rps.py
index 4dd9c6f..ef35275 100755
--- a/servers/rps/rps.py
+++ b/servers/rps/rps.py
@@ -27,6 +27,8 @@
import sys
import time
import urllib
+import math
+import execjs
from urlparse import urlparse
import tornado.autoreload
@@ -130,6 +132,12 @@
define("mobileConfig", default=None, type=list)
define("useNFC", default=False, type=bool)
+# eMpin AUTHENTICATION PROTOCOL
+define("maxTimeGap", default=300, type=int)
+define("nonceLifetime", default=600, type=int)
+define("jsLibrary", default=os.path.dirname(os.path.abspath(__file__)) + "/../../lib/" + "amcl.js", type=unicode)
+
+
# Mapping between local names of dynamic options and names from json
# in the form `remote_name`: `local_name`
@@ -309,6 +317,10 @@
"accessNumberDigits": 7 if options.accessNumberUseCheckSum else 6,
"cSum": 1,
"useNFC": options.useNFC,
+
+ "eMpinAuthenticationURL": "{0}/eMpinAuthentication".format(baseURL),
+ "eMpinActivationURL": "{0}/eMpinActivation".format(baseURL),
+ "eMpinActivationVerifyURL": "{0}/eMpinActivationVerify".format(baseURL),
}
if not options.requestOTP:
@@ -727,6 +739,20 @@
authOTT = I.authOTT
if authOTT and (str(I.status) == "200"):
+ authToken = I.authToken
+ if authToken:
+ mpin_id = authToken["mpin_id"]
+ if not mpin_id:
+ log.error("no mpin_id: {0}".format(I.authToken))
+ else:
+ identity = json.loads(mpin_id)
+ userId = identity["userID"]
+ if userId:
+ self.write({"authOTT": authOTT, "userId": userId})
+ else:
+ self.write({"authOTT": authOTT, "userId": ""})
+ self.finish()
+
self.write({"authOTT": authOTT})
self.finish()
else:
@@ -1493,6 +1519,684 @@
self.write(json.dumps(options.mobileConfig))
+
+def add_nonce(storage, mpin_id, nonce):
+ """ eMpin authencitication protocol sub-module
+
+ Add the nonce to Mpin-ID's nonce list of server storage.
+ """
+ tmp = storage.find(
+ stage="empin-auth-nonce-list-check",
+ mpinId=mpin_id
+ )
+
+ if tmp is not None:
+ tmp.nonce_list.append(nonce)
+ storage.update_item(tmp)
+ else:
+ storage.add(
+ stage="empin-auth-nonce-list-check",
+ mpinId=mpin_id,
+ nonce_list=[nonce]
+ )
+
+ nonce_expires = Time.syncedISO(seconds=options.nonceLifetime)
+
+ storage.add(
+ expire_time=Time.ISOtoDateTime(nonce_expires),
+ stage="empin-auth-nonce-check",
+ mpinId=mpin_id,
+ nonce=nonce
+ )
+
+def update_nonce_list(storage, mpin_id):
+ """ eMpin authencitication protocol sub-module
+
+ Update Mpin-ID's nonce list of server storage.
+ Expired nonces are removed in nonce list.
+ """
+ tmp = storage.find(
+ stage="empin-auth-nonce-list-check",
+ mpinId=mpin_id
+ )
+
+ if tmp is not None:
+ i = 0
+ while i < len(tmp.nonce_list):
+ item = storage.find(
+ stage="empin-auth-nonce-check",
+ mpinId=mpin_id,
+ nonce=tmp.nonce_list[i]
+ )
+ if item is None:
+ del tmp.nonce_list[i]
+ else:
+ i = i + 1
+
+ storage.update_item(tmp)
+ return tmp.nonce_list
+ else:
+ return []
+
+def check_nonce(storage, mpin_id, nonce):
+ """ eMpin authencitication protocol sub-module
+
+ Check the nonce is in Mpin-ID's nonce list.
+ """
+ nonce_list = update_nonce_list(storage, mpin_id)
+
+ if nonce in nonce_list:
+ return False
+
+ return True
+
+
+class VerifyError(Exception):
+ pass
+
+class InvalidNonceError(Exception):
+ pass
+
+class InvalidClientTimeError(Exception):
+ pass
+
+class AttemptsCountLimitError(Exception):
+ pass
+
+
+class EMpinAuthenticationHandler(BaseHandler):
+ """
+ .. apiTextStart
+
+ *Description*
+
+ Implements the eM-Pin Non-intaractive Authencitaion Protocol
+
+ *URL structure*
+
+ ``/eMpinAuthentication``
+
+ *HTTP Request Method*
+
+ POST
+
+ *Request Data*
+
+ JSON request::
+
+ {
+ "MpinId": [Client's Mpin ID in Hex-string],
+ "U": [Elliptic curve point in Hex-string],
+ "V": [Elliptic curve point in Hex-string],
+ "W": [Elliptic curve point in Hex-string],
+ "CCT": [Client's current time in Hex-string],
+ "Nonce": [Random value in Hex-string],
+ }
+
+ *Returns*
+
+ JSON response::
+
+ {
+ 'version': [Version in String],
+ 'authOTT': [Random value in Hex-string],
+ 'message': [OK/NG message in String],
+ }
+
+ *Status-Codes and Response-Phrases*
+
+ ::
+
+ Status-Code Response-Phrase
+
+ 200 OK
+ 403 Invalid signature received.
+ 403 Invalid nonce received.
+ 403 Invalid client time received.
+ 410 Attempts count is the limit.
+ 403 Invalid data received.
+ 500 Server-side Failed
+ .. apiTextEnd
+
+ """
+ @tornado.web.asynchronous
+ @tornado.gen.engine
+ 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)
+
+ server_secret_hex = self.application.server_secret.server_secret.encode("hex")
+
+ mpin_id_hex = receive_data['MpinId']
+ mpin_id = mpin_id_hex.decode('hex')
+
+ # NONCE CHECK
+ nonce_hex = receive_data['Nonce']
+ if not check_nonce(self.storage, mpin_id_hex, nonce_hex):
+ raise InvalidNonceError()
+
+ # TIMEGAP CHECK
+ client_time = int(receive_data['CCT'], 16)
+ server_time = Time.DateTimetoEpoch(datetime.datetime.now())
+ timegap = int(math.fabs(client_time - server_time))
+
+ if timegap > (options.maxTimeGap * 1000):
+ raise InvalidClientTimeError
+
+ # VERIFY (JS lib)
+ compiled_jslib = execjs.compile(open(options.jsLibrary).read())
+ verify_data = compiled_jslib.call("eMpinAuth.verify", receive_data, server_secret_hex)
+
+ aI = self.storage.find(stage="empin-auth-attempts", mpinId=mpin_id)
+ log.debug("aI: {0}".format(aI))
+
+ attempts_count = aI and aI.attemptsCount or 0
+ if attempts_count >= options.maxInvalidLoginAttempts:
+ raise AttemptsCountLimitError()
+
+ if verify_data['result'] == 'ng':
+ attempts_count += 1
+ log.debug("attemptsCount: {0}".format(attempts_count))
+ if aI:
+ aI.update(attemptsCount=attempts_count)
+ else:
+ self.storage.add(stage="empin-auth-attempts", mpinId=mpin_id, attemptsCount=attempts_count)
+
+ if attempts_count >= options.maxInvalidLoginAttempts:
+ raise AttemptsCountLimitError()
+ else:
+ raise VerifyError()
+ else:
+ add_nonce(self.storage, mpin_id_hex, nonce_hex)
+ success_code = 0
+ if aI:
+ aI.delete()
+
+ except VerifyError as ex:
+ reason = "Invalid signature received."
+ 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 InvalidNonceError as ex:
+ reason = "Invalid nonce received."
+ 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 InvalidClientTimeError as ex:
+ reason = "Invalid client time received."
+ log.error("%s %s (timegap %s sec)" % (request_info, reason, timegap / 1000))
+ self.set_status(403, reason=reason)
+ self.content_type = 'application/json'
+ self.write({'version': VERSION, 'message': reason})
+ self.finish()
+ return
+ except AttemptsCountLimitError as ex:
+ reason = "Attempts count is the limit."
+ log.error("%s %s" % (request_info, reason))
+ self.set_status(410, reason=reason)
+ self.content_type = 'application/json'
+ self.write({'version': VERSION, 'message': reason})
+ self.finish()
+ return
+ 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))
+
+
+ # 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": success_code,
+ "pinError": 0,
+ "pinErrorCost": 0,
+ "expires": expires,
+ }
+ log.debug("%s eM-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)
+
+ # Response
+ return_data = {
+ 'version': VERSION,
+ 'authOTT': authOTT,
+ 'message': "eMpin Authentication is valid.",
+ }
+
+ self.storage.add(
+ expire_time=Time.ISOtoDateTime(expires),
+ stage="auth",
+ authOTT=authOTT,
+ mpinId=mpin_id,
+ wid="",
+ webOTT=0,
+ authToken=token,
+ )
+
+ 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
+
+
+class EMpinActivationHandler(BaseHandler):
+ """
+ .. apiTextStart
+
+ *Description*
+
+ Implements the activation phase of the eM-Pin Non-intaractive Authencitaion Protocol
+
+ *URL structure*
+
+ ``/eMpinActivation``
+
+ *HTTP Request Method*
+
+ PUT
+
+ *Request Data*
+
+ JSON request::
+
+ {
+ "userId": [User ID in String (e.g. mail address)],
+ "mobile": [Flag (mobile or not) in Number],
+ }
+
+ *Returns*
+
+ JSON response::
+
+ {
+ "mpinId": [Client's Mpin ID in Hex-string],
+ "expireTime": [Activation expire timeServer's in String],
+ "nowTime": [Server's current time in String],
+ "active": [Flag (force activation is true or false)],
+ "activationCode": [Random value in Number],
+ "params": [URL parameters in String],
+ "clientSecretShare": [Encoded client secret (share) in Hex-string],
+ }
+
+ *Status-Codes and Response-Phrases*
+
+ ::
+
+ Status-Code Response-Phrase
+
+ 200 OK
+ 400 BAD REQUEST.
+ 500 Server-side Failed
+ .. apiTextEnd
+
+ """
+ @tornado.web.asynchronous
+ @tornado.gen.engine
+ def put(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 = json.loads(self.request.body)
+
+ mobile = int(receive_data.get("mobile", "0"))
+ user_id = receive_data.get("userId")
+ device_name = receive_data.get("deviceName", "")
+ user_data = receive_data.get("userData")
+
+ if not user_id:
+ 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
+ log.debug("%s %s" % (request_info, receive_data))
+
+ mpin_id_hex = makeMPinID(user_id, mobile)
+ log.debug("New mpinID generated for user {0}: {1}".format(user_id, mpin_id_hex))
+
+
+ ## GET CLIENT SECRET from DTA
+ hash_mpin_id_hex = hashlib.sha256(mpin_id_hex.decode("hex")).hexdigest()
+
+ # Generate signed params
+ path = "clientSecret"
+ expires = Time.syncedISO(seconds=options.VerifyUserExpireSeconds)
+ 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)
+ url_params = url_concat(url, param_values)
+
+ client = tornado.httpclient.AsyncHTTPClient()
+ response = yield tornado.gen.Task(client.fetch, url_params, method="GET")
+
+ if response.error:
+ log.error("DTA clientSecret Failed. URL: {0}, Code: {1}, Message: {2}".format(url_params, response.error.code, response.error.message))
+ self.set_status(500)
+ self.finish()
+ return
+
+ if response.body:
+ try:
+ response_data = json.loads(response.body)
+ client_secret_share = response_data["clientSecret"]
+
+ except:
+ log.error("DTA /clientSecret Failed. Invalid JSON response".format(response.body))
+ self.set_status(500)
+ self.finish()
+ return
+
+ log.debug("params: %s" % param_values)
+ log.debug("client secret (share): %s" % client_secret_share)
+
+
+ ## GET ACTIVATION CODE
+ activation_code = secrets.get_random_integer(self.application.server_secret.rng, 12)
+
+
+ ## POINT CALC with ACTIVATION CODE (JS lib)
+ compiled_jslib = execjs.compile(open(options.jsLibrary).read())
+ encoded_client_secret_share_hex = compiled_jslib.call("eMpinAuth.calcClientSecretWithActivationCode", mpin_id_hex, client_secret_share, activation_code, True)
+
+ log.debug("encoded client secret (share): %s" % encoded_client_secret_share_hex)
+
+
+ ### MAIL REQUEST for ACTIVATION CODE to RPA
+ # Generate activateKey
+ now_time = Time.syncedNow()
+ expire_time = now_time + datetime.timedelta(seconds=options.VerifyUserExpireSeconds)
+
+ requestBody = json.dumps({
+ "userId": user_id,
+ "mpinId": mpin_id_hex,
+ "mobile": mobile,
+ "activationCode": activation_code,
+ "expireTime": Time.DateTimeToISO(expire_time),
+ "resend": bool(None),
+ "deviceName": device_name,
+ "userData": user_data or ""
+ })
+
+ 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 or data.get('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. Code: {0}, Message: {1}".format(response.error.code, response.error.message))
+ error = response.error.code
+ if error >= 500:
+ error = 500
+
+ self.set_status(error)
+ self.finish()
+ return
+
+ force_activate = False
+ if response.body:
+ try:
+ response_data = json.loads(response.body)
+ force_activate = response_data.get("forceActivate", force_activate)
+
+ except:
+ log.error("RPA verify request: Invalid JSON response: {0}".format(response.body))
+ self.set_status(500)
+ self.finish()
+ return
+
+ if not force_activate:
+ activation_code = 0
+
+ log.debug("ActivationCode: {0}. ForceActivate: {1}. Activating UserID {2}".format(activation_code, force_activate, user_id))
+
+ params = urllib.urlencode(param_values)
+
+ # Response to the client
+ return_data = {
+ "mpinId": mpin_id_hex,
+ "expireTime": expire_time.isoformat(),
+ "nowTime": now_time.isoformat(),
+ "active": force_activate,
+ "activationCode": activation_code,
+ "params": params,
+ "clientSecretShare": encoded_client_secret_share_hex
+ }
+
+ 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
+
+
+class EMpinActivationVerifyHandler(BaseHandler):
+ """
+ .. apiTextStart
+
+ *Description*
+
+ Implements the activation phase of the eM-Pin Non-intaractive Authencitaion Protocol
+
+ *URL structure*
+
+ ``/eMpinActivationVerify``
+
+ *HTTP Request Method*
+
+ POST
+
+ *Request Data*
+
+ JSON request::
+
+ {
+ "MpinId": [Client's Mpin ID in Hex-string],
+ "U": [Elliptic curve point in Hex-string],
+ "V": [Elliptic curve point in Hex-string],
+ }
+
+ *Returns*
+
+ JSON response::
+
+ {
+ 'version': [Version in String],
+ 'result': [true/false in Boolean],
+ 'message': [OK/NG message in String],
+ }
+
+ *Status-Codes and Response-Phrases*
+
+ ::
+
+ Status-Code Response-Phrase
+
+ 200 OK
+ 403 Invalid signature received.
+ 410 Attempts count is the limit.
+ 403 Invalid data received.
+ 500 Server-side Failed
+ .. apiTextEnd
+
+ """
+ @tornado.web.asynchronous
+ @tornado.gen.engine
+ def post(self, mpinId):
+ # 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)
+
+ server_secret_hex = self.application.server_secret.server_secret.encode("hex")
+
+ mpin_id_hex = receive_data['MpinId']
+ mpin_id = mpin_id_hex.decode('hex')
+
+ # ACTIVATION VERIFY (JS lib)
+ compiled_jslib = execjs.compile(open(options.jsLibrary).read())
+ verify_data = compiled_jslib.call("eMpinAuth.activationVerify", receive_data, server_secret_hex)
+
+ aI = self.storage.find(stage="empin-auth-attempts", mpinId=mpin_id)
+ log.debug("aI: {0}".format(aI))
+
+ attempts_count = aI and aI.attemptsCount or 0
+ if attempts_count >= options.maxInvalidLoginAttempts:
+ raise AttemptsCountLimitError()
+
+ if verify_data['result'] == 'ng':
+ attempts_count += 1
+ log.debug("attemptsCount: {0}".format(attempts_count))
+ if aI:
+ aI.update(attemptsCount=attempts_count)
+ else:
+ self.storage.add(stage="empin-auth-attempts", mpinId=mpin_id, attemptsCount=attempts_count)
+
+ if attempts_count >= options.maxInvalidLoginAttempts:
+ raise AttemptsCountLimitError()
+ else:
+ raise VerifyError()
+ else:
+ if aI:
+ aI.delete()
+
+ except VerifyError as ex:
+ reason = "Invalid signature received."
+ log.error("%s %s" % (request_info, reason))
+ self.set_status(403, reason=reason)
+ self.content_type = 'application/json'
+ self.write({'version': VERSION, 'result': False, 'message': reason})
+ self.finish()
+ return
+ except AttemptsCountLimitError as ex:
+ reason = "Attempts Limits."
+ log.error("%s %s" % (request_info, reason))
+ self.set_status(410, reason=reason)
+ self.content_type = 'application/json'
+ self.write({'version': VERSION, 'message': reason})
+ self.finish()
+ return
+ 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))
+
+ # Response
+ return_data = {
+ 'version': VERSION,
+ 'result': True,
+ 'message': "eMpin Activation is valid.",
+ }
+
+ 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
+
+
+
# MAIN
class Application(tornado.web.Application):
def __init__(self):
@@ -1511,6 +2215,11 @@
(r"/{0}/pass1".format(rpsPrefix), Pass1Handler),
(r"/{0}/pass2".format(rpsPrefix), Pass2Handler),
+ # eMpin Handlers
+ (r"/{0}/eMpinAuthentication".format(rpsPrefix), EMpinAuthenticationHandler),
+ (r"/{0}/eMpinActivation".format(rpsPrefix), EMpinActivationHandler), # PUT
+ (r"/{0}/eMpinActivationVerify(/?[0-9A-Fa-f]*)".format(rpsPrefix), EMpinActivationVerifyHandler), # POST
+
(r"/authenticate", AuthenticateHandler), # POST
(r"/manage/getStackInfo", ManageGetStackInfoHandler), # GET
@@ -1536,6 +2245,7 @@
self.storage = storage_cls(
tornado.ioloop.IOLoop.instance(),
"stage,mpinId",
+ "stage,mpinId,nonce",
"stage,authOTT",
"stage,wid",
"stage,webOTT",