package at.gv.egiz.eaaf.core.impl.utils; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; import javax.annotation.PostConstruct; import javax.crypto.Mac; import javax.crypto.SecretKey; 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; import at.gv.egiz.eaaf.core.impl.credential.EaafKeyStoreFactory; import at.gv.egiz.eaaf.core.impl.credential.SymmetricKeyConfiguration; import at.gv.egiz.eaaf.core.impl.credential.SymmetricKeyConfiguration.SymmetricKeyType; /** * 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; @Autowired EaafKeyStoreFactory keyStoreFactory; private static final String FRIENDLYNAME = "pendingRequestId key"; public static final String CONFIG_PROP_PENDINGREQUESTID_DIGIST_TYPE = "core.pendingrequestid.digist.type"; public static final String CONFIG_PROP_PENDINGREQUESTID_DIGIST_SECRET = "core.pendingrequestid.digist.secret"; public static final String CONFIG_PROP_PENDINGREQUESTID_DIGIST_HSM_KEYSTORE = "core.pendingrequestid.digist.keystore.name"; public static final String CONFIG_PROP_PENDINGREQUESTID_DIGIST_HSM_ALIAS = "core.pendingrequestid.digist.key.alias"; 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 ZZ").withZoneUTC(); private int maxPendingRequestIdLifeTime = 300; private final int maxPendingReqIdSize = 1024; private String digistAlgorithm = null; private SecretKey key = null; private final String salt = "notRequiredInThisScenario"; @Override public String generateExternalPendingRequestId() throws EaafException { 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(StandardCharsets.UTF_8)); } @Override public String getPendingRequestIdWithOutChecks(final String externalPendingReqId) throws PendingReqIdValidationException { final String[] tokenElements = extractTokens(externalPendingReqId); return tokenElements[1]; } @Override public String validateAndGetPendingRequestId(final 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 (!MessageDigest.isEqual(refDigist,tokenDigest)) { log.warn("Digest of Token does NOT match"); log.debug("Token: {} | Ref: {}", tokenDigest, refDigist); throw new PendingReqIdValidationException(null, "internal.pendingreqid.04"); } 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, "internal.pendingreqid.06"); } 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, "internal.pendingreqid.06", e); } } @NonNull private String[] extractTokens(@Nullable final String externalPendingReqId) throws PendingReqIdValidationException { if (StringUtils.isEmpty(externalPendingReqId)) { log.info("PendingReqId is 'null' or empty"); throw new PendingReqIdValidationException(null, "internal.pendingreqid.00"); } 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, "internal.pendingreqid.03"); } final String stringToken = new String(externalPendingReqIdBytes, StandardCharsets.UTF_8); 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, "internal.pendingreqid.01"); } } @PostConstruct private void initialize() throws EaafConfigurationException { log.debug("Initializing " + this.getClass().getName() + " ... "); digistAlgorithm = baseConfig.getBasicConfiguration( CONFIG_PROP_PENDINGREQUESTID_DIGIST_ALGORITHM, DEFAULT_PENDINGREQUESTID_DIGIST_ALGORITHM); maxPendingRequestIdLifeTime = Integer.parseInt(baseConfig.getBasicConfiguration(CONFIG_PROP_PENDINGREQUESTID_MAX_LIFETIME, DEFAULT_PENDINGREQUESTID_MAX_LIFETIME)); SymmetricKeyConfiguration secretKeyConfig = new SymmetricKeyConfiguration(); secretKeyConfig.setFriendlyName(FRIENDLYNAME); secretKeyConfig.setKeyType( baseConfig.getBasicConfiguration(CONFIG_PROP_PENDINGREQUESTID_DIGIST_TYPE, SymmetricKeyType.PASSPHRASE.name())); secretKeyConfig.setSoftKeyPassphrase( baseConfig.getBasicConfiguration(CONFIG_PROP_PENDINGREQUESTID_DIGIST_SECRET)); secretKeyConfig.setSoftKeySalt(salt); secretKeyConfig.setKeyStoreName( baseConfig.getBasicConfiguration(CONFIG_PROP_PENDINGREQUESTID_DIGIST_HSM_KEYSTORE)); secretKeyConfig.setKeyAlias( baseConfig.getBasicConfiguration(CONFIG_PROP_PENDINGREQUESTID_DIGIST_HSM_ALIAS)); //validate symmetric-key configuration secretKeyConfig.validate(); try { key = keyStoreFactory.buildNewSymmetricKey(secretKeyConfig).getFirst(); } catch (EaafException 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(final String internalPendingReqId, final DateTime now) { return new StringBuilder().append(TOKEN_TEXTUAL_DATE_FORMAT.print(now)).append(TOKEN_SEPARATOR) .append(internalPendingReqId).toString(); } private byte[] calculateHmac(final String toSign) throws EaafIllegalStateException { try { final Mac mac = Mac.getInstance(digistAlgorithm); mac.init(key); return mac.doFinal(toSign.getBytes(StandardCharsets.UTF_8)); } catch (NoSuchAlgorithmException | InvalidKeyException e) { log.error("Can NOT generate secure pendingRequestId", e); throw new EaafIllegalStateException( new Object[] { "Can NOT caluclate digist for secure pendingRequestId" }, e); } } }