Skip to content

Commit

Permalink
Merge pull request #1457 from LoganathanSekar7627/MOSIP-29931-otp-man…
Browse files Browse the repository at this point in the history
…ager-security-fix-1.2.0.1

MOSIP-29931 Fix to store hash of otp and key
  • Loading branch information
gsasikumar authored Oct 27, 2023
2 parents f2fc5db + 0939574 commit 1086447
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/
public enum SqlQueryConstants {
UPDATE("UPDATE"), ID("id"), NEW_OTP_STATUS("newOtpStatus"), NEW_NUM_OF_ATTEMPT("newNumOfAttempt"),
NEW_VALIDATION_TIME("newValidationTime");
NEW_VALIDATION_TIME("newValidationTime"), REF_ID("refId");

/**
* The property.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.mosip.kernel.otpmanager.repository;

import java.util.Optional;

import org.springframework.stereotype.Repository;

import io.mosip.kernel.core.dataaccess.spi.repository.BaseRepository;
Expand All @@ -16,4 +18,5 @@
*/
@Repository
public interface OtpRepository extends BaseRepository<OtpEntity, String> {
Optional<OtpEntity> findByRefId(String refId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
Expand Down Expand Up @@ -33,7 +32,6 @@ public class OtpGeneratorServiceImpl implements OtpGenerator<OtpGeneratorRequest
/**
* The reference that autowires OtpRepository class.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(OtpGeneratorServiceImpl.class);
@Autowired
private OtpRepository otpRepository;

Expand Down Expand Up @@ -89,9 +87,10 @@ public OtpGeneratorResponseDto getOtp(OtpGeneratorRequestDto otpDto) {
/*
* Checking whether the key exists in the repository.
*/
OtpEntity keyCheck = otpRepository.findById(OtpEntity.class, otpDto.getKey());
if ((keyCheck != null) && (keyCheck.getStatusCode().equals(OtpStatusConstants.KEY_FREEZED.getProperty()))
&& (OtpManagerUtils.timeDifferenceInSeconds(keyCheck.getUpdatedDtimes(),
String refIdHash = OtpManagerUtils.getHash(otpDto.getKey());
Optional<OtpEntity> entityOpt = otpRepository.findByRefId(refIdHash);
if (entityOpt.isPresent() && (entityOpt.get().getStatusCode().equals(OtpStatusConstants.KEY_FREEZED.getProperty()))
&& (OtpManagerUtils.timeDifferenceInSeconds(entityOpt.get().getUpdatedDtimes(),
LocalDateTime.now(ZoneId.of("UTC"))) <= Integer.parseInt(keyFreezeTime))) {
response.setOtp(OtpStatusConstants.SET_AS_NULL_IN_STRING.getProperty());
response.setStatus(OtpStatusConstants.BLOCKED_USER.getProperty());
Expand All @@ -103,9 +102,9 @@ public OtpGeneratorResponseDto getOtp(OtpGeneratorRequestDto otpDto) {
}

OtpEntity otp = new OtpEntity();
otp.setId(otpDto.getKey());
otp.setId(OtpManagerUtils.getKeyOtpHash(otpDto.getKey(), generatedOtp));
otp.setRefId(refIdHash);
otp.setValidationRetryCount(0);
otp.setOtp(generatedOtp);
otpRepository.save(otp);
response.setOtp(generatedOtp);
response.setStatus(OtpStatusConstants.GENERATION_SUCCESSFUL.getProperty());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -35,6 +36,11 @@
@RefreshScope
@Service
public class OtpValidatorServiceImpl implements OtpValidator<ResponseEntity<OtpValidatorResponseDto>> {

private static final String UPDATE_VALIDATION_RETRY_COUNT_QUERY = "%s %s SET validation_retry_count = :newNumOfAttempt,upd_dtimes = :newValidationTime WHERE refId=:refId";

private static final String UPDATE_STATUS_CODE_AND_RETRY_COUNT_QUERY = "%s %s SET status_code = :newOtpStatus, validation_retry_count = :newNumOfAttempt, upd_dtimes = :newValidationTime WHERE refId=:refId";

/**
* The reference that autowires OtpRepository.
*/
Expand Down Expand Up @@ -82,31 +88,33 @@ public ResponseEntity<OtpValidatorResponseDto> validateOtp(String key, String ot
OtpValidatorResponseDto responseDto;

// The OTP entity for a specific key.
OtpEntity otpResponse = otpRepository.findById(OtpEntity.class, key);
String refIdHash = OtpManagerUtils.getHash(key);
Optional<OtpEntity> otpEntityOpt = otpRepository.findByRefId(refIdHash);
responseDto = new OtpValidatorResponseDto();
responseDto.setMessage(OtpStatusConstants.FAILURE_MESSAGE.getProperty());
responseDto.setStatus(OtpStatusConstants.FAILURE_STATUS.getProperty());
validationResponseEntity = new ResponseEntity<>(responseDto, HttpStatus.OK);

requireKeyNotFound(otpResponse);
requireKeyNotFound(otpEntityOpt);
// This variable holds the update query to be performed.
String updateString;
// This variable holds the count of number
int attemptCount = otpResponse.getValidationRetryCount();
if ((OtpManagerUtils.timeDifferenceInSeconds(otpResponse.getGeneratedDtimes(),
OtpEntity otpEntity = otpEntityOpt.get();
int attemptCount = otpEntity.getValidationRetryCount();
if ((OtpManagerUtils.timeDifferenceInSeconds(otpEntity.getGeneratedDtimes(),
OtpManagerUtils.getCurrentLocalDateTime())) > (Integer.parseInt(otpExpiryLimit))) {

responseDto.setStatus(OtpStatusConstants.FAILURE_STATUS.getProperty());
responseDto.setMessage(OtpStatusConstants.OTP_EXPIRED_STATUS.getProperty());
return new ResponseEntity<>(responseDto, HttpStatus.OK);
}
String keyOtpHash = OtpManagerUtils.getKeyOtpHash(key, otp);
// This condition increases the validation attempt count.
if ((attemptCount < Integer.parseInt(numberOfValidationAttemptsAllowed))
&& (otpResponse.getStatusCode().equals(OtpStatusConstants.UNUSED_OTP.getProperty()))) {
updateString = SqlQueryConstants.UPDATE.getProperty() + " " + OtpEntity.class.getSimpleName()
+ " SET validation_retry_count = :newNumOfAttempt,"
+ "upd_dtimes = :newValidationTime WHERE id=:id";
HashMap<String, Object> updateMap = createUpdateMap(key, null, attemptCount + 1,
&& (otpEntity.getStatusCode().equals(OtpStatusConstants.UNUSED_OTP.getProperty()))) {
updateString = String.format(UPDATE_VALIDATION_RETRY_COUNT_QUERY, SqlQueryConstants.UPDATE.getProperty(),
OtpEntity.class.getSimpleName());
HashMap<String, Object> updateMap = createUpdateMap(otpEntity.getRefId(), null, attemptCount + 1,
LocalDateTime.now(ZoneId.of("UTC")));
updateData(updateString, updateMap);
}
Expand All @@ -115,11 +123,10 @@ public ResponseEntity<OtpValidatorResponseDto> validateOtp(String key, String ot
* reaches the maximum allowed limit.
*/
if ((attemptCount == Integer.parseInt(numberOfValidationAttemptsAllowed) - 1)
&& (!otp.equals(otpResponse.getOtp()))) {
updateString = SqlQueryConstants.UPDATE.getProperty() + " " + OtpEntity.class.getSimpleName()
+ " SET status_code = :newOtpStatus," + "upd_dtimes = :newValidationTime,"
+ "validation_retry_count = :newNumOfAttempt WHERE id=:id";
HashMap<String, Object> updateMap = createUpdateMap(key, OtpStatusConstants.KEY_FREEZED.getProperty(), 0,
&& (!keyOtpHash.equals(otpEntity.getId()))) {
updateString = String.format(UPDATE_STATUS_CODE_AND_RETRY_COUNT_QUERY, SqlQueryConstants.UPDATE.getProperty(),
OtpEntity.class.getSimpleName());
HashMap<String, Object> updateMap = createUpdateMap(otpEntity.getRefId(), OtpStatusConstants.KEY_FREEZED.getProperty(), 0,
OtpManagerUtils.getCurrentLocalDateTime());
updateData(updateString, updateMap);
responseDto.setStatus(OtpStatusConstants.FAILURE_STATUS.getProperty());
Expand All @@ -128,7 +135,7 @@ public ResponseEntity<OtpValidatorResponseDto> validateOtp(String key, String ot
return validationResponseEntity;

}
validationResponseEntity = unFreezeKey(key, otp, otpResponse, attemptCount, responseDto,
validationResponseEntity = unFreezeKey(keyOtpHash, otpEntity, attemptCount, responseDto,
validationResponseEntity);
/*
* This condition validates the OTP if neither the key is in freezed condition,
Expand All @@ -137,24 +144,24 @@ public ResponseEntity<OtpValidatorResponseDto> validateOtp(String key, String ot
* is expired, the specific message is returned as response and the entire
* record is deleted.
*/
if ((otpResponse.getOtp().equals(otp))
&& (otpResponse.getStatusCode().equals(OtpStatusConstants.UNUSED_OTP.getProperty())
&& ((OtpManagerUtils.timeDifferenceInSeconds(otpResponse.getGeneratedDtimes(),
if ((otpEntity.getId().equals(keyOtpHash))
&& (otpEntity.getStatusCode().equals(OtpStatusConstants.UNUSED_OTP.getProperty())
&& ((OtpManagerUtils.timeDifferenceInSeconds(otpEntity.getGeneratedDtimes(),
OtpManagerUtils.getCurrentLocalDateTime())) <= (Integer.parseInt(otpExpiryLimit))))) {
responseDto.setStatus(OtpStatusConstants.SUCCESS_STATUS.getProperty());
responseDto.setMessage(OtpStatusConstants.SUCCESS_MESSAGE.getProperty());
otpRepository.deleteById(key);
otpRepository.deleteById(keyOtpHash);
return new ResponseEntity<>(responseDto, HttpStatus.OK);
}
return validationResponseEntity;
}

private void requireKeyNotFound(OtpEntity otpResponse) {
private void requireKeyNotFound(Optional<OtpEntity> entityOpt) {
/*
* Checking whether the key exists in repository or not. If not, throw an
* exception.
*/
if (otpResponse == null) {
if (entityOpt.isEmpty()) {
List<ServiceError> validationErrorsList = new ArrayList<>();
validationErrorsList.add(new ServiceError(OtpErrorConstants.OTP_VAL_KEY_NOT_FOUND.getErrorCode(),
OtpErrorConstants.OTP_VAL_KEY_NOT_FOUND.getErrorMessage()));
Expand Down Expand Up @@ -186,29 +193,28 @@ private ResponseEntity<OtpValidatorResponseDto> proxyForLocalProfile(String otp)
*
* @param key the key.
* @param otp the OTP.
* @param otpResponse the OTP response.
* @param otpEntity the OTP response.
* @param attemptCount the attempt count.
* @param responseDto the response dto.
* @param validationResponseEntity the validation response entity.
* @return the response entity.
*/
private ResponseEntity<OtpValidatorResponseDto> unFreezeKey(String key, String otp, OtpEntity otpResponse,
private ResponseEntity<OtpValidatorResponseDto> unFreezeKey(String keyOtpHash, OtpEntity otpEntity,
int attemptCount, OtpValidatorResponseDto responseDto,
ResponseEntity<OtpValidatorResponseDto> validationResponseEntity) {
String updateString;
if (otpResponse.getStatusCode().equals(OtpStatusConstants.KEY_FREEZED.getProperty())) {
if ((OtpManagerUtils.timeDifferenceInSeconds(otpResponse.getUpdatedDtimes(),
if (otpEntity.getStatusCode().equals(OtpStatusConstants.KEY_FREEZED.getProperty())) {
if ((OtpManagerUtils.timeDifferenceInSeconds(otpEntity.getUpdatedDtimes(),
OtpManagerUtils.getCurrentLocalDateTime())) > (Integer.parseInt(keyFreezeDuration))) {
updateString = SqlQueryConstants.UPDATE.getProperty() + " " + OtpEntity.class.getSimpleName()
+ " SET status_code = :newOtpStatus," + " validation_retry_count = :newNumOfAttempt,"
+ " upd_dtimes = :newValidationTime WHERE id=:id";
HashMap<String, Object> updateMap = createUpdateMap(key, OtpStatusConstants.UNUSED_OTP.getProperty(),
updateString = String.format(UPDATE_STATUS_CODE_AND_RETRY_COUNT_QUERY, SqlQueryConstants.UPDATE.getProperty(),
OtpEntity.class.getSimpleName());
HashMap<String, Object> updateMap = createUpdateMap(otpEntity.getRefId(), OtpStatusConstants.UNUSED_OTP.getProperty(),
Integer.valueOf(attemptCount + 1), OtpManagerUtils.getCurrentLocalDateTime());
if (otp.equals(otpResponse.getOtp())) {
if (keyOtpHash.equals(otpEntity.getId())) {
responseDto.setStatus(OtpStatusConstants.SUCCESS_STATUS.getProperty());
responseDto.setMessage(OtpStatusConstants.SUCCESS_MESSAGE.getProperty());
validationResponseEntity = new ResponseEntity<>(responseDto, HttpStatus.OK);
otpRepository.deleteById(key);
otpRepository.deleteById(keyOtpHash);
} else {
updateData(updateString, updateMap);
}
Expand All @@ -233,7 +239,7 @@ private HashMap<String, Object> createUpdateMap(String key, String status, Integ
LocalDateTime localDateTime) {
HashMap<String, Object> updateMap = new HashMap<>();
if (key != null) {
updateMap.put(SqlQueryConstants.ID.getProperty(), key);
updateMap.put(SqlQueryConstants.REF_ID.getProperty(), key);
}
if (status != null) {
updateMap.put(SqlQueryConstants.NEW_OTP_STATUS.getProperty(), status);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.mosip.kernel.otpmanager.util;

import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
Expand All @@ -10,7 +11,9 @@
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;

import io.mosip.kernel.core.exception.BaseUncheckedException;
import io.mosip.kernel.core.exception.ServiceError;
import io.mosip.kernel.core.util.HMACUtils2;
import io.mosip.kernel.core.util.StringUtils;
import io.mosip.kernel.otpmanager.constant.OtpErrorConstants;
import io.mosip.kernel.otpmanager.exception.OtpInvalidArgumentException;
Expand All @@ -33,6 +36,8 @@ public class OtpManagerUtils {

@Value("${mosip.kernel.otp.max-key-length}")
String keyMaxLength;

private static final String KEY_OTP_SEPARATOR = ":";

/**
* This method returns the difference between two LocalDateTime objects in
Expand Down Expand Up @@ -88,4 +93,17 @@ public void validateOtpRequestArguments(String key, String otp) {
throw new OtpInvalidArgumentException(validationErrorsList);
}
}

public static String getKeyOtpHash(String key, String otp) {
return getHash(key + KEY_OTP_SEPARATOR + otp);
}

public static String getHash(String string) {
try {
return HMACUtils2.digestAsPlainText(string.getBytes());
} catch (NoSuchAlgorithmException e) {
throw new BaseUncheckedException(OtpErrorConstants.OTP_GEN_ALGO_FAILURE.getErrorCode(),
OtpErrorConstants.OTP_GEN_ALGO_FAILURE.getErrorMessage(), e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Optional;

import org.junit.Test;
import org.junit.runner.RunWith;
Expand Down Expand Up @@ -40,12 +41,14 @@ public class OtpValidatorServiceTest {
@Test
public void testOtpValidatorServicePositiveCase() throws Exception {
OtpEntity entity = new OtpEntity();
entity.setOtp("1234");
entity.setId("testKey");
//Hash of testKey:1234
entity.setId("6DB5C886D3E9375E2C7BFBCE326A708734836151585059CD38F3CF586A125732");
// Hash of testKey
entity.setRefId("15291F67D99EA7BC578C3544DADFBB991E66FA69CB36FF70FE30E798E111FF5F");
entity.setValidationRetryCount(0);
entity.setStatusCode("OTP_UNUSED");
entity.setUpdatedDtimes(LocalDateTime.now(ZoneId.of("UTC")).plusSeconds(50));
when(repository.findById(OtpEntity.class, "testKey")).thenReturn(entity);
when(repository.findByRefId("15291F67D99EA7BC578C3544DADFBB991E66FA69CB36FF70FE30E798E111FF5F")).thenReturn(Optional.of(entity));
mockMvc.perform(get("/otp/validate?key=testKey&otp=1234").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.response.status", is("success")));
}
Expand All @@ -54,12 +57,14 @@ public void testOtpValidatorServicePositiveCase() throws Exception {
@Test
public void testOtpValidatorServiceNegativeCase() throws Exception {
OtpEntity entity = new OtpEntity();
entity.setOtp("1234");
entity.setId("testKey");
//Hash of testKey:1234
entity.setId("6DB5C886D3E9375E2C7BFBCE326A708734836151585059CD38F3CF586A125732");
// Hash of testKey
entity.setRefId("15291F67D99EA7BC578C3544DADFBB991E66FA69CB36FF70FE30E798E111FF5F");
entity.setValidationRetryCount(0);
entity.setStatusCode("OTP_UNUSED");
entity.setUpdatedDtimes(LocalDateTime.now());
when(repository.findById(OtpEntity.class, "testKey")).thenReturn(entity);
when(repository.findByRefId("15291F67D99EA7BC578C3544DADFBB991E66FA69CB36FF70FE30E798E111FF5F")).thenReturn(Optional.of(entity));
mockMvc.perform(get("/otp/validate?key=testKey&otp=5431").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.response.status", is("failure")));
}
Expand All @@ -68,12 +73,14 @@ public void testOtpValidatorServiceNegativeCase() throws Exception {
@Test
public void testOtpValidatorServiceWhenMaxAttemptReached() throws Exception {
OtpEntity entity = new OtpEntity();
entity.setOtp("1234");
entity.setId("testKey");
//Hash of 1234:testKey
entity.setId("6DB5C886D3E9375E2C7BFBCE326A708734836151585059CD38F3CF586A125732");
// Hash of testKey
entity.setRefId("15291F67D99EA7BC578C3544DADFBB991E66FA69CB36FF70FE30E798E111FF5F");
entity.setValidationRetryCount(3);
entity.setStatusCode("OTP_UNUSED");
entity.setUpdatedDtimes(LocalDateTime.now());
when(repository.findById(OtpEntity.class, "testKey")).thenReturn(entity);
when(repository.findByRefId("15291F67D99EA7BC578C3544DADFBB991E66FA69CB36FF70FE30E798E111FF5F")).thenReturn(Optional.of(entity));
mockMvc.perform(get("/otp/validate?key=testKey&otp=5431").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.response.status", is("failure")));
}
Expand All @@ -82,12 +89,14 @@ public void testOtpValidatorServiceWhenMaxAttemptReached() throws Exception {
@Test
public void testOtpValidatorServiceWhenKeyFreezedPositiveCase() throws Exception {
OtpEntity entity = new OtpEntity();
entity.setOtp("1234");
entity.setId("testKey");
//Hash of 1234:testKey
entity.setId("6DB5C886D3E9375E2C7BFBCE326A708734836151585059CD38F3CF586A125732");
// Hash of testKey
entity.setRefId("15291F67D99EA7BC578C3544DADFBB991E66FA69CB36FF70FE30E798E111FF5F");
entity.setValidationRetryCount(3);
entity.setStatusCode("KEY_FREEZED");
entity.setUpdatedDtimes(LocalDateTime.now(ZoneId.of("UTC")).minus(1, ChronoUnit.MINUTES));
when(repository.findById(OtpEntity.class, "testKey")).thenReturn(entity);
when(repository.findByRefId("15291F67D99EA7BC578C3544DADFBB991E66FA69CB36FF70FE30E798E111FF5F")).thenReturn(Optional.of(entity));
mockMvc.perform(get("/otp/validate?key=testKey&otp=2345").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.response.status", is("failure")));
}
Expand All @@ -96,12 +105,14 @@ public void testOtpValidatorServiceWhenKeyFreezedPositiveCase() throws Exception
@Test
public void testOtpValidatorServiceWhenKeyFreezedNegativeCase() throws Exception {
OtpEntity entity = new OtpEntity();
entity.setOtp("1234");
entity.setId("testKey");
//Hash of 1234:testKey
entity.setId("6DB5C886D3E9375E2C7BFBCE326A708734836151585059CD38F3CF586A125732");
// Hash of testKey
entity.setRefId("15291F67D99EA7BC578C3544DADFBB991E66FA69CB36FF70FE30E798E111FF5F");
entity.setValidationRetryCount(0);
entity.setStatusCode("KEY_FREEZED");
entity.setUpdatedDtimes(LocalDateTime.now().minus(20, ChronoUnit.SECONDS));
when(repository.findById(OtpEntity.class, "testKey")).thenReturn(entity);
when(repository.findByRefId("15291F67D99EA7BC578C3544DADFBB991E66FA69CB36FF70FE30E798E111FF5F")).thenReturn(Optional.of(entity));
mockMvc.perform(get("/otp/validate?key=testKey&otp=1234").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.response.status", is("failure")));
}
Expand Down

0 comments on commit 1086447

Please sign in to comment.