package at.gv.egiz.eaaf.core.impl.credential.inline; import java.io.IOException; import java.net.URI; import java.net.URL; import java.net.URLConnection; import java.net.URLDecoder; import java.net.URLStreamHandler; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.util.AbstractMap; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.springframework.core.io.ResourceLoader; import at.gv.egiz.eaaf.core.exceptions.EaafConfigurationException; import lombok.extern.slf4j.Slf4j; /** * Parse in-line keystore configuration in BRZ format. * * @author tlenz * */ @Slf4j public class InlineKeyStoreParser extends URLStreamHandler { @Override protected URLConnection openConnection(URL u) throws IOException { log.error("openConnection is not implemented by {}", InlineKeyStoreParser.class); return null; } /** * Build a {@link KeyStore} object from an in-line configuration {@link URL}. * * @param configUrl configuration URL * @param configRootDirectory Root directory for configuration * @param resourceLoader Spring ResourceLoader * @return KeyStore created by configuration URL * @throws IOException In case of a configuration read error * @throws GeneralSecurityException In case of a KeyStore configuration error * @throws EaafConfigurationException If keyfile can not be found */ public static KeyStore buildKeyStore(URL configUrl, ResourceLoader resourceLoader, URI configRootDirectory) throws IOException, GeneralSecurityException, EaafConfigurationException { log.trace("Parsing keystore config from URL: {}", configUrl); InlineKeyStoreBuilder keyStoreBuilder = new InlineKeyStoreBuilder( configUrl.getProtocol(), resourceLoader, configRootDirectory); // parse basis properties from URL final Map> queryParams = parseQuery(configUrl.getQuery()); final String[] certificateFiles = queryParams.getOrDefault("cert", Collections.emptyList()) .toArray(String[]::new); if ("keystore".equalsIgnoreCase(configUrl.getPath())) { parseKeyStore(keyStoreBuilder, queryParams, certificateFiles); } else if ("truststore".equalsIgnoreCase(configUrl.getPath())) { keyStoreBuilder.setCertificateEntries(certificateFiles); } else { throw new IllegalArgumentException("Unknown store type: " + configUrl.getPath()); } return keyStoreBuilder.getKeyStore(); } /** * Takes the queryParams and builds based on them a {@link KeyStore} with * secret and/or private key. * * @param keyStoreBuilder * * @param queryParams a map of all query params * @param certificateFiles list of certificates * @throws GeneralSecurityException if private or secret key cannot be parsed * @throws IOException if private or secret key cannot be parsed * @throws EaafConfigurationException if keyfile can not be found */ private static void parseKeyStore(InlineKeyStoreBuilder keyStoreBuilder, final Map> queryParams, final String[] certificateFiles) throws GeneralSecurityException, IOException, EaafConfigurationException { final List privateKeyList = queryParams.get("private"); final List secretKeyList = queryParams.get("inlineSecret"); if (privateKeyList == null && secretKeyList == null) { throw new IllegalArgumentException("Neither secret key nor private key are configured!"); } if (privateKeyList != null) { if (privateKeyList.size() == 1) { final String privateKeyFile = queryParams.get("private").get(0); keyStoreBuilder.setKeyEntry(privateKeyFile, certificateFiles); } else { throw new IllegalArgumentException("Exactly one private key must be specified!"); } } if (secretKeyList != null) { if (secretKeyList.size() == 1) { final String secret = URLDecoder.decode(secretKeyList.get(0), StandardCharsets.UTF_8); keyStoreBuilder.setSecretEntry(secret); } else { throw new IllegalArgumentException("Exactly one secret key must be specified!"); } } } private static Map> parseQuery(String query) { if (query != null) { final Map> queryParams = Arrays .stream(query.split("&")) // generate a stream of key=value pairs .map(keyValuePair -> keyValuePair.split("=")) // map key=value strings to arrays .map(keyValuePair -> new AbstractMap.SimpleEntry(keyValuePair[0], keyValuePair[1])) // map arrays to Entry objects .collect(Collectors // group entries with identical keys to a map of lists .groupingBy(AbstractMap.SimpleEntry::getKey)) .entrySet().stream() // stream the map again .map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue().stream().map(AbstractMap.SimpleEntry::getValue).collect(Collectors .toList()))) // map the lists of Entry objects to lists of Strings .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); // collect the stream to a map log.trace("Query map: {}", queryParams); return queryParams; } else { log.warn("Empty query string"); return Collections.emptyMap(); } } }