blob: dcaecbed49c17773ad4d60fe9be8c276bc361b1a [file] [log] [blame]
/*
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.
*/
/*
* M-Pin SDK implementation
*/
#include "mpin_sdk.h"
#include "mpin_crypto_non_tee.h"
#include "version.h"
#include "json/visitor.h"
#include <sstream>
typedef MPinSDK::Status Status;
typedef MPinSDK::User User;
typedef MPinSDK::UserPtr UserPtr;
typedef MPinSDK::String String;
typedef MPinSDK::StringMap StringMap;
typedef MPinSDK::IHttpRequest::Method HttpMethod;
/*
* Status class
*/
Status::Status() : m_statusCode(OK)
{
}
Status::Status(Code statucCode) : m_statusCode(statucCode)
{
}
Status::Status(Code statucCode, const String& error) : m_statusCode(statucCode), m_errorMessage(error)
{
}
Status::Code Status::GetStatusCode() const
{
return m_statusCode;
}
const String& Status::GetErrorMessage() const
{
return m_errorMessage;
}
void Status::SetStatusCode(Code statusCode)
{
m_statusCode = statusCode;
}
void Status::SetErrorMessage(const String& error)
{
m_errorMessage = error;
}
bool Status::operator==(Code statusCode) const
{
return m_statusCode == statusCode;
}
bool Status::operator!=(Code statusCode) const
{
return m_statusCode != statusCode;
}
/*
* TimePermitCache class
*/
MPinSDK::TimePermitCache::TimePermitCache() : m_date(0)
{
}
const String& MPinSDK::TimePermitCache::GetTimePermit() const
{
return m_timePermit;
}
int MPinSDK::TimePermitCache::GetDate() const
{
return m_date;
}
void MPinSDK::TimePermitCache::Set(const String& timePermit, int date)
{
m_timePermit = timePermit;
m_date = date;
}
void MPinSDK::TimePermitCache::Invalidate()
{
m_timePermit.Overwrite();
m_date = 0;
}
/*
* User class
*/
User::User(const String& id, const String& deviceName) : m_id(id), m_deviceName(deviceName), m_state(INVALID)
{
}
const String& User::GetId() const
{
return m_id;
}
const String& User::GetDeviceName() const
{
return m_deviceName;
}
User::State User::GetState() const
{
return m_state;
}
const String& User::GetMPinId() const
{
return m_mpinId;
}
const String& User::GetMPinIdHex() const
{
return m_mpinIdHex;
}
const String& User::GetRegOTT() const
{
return m_regOTT;
}
const MPinSDK::TimePermitCache& User::GetTimePermitCache() const
{
return m_timePermitCache;
}
void User::CacheTimePermit(const String& timePermit, int date)
{
m_timePermitCache.Set(timePermit, date);
}
void User::SetStartedRegistration(const String& mpinIdHex, const String& regOTT)
{
m_state = STARTED_REGISTRATION;
m_mpinIdHex = mpinIdHex;
m_mpinId = util::HexDecode(mpinIdHex);
m_regOTT = regOTT;
}
void User::SetActivated()
{
m_state = ACTIVATED;
}
void User::SetRegistered()
{
m_regOTT.Overwrite();
m_state = REGISTERED;
}
void User::Invalidate()
{
m_regOTT.Overwrite();
m_timePermitCache.Invalidate();
m_state = INVALID;
}
void User::Block()
{
Invalidate();
m_state = BLOCKED;
}
Status User::RestoreState(const String& stateString, const String& mpinIdHex, const String& regOTT)
{
SetStartedRegistration(mpinIdHex, regOTT);
State state = StringToState(stateString);
switch(state)
{
case INVALID:
return Status(Status::STORAGE_ERROR, String().Format("Invalid user state found for user '%s': '%s'", m_id.c_str(), stateString.c_str()));
case ACTIVATED:
SetActivated();
case STARTED_REGISTRATION:
// regOTT *must* be valid
if(regOTT.empty())
{
return Status(Status::STORAGE_ERROR, String().Format("Corrupted data for user '%s': state is '%s' but no regOTT was found", m_id.c_str(), stateString.c_str()));
}
return Status(Status::OK);
case REGISTERED:
SetRegistered();
// regOTT *must* be empty
if(!regOTT.empty())
{
return Status(Status::STORAGE_ERROR, String().Format("Corrupted data for user '%s': state is '%s' but regOTT is still saved", m_id.c_str(), stateString.c_str()));
}
return Status(Status::OK);
case BLOCKED:
Block();
return Status(Status::OK);
default:
assert(false);
return Status(Status::STORAGE_ERROR, String().Format("Internal error: invalid state %d returned from User::StringToState(%s)", (int) state, stateString.c_str()));
}
}
String User::GetStateString() const
{
return User::StateToString(m_state);
}
String User::StateToString(State state)
{
switch(state)
{
case STARTED_REGISTRATION:
return "STARTED_REGISTRATION";
case ACTIVATED:
return "ACTIVATED";
case REGISTERED:
return "REGISTERED";
case BLOCKED:
return "BLOCKED";
default:
assert(false);
return "INVALID";
}
}
User::State User::StringToState(const String& stateString)
{
if(stateString == StateToString(STARTED_REGISTRATION))
{
return STARTED_REGISTRATION;
}
if(stateString == StateToString(ACTIVATED))
{
return ACTIVATED;
}
if(stateString == StateToString(REGISTERED))
{
return REGISTERED;
}
if(stateString == StateToString(BLOCKED))
{
return BLOCKED;
}
return INVALID;
}
/*
* MPinSDK::IHttpRequest static string constants
*/
const char *MPinSDK::IHttpRequest::CONTENT_TYPE_HEADER = "Content-Type";
const char *MPinSDK::IHttpRequest::ACCEPT_HEADER = "Accept";
#define JSON_CONTENT_TYPE_STRING "application/json"
const char *MPinSDK::IHttpRequest::JSON_CONTENT_TYPE = JSON_CONTENT_TYPE_STRING "; charset=UTF-8";
const char *MPinSDK::IHttpRequest::TEXT_PLAIN_CONTENT_TYPE = "text/plain";
/*
* MPinSDK::HttpResponse class
*/
MPinSDK::HttpResponse::HttpResponse(const String& requestUrl, const String& requestBody)
: m_httpStatus(HTTP_OK), m_dataType(JSON), m_requestUrl(requestUrl), m_requestBody(requestBody)
{
}
int MPinSDK::HttpResponse::GetStatus() const
{
return m_httpStatus;
}
MPinSDK::HttpResponse::DataType MPinSDK::HttpResponse::GetDataType() const
{
return m_dataType;
}
MPinSDK::HttpResponse::DataType MPinSDK::HttpResponse::DetermineDataType(const String& contentTypeStr) const
{
if(contentTypeStr.compare(0, strlen(JSON_CONTENT_TYPE_STRING), JSON_CONTENT_TYPE_STRING) == 0)
{
return JSON;
}
return RAW;
}
bool MPinSDK::HttpResponse::SetData(const String& rawData, const StringMap& headers, DataType expectedType)
{
m_rawData = rawData;
m_headers = headers;
String contentTypeStr = headers.Get(IHttpRequest::CONTENT_TYPE_HEADER);
m_dataType = DetermineDataType(contentTypeStr);
String data = rawData;
data.Trim();
if(data.length() > 0 && !m_jsonData.Parse(data.c_str()))
{
SetResponseJsonParseError(data, m_jsonData.GetParseError());
return false;
}
return true;
}
const util::JsonObject& MPinSDK::HttpResponse::GetJsonData() const
{
return m_jsonData;
}
const String& MPinSDK::HttpResponse::GetRawData() const
{
return m_rawData;
}
const StringMap& MPinSDK::HttpResponse::GetHeaders() const
{
return m_headers;
}
void MPinSDK::HttpResponse::SetNetworkError(const String& error)
{
m_httpStatus = NON_HTTP_ERROR;
m_mpinStatus.SetStatusCode(Status::NETWORK_ERROR);
m_mpinStatus.SetErrorMessage(String().Format("HTTP request to '%s' failed. Error: '%s'", m_requestUrl.c_str(), error.c_str()));
}
void MPinSDK::HttpResponse::SetResponseJsonParseError(const String& responseJson, const String& jsonParseError)
{
m_httpStatus = NON_HTTP_ERROR;
m_mpinStatus.SetStatusCode(Status::RESPONSE_PARSE_ERROR);
m_mpinStatus.SetErrorMessage(String().Format("Failed to parse '%s' response json in request to '%s' (body='%s'). Error: '%s'",
responseJson.c_str(), m_requestUrl.c_str(), m_requestBody.c_str(), jsonParseError.c_str()));
}
void MPinSDK::HttpResponse::SetUnexpectedContentTypeError(DataType expectedType, const String& responseContentType, const String& responseRawData)
{
m_httpStatus = NON_HTTP_ERROR;
m_mpinStatus.SetStatusCode(Status::RESPONSE_PARSE_ERROR);
assert(expectedType == JSON || expectedType == RAW);
m_mpinStatus.SetErrorMessage(String().Format("HTTP request to '%s' (body='%s') returned unexpected content type '%s'. Expected was '%s'",
m_requestUrl.c_str(), m_requestBody.c_str(), responseContentType.c_str(), (expectedType == JSON) ? "JSON" : "RAW"));
}
void MPinSDK::HttpResponse::SetHttpError(int httpStatus)
{
m_httpStatus = httpStatus;
m_mpinStatus.SetErrorMessage(String().Format("HTTP request to '%s' (body='%s') returned status code %d",
m_requestUrl.c_str(), m_requestBody.c_str(), httpStatus));
if(httpStatus >= 500)
{
m_mpinStatus.SetStatusCode(Status::HTTP_SERVER_ERROR);
}
else if(httpStatus >= 400)
{
m_mpinStatus.SetStatusCode(Status::HTTP_REQUEST_ERROR);
}
else if(httpStatus >= 300)
{
m_mpinStatus.SetStatusCode(Status::NETWORK_ERROR);
}
else
{
// TODO: What to do if server returns 2xx (but not 200) or 3xx?
assert(false);
}
}
Status MPinSDK::HttpResponse::TranslateToMPinStatus(Context context)
{
switch(context)
{
case GET_CLIENT_SETTINGS:
case AUTHENTICATE_PASS1:
case AUTHENTICATE_PASS2:
break;
case REGISTER:
if(m_httpStatus == HTTP_FORBIDDEN)
{
m_mpinStatus.SetStatusCode(Status::IDENTITY_NOT_AUTHORIZED);
m_mpinStatus.SetErrorMessage("Identity not authorized");
}
break;
case GET_CLIENT_SECRET1:
if(m_httpStatus == HTTP_BAD_REQUEST || m_httpStatus == HTTP_UNAUTHORIZED)
{
m_mpinStatus.SetStatusCode(Status::IDENTITY_NOT_VERIFIED);
m_mpinStatus.SetErrorMessage("Identity not verified");
}
break;
case GET_CLIENT_SECRET2:
if(m_httpStatus == HTTP_REQUEST_TIMEOUT)
{
m_mpinStatus.SetStatusCode(Status::REQUEST_EXPIRED);
m_mpinStatus.SetErrorMessage("Request expired");
}
break;
case GET_TIME_PERMIT1:
case GET_TIME_PERMIT2:
if(m_httpStatus == HTTP_GONE)
{
m_mpinStatus.SetStatusCode(Status::REVOKED);
m_mpinStatus.SetErrorMessage("User revoked");
}
break;
case AUTHENTICATE_RPA:
if(m_httpStatus == HTTP_UNAUTHORIZED)
{
m_mpinStatus.SetStatusCode(Status::INCORRECT_PIN);
m_mpinStatus.SetErrorMessage("Incorrect pin");
}
else if(m_httpStatus == HTTP_REQUEST_TIMEOUT)
{
m_mpinStatus.SetStatusCode(Status::REQUEST_EXPIRED);
m_mpinStatus.SetErrorMessage("Request expired");
}
else if(m_httpStatus == HTTP_GONE)
{
m_mpinStatus.SetStatusCode(Status::INCORRECT_PIN);
m_mpinStatus.SetErrorMessage("User blocked");
}
else if(m_httpStatus == HTTP_PRECONDITION_FAILED)
{
m_mpinStatus.SetStatusCode(Status::INCORRECT_ACCESS_NUMBER);
m_mpinStatus.SetErrorMessage("Invalid access number");
}
else if(m_httpStatus == HTTP_FORBIDDEN)
{
m_mpinStatus.SetStatusCode(Status::IDENTITY_NOT_AUTHORIZED);
m_mpinStatus.SetErrorMessage("Identity not authorized");
}
break;
}
return m_mpinStatus;
}
/*
* MPinSDK class
*/
const char *MPinSDK::DEFAULT_RPS_PREFIX = "rps";
const char *MPinSDK::CONFIG_BACKEND = "backend";
// TODO: Remove this
static const char *CONFIG_BACKEND_OLD = "RPA_server";
const char *MPinSDK::CONFIG_RPS_PREFIX = "rps_prefix";
MPinSDK::MPinSDK() : m_state(NOT_INITIALIZED), m_context(NULL), m_crypto(NULL)
{
}
MPinSDK::~MPinSDK()
{
Destroy();
}
bool MPinSDK::IsInitilized() const
{
return m_state != NOT_INITIALIZED;
}
bool MPinSDK::IsBackendSet() const
{
return m_state == BACKEND_SET;
}
Status MPinSDK::CheckIfIsInitialized() const
{
if(IsInitilized())
{
return Status(Status::OK);
}
return Status(Status::FLOW_ERROR, "MPinSDK not initialized");
}
Status MPinSDK::CheckIfBackendIsSet() const
{
if(IsBackendSet())
{
return Status(Status::OK);
}
return Status(Status::FLOW_ERROR, "MPinSDK backend was not set");
}
MPinSDK::HttpResponse MPinSDK::MakeRequest(const String& url, HttpMethod method, const util::JsonObject& bodyJson, HttpResponse::DataType expectedResponseType) const
{
IHttpRequest *r = m_context->CreateHttpRequest();
String requestBody = bodyJson.ToString();
HttpResponse response(url, requestBody);
if(method != IHttpRequest::GET)
{
StringMap headers;
headers.Put(IHttpRequest::CONTENT_TYPE_HEADER, IHttpRequest::JSON_CONTENT_TYPE);
headers.Put(IHttpRequest::ACCEPT_HEADER, IHttpRequest::TEXT_PLAIN_CONTENT_TYPE);
r->SetHeaders(headers);
r->SetContent(requestBody);
}
if(!r->Execute(method, url))
{
response.SetNetworkError(r->GetExecuteErrorMessage());
m_context->ReleaseHttpRequest(r);
return response;
}
int httpStatus = r->GetHttpStatusCode();
if(httpStatus != HttpResponse::HTTP_OK)
{
response.SetHttpError(httpStatus);
m_context->ReleaseHttpRequest(r);
return response;
}
if ( !r->GetResponseData().empty() )
{
response.SetData(r->GetResponseData(), r->GetResponseHeaders(), expectedResponseType);
}
m_context->ReleaseHttpRequest(r);
return response;
}
MPinSDK::HttpResponse MPinSDK::MakeGetRequest(const String& url, HttpResponse::DataType expectedResponseType) const
{
return MakeRequest(url, IHttpRequest::GET, util::JsonObject(), expectedResponseType);
}
class RewriteUrlVisitor : public json::Visitor
{
public:
RewriteUrlVisitor(const String& rpaServer) : m_rpaServer(rpaServer) {}
virtual ~RewriteUrlVisitor() {}
virtual void Visit(json::Array& array) {}
virtual void Visit(json::Object& object) {}
virtual void Visit(json::Number& number) {}
virtual void Visit(json::Boolean& boolean) {}
virtual void Visit(json::Null& null) {}
virtual void Visit(json::String& string)
{
String url = string.Value();
if (url[0] == '/')
{
url.insert(0, m_rpaServer);
}
else
{
// Replace wss:// with https:// and ws:// with http://
url.ReplaceAll("wss://", "https://");
url.ReplaceAll("ws://", "http://");
}
string = url;
}
private:
String m_rpaServer;
};
Status MPinSDK::RewriteRelativeUrls()
{
try
{
RewriteUrlVisitor visitor(m_RPAServer);
for(json::Object::iterator i = m_clientSettings.Begin(); i != m_clientSettings.End(); ++i)
{
i->element.Accept(visitor);
}
}
catch(json::Exception&)
{
return Status(Status::RESPONSE_PARSE_ERROR, String().Format("Unexpected client settings json: '%s'", m_clientSettings.ToString().c_str()));
}
return Status(Status::OK);
}
Status MPinSDK::Init(const StringMap& config, IContext* ctx)
{
if(IsInitilized())
{
return Status(Status::OK);
}
m_context = ctx;
if(ctx->GetMPinCryptoType() == CRYPTO_NON_TEE)
{
MPinCryptoNonTee *nonteeCrypto = new MPinCryptoNonTee();
Status s = nonteeCrypto->Init(ctx->GetStorage(IStorage::SECURE));
if(s != Status::OK)
{
delete nonteeCrypto;
return s;
}
m_crypto = nonteeCrypto;
}
else
{
return Status(Status::FLOW_ERROR, String("CRYPTO_TEE crypto type is currently not supported"));
}
m_state = INITIALIZED;
String backend = config.Get(CONFIG_BACKEND);
if(backend.empty())
{
backend = config.Get(CONFIG_BACKEND_OLD);
if(backend.empty())
{
return Status(Status::OK);
}
}
StringMap::const_iterator i = config.find(CONFIG_RPS_PREFIX);
String rpsPrefix = (i != config.end()) ? i->second : DEFAULT_RPS_PREFIX;
return SetBackend(backend, rpsPrefix);
}
void MPinSDK::Destroy()
{
if(!IsInitilized())
{
return;
}
ClearUsers();
delete m_crypto;
m_crypto = NULL;
m_context = NULL;
m_state = NOT_INITIALIZED;
}
void MPinSDK::ClearUsers()
{
for (UsersMap::iterator i = m_users.begin(); i != m_users.end(); ++i)
{
i->second->Invalidate();
}
m_users.clear();
m_logoutData.clear();
}
Status MPinSDK::GetClientSettings(const String& backend, const String& rpsPrefix, OUT util::JsonObject *clientSettings) const
{
HttpResponse response = MakeGetRequest(String().Format("%s/%s/clientSettings", backend.c_str(), String(rpsPrefix).Trim("/").c_str()));
if(response.GetStatus() != HttpResponse::HTTP_OK)
{
return response.TranslateToMPinStatus(HttpResponse::GET_CLIENT_SETTINGS);
}
if(clientSettings != NULL)
{
*clientSettings = response.GetJsonData();
}
return Status(Status::OK);
}
Status MPinSDK::TestBackend(const String& backend, const String& rpsPrefix) const
{
Status s = CheckIfIsInitialized();
if(s != Status::OK)
{
return s;
}
return GetClientSettings(String(backend).TrimRight("/"), rpsPrefix, NULL);
}
Status MPinSDK::SetBackend(const String& backend, const String& rpsPrefix)
{
Status s = CheckIfIsInitialized();
if(s != Status::OK)
{
return s;
}
m_RPAServer = backend;
m_RPAServer.TrimRight("/");
s = GetClientSettings(m_RPAServer, rpsPrefix, &m_clientSettings);
if(s != Status::OK)
{
return s;
}
s = RewriteRelativeUrls();
// Check to see if the old access number algorithm is used and if yes, disable check sum validation
if(m_clientSettings.GetIntParam("cSum", 0) == 0)
{
m_clientSettings["accessNumberUseCheckSum"] = json::Boolean(false);
}
if(s != Status::OK)
{
return s;
}
s = LoadUsersFromStorage();
if(s != Status::OK)
{
return s;
}
m_state = BACKEND_SET;
return Status(Status::OK);
}
UserPtr MPinSDK::MakeNewUser(const String& id, const String& deviceName) const
{
return UserPtr(new User(id, deviceName));
}
Status MPinSDK::StartRegistration(UserPtr user, const String& activateCode, const String& userData)
{
Status s = CheckIfBackendIsSet();
if(s != Status::OK)
{
return s;
}
s = CheckUserState(user, User::INVALID);
if(s != Status::OK)
{
return s;
}
return RequestRegistration(user, activateCode, userData);
}
Status MPinSDK::RestartRegistration(UserPtr user, const String& userData)
{
Status s = CheckIfBackendIsSet();
if(s != Status::OK)
{
return s;
}
s = CheckUserState(user, User::STARTED_REGISTRATION);
if(s != Status::OK)
{
return s;
}
return RequestRegistration(user, "", userData);
}
Status MPinSDK::RequestRegistration(UserPtr user, const String& activateCode, const String& userData)
{
// Make request to RPA to add M-Pin ID
util::JsonObject data;
data["userId"] = json::String(user->GetId());
data["mobile"] = json::Number(1);
if(!user->GetDeviceName().empty())
{
data["deviceName"] = json::String(user->GetDeviceName());
}
if(!userData.empty())
{
data["userData"] = json::String(userData);
}
if(!activateCode.empty())
{
data["activateCode"] = json::String(activateCode);
}
String url;
if(user->GetState() == User::STARTED_REGISTRATION)
{
data["regOTT"] = json::String(user->GetRegOTT());
url.Format("%s/%s", m_clientSettings.GetStringParam("registerURL"), user->GetMPinIdHex().c_str());
}
else
{
url = m_clientSettings.GetStringParam("registerURL");
}
HttpResponse response = MakeRequest(url, IHttpRequest::PUT, data);
if(response.GetStatus() != HttpResponse::HTTP_OK)
{
return response.TranslateToMPinStatus(HttpResponse::REGISTER);
}
bool writeUsersToStorage = false;
bool userIsNew = (user->GetState() == User::INVALID);
if(userIsNew)
{
AddUser(user);
}
String mpinIdHex = response.GetJsonData().GetStringParam("mpinId");
String regOTT = response.GetJsonData().GetStringParam("regOTT");
bool userDataChanged = (regOTT != user->GetRegOTT() || mpinIdHex != user->GetMPinIdHex());
if(userIsNew || userDataChanged)
{
user->SetStartedRegistration(mpinIdHex, regOTT);
writeUsersToStorage = true;
}
if(response.GetJsonData().GetBoolParam("active"))
{
user->SetActivated();
writeUsersToStorage = true;
}
if(writeUsersToStorage)
{
Status s = WriteUsersToStorage();
if(s != Status::OK)
{
return s;
}
}
return Status(Status::OK);
}
Status MPinSDK::ConfirmRegistration(INOUT UserPtr user, const String& pushMessageIdentifier)
{
Status s = CheckIfBackendIsSet();
if(s != Status::OK)
{
return s;
}
// A user can get here either in STARTED_REGISTRATION state or in ACTIVATED state (force-activate flow)
// In the first case, the method might fail if the user identity has not been verified yet, and the user state
// should stay as it was - STARTED_REGISTRATION
s = CheckUserState(user, User::STARTED_REGISTRATION);
if(s != Status::OK)
{
Status sSave = s;
s = CheckUserState(user, User::ACTIVATED);
if ( s != Status::OK )
{
return sSave;
}
}
// Request a client secret share from the customer's D-TA and a signed request for a client secret share from CertiVox's D-TA.
String mpinId = user->GetMPinId();
String mpinIdHex = user->GetMPinIdHex();
String regOTT = user->GetRegOTT();
String url = String().Format("%s/%s?regOTT=%s", m_clientSettings.GetStringParam("signatureURL"), mpinIdHex.c_str(), regOTT.c_str());
if(!pushMessageIdentifier.empty())
{
url += "&pmiToken=" + pushMessageIdentifier;
}
HttpResponse response = MakeGetRequest(url);
if(response.GetStatus() != HttpResponse::HTTP_OK)
{
return response.TranslateToMPinStatus(HttpResponse::GET_CLIENT_SECRET1);
}
user->m_clientSecret1 = util::HexDecode(response.GetJsonData().GetStringParam("clientSecretShare"));
// Request the client secret share from CertiVox's D-TA.
String cs2Params = response.GetJsonData().GetStringParam("params");
url.Format("%sclientSecret?%s", m_clientSettings.GetStringParam("certivoxURL"), cs2Params.c_str());
response = MakeGetRequest(url);
if(response.GetStatus() != HttpResponse::HTTP_OK)
{
return response.TranslateToMPinStatus(HttpResponse::GET_CLIENT_SECRET2);
}
user->m_clientSecret2 = util::HexDecode(response.GetJsonData().GetStringParam("clientSecret"));
return Status::OK;
}
Status MPinSDK::FinishRegistration(INOUT UserPtr user, const String& pin)
{
Status s = CheckIfBackendIsSet();
if(s != Status::OK)
{
return s;
}
// A user can get here either in STARTED_REGISTRATION state or in ACTIVATED state (force-activate flow)
// In the first case, the method might fail if the user identity has not been verified yet, and the user state
// should stay as it was - STARTED_REGISTRATION
s = CheckUserState(user, User::STARTED_REGISTRATION);
if(s != Status::OK)
{
Status sSave = s;
s = CheckUserState(user, User::ACTIVATED);
if ( s != Status::OK )
{
return sSave;
}
}
// In addition, client secret shares must be retrieved
if(user->m_clientSecret1.empty() || user->m_clientSecret2.empty())
{
return Status(Status::FLOW_ERROR, String().Format("Cannot finish user '%s' registration: User identity not verified", user->GetId().c_str()));
}
s = m_crypto->OpenSession();
if(s != Status::OK)
{
return s;
}
std::vector<String> clientSecretShares;
clientSecretShares.push_back(user->m_clientSecret1);
clientSecretShares.push_back(user->m_clientSecret2);
s = m_crypto->Register(user, pin, clientSecretShares);
if(s != Status::OK)
{
m_crypto->CloseSession();
return s;
}
m_crypto->CloseSession();
user->SetRegistered();
s = WriteUsersToStorage();
if(s != Status::OK)
{
return s;
}
return Status::OK;
}
Status MPinSDK::StartAuthentication(INOUT UserPtr user, const String& accessCode)
{
Status s = CheckIfBackendIsSet();
if(s != Status::OK)
{
return s;
}
// Check if the user is already registered
s = CheckUserState(user, User::REGISTERED);
if(s != Status::OK)
{
return s;
}
String codeStatusURL = m_clientSettings.GetStringParam("codeStatusURL");
if(!codeStatusURL.empty() && !accessCode.empty())
{
util::JsonObject data;
data["status"] = json::String("user");
data["wid"] = json::String(accessCode);
data["userId"] = json::String(user->GetId());
MakeRequest(codeStatusURL, IHttpRequest::POST, data);
}
bool useTimePermits = m_clientSettings.GetBoolParam("usePermits", true);
if(!useTimePermits)
{
return Status::OK;
}
// Request a time permit share from the customer's D-TA and a signed request for a time permit share from CertiVox's D-TA.
String mpinIdHex = user->GetMPinIdHex();
String url = String().Format("%s/%s", m_clientSettings.GetStringParam("timePermitsURL"), mpinIdHex.c_str());
HttpResponse response = MakeGetRequest(url);
if(response.GetStatus() != HttpResponse::HTTP_OK)
{
return response.TranslateToMPinStatus(HttpResponse::GET_TIME_PERMIT1);
}
user->m_timePermitShare1 = util::HexDecode(response.GetJsonData().GetStringParam("timePermit"));
// Request time permit share from CertiVox's D-TA (Searches first in user cache, than in S3 cache)
s = GetCertivoxTimePermitShare(user, response.GetJsonData(), user->m_timePermitShare2);
if(s != Status::OK)
{
return s;
}
return Status::OK;
}
Status MPinSDK::CheckAccessNumber(const String& accessNumber)
{
if(accessNumber.empty() || !ValidateAccessNumber(accessNumber))
{
return Status(Status::INCORRECT_ACCESS_NUMBER, "Invalid access number");
}
return Status::OK;
}
Status MPinSDK::FinishAuthentication(INOUT UserPtr user, const String& pin)
{
util::JsonObject authResult;
return FinishAuthenticationImpl(user, pin, "", NULL, authResult);
}
Status MPinSDK::FinishAuthentication(INOUT UserPtr user, const String& pin, OUT String& authResultData)
{
util::JsonObject authResult;
Status s = FinishAuthenticationImpl(user, pin, "", NULL, authResult);
authResultData = authResult.ToString();
return s;
}
Status MPinSDK::FinishAuthenticationOTP(INOUT UserPtr user, const String& pin, OUT OTP& otp)
{
util::JsonObject authResult;
String otpNumber;
Status s = FinishAuthenticationImpl(user, pin, "", &otpNumber, authResult);
otp.ExtractFrom(otpNumber, authResult);
return s;
}
Status MPinSDK::FinishAuthenticationAN(INOUT UserPtr user, const String& pin, const String& accessNumber)
{
util::JsonObject authResult;
Status s = FinishAuthenticationImpl(user, pin, accessNumber, NULL, authResult);
LogoutData logoutData;
if(logoutData.ExtractFrom(authResult))
{
m_logoutData.insert(std::make_pair(user, logoutData));
}
return s;
}
Status MPinSDK::FinishAuthenticationImpl(INOUT UserPtr user, const String& pin, const String& accessNumber, OUT String *otp, OUT util::JsonObject& authResultData)
{
Status s = CheckIfBackendIsSet();
if(s != Status::OK)
{
return s;
}
// Check if the user is already registered
s = CheckUserState(user, User::REGISTERED);
if(s != Status::OK)
{
return s;
}
bool useTimePermits = m_clientSettings.GetBoolParam("usePermits", true);
// Check if time permit was obtained from StartAuthentication
if(useTimePermits && (user->m_timePermitShare1.empty() || user->m_timePermitShare2.empty()))
{
return Status(Status::FLOW_ERROR, String().Format("Cannot finish user '%s' authentication: Invalid time permit", user->GetId().c_str()));
}
String mpinIdHex = user->GetMPinIdHex();
s = m_crypto->OpenSession();
if(s != Status::OK)
{
return s;
}
std::vector<String> timePermitShares;
int date = 0;
if(useTimePermits)
{
timePermitShares.push_back(user->m_timePermitShare1);
timePermitShares.push_back(user->m_timePermitShare2);
date = user->GetTimePermitCache().GetDate();
}
// Authentication pass 1
String u, ut;
s = m_crypto->AuthenticatePass1(user, pin, date, timePermitShares, u, ut);
if(s != Status::OK)
{
m_crypto->CloseSession();
return s;
}
util::JsonObject requestData;
requestData["pass"] = json::Number(1);
requestData["mpin_id"] = json::String(mpinIdHex);
requestData["UT"] = json::String(util::HexEncode(ut));
requestData["U"] = json::String(util::HexEncode(u));
String mpinAuthServerURL = m_clientSettings.GetStringParam("mpinAuthServerURL");
String url = String().Format("%s/pass1", mpinAuthServerURL.c_str());
HttpResponse response = MakeRequest(url, IHttpRequest::POST, requestData);
if(response.GetStatus() != HttpResponse::HTTP_OK)
{
m_crypto->CloseSession();
return response.TranslateToMPinStatus(HttpResponse::AUTHENTICATE_PASS1);
}
String y = util::HexDecode(response.GetJsonData().GetStringParam("y"));
// Authentication pass 2
String v;
m_crypto->AuthenticatePass2(user, y, v);
if(s != Status::OK)
{
m_crypto->CloseSession();
return s;
}
requestData.Clear();
requestData["pass"] = json::Number(2);
requestData["OTP"] = json::Boolean(otp != NULL ? true : false);
requestData["WID"] = json::String(accessNumber.empty() ? "0" : accessNumber);
requestData["V"] = json::String(util::HexEncode(v));
requestData["mpin_id"] = json::String(mpinIdHex);
url.Format("%s/pass2", mpinAuthServerURL.c_str());
response = MakeRequest(url, IHttpRequest::POST, requestData);
if(response.GetStatus() != HttpResponse::HTTP_OK)
{
m_crypto->CloseSession();
return response.TranslateToMPinStatus(HttpResponse::AUTHENTICATE_PASS2);
}
// Save OTP data to be used if otp was requested
if(otp != NULL)
{
*otp = response.GetJsonData().GetStringParam("OTP");
}
// Send response data from M-Pin authentication server to RPA
url = m_clientSettings.GetStringParam(accessNumber.empty() ? "authenticateURL" : "mobileAuthenticateURL");
requestData.Clear();
requestData["mpinResponse"] = response.GetJsonData();
response = MakeRequest(url, IHttpRequest::POST, requestData);
if(response.GetStatus() != HttpResponse::HTTP_OK)
{
m_crypto->CloseSession();
s = response.TranslateToMPinStatus(HttpResponse::AUTHENTICATE_RPA);
if(response.GetStatus() == HttpResponse::HTTP_GONE)
{
user->Block();
m_crypto->DeleteToken(user->GetMPinId());
WriteUsersToStorage();
}
return s;
}
// You are now logged in with M-Pin!
m_crypto->CloseSession();
authResultData = response.GetJsonData();
return Status::OK;
}
Status MPinSDK::GetCertivoxTimePermitShare(INOUT UserPtr user, const util::JsonObject& cutomerTimePermitData, OUT String& resultTimePermit)
{
// First check if we have cached time permit in user and if it is still valid (for today)
int date = cutomerTimePermitData.GetIntParam("date");
const TimePermitCache& userCache = user->GetTimePermitCache();
const String& cachedTimePermit = userCache.GetTimePermit();
if(!cachedTimePermit.empty() && userCache.GetDate() == date)
{
resultTimePermit = cachedTimePermit;
return Status(Status::OK);
}
// No or too old cached time permit - try get time permit from S3
String s3Url = m_clientSettings.GetStringParam("timePermitsStorageURL");
String appId = m_clientSettings.GetStringParam("appID");
String storageId = cutomerTimePermitData.GetStringParam("storageId");
// Make GET request to s3Url/app_id/date/storageId
String url = String().Format("%s/%s/%d/%s", s3Url.c_str(), appId.c_str(), date, storageId.c_str());
HttpResponse response = MakeGetRequest(url, HttpResponse::RAW);
if(response.GetStatus() == HttpResponse::HTTP_OK)
{
// OK - add time permit to user cache
resultTimePermit = util::HexDecode(response.GetRawData());
user->CacheTimePermit(resultTimePermit, date);
WriteUsersToStorage();
return Status(Status::OK);
}
// No cached time permit in S3 or something other went wrong
// Finally request time permit share from CertiVox's D-TA
String signature = cutomerTimePermitData.GetStringParam("signature");
String t2Params = String().Format("hash_mpin_id=%s&app_id=%s&mobile=1&signature=%s",
storageId.c_str(), appId.c_str(), signature.c_str());
url.Format("%stimePermit?%s", m_clientSettings.GetStringParam("certivoxURL"), t2Params.c_str());
response = MakeGetRequest(url);
if(response.GetStatus() != HttpResponse::HTTP_OK)
{
return response.TranslateToMPinStatus(HttpResponse::GET_TIME_PERMIT2);
}
resultTimePermit = util::HexDecode(response.GetJsonData().GetStringParam("timePermit"));
// OK - add time permit to user cache
user->CacheTimePermit(resultTimePermit, date);
WriteUsersToStorage();
return Status(Status::OK);
}
void MPinSDK::OTP::ExtractFrom(const String& otpData, const util::JsonObject& json)
{
if(otpData.empty())
{
status = Status(Status::RESPONSE_PARSE_ERROR, "OTP not issued");
return;
}
otp = otpData;
ttlSeconds = json.GetIntParam("ttlSeconds");
int64_t tmp = json.GetInt64Param("expireTime");
expireTime = (long)(tmp / 1000);
tmp = json.GetInt64Param("nowTime");
nowTime = (long)(tmp / 1000);
if(expireTime == 0 || ttlSeconds == 0 || nowTime == 0)
{
status = Status(Status::RESPONSE_PARSE_ERROR, "OTP data is malformed");
}
}
bool MPinSDK::LogoutData::ExtractFrom(const util::JsonObject& json)
{
logoutURL = json.GetStringParam("logoutURL");
json::Object::const_iterator i = json.Find("logoutData");
if(i == json.End())
{
return false;
}
try
{
logoutData = util::JsonObject(i->element).ToString();
}
catch(json::Exception&)
{
logoutData = "";
}
return true;
}
bool MPinSDK::ValidateAccessNumber(const String& accessNumber)
{
bool accessNumberUseCheckSum = m_clientSettings.GetBoolParam("accessNumberUseCheckSum", true);
int accessNumberDigits = m_clientSettings.GetIntParam("accessNumberDigits", AN_WITH_CHECKSUM_LEN);
if(!accessNumberUseCheckSum || accessNumberDigits != AN_WITH_CHECKSUM_LEN)
{
return true;
}
return ValidateAccessNumberChecksum(accessNumber);
}
bool MPinSDK::ValidateAccessNumberChecksum(const String& accessNumber)
{
size_t len = accessNumber.length();
const char *data = accessNumber.data();
// Checksum is the last number digit (0 - 9)
int checkSum = data[len - 1];
// We are working with AN_WITH_CHECKSUM_LEN *digit* access numbers only
if(len != AN_WITH_CHECKSUM_LEN || !isdigit(checkSum))
{
return false;
}
checkSum -= '0';
int calculatedCheckSum = 0;
for( ; len > 1; --len, ++data)
{
if(!isdigit(*data))
{
return false;
}
calculatedCheckSum += (*data - '0') * len;
}
calculatedCheckSum = ((11 - calculatedCheckSum % 11) % 11) % 10;
return calculatedCheckSum == checkSum;
}
String MPinSDK::GetPrerollUserId(const String& accessCode)
{
Status s = CheckIfBackendIsSet();
if(s != Status::OK)
{
return "";
}
String codeStatusUrl = m_clientSettings.GetStringParam("codeStatusURL");
if(codeStatusUrl.empty())
{
return "";
}
util::JsonObject data;
data["status"] = json::String("wid");
data["wid"] = json::String(accessCode);
HttpResponse response = MakeRequest(codeStatusUrl, IHttpRequest::POST, data);
if(response.GetStatus() != HttpResponse::HTTP_OK)
{
return "";
}
return response.GetJsonData().GetStringParam("prerollId");
}
void MPinSDK::DeleteUser(UserPtr user)
{
UsersMap::iterator i = m_users.find(user->GetId());
if(i == m_users.end() || user != i->second)
{
return;
}
m_crypto->DeleteRegOTT(i->second->GetMPinId());
m_crypto->DeleteToken(i->second->GetMPinId());
i->second->Invalidate();
m_users.erase(i);
WriteUsersToStorage();
m_logoutData.erase(user);
}
Status MPinSDK::ListUsers(std::vector<UserPtr>& users) const
{
Status s = CheckIfBackendIsSet();
if(s != Status::OK)
{
return s;
}
ListUsers(users, m_users);
return Status::OK;
}
Status MPinSDK::ListUsers(OUT std::vector<UserPtr>& users, const String& backend) const
{
Status s = CheckIfIsInitialized();
if(s != Status::OK)
{
return s;
}
UsersMap usersMap;
s = LoadUsersFromStorage(backend, usersMap);
if(s != Status::OK)
{
return s;
}
ListUsers(users, usersMap);
return Status::OK;
}
void MPinSDK::ListUsers(OUT std::vector<UserPtr>& users, const UsersMap& usersMap) const
{
users.clear();
users.reserve(usersMap.size());
for(UsersMap::const_iterator i = usersMap.begin(); i != usersMap.end(); ++i)
{
users.push_back(i->second);
}
}
void MPinSDK::AddUser(UserPtr user)
{
m_users[user->GetId()] = user;
}
Status MPinSDK::CheckUserState(UserPtr user, User::State expectedState)
{
UsersMap::iterator i = m_users.find(user->GetId());
if(expectedState == User::INVALID)
{
if(i != m_users.end())
{
return Status(Status::FLOW_ERROR, String().Format("User '%s' was already added", user->GetId().c_str()));
}
if(user->GetState() != User::INVALID)
{
return Status(Status::FLOW_ERROR, String().Format("Invalid '%s' user state: current state=%s, expected state=%s",
user->GetId().c_str(), User::StateToString( user->GetState() ).c_str(), User::StateToString( expectedState ).c_str()));
}
return Status(Status::OK);
}
if(i == m_users.end())
{
return Status(Status::FLOW_ERROR, String().Format("User '%s' was not added or has been deleted", user->GetId().c_str()));
}
if(user != i->second)
{
return Status(Status::FLOW_ERROR, String().Format("Different user with the '%s' id was previously added", user->GetId().c_str()));
}
if(user->GetState() != expectedState)
{
return Status(Status::FLOW_ERROR, String().Format("Invalid '%s' user state: current state=%s, expected state=%s",
user->GetId().c_str(), User::StateToString( user->GetState() ).c_str(), User::StateToString( expectedState ).c_str()));
}
return Status(Status::OK);
}
Status MPinSDK::WriteUsersToStorage()
{
IStorage* storage = m_context->GetStorage(IStorage::NONSECURE);
String data;
storage->GetData(data);
data.Trim();
try
{
json::Object allBackendsObject;
if(!data.empty())
{
std::istringstream strIn(data);
json::Reader::Read(allBackendsObject, strIn);
}
String backend = m_RPAServer;
backend.ReplaceAll("https://", "");
backend.ReplaceAll("http://", "");
json::Object& rootObject = (json::Object&) allBackendsObject[backend];
rootObject.Clear();
for (UsersMap::iterator i = m_users.begin(); i != m_users.end(); ++i)
{
UserPtr user = i->second;
json::Object timePermitCacheObject;
timePermitCacheObject["date"] = json::Number(user->GetTimePermitCache().GetDate());
timePermitCacheObject["timePermit"] = json::String(util::HexEncode(user->GetTimePermitCache().GetTimePermit()));
json::Object userObject;
userObject["timePermitCache"] = timePermitCacheObject;
if(!user->GetDeviceName().empty())
{
userObject["deviceName"] = json::String(user->GetDeviceName());
}
userObject["state"] = json::String(user->GetStateString());
rootObject[user->GetMPinIdHex()] = userObject;
Status s;
switch(user->GetState())
{
case User::STARTED_REGISTRATION:
case User::ACTIVATED:
s = m_crypto->SaveRegOTT(user->GetMPinId(), user->GetRegOTT());
break;
case User::REGISTERED:
s = m_crypto->DeleteRegOTT(user->GetMPinId());
break;
default:
break;
}
if(s != Status::OK)
{
return s;
}
}
std::stringstream strOut;
json::Writer::Write(allBackendsObject, strOut);
storage->SetData(strOut.str());
}
catch(const json::Exception& e)
{
return Status(Status::STORAGE_ERROR, e.what());
}
return Status(Status::OK);
}
Status MPinSDK::LoadUsersFromStorage()
{
ClearUsers();
return LoadUsersFromStorage(m_RPAServer, m_users);
}
Status MPinSDK::LoadUsersFromStorage(const String& backendServer, UsersMap& usersMap) const
{
IStorage* storage = m_context->GetStorage(IStorage::NONSECURE);
String data;
storage->GetData(data);
data.Trim();
if(data.empty())
{
return Status(Status::OK);
}
try
{
json::Object allBackendsObject;
std::istringstream str(data);
json::Reader::Read(allBackendsObject, str);
String backend = backendServer;
backend.ReplaceAll("https://", "");
backend.ReplaceAll("http://", "");
json::Object::const_iterator i = allBackendsObject.Find(backend);
if(i == allBackendsObject.End())
{
return Status(Status::OK);
}
const json::Object& rootObject = (const json::Object&) i->element;
for(i = rootObject.Begin(); i != rootObject.End(); ++i)
{
const String& mpinIdHex = i->name;
String mpinId = util::HexDecode(mpinIdHex);
util::JsonObject mpinIdJson;
if(!mpinIdJson.Parse(mpinId.c_str()))
{
return Status(Status::STORAGE_ERROR, String().Format("Failed to parse mpinId json: '%s'", mpinId.c_str()));
}
const json::Object& userObject = (const json::Object&) i->element;
const std::string& id = ((const json::String&) mpinIdJson["userID"]).Value();
std::string deviceName;
json::Object::const_iterator dni = userObject.Find("deviceName");
if(dni != userObject.End())
{
deviceName = ((const json::String&) dni->element).Value();
}
String regOTT;
Status s = m_crypto->LoadRegOTT(mpinId, regOTT);
if(s != Status::OK)
{
return s;
}
UserPtr user = MakeNewUser(id, deviceName);
s = user->RestoreState(((const json::String&) userObject["state"]).Value(), mpinIdHex, regOTT);
if(s != Status::OK)
{
return s;
}
const json::Object& timePermitCacheObject = (const json::Object&) userObject["timePermitCache"];
int date = (int) ((const json::Number&) timePermitCacheObject["date"]).Value();
const String& timePermit = util::HexDecode(((const json::String&) timePermitCacheObject["timePermit"]).Value());
user->CacheTimePermit(timePermit, date);
usersMap[id] = user;
}
}
catch(const json::Exception& e)
{
return Status(Status::STORAGE_ERROR, e.what());
}
return Status(Status::OK);
}
Status MPinSDK::ListBackends(OUT std::vector<String>& backends) const
{
Status s = CheckIfIsInitialized();
if(s != Status::OK)
{
return s;
}
IStorage* storage = m_context->GetStorage(IStorage::NONSECURE);
String data;
storage->GetData(data);
data.Trim();
if(data.empty())
{
return Status::OK;
}
try
{
json::Object allBackendsObject;
std::istringstream str(data);
json::Reader::Read(allBackendsObject, str);
for(json::Object::const_iterator i = allBackendsObject.Begin(); i != allBackendsObject.End(); ++i)
{
backends.push_back(i->name);
}
}
catch(const json::Exception& e)
{
return Status(Status::STORAGE_ERROR, e.what());
}
return Status::OK;
}
const char * MPinSDK::GetVersion()
{
return MPIN_SDK_V2_VERSION;
}
bool MPinSDK::CanLogout(UserPtr user)
{
LogoutDataMap::iterator i = m_logoutData.find(user);
if (i == m_logoutData.end()) return false;
if (i->second.logoutURL.empty()) return false;
return true;
}
bool MPinSDK::Logout(UserPtr user)
{
LogoutDataMap::iterator i = m_logoutData.find(user);
if (i == m_logoutData.end()) return false;
if (i->second.logoutURL.empty()) return false;
util::JsonObject logoutData;
if (!logoutData.Parse(i->second.logoutData.c_str()))
{
return false;
}
String url = String().Format("%s%s", m_RPAServer.c_str(), i->second.logoutURL.c_str());
HttpResponse response = MakeRequest(url, IHttpRequest::POST, logoutData);
if(response.GetStatus() != HttpResponse::HTTP_OK)
{
return false;
}
m_logoutData.erase(i);
return true;
}
class StringVisitor:public json::Visitor
{
public:
virtual ~StringVisitor() {}
virtual void Visit(json::Array& array) {}
virtual void Visit(json::Object& object) {}
virtual void Visit(json::Null& null){}
virtual void Visit(json::Number& number)
{
data << (int) number.Value();
}
virtual void Visit(json::String& string)
{
data << string.Value();
}
virtual void Visit(json::Boolean& boolean)
{
data << (boolean.Value() ? "true" : "false");
}
String GetData()
{
return data.str();
}
private:
std::stringstream data;
};
String MPinSDK::GetClientParam(const String& key)
{
StringVisitor sv;
m_clientSettings[key].Accept(sv);
return sv.GetData();
}