/** * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2011-2015 ForgeRock AS. All Rights Reserved * * The contents of this file are subject to the terms * of the Common Development and Distribution License * (the License). You may not use this file except in * compliance with the License. * * You can obtain a copy of the License at * http://forgerock.org/license/CDDLv1.0.html * See the License for the specific language governing * permission and limitations under the License. * * When distributing Covered Code, include this CDDL * Header Notice in each file and include the License file * at http://forgerock.org/license/CDDLv1.0.html * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * */ package org.forgerock.openidm.config.crypto; import java.util.Arrays; import java.util.Collection; import java.util.Dictionary; import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.Map; import org.forgerock.json.JsonPointer; import org.forgerock.json.JsonValue; import org.forgerock.json.JsonValueException; import org.forgerock.json.crypto.JsonCryptoException; import org.forgerock.openidm.config.enhanced.InternalErrorException; import org.forgerock.openidm.config.enhanced.InvalidException; import org.forgerock.openidm.config.enhanced.JSONEnhancedConfig; import org.forgerock.openidm.config.installer.JSONConfigInstaller; import org.forgerock.openidm.config.installer.JSONPrettyPrint; import org.forgerock.openidm.core.IdentityServer; import org.forgerock.openidm.core.ServerConstants; import org.forgerock.openidm.crypto.CryptoService; import org.forgerock.openidm.metadata.MetaDataProvider; import org.forgerock.openidm.metadata.NotConfiguration; import org.forgerock.openidm.metadata.WaitForMetaData; import org.forgerock.openidm.metadata.impl.ProviderListener; import org.forgerock.openidm.metadata.impl.ProviderTracker; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.framework.Filter; import org.osgi.util.tracker.ServiceTracker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; /** * Configuration encryption support * * */ public class ConfigCrypto { final static Logger logger = LoggerFactory.getLogger(ConfigCrypto.class); static ServiceTracker cryptoTracker; static ConfigCrypto instance; BundleContext context; ObjectMapper mapper = new ObjectMapper(); String alias = "openidm-config-default"; JSONPrettyPrint prettyPrint = new JSONPrettyPrint(); ProviderTracker providerTracker; ProviderListener delayedHandler; private ConfigCrypto(BundleContext context, ProviderListener delayedHandler) { this.context = context; this.delayedHandler = delayedHandler; this.delayedHandler.init(this); alias = IdentityServer.getInstance().getProperty("openidm.config.crypto.alias", "openidm-config-default"); logger.info("Using keystore alias {} to handle config encryption", alias); providerTracker = new ProviderTracker(context, delayedHandler, false); // TODO: add bundle listeners to track new installs and remove uninstalls } public synchronized static ConfigCrypto getInstance(BundleContext context, ProviderListener providerListener) { if (instance == null) { instance = new ConfigCrypto(context, providerListener); } return instance; } /** * Check each provider for meta-data for a given pid until the first match is found * Requested each time configuration is changed so that meta data providers can handle additional plug-ins * * @param pidOrFactory the pid or factory pid * @param factoryAlias the alias of the factory configuration instance * @return the list of properties to encrypt */ public List<JsonPointer> getPropertiesToEncrypt(String pidOrFactory, String factoryAlias, JsonValue parsed) throws WaitForMetaData { Collection<MetaDataProvider> providers = providerTracker.getProviders(); WaitForMetaData lastWaitException = null; for (MetaDataProvider provider : providers) { try { List<JsonPointer> result = provider.getPropertiesToEncrypt(pidOrFactory, factoryAlias, parsed); if (result != null) { return result; } } catch (WaitForMetaData ex) { // Continue to check if another meta data provider can resolve the meta data lastWaitException = ex; } catch (NotConfiguration e) { logger.error("Error getting additional properties to encrypt", e); } } if (lastWaitException != null) { throw lastWaitException; } return null; } /** * Encrypt properties in the configuration if necessary * Also results in pretty print formatting of the JSON configuration. * * @param pidOrFactory the PID of either the managed service; or for factory configuration the PID of the Managed Service Factory * @param instanceAlias null for plain managed service, or the subname (alias) for the managed factory configuration instance * @param config The OSGi configuration * @return The configuration with any properties encrypted that a component's meta data marks as encrypted * @throws InvalidException if the configuration was not valid JSON and could not be parsed * @throws InternalErrorException if parsing or encryption failed for technical, possibly transient reasons */ public Dictionary encrypt(String pidOrFactory, String instanceAlias, Dictionary config) throws InvalidException, InternalErrorException, WaitForMetaData { JsonValue parsed = parse(config, pidOrFactory); return encrypt(pidOrFactory, instanceAlias, config, parsed); } public Dictionary encrypt(String pidOrFactory, String instanceAlias, Dictionary existingConfig, JsonValue newConfig) throws WaitForMetaData { JsonValue parsed = newConfig; Dictionary encrypted = (existingConfig == null ? new Hashtable() : existingConfig); // Default to existing List<JsonPointer> props = getPropertiesToEncrypt(pidOrFactory, instanceAlias, parsed); if (logger.isTraceEnabled()) { logger.trace("Properties to encrypt for {} {}: {}", new Object[] {pidOrFactory, instanceAlias, props}); } if (props != null && !props.isEmpty()) { boolean modified = false; CryptoService crypto = getCryptoService(context); for (JsonPointer pointer : props) { logger.trace("Handling property to encrypt {}", pointer); JsonValue valueToEncrypt = parsed.get(pointer); if (null != valueToEncrypt && !valueToEncrypt.isNull() && !crypto.isEncrypted(valueToEncrypt)) { if (logger.isTraceEnabled()) { logger.trace("Encrypting {} with cipher {} and alias {}", new Object[] {pointer, ServerConstants.SECURITY_CRYPTOGRAPHY_DEFAULT_CIPHER, alias}); } // Encrypt and replace value try { JsonValue encryptedValue = crypto.encrypt(valueToEncrypt, ServerConstants.SECURITY_CRYPTOGRAPHY_DEFAULT_CIPHER, alias); parsed.put(pointer, encryptedValue.getObject()); modified = true; } catch (JsonCryptoException ex) { throw new InternalErrorException("Failure during encryption of configuration " + pidOrFactory + "-" + instanceAlias + " for property " + pointer.toString() + " : " + ex.getMessage(), ex); } } } } String value = null; try { ObjectWriter writer = prettyPrint.getWriter(); value = writer.writeValueAsString(parsed.asMap()); } catch (Exception ex) { throw new InternalErrorException("Failure in writing formatted and encrypted configuration " + pidOrFactory + "-" + instanceAlias + " : " + ex.getMessage(), ex); } encrypted.put(JSONConfigInstaller.JSON_CONFIG_PROPERTY, value); if (logger.isDebugEnabled()) { logger.debug("Config with senstiive data encrypted {} {} : {}", new Object[] {pidOrFactory, instanceAlias, encrypted}); } return encrypted; } /** * Parse the OSGi configuration in JSON format * * @param dict the OSGi configuration * @param serviceName a name for the configuration getting parsed for logging purposes * @return The parsed JSON structure * @throws InvalidException if the configuration was not valid JSON and could not be parsed * @throws InternalErrorException if parsing failed for technical, possibly transient reasons */ public JsonValue parse(Dictionary<String, Object> dict, String serviceName) throws InvalidException, InternalErrorException { JsonValue jv = new JsonValue(new HashMap<String, Object>()); if (dict != null) { Map<String, Object> parsedConfig = null; String jsonConfig = (String) dict.get(JSONConfigInstaller.JSON_CONFIG_PROPERTY); try { if (jsonConfig != null && jsonConfig.trim().length() > 0) { parsedConfig = mapper.readValue(jsonConfig, Map.class); } } catch (Exception ex) { throw new InvalidException("Configuration for " + serviceName + " could not be parsed and may not be valid JSON : " + ex.getMessage(), ex); } try { jv = new JsonValue(parsedConfig); } catch (JsonValueException ex) { throw new InvalidException("Component configuration for " + serviceName + " is invalid: " + ex.getMessage(), ex); } } logger.debug("Parsed configuration for {}", serviceName); return jv; } private CryptoService getCryptoService(BundleContext context) throws InternalErrorException { CryptoService crypto = null; try { synchronized (JSONEnhancedConfig.class) { if (cryptoTracker == null) { Filter cryptoFilter = context.createFilter("(" + Constants.OBJECTCLASS + "=" + CryptoService.class.getName() + ")"); cryptoTracker = new ServiceTracker(context, cryptoFilter, null); cryptoTracker.open(); } } crypto = (CryptoService) cryptoTracker.waitForService(5000); if (crypto != null) { logger.trace("Obtained crypto service"); } else { logger.warn("Failed to get crypto service to handle configuration encryption"); if (logger.isTraceEnabled()) { logger.trace("List of available service {}", Arrays.asList(context.getAllServiceReferences(null, null))); } throw new InternalErrorException( "Configuration handling could not locate cryptography service to encrypt configuration." + " Cryptography service is not registered.."); } } catch (Exception ex) { logger.warn("Exception in getting crypto service to handle configuration encryption", ex); throw new InternalErrorException("Exception in getting cryptography service to encrypt configuration " + ex.getMessage(), ex); } return crypto; } }