From 500c277391c565bbccc0a9a69b9b99bc5b50624b Mon Sep 17 00:00:00 2001 From: Robert Varga Date: Sun, 5 Feb 2023 15:16:19 +0100 Subject: [PATCH] Split out AAAEncryptionServiceConfigurator The configuration update bits are quite independent from the actual configuration. This splits out the configuration update handler from the actual service. JIRA: AAA-250 Change-Id: Id971a57cac68293a57fc0c21e863742b38980d77 Signed-off-by: Robert Varga --- .../AAAEncryptionServiceConfigurator.java | 183 ++++++++++++++++++ .../impl/AAAEncryptionServiceImpl.java | 73 +------ .../aaa/encrypt/impl/MdsalUtils.java | 92 --------- .../OSGI-INF/blueprint/encryptservice.xml | 10 +- .../impl/AAAEncryptServiceImplTest.java | 33 ++-- 5 files changed, 209 insertions(+), 182 deletions(-) create mode 100644 aaa-encrypt-service/impl/src/main/java/org/opendaylight/aaa/encrypt/impl/AAAEncryptionServiceConfigurator.java delete mode 100644 aaa-encrypt-service/impl/src/main/java/org/opendaylight/aaa/encrypt/impl/MdsalUtils.java diff --git a/aaa-encrypt-service/impl/src/main/java/org/opendaylight/aaa/encrypt/impl/AAAEncryptionServiceConfigurator.java b/aaa-encrypt-service/impl/src/main/java/org/opendaylight/aaa/encrypt/impl/AAAEncryptionServiceConfigurator.java new file mode 100644 index 000000000..cc7471869 --- /dev/null +++ b/aaa-encrypt-service/impl/src/main/java/org/opendaylight/aaa/encrypt/impl/AAAEncryptionServiceConfigurator.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.aaa.encrypt.impl; + +import com.google.common.base.Strings; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.File; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.concurrent.ExecutionException; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import org.apache.commons.lang3.RandomStringUtils; +import org.eclipse.jdt.annotation.NonNull; +import org.opendaylight.mdsal.binding.api.DataBroker; +import org.opendaylight.mdsal.common.api.CommitInfo; +import org.opendaylight.mdsal.common.api.LogicalDatastoreType; +import org.opendaylight.yang.gen.v1.config.aaa.authn.encrypt.service.config.rev160915.AaaEncryptServiceConfig; +import org.opendaylight.yang.gen.v1.config.aaa.authn.encrypt.service.config.rev160915.AaaEncryptServiceConfigBuilder; +import org.opendaylight.yang.gen.v1.config.aaa.authn.encrypt.service.config.rev160915.EncryptServiceConfig; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + +/** + * Intermediate component dealing with establishing initial configuration for {@link AAAEncryptionServiceImpl}. In + * particular it deals with generating and persisting of encryption salt and encryption password. + */ +public final class AAAEncryptionServiceConfigurator implements EncryptServiceConfig { + private static final Logger LOG = LoggerFactory.getLogger(AAAEncryptionServiceConfigurator.class); + + // Note: this is a strong binding to Blueprint, which is loading etc/opendaylight/datastore/initial/config + private static final String DEFAULT_CONFIG_FILE_PATH = "etc" + File.separator + "opendaylight" + File.separator + + "datastore" + File.separator + "initial" + File.separator + "config" + File.separator + + "aaa-encrypt-service-config.xml"; + private static final SecureRandom RANDOM = new SecureRandom(); + + private final EncryptServiceConfig delegate; + + public AAAEncryptionServiceConfigurator(final DataBroker dataBroker, + final AaaEncryptServiceConfig blueprintConfig) { + if (Strings.isNullOrEmpty(blueprintConfig.getEncryptSalt()) + || Strings.isNullOrEmpty(blueprintConfig.getEncryptKey())) { + final var generatedConfig = generateConfig(blueprintConfig); + + // Update initial configuration and config datastore + updateEncrySrvConfig(generatedConfig.requireEncryptKey(), generatedConfig.requireEncryptSalt()); + updateDatastore(dataBroker, generatedConfig); + + delegate = generatedConfig; + } else { + delegate = blueprintConfig; + } + } + + private static @NonNull AaaEncryptServiceConfig generateConfig(final EncryptServiceConfig blueprintConfig) { + LOG.debug("Set the Encryption service password and encrypt salt"); + final var salt = new byte[16]; + RANDOM.nextBytes(salt); + + return new AaaEncryptServiceConfigBuilder(blueprintConfig) + .setEncryptKey(RandomStringUtils.random(blueprintConfig.requirePasswordLength(), true, true)) + .setEncryptSalt(Base64.getEncoder().encodeToString(salt)) + .build(); + } + + // FIXME: Update configuration datastore, but only if not present?! this looks weird lifecycle-wise + private static void updateDatastore(final DataBroker dataBroker, final @NonNull AaaEncryptServiceConfig config) { + final var iid = InstanceIdentifier.create(AaaEncryptServiceConfig.class); + + final var tx = dataBroker.newReadWriteTransaction(); + final var existsFuture = tx.exists(LogicalDatastoreType.CONFIGURATION, iid); + + final boolean exists; + try { + exists = existsFuture.get(); + } catch (InterruptedException | ExecutionException e) { + tx.cancel(); + LOG.error("Failed to read configuration", e); + return; + } + if (exists) { + tx.cancel(); + LOG.info("Configuration already present, skipping update"); + return; + } + + LOG.debug("Populating configuration: {}, {}", iid, config); + tx.put(LogicalDatastoreType.CONFIGURATION, iid, config); + // Perform the transaction.submit asynchronously + Futures.addCallback(tx.commit(), new FutureCallback() { + @Override + public void onFailure(final Throwable throwable) { + LOG.error("initDatastore: configuration data populated: {}", iid); + } + + @Override + public void onSuccess(final CommitInfo result) { + LOG.info("initDatastore: transaction succeeded"); + } + }, MoreExecutors.directExecutor()); + } + + private static void updateEncrySrvConfig(final String newPwd, final String newSalt) { + LOG.debug("Update encryption service config file"); + try { + final File configFile = new File(DEFAULT_CONFIG_FILE_PATH); + if (configFile.exists()) { + final Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(configFile); + final Node keyNode = doc.getElementsByTagName("encrypt-key").item(0); + keyNode.setTextContent(newPwd); + final Node salt = doc.getElementsByTagName("encrypt-salt").item(0); + salt.setTextContent(newSalt); + TransformerFactory.newInstance().newTransformer().transform(new DOMSource(doc), + new StreamResult(new File(DEFAULT_CONFIG_FILE_PATH))); + } else { + LOG.warn("The encryption service config file does not exist {}", DEFAULT_CONFIG_FILE_PATH); + } + } catch (ParserConfigurationException | TransformerException | SAXException | IOException e) { + LOG.error("Error while updating the encryption service config file", e); + } + } + + @Override + public Class implementedInterface() { + throw new UnsupportedOperationException(); + } + + @Override + public String getEncryptKey() { + return delegate.getEncryptKey(); + } + + @Override + public Integer getPasswordLength() { + return delegate.getPasswordLength(); + } + + @Override + public String getEncryptSalt() { + return delegate.getEncryptSalt(); + } + + @Override + public String getEncryptMethod() { + return delegate.getEncryptMethod(); + } + + @Override + public String getEncryptType() { + return delegate.getEncryptType(); + } + + @Override + public Integer getEncryptIterationCount() { + return delegate.getEncryptIterationCount(); + } + + @Override + public Integer getEncryptKeyLength() { + return delegate.getEncryptKeyLength(); + } + + @Override + public String getCipherTransforms() { + return delegate.getCipherTransforms(); + } +} diff --git a/aaa-encrypt-service/impl/src/main/java/org/opendaylight/aaa/encrypt/impl/AAAEncryptionServiceImpl.java b/aaa-encrypt-service/impl/src/main/java/org/opendaylight/aaa/encrypt/impl/AAAEncryptionServiceImpl.java index b6f4c11af..8ffba0059 100644 --- a/aaa-encrypt-service/impl/src/main/java/org/opendaylight/aaa/encrypt/impl/AAAEncryptionServiceImpl.java +++ b/aaa-encrypt-service/impl/src/main/java/org/opendaylight/aaa/encrypt/impl/AAAEncryptionServiceImpl.java @@ -7,13 +7,10 @@ */ package org.opendaylight.aaa.encrypt.impl; -import java.io.File; -import java.io.IOException; import java.nio.charset.Charset; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import java.util.Base64; @@ -26,23 +23,10 @@ import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; -import org.apache.commons.lang3.RandomStringUtils; import org.opendaylight.aaa.encrypt.AAAEncryptionService; -import org.opendaylight.mdsal.binding.api.DataBroker; -import org.opendaylight.mdsal.common.api.LogicalDatastoreType; -import org.opendaylight.yang.gen.v1.config.aaa.authn.encrypt.service.config.rev160915.AaaEncryptServiceConfig; -import org.opendaylight.yang.gen.v1.config.aaa.authn.encrypt.service.config.rev160915.AaaEncryptServiceConfigBuilder; +import org.opendaylight.yang.gen.v1.config.aaa.authn.encrypt.service.config.rev160915.EncryptServiceConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.xml.sax.SAXException; /** * Provides a basic encryption service implementation with configuration knobs. @@ -52,38 +36,18 @@ import org.xml.sax.SAXException; @Deprecated public class AAAEncryptionServiceImpl implements AAAEncryptionService { private static final Logger LOG = LoggerFactory.getLogger(AAAEncryptionServiceImpl.class); - private static final String DEFAULT_CONFIG_FILE_PATH = "etc" + File.separator + "opendaylight" + File.separator - + "datastore" + File.separator + "initial" + File.separator + "config" + File.separator - + "aaa-encrypt-service-config.xml"; - private static final SecureRandom RANDOM = new SecureRandom(); private final SecretKey key; private final Cipher encryptCipher; private final Cipher decryptCipher; - public AAAEncryptionServiceImpl(AaaEncryptServiceConfig encrySrvConfig, final DataBroker dataBroker) { - if (encrySrvConfig.getEncryptSalt() == null) { - throw new IllegalArgumentException( - "null encryptSalt in AaaEncryptServiceConfig: " + encrySrvConfig.toString()); - } - if (encrySrvConfig.getEncryptKey() != null && encrySrvConfig.getEncryptKey().isEmpty()) { - LOG.debug("Set the Encryption service password and encrypt salt"); - String newPwd = RandomStringUtils.random(encrySrvConfig.getPasswordLength(), true, true); - byte[] salt = new byte[16]; - RANDOM.nextBytes(salt); - String encodedSalt = Base64.getEncoder().encodeToString(salt); - encrySrvConfig = new AaaEncryptServiceConfigBuilder(encrySrvConfig).setEncryptKey(newPwd) - .setEncryptSalt(encodedSalt).build(); - updateEncrySrvConfig(newPwd, encodedSalt); - initializeConfigDataTree(encrySrvConfig, dataBroker); - } - - final byte[] encryptionKeySalt = Base64.getDecoder().decode(encrySrvConfig.getEncryptSalt()); + public AAAEncryptionServiceImpl(final EncryptServiceConfig encrySrvConfig) { + final byte[] encryptionKeySalt = Base64.getDecoder().decode(encrySrvConfig.requireEncryptSalt()); IvParameterSpec tempIvSpec = null; SecretKey tempKey = null; try { final SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encrySrvConfig.getEncryptMethod()); - final KeySpec spec = new PBEKeySpec(encrySrvConfig.getEncryptKey().toCharArray(), encryptionKeySalt, + final KeySpec spec = new PBEKeySpec(encrySrvConfig.requireEncryptKey().toCharArray(), encryptionKeySalt, encrySrvConfig.getEncryptIterationCount(), encrySrvConfig.getEncryptKeyLength()); tempKey = new SecretKeySpec(keyFactory.generateSecret(spec).getEncoded(), encrySrvConfig.getEncryptType()); tempIvSpec = new IvParameterSpec(encryptionKeySalt); @@ -182,33 +146,4 @@ public class AAAEncryptionServiceImpl implements AAAEncryptionService { } return encryptedData; } - - private static void updateEncrySrvConfig(final String newPwd, final String newSalt) { - LOG.debug("Update encryption service config file"); - try { - final File configFile = new File(DEFAULT_CONFIG_FILE_PATH); - if (configFile.exists()) { - final Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(configFile); - final Node keyNode = doc.getElementsByTagName("encrypt-key").item(0); - keyNode.setTextContent(newPwd); - final Node salt = doc.getElementsByTagName("encrypt-salt").item(0); - salt.setTextContent(newSalt); - TransformerFactory.newInstance().newTransformer().transform(new DOMSource(doc), - new StreamResult(new File(DEFAULT_CONFIG_FILE_PATH))); - } else { - LOG.warn("The encryption service config file does not exist {}", DEFAULT_CONFIG_FILE_PATH); - } - } catch (ParserConfigurationException | TransformerException | SAXException | IOException e) { - LOG.error("Error while updating the encryption service config file", e); - } - } - - private static void initializeConfigDataTree(final AaaEncryptServiceConfig encrySrvConfig, - final DataBroker dataBroker) { - if (MdsalUtils.read(dataBroker, LogicalDatastoreType.CONFIGURATION, - MdsalUtils.getEncryptionSrvConfigIid()) == null) { - MdsalUtils.initalizeDatastore(LogicalDatastoreType.CONFIGURATION, dataBroker, - MdsalUtils.getEncryptionSrvConfigIid(), encrySrvConfig); - } - } } diff --git a/aaa-encrypt-service/impl/src/main/java/org/opendaylight/aaa/encrypt/impl/MdsalUtils.java b/aaa-encrypt-service/impl/src/main/java/org/opendaylight/aaa/encrypt/impl/MdsalUtils.java deleted file mode 100644 index ef1ee8f4b..000000000 --- a/aaa-encrypt-service/impl/src/main/java/org/opendaylight/aaa/encrypt/impl/MdsalUtils.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) 2017 Inocybe Technologies. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.aaa.encrypt.impl; - -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.MoreExecutors; -import java.util.Optional; -import java.util.concurrent.ExecutionException; -import org.opendaylight.mdsal.binding.api.DataBroker; -import org.opendaylight.mdsal.binding.api.ReadTransaction; -import org.opendaylight.mdsal.binding.api.WriteTransaction; -import org.opendaylight.mdsal.common.api.CommitInfo; -import org.opendaylight.mdsal.common.api.LogicalDatastoreType; -import org.opendaylight.yang.gen.v1.config.aaa.authn.encrypt.service.config.rev160915.AaaEncryptServiceConfig; -import org.opendaylight.yangtools.yang.binding.DataObject; -import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * MdsalUtils manages all the mdsal data operation. - * - * @author mserngawy - */ -public final class MdsalUtils { - private static final Logger LOG = LoggerFactory.getLogger(MdsalUtils.class); - - private MdsalUtils() { - - } - - public static InstanceIdentifier getEncryptionSrvConfigIid() { - return InstanceIdentifier.builder(AaaEncryptServiceConfig.class).build(); - } - - /** - * initialize the data tree for the given InstanceIdentifier type. - * - * @param type data store type - * @param dataBroker Mdsal data Broker - * @param iid InstanceIdentifier type - * @param object data object - */ - public static void initalizeDatastore(final LogicalDatastoreType type, - final DataBroker dataBroker, final InstanceIdentifier iid, final T object) { - // Put data to MD-SAL data store - final WriteTransaction transaction = dataBroker.newWriteOnlyTransaction(); - transaction.put(type, iid, object); - - // Perform the transaction.submit asynchronously - Futures.addCallback(transaction.commit(), new FutureCallback() { - @Override - public void onFailure(final Throwable throwable) { - LOG.error("initDatastore: transaction failed"); - } - - @Override - public void onSuccess(final CommitInfo result) { - LOG.debug("initDatastore: transaction succeeded"); - } - }, MoreExecutors.directExecutor()); - LOG.info("initDatastore: data populated: {}, {}, {}", type, iid, object); - } - - /** - * Executes read as a blocking transaction. - * - * @param store {@link LogicalDatastoreType} to read - * @param path {@link InstanceIdentifier} for path to read - * @param the data object type - * @return the result as the data object requested - */ - public static D read( - final DataBroker dataBroker, final LogicalDatastoreType store, final InstanceIdentifier path) { - try (ReadTransaction transaction = dataBroker.newReadOnlyTransaction()) { - Optional optionalDataObject = transaction.read(store, path).get(); - if (optionalDataObject.isPresent()) { - return optionalDataObject.get(); - } - } catch (InterruptedException | ExecutionException e) { - LOG.warn("Failed to read {} ", path, e); - } - - return null; - } -} diff --git a/aaa-encrypt-service/impl/src/main/resources/OSGI-INF/blueprint/encryptservice.xml b/aaa-encrypt-service/impl/src/main/resources/OSGI-INF/blueprint/encryptservice.xml index 36c247104..800ef51a2 100644 --- a/aaa-encrypt-service/impl/src/main/resources/OSGI-INF/blueprint/encryptservice.xml +++ b/aaa-encrypt-service/impl/src/main/resources/OSGI-INF/blueprint/encryptservice.xml @@ -1,18 +1,20 @@ - + xmlns:odl="http://opendaylight.org/xmlns/blueprint/v1.0.0"> - - + + + + + diff --git a/aaa-encrypt-service/impl/src/test/java/org/opendaylight/aaa/encrypt/impl/AAAEncryptServiceImplTest.java b/aaa-encrypt-service/impl/src/test/java/org/opendaylight/aaa/encrypt/impl/AAAEncryptServiceImplTest.java index fd558b841..f04fb96cc 100644 --- a/aaa-encrypt-service/impl/src/test/java/org/opendaylight/aaa/encrypt/impl/AAAEncryptServiceImplTest.java +++ b/aaa-encrypt-service/impl/src/test/java/org/opendaylight/aaa/encrypt/impl/AAAEncryptServiceImplTest.java @@ -7,19 +7,17 @@ */ package org.opendaylight.aaa.encrypt.impl; -import static org.mockito.ArgumentMatchers.any; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import java.util.Optional; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.opendaylight.mdsal.binding.api.DataBroker; -import org.opendaylight.mdsal.binding.api.ReadTransaction; +import org.opendaylight.mdsal.binding.api.ReadWriteTransaction; import org.opendaylight.mdsal.common.api.LogicalDatastoreType; import org.opendaylight.yang.gen.v1.config.aaa.authn.encrypt.service.config.rev160915.AaaEncryptServiceConfig; import org.opendaylight.yang.gen.v1.config.aaa.authn.encrypt.service.config.rev160915.AaaEncryptServiceConfigBuilder; @@ -32,41 +30,42 @@ import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; @Deprecated @RunWith(MockitoJUnitRunner.StrictStubs.class) public class AAAEncryptServiceImplTest { - - private AAAEncryptionServiceImpl impl; @Mock private DataBroker dataBroker; + @Mock + private ReadWriteTransaction tx; + + private AAAEncryptionServiceImpl impl; @Before public void setup() { - AaaEncryptServiceConfig module = new AaaEncryptServiceConfigBuilder() + final var module = new AaaEncryptServiceConfigBuilder() .setCipherTransforms("AES/CBC/PKCS5Padding").setEncryptIterationCount(32768).setEncryptKey("") .setEncryptKeyLength(128).setEncryptMethod("PBKDF2WithHmacSHA1").setEncryptSalt("") .setEncryptType("AES").setPasswordLength(12).build(); - final ReadTransaction rtx = mock(ReadTransaction.class); - doReturn(rtx).when(dataBroker).newReadOnlyTransaction(); - doReturn(FluentFutures.immediateFluentFuture(Optional.of(module))).when(rtx).read( - any(LogicalDatastoreType.class), any(InstanceIdentifier.class)); + doReturn(tx).when(dataBroker).newReadWriteTransaction(); + doReturn(FluentFutures.immediateTrueFluentFuture()).when(tx) + .exists(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(AaaEncryptServiceConfig.class)); - impl = new AAAEncryptionServiceImpl(module, dataBroker); + impl = new AAAEncryptionServiceImpl(new AAAEncryptionServiceConfigurator(dataBroker, module)); } @Test public void testShortString() { String before = "shortone"; String encrypt = impl.encrypt(before); - Assert.assertNotEquals(before, encrypt); + assertNotEquals(before, encrypt); String after = impl.decrypt(encrypt); - Assert.assertEquals(before, after); + assertEquals(before, after); } @Test public void testLongString() { String before = "This is a very long string to encrypt for testing 1...2...3"; String encrypt = impl.encrypt(before); - Assert.assertNotEquals(before, encrypt); + assertNotEquals(before, encrypt); String after = impl.decrypt(encrypt); - Assert.assertEquals(before, after); + assertEquals(before, after); } } -- 2.36.6