package at.gv.egiz.eaaf.core.impl.utils; import java.nio.charset.StandardCharsets; import java.security.Provider; import java.util.Base64; import javax.annotation.Nonnull; import javax.annotation.PostConstruct; 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.jose4j.jca.ProviderContext; import org.jose4j.jwa.AlgorithmConstraints; import org.jose4j.jwa.AlgorithmConstraints.ConstraintType; import org.jose4j.jwe.ContentEncryptionAlgorithmIdentifiers; import org.jose4j.jwe.JsonWebEncryption; import org.jose4j.jwe.KeyManagementAlgorithmIdentifiers; import org.jose4j.lang.JoseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; 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.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; import at.gv.egiz.eaaf.core.impl.data.Pair; /** * PendingRequestId generation strategy based on signed tokens that facilitates * extended token validation. * * @author tlenz * */ public class AuthenticatedEncryptionPendingRequestIdGenerationStrategy implements IPendingRequestIdGenerationStrategy { private static final Logger log = LoggerFactory.getLogger(AuthenticatedEncryptionPendingRequestIdGenerationStrategy.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_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 = 2; 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 Pair key = null; private final String salt = "notRequiredInThisScenario"; @Override public String generateExternalPendingRequestId() throws EaafException { try { final String toSign = buildInternalToken(Random.nextLongRandom(), DateTime.now()); return encryptAndEncodeToken(toSign); } catch (final JoseException e) { throw new EaafException("internal.pendingreqid.02", new Object[] { e.getMessage() }, e); } } @Override public String getPendingRequestIdWithOutChecks(final String externalPendingReqId) throws PendingReqIdValidationException { try { String stringToken = getDecryptedExternalPendingRequestId(externalPendingReqId); log.debug("Token decryption successful"); if (!(StringUtils.countMatches(stringToken, TOKEN_SEPARATOR) == ENCODED_TOKEN_PARTS - 1)) { log.warn("PendingRequestId has an unvalid format"); log.debug("PendingRequestId: {}", stringToken); throw new PendingReqIdValidationException(null, "internal.pendingreqid.01"); } final String[] tokenElements = StringUtils.split(stringToken, TOKEN_SEPARATOR, ENCODED_TOKEN_PARTS); return tokenElements[1]; } catch (JoseException e) { log.warn("Token is NOT a valid String. Msg: {}", e.getMessage()); log.info("TokenValue: {}", externalPendingReqId); throw new PendingReqIdValidationException(null, "internal.pendingreqid.05", e); } } @Override public String validateAndGetPendingRequestId(final String externalPendingReqId) throws PendingReqIdValidationException { try { String stringToken = getDecryptedExternalPendingRequestId(externalPendingReqId); log.debug("Token decryption successful"); if (!(StringUtils.countMatches(stringToken, TOKEN_SEPARATOR) == ENCODED_TOKEN_PARTS - 1)) { log.info("PendingRequestId: {}", stringToken); throw new PendingReqIdValidationException(null, "internal.pendingreqid.01"); } final String[] tokenElements = StringUtils.split(stringToken, TOKEN_SEPARATOR, ENCODED_TOKEN_PARTS); final String internalPendingReqId = tokenElements[1]; final DateTime timeStamp = TOKEN_TEXTUAL_DATE_FORMAT.parseDateTime(tokenElements[0]); log.trace("Checking valid period ... "); final DateTime now = DateTime.now(); if (timeStamp.withFieldAdded(DurationFieldType.seconds(), maxPendingRequestIdLifeTime) .isBefore(now)) { log.info("Token exceeds the valid period. Token: {} | Now: {}", timeStamp, now); throw new PendingReqIdValidationException(internalPendingReqId, "internal.pendingreqid.06"); } log.debug("Token valid-period check successful"); return internalPendingReqId; } catch (JoseException e) { log.warn("Token is NOT a valid encrypt. Msg: {}", e.getMessage()); log.info("TokenValue: {}", externalPendingReqId); throw new PendingReqIdValidationException(null, "internal.pendingreqid.04", e); } catch (final IllegalArgumentException e) { log.warn("Token is NOT a valid String. Msg: {}", e.getMessage()); log.info("TokenValue: {}", externalPendingReqId); throw new PendingReqIdValidationException(null, "internal.pendingreqid.05", e); } } @Nonnull private String getDecryptedExternalPendingRequestId(String externalPendingReqId) throws JoseException, 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"); } JsonWebEncryption encToken = new JsonWebEncryption(); encToken.setContentEncryptionAlgorithmConstraints(new AlgorithmConstraints( ConstraintType.WHITELIST, ContentEncryptionAlgorithmIdentifiers.AES_128_GCM)); encToken.setAlgorithmConstraints(new AlgorithmConstraints( ConstraintType.WHITELIST, KeyManagementAlgorithmIdentifiers.DIRECT, KeyManagementAlgorithmIdentifiers.A128GCMKW )); encToken.setKey(key.getFirst()); if (key.getSecond() != null) { final ProviderContext providerCtx = new ProviderContext(); providerCtx.getSuppliedKeyProviderContext().setSignatureProvider( key.getSecond().getName()); encToken.setProviderContext(providerCtx); } encToken.setCompactSerialization(new String(externalPendingReqIdBytes, StandardCharsets.UTF_8)); return encToken.getPayload(); } private String selectKeyWrappingAlgorithm(SecretKey first) { if ("AES".equals(first.getAlgorithm())) { return KeyManagementAlgorithmIdentifiers.A128GCMKW; } else { return KeyManagementAlgorithmIdentifiers.DIRECT; } } @PostConstruct private void initialize() throws EaafConfigurationException { log.debug("Initializing " + this.getClass().getName() + " ... "); 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); } 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 Alg: {} and maxLifeTime: {}", ContentEncryptionAlgorithmIdentifiers.AES_128_GCM, maxPendingRequestIdLifeTime); } protected String buildInternalToken(final String internalPendingReqId, final DateTime now) { return new StringBuilder().append(TOKEN_TEXTUAL_DATE_FORMAT.print(now)).append(TOKEN_SEPARATOR) .append(internalPendingReqId).toString(); } protected String encryptAndEncodeToken(String token) throws JoseException { JsonWebEncryption encToken = new JsonWebEncryption(); encToken.setAlgorithmHeaderValue(selectKeyWrappingAlgorithm(key.getFirst())); encToken.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_128_GCM); encToken.setKey(key.getFirst()); encToken.setPayload(token); if (key.getSecond() != null) { final ProviderContext providerCtx = new ProviderContext(); providerCtx.getSuppliedKeyProviderContext().setSignatureProvider( key.getSecond().getName()); encToken.setProviderContext(providerCtx); } return Base64.getUrlEncoder() .encodeToString(encToken.getCompactSerialization().getBytes(StandardCharsets.UTF_8)); } }