/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 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]" */ // TODO: Expose as a set of resource actions. package org.forgerock.openidm.crypto.impl; import static org.forgerock.json.JsonValue.field; import static org.forgerock.json.JsonValue.json; import static org.forgerock.json.JsonValue.object; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.GeneralSecurityException; import java.security.Key; import java.security.KeyStore; import java.security.KeyStore.SecretKeyEntry; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import org.forgerock.json.JsonException; import org.forgerock.json.JsonTransformer; import org.forgerock.json.JsonValue; import org.forgerock.json.JsonValueException; import org.forgerock.json.crypto.JsonCrypto; import org.forgerock.json.crypto.JsonCryptoException; import org.forgerock.json.crypto.JsonCryptoTransformer; import org.forgerock.json.crypto.JsonEncryptor; import org.forgerock.json.crypto.simple.SimpleDecryptor; import org.forgerock.json.crypto.simple.SimpleEncryptor; import org.forgerock.openidm.cluster.ClusterUtils; import org.forgerock.openidm.core.IdentityServer; import org.forgerock.openidm.crypto.CryptoConstants; import org.forgerock.openidm.crypto.CryptoService; import org.forgerock.openidm.crypto.FieldStorageScheme; import org.forgerock.openidm.crypto.SaltedMD5FieldStorageScheme; import org.forgerock.openidm.crypto.SaltedSHA1FieldStorageScheme; import org.forgerock.openidm.crypto.SaltedSHA256FieldStorageScheme; import org.forgerock.openidm.crypto.SaltedSHA384FieldStorageScheme; import org.forgerock.openidm.crypto.SaltedSHA512FieldStorageScheme; import org.forgerock.openidm.crypto.factory.CryptoUpdateService; import org.forgerock.openidm.util.JsonUtil; import org.osgi.framework.BundleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Cryptography Service * */ public class CryptoServiceImpl implements CryptoService, CryptoUpdateService { /** * Setup logging for the {@link CryptoServiceImpl}. */ private final static Logger logger = LoggerFactory.getLogger(CryptoServiceImpl.class); /** TODO: Description. */ private UpdatableKeyStoreSelector keySelector; /** TODO: Description. */ private final ArrayList<JsonTransformer> decryptionTransformers = new ArrayList<JsonTransformer>(); /** * Opens a connection to the specified URI location and returns an input * stream with which to read its content. If the URI is not absolute, it is * resolved against the root of the local file system. If the specified * location is or contains {@code null}, this method returns {@code null}. * * @param location * the location to open the stream for. * @return an input stream for reading the content of the location, or * {@code null} if no location. * @throws IOException * if there was exception opening the stream. */ private InputStream openStream(String location) throws IOException { InputStream result = null; if (location != null) { File configFile = IdentityServer.getFileForPath(location, IdentityServer.getInstance() .getInstallLocation()); if (configFile.exists()) { result = new FileInputStream(configFile); } else { logger.error("ERROR - KeyStore not found under CryptoService#location {}", configFile.getAbsolutePath()); } } return result; } public void activate(BundleContext context) { logger.debug("Activating cryptography service"); try { int keyCount = 0; String password = IdentityServer.getInstance().getProperty("openidm.keystore.password"); if (password != null) { // optional String instanceType = IdentityServer.getInstance().getProperty("openidm.instance.type", ClusterUtils.TYPE_STANDALONE); String type = IdentityServer.getInstance().getProperty("openidm.keystore.type", KeyStore.getDefaultType()); String provider = IdentityServer.getInstance().getProperty("openidm.keystore.provider"); String location = IdentityServer.getInstance().getProperty("openidm.keystore.location"); String[] configAliases = new String[] { IdentityServer.getInstance().getProperty("openidm.config.crypto.alias"), IdentityServer.getInstance().getProperty("openidm.config.crypto.selfservice.sharedkey.alias") }; try { logger.info( "Activating cryptography service of type: {} provider: {} location: {}", type, provider, location); KeyStore ks = (provider == null || provider.trim().length() == 0) ? KeyStore.getInstance(type) : KeyStore.getInstance(type, provider); InputStream in = openStream(location); if (null != in) { char[] clearPassword = Main.unfold(password); ks.load(in, password == null ? null : clearPassword); if (instanceType.equals(ClusterUtils.TYPE_STANDALONE) || instanceType.equals(ClusterUtils.TYPE_CLUSTERED_FIRST)) { for (String alias : configAliases) { Key key = ks.getKey(alias, clearPassword); if (key == null) { // Initialize the keys logger.debug("Initializing secret key entry {} in the keystore", alias); generateDefaultKey(ks, alias, location, clearPassword); } } } keySelector = new UpdatableKeyStoreSelector(ks, new String(clearPassword)); Enumeration<String> aliases = ks.aliases(); while (aliases.hasMoreElements()) { logger.info("Available cryptography key: {}", aliases.nextElement()); keyCount++; } } } catch (IOException ioe) { logger.error("IOException when loading KeyStore file of type: " + type + " provider: " + provider + " location:" + location, ioe); throw new RuntimeException("IOException when loading KeyStore file of type: " + type + " provider: " + provider + " location:" + location + " message: " + ioe.getMessage(), ioe); } catch (GeneralSecurityException gse) { logger.error("GeneralSecurityException when loading KeyStore file", gse); throw new RuntimeException( "GeneralSecurityException when loading KeyStore file of type: " + type + " provider: " + provider + " location:" + location + " message: " + gse.getMessage(), gse); } decryptionTransformers.add(new JsonCryptoTransformer(new SimpleDecryptor( keySelector))); } logger.info("CryptoService is initialized with {} keys.", keyCount); } catch (final JsonValueException jve) { logger.error("Exception when loading CryptoService configuration", jve); throw jve; } } /** * Generates a default secret key entry in the keystore. * * @param ks the keystore * @param alias the alias of the secret key * @param location the keystore location * @param password the keystore password * @throws IOException * @throws GeneralSecurityException */ private void generateDefaultKey(KeyStore ks, String alias, String location, char[] password) throws IOException, GeneralSecurityException { SecretKey newKey = KeyGenerator.getInstance("AES").generateKey(); ks.setEntry(alias, new SecretKeyEntry(newKey), new KeyStore.PasswordProtection(password)); OutputStream out = new FileOutputStream(location); try { ks.store(out, password); } finally { out.close(); } } public void updateKeySelector(KeyStore ks, String password) { keySelector.update(ks, password); decryptionTransformers.add(new JsonCryptoTransformer(new SimpleDecryptor(keySelector))); } public void deactivate(BundleContext context) { decryptionTransformers.clear(); keySelector = null; logger.info("CryptoService stopped."); } @Override public JsonEncryptor getEncryptor(String cipher, String alias) throws JsonCryptoException { Key key = keySelector.select(alias); if (key == null) { String msg = "Encryption key " + alias + " not found"; logger.error(msg); throw new JsonCryptoException(msg); } return new SimpleEncryptor(cipher, key, alias); } @Override public List<JsonTransformer> getDecryptionTransformers() { return decryptionTransformers; } @Override public JsonValue encrypt(JsonValue value, String cipher, String alias) throws JsonCryptoException, JsonException { JsonValue result = null; if (value != null) { JsonEncryptor encryptor = getEncryptor(cipher, alias); result = new JsonCrypto(encryptor.getType(), encryptor.encrypt(value)).toJsonValue(); } return result; } @Override public JsonValue decrypt(JsonValue value) throws JsonException { JsonValue result = null; if (value != null) { result = new JsonValue(value); result.getTransformers().addAll(0, getDecryptionTransformers()); result.applyTransformers(); result = result.copy(); } return result; } @Override public JsonValue decrypt(String value) throws JsonException { JsonValue jsonValue = JsonUtil.parseStringified(value); return decrypt(jsonValue); } @Override public JsonValue decryptIfNecessary(JsonValue value) throws JsonException { if (value == null) { return new JsonValue(null); } if (value.isNull() || !isEncrypted(value)) { return value; } return decrypt(value); } @Override public JsonValue decryptIfNecessary(String value) throws JsonException { JsonValue jsonValue = null; if (value != null) { jsonValue = JsonUtil.parseStringified(value); } return decryptIfNecessary(jsonValue); } @Override public boolean isEncrypted(JsonValue value) { return JsonCrypto.isJsonCrypto(value); } @Override public boolean isEncrypted(String value) { return JsonUtil.isEncrypted(value); } @Override public JsonValue hash(JsonValue value, String algorithm) throws JsonException, JsonCryptoException { final FieldStorageScheme fieldStorageScheme = getFieldStorageScheme(algorithm); final String plainTextField = value.asString(); final String encodedField = fieldStorageScheme.hashField(plainTextField); return json(object( field("$crypto", object( field("value", object( field("algorithm", algorithm), field("data", encodedField))), field("type", CryptoConstants.STORAGE_TYPE_HASH))))); } @Override public boolean isHashed(JsonValue value) { return value != null &&!value.isNull() && JsonCrypto.isJsonCrypto(value) && value.get("$crypto").get("value").isDefined("algorithm"); } /** * Returns a {@link FieldStorageScheme} instance based on the supplied algorithm. * * @param algorithm a string representing a storage scheme algorithm * @return a field storage scheme implementation. * @throws JsonCryptoException */ private FieldStorageScheme getFieldStorageScheme(String algorithm) throws JsonCryptoException { try { if (algorithm.equals(CryptoConstants.ALGORITHM_MD5)) { return new SaltedMD5FieldStorageScheme(); } else if (algorithm.equals(CryptoConstants.ALGORITHM_SHA_1)) { return new SaltedSHA1FieldStorageScheme(); } else if (algorithm.equals(CryptoConstants.ALGORITHM_SHA_256)) { return new SaltedSHA256FieldStorageScheme(); } else if (algorithm.equals(CryptoConstants.ALGORITHM_SHA_384)) { return new SaltedSHA384FieldStorageScheme(); } else if (algorithm.equals(CryptoConstants.ALGORITHM_SHA_512)) { return new SaltedSHA512FieldStorageScheme(); } else { throw new JsonCryptoException("Unsupported field storage algorithm " + algorithm); } } catch (JsonCryptoException e) { throw e; } catch (Exception e) { throw new JsonCryptoException(e.getMessage(), e); } } @Override public boolean matches(String plainTextValue, JsonValue value) throws JsonCryptoException { if (isHashed(value)) { JsonValue cryptoValue = value.get("$crypto").get("value"); String algorithm = cryptoValue.get("algorithm").asString(); final FieldStorageScheme fieldStorageScheme = getFieldStorageScheme(algorithm); return fieldStorageScheme.fieldMatches(plainTextValue, cryptoValue.get("data").asString()); } return false; } }