package at.gv.egiz.eaaf.core.impl.utils; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import java.util.Arrays; import java.util.Base64; import javax.annotation.PostConstruct; import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.joda.time.DurationFieldType; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import at.gv.egiz.eaaf.core.api.idp.IConfiguration; import at.gv.egiz.eaaf.core.api.utils.IPendingRequestIdGenerationStrategy; import at.gv.egiz.eaaf.core.exceptions.EAAFConfigurationException; import at.gv.egiz.eaaf.core.exceptions.EAAFException; import at.gv.egiz.eaaf.core.exceptions.EAAFIllegalStateException; import at.gv.egiz.eaaf.core.exceptions.PendingReqIdValidationException; /** * PendingRequestId generation strategy based on signed tokens that facilitates extended token validation * * @author tlenz * */ public class SecurePendingRequestIdGenerationStrategy implements IPendingRequestIdGenerationStrategy { private static final Logger log = LoggerFactory.getLogger(SecurePendingRequestIdGenerationStrategy.class); @Autowired(required=true) IConfiguration baseConfig; public static final String CONFIG_PROP_PENDINGREQUESTID_DIGIST_SECRET = "core.pendingrequestid.digist.secret"; public static final String CONFIG_PROP_PENDINGREQUESTID_DIGIST_ALGORITHM = "core.pendingrequestid.digist.algorithm"; public static final String CONFIG_PROP_PENDINGREQUESTID_MAX_LIFETIME = "core.pendingrequestid.maxlifetime"; public static final String DEFAULT_PENDINGREQUESTID_DIGIST_ALGORITHM = "HmacSHA256"; public static final String DEFAULT_PENDINGREQUESTID_MAX_LIFETIME = "300"; private static final int ENCODED_TOKEN_PARTS = 3; private static final String TOKEN_SEPARATOR = "|"; private static final DateTimeFormatter TOKEN_TEXTUAL_DATE_FORMAT = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss SSS"); private int maxPendingRequestIdLifeTime = 300; private final int maxPendingReqIdSize = 1024; private String digistAlgorithm = null; private SecretKey key = null; private final byte[] salt = "notRequiredInThisScenario".getBytes(); @Override public String generateExternalPendingRequestId() throws EAAFException { try { final String toSign = buildInternalToken(Random.nextLongRandom(), DateTime.now()); final StringBuilder externalPendingRequestId= new StringBuilder(); externalPendingRequestId.append(toSign); externalPendingRequestId.append(TOKEN_SEPARATOR); externalPendingRequestId.append(Base64.getEncoder().encodeToString(calculateHMAC(toSign))); return Base64.getUrlEncoder().encodeToString(externalPendingRequestId.toString().getBytes("UTF-8")); } catch (final UnsupportedEncodingException e) { throw new EAAFException("internal.99", new Object[] {e.getMessage()}, e); } } @Override public String getPendingRequestIdWithOutChecks(String externalPendingReqId) throws PendingReqIdValidationException { final String[] tokenElements = extractTokens(externalPendingReqId); return tokenElements[1]; } @Override public String validateAndGetPendingRequestId(String externalPendingReqId) throws PendingReqIdValidationException { try { final String[] tokenElements = extractTokens(externalPendingReqId); final String internalPendingReqId = tokenElements[1]; final DateTime timeStamp = TOKEN_TEXTUAL_DATE_FORMAT.parseDateTime(tokenElements[0]); log.trace("Checking HMAC from externalPendingReqId ... "); final byte[] tokenDigest = Base64.getDecoder().decode(tokenElements[2]); final byte[] refDigist = calculateHMAC(buildInternalToken(internalPendingReqId, timeStamp)); if (!Arrays.equals(tokenDigest, refDigist)) { log.warn("Digest of Token does NOT match"); log.debug("Token: {} | Ref: {}", tokenDigest, refDigist); throw new PendingReqIdValidationException(null, "Digest of pendingRequestId does NOT match"); } log.debug("PendingRequestId HMAC digest check successful"); log.trace("Checking valid period ... "); final DateTime now = DateTime.now(); if (timeStamp.withFieldAdded( DurationFieldType.seconds(), maxPendingRequestIdLifeTime).isBefore(now)) { log.warn("Token exceeds the valid period"); log.debug("Token: {} | Now: {}", timeStamp, now ); throw new PendingReqIdValidationException(internalPendingReqId, "PendingRequestId exceeds the valid period"); } log.debug("Token valid-period check successful"); return internalPendingReqId; } catch (final IllegalArgumentException | EAAFIllegalStateException e) { log.warn("Token is NOT a valid String. Msg: {}", e.getMessage()); log.debug("TokenValue: {}", externalPendingReqId); throw new PendingReqIdValidationException(null, "PendingReqId is NOT a valid String", e); } } @NonNull private String[] extractTokens(@Nullable String externalPendingReqId) throws PendingReqIdValidationException { if (StringUtils.isEmpty(externalPendingReqId)) { log.info("PendingReqId is 'null' or empty"); throw new PendingReqIdValidationException(null, "PendingReqId is 'null' or empty"); } log.trace("RAW external pendingReqId: {}", externalPendingReqId); final byte[] externalPendingReqIdBytes = Base64.getUrlDecoder().decode(externalPendingReqId); if (externalPendingReqIdBytes.length > maxPendingReqIdSize) { log.warn("pendingReqId size exceeds {}", maxPendingReqIdSize); throw new PendingReqIdValidationException(null, "pendingReqId exceeds max.size: " + maxPendingReqIdSize); } final String stringToken = new String(externalPendingReqIdBytes); if (StringUtils.countMatches(stringToken, TOKEN_SEPARATOR) == ENCODED_TOKEN_PARTS - 1) { final String[] tokenElements = StringUtils.split(stringToken, TOKEN_SEPARATOR, ENCODED_TOKEN_PARTS); return tokenElements; } else { log.warn("PendingRequestId has an unvalid format"); log.debug("PendingRequestId: {}", stringToken); throw new PendingReqIdValidationException(null, "PendingReqId has an unvalid format"); } } @PostConstruct private void initialize() throws EAAFConfigurationException { log.debug("Initializing " + this.getClass().getName() + " ... "); final String pendingReqIdDigistSecret = baseConfig.getBasicConfiguration(CONFIG_PROP_PENDINGREQUESTID_DIGIST_SECRET); if (StringUtils.isEmpty(pendingReqIdDigistSecret)) throw new EAAFConfigurationException("config.08", new Object[] {CONFIG_PROP_PENDINGREQUESTID_DIGIST_SECRET}); digistAlgorithm = baseConfig.getBasicConfiguration(CONFIG_PROP_PENDINGREQUESTID_DIGIST_ALGORITHM, DEFAULT_PENDINGREQUESTID_DIGIST_ALGORITHM); maxPendingRequestIdLifeTime = Integer.valueOf( baseConfig.getBasicConfiguration(CONFIG_PROP_PENDINGREQUESTID_MAX_LIFETIME, DEFAULT_PENDINGREQUESTID_MAX_LIFETIME)); try { final SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WITHHMACSHA256"); final KeySpec spec = new PBEKeySpec(pendingReqIdDigistSecret.toCharArray(), salt, 10000, 128); key = keyFactory.generateSecret(spec); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { log.error("Can NOT initialize TokenService with configuration object", e); throw new EAAFConfigurationException("config.09", new Object[] { CONFIG_PROP_PENDINGREQUESTID_DIGIST_SECRET, "Can NOT generate HMAC key"}, e); } log.info(this.getClass().getName() + " initialized with digistAlg: {} and maxLifeTime: {}", digistAlgorithm, maxPendingRequestIdLifeTime); } private String buildInternalToken(String internalPendingReqId, DateTime now) { return new StringBuilder() .append(TOKEN_TEXTUAL_DATE_FORMAT.print(now)) .append(TOKEN_SEPARATOR) .append(internalPendingReqId).toString(); } private byte[] calculateHMAC(String toSign) throws EAAFIllegalStateException { try { final Mac mac = Mac.getInstance(digistAlgorithm); mac.init(key); return mac.doFinal(toSign.getBytes("UTF-8")); } catch (UnsupportedEncodingException | NoSuchAlgorithmException | InvalidKeyException e) { log.error("Can NOT generate secure pendingRequestId", e); throw new EAAFIllegalStateException(new Object[] {"Can NOT caluclate digist for secure pendingRequestId"}, e); } } }