From 234cee5815f8688d45146b792ebe3c8015a7dc74 Mon Sep 17 00:00:00 2001 From: Thomas Lenz Date: Tue, 23 Jun 2020 08:57:18 +0200 Subject: add new pendingRequestId generation-strategy that uses authenticated-encryption protect the internal pendingRequesttId --- ...cryptionPendingRequestIdGenerationStrategy.java | 291 +++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/utils/AuthenticatedEncryptionPendingRequestIdGenerationStrategy.java (limited to 'eaaf_core_utils/src/main') diff --git a/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/utils/AuthenticatedEncryptionPendingRequestIdGenerationStrategy.java b/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/utils/AuthenticatedEncryptionPendingRequestIdGenerationStrategy.java new file mode 100644 index 00000000..ebfe7500 --- /dev/null +++ b/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/utils/AuthenticatedEncryptionPendingRequestIdGenerationStrategy.java @@ -0,0 +1,291 @@ +package at.gv.egiz.eaaf.core.impl.utils; + +import java.io.UnsupportedEncodingException; +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"); + + 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()); + JsonWebEncryption encToken = new JsonWebEncryption(); + encToken.setAlgorithmHeaderValue(selectKeyWrappingAlgorithm(key.getFirst())); + encToken.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_128_GCM); + encToken.setKey(key.getFirst()); + encToken.setPayload(toSign); + + + + 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("UTF-8")); + + } catch (final JoseException | UnsupportedEncodingException e) { + throw new EaafException("internal.99", 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, "PendingReqId has an unvalid format"); + + } + + final String[] tokenElements = + StringUtils.split(stringToken, TOKEN_SEPARATOR, ENCODED_TOKEN_PARTS); + return tokenElements[1]; + + } catch (final UnsupportedEncodingException e) { + throw new RuntimeException(e); + + } catch (JoseException 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); + + } + } + + @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.warn("PendingRequestId has an unvalid format"); + log.debug("PendingRequestId: {}", stringToken); + throw new PendingReqIdValidationException(null, "PendingReqId has an unvalid format"); + + } + + 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.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 (JoseException e) { + log.warn("Token is NOT a valid encrypt. Msg: {}", e.getMessage()); + log.debug("TokenValue: {}", externalPendingReqId); + throw new PendingReqIdValidationException(null, "PendingReqId is NOT a valid encrypted", e); + + } catch (final IllegalArgumentException 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); + + } catch (final UnsupportedEncodingException e) { + throw new RuntimeException(e); + + } + } + + @Nonnull + private String getDecryptedExternalPendingRequestId(String externalPendingReqId) + throws JoseException, PendingReqIdValidationException, UnsupportedEncodingException { + 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); + + } + + + 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, "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); + + } + + 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(); + } + +} -- cgit v1.2.3