2 * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.aaa.encrypt.impl;
10 import static java.util.Objects.requireNonNull;
12 import com.google.common.annotations.VisibleForTesting;
13 import com.google.common.util.concurrent.FutureCallback;
14 import com.google.common.util.concurrent.Futures;
15 import com.google.common.util.concurrent.MoreExecutors;
16 import java.security.SecureRandom;
17 import java.util.Base64;
18 import java.util.Objects;
19 import java.util.concurrent.ExecutionException;
20 import org.apache.commons.lang3.RandomStringUtils;
21 import org.checkerframework.checker.lock.qual.GuardedBy;
22 import org.checkerframework.checker.lock.qual.Holding;
23 import org.eclipse.jdt.annotation.NonNull;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.opendaylight.mdsal.binding.api.DataBroker;
26 import org.opendaylight.mdsal.binding.api.DataListener;
27 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
28 import org.opendaylight.mdsal.common.api.CommitInfo;
29 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
30 import org.opendaylight.odlparent.logging.markers.Markers;
31 import org.opendaylight.yang.gen.v1.config.aaa.authn.encrypt.service.config.rev160915.AaaEncryptServiceConfig;
32 import org.opendaylight.yang.gen.v1.config.aaa.authn.encrypt.service.config.rev160915.AaaEncryptServiceConfigBuilder;
33 import org.opendaylight.yangtools.concepts.Registration;
34 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
35 import org.osgi.framework.FrameworkUtil;
36 import org.osgi.service.component.ComponentException;
37 import org.osgi.service.component.ComponentFactory;
38 import org.osgi.service.component.ComponentInstance;
39 import org.osgi.service.component.annotations.Activate;
40 import org.osgi.service.component.annotations.Component;
41 import org.osgi.service.component.annotations.Deactivate;
42 import org.osgi.service.component.annotations.Reference;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
47 * Intermediate component dealing with establishing initial configuration for {@link AAAEncryptionServiceImpl}. In
48 * particular it deals with generating and persisting of encryption salt and encryption password.
51 * We primarily listen to the configuration being present. Whenever the salt is missing or the password does not match
52 * the required length, we generate them and persist them. This mode of operation means we potentially have a loop, i.e.
53 * our touching the datastore will trigger again {@link #dataChangedTo(AaaEncryptServiceConfig)}, which will re-evaluate
54 * the conditions and we try again.
56 @Component(service = { })
57 public final class OSGiEncryptionServiceConfigurator implements DataListener<AaaEncryptServiceConfig> {
58 private static final Logger LOG = LoggerFactory.getLogger(OSGiEncryptionServiceConfigurator.class);
59 private static final SecureRandom RANDOM = new SecureRandom();
60 private static final @NonNull AaaEncryptServiceConfig DEFAULT_CONFIG = new AaaEncryptServiceConfigBuilder()
61 // Note: mirrors defaults from YANG file
62 .setEncryptMethod("PBKDF2WithHmacSHA1")
63 .setEncryptType("AES")
64 .setEncryptIterationCount(32768)
65 .setEncryptKeyLength(128)
66 .setCipherTransforms("AES/CBC/PKCS5Padding")
67 .setPasswordLength(12)
70 private final ComponentFactory<AAAEncryptionServiceImpl> factory;
71 private final DataBroker dataBroker;
74 private Registration reg;
76 private ComponentInstance<AAAEncryptionServiceImpl> instance;
78 private AaaEncryptServiceConfig current;
81 public OSGiEncryptionServiceConfigurator(@Reference final DataBroker dataBroker,
82 @Reference(target = "(component.factory=" + AAAEncryptionServiceImpl.FACTORY_NAME + ")")
83 final ComponentFactory<AAAEncryptionServiceImpl> factory) {
84 this.dataBroker = requireNonNull(dataBroker);
85 this.factory = requireNonNull(factory);
86 reg = dataBroker.registerDataListener(
87 DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION,
88 InstanceIdentifier.create(AaaEncryptServiceConfig.class)),
90 LOG.debug("AAA Encryption Service configurator started");
94 public synchronized void deactivate() {
98 LOG.debug("AAA Encryption Service configurator stopped");
102 public void dataChangedTo(final AaaEncryptServiceConfig data) {
103 // Acquire the last reported configuration and check if it needs to have salt/password generated.
104 if (data == null || needKey(data) || needSalt(data)) {
105 // Generate salt/key as needed and persist it -- causing us to be re-invoked later.
106 updateDatastore(data);
108 // Configuration is self-consistent, proceed to activate an instance based on it
109 updateInstance(data);
114 static @NonNull AaaEncryptServiceConfig generateConfig(final @Nullable AaaEncryptServiceConfig datastoreConfig) {
115 // Select template and decide which parts need to be updated
116 final var template = datastoreConfig != null ? datastoreConfig : DEFAULT_CONFIG;
117 final var builder = new AaaEncryptServiceConfigBuilder(template);
118 if (needKey(template)) {
119 LOG.debug("Set the Encryption Service salt");
120 builder.setEncryptKey(RandomStringUtils.random(template.requirePasswordLength(), true, true));
122 if (needSalt(template)) {
123 LOG.debug("Set the Encryption Service salt");
124 final var salt = new byte[16];
125 RANDOM.nextBytes(salt);
126 builder.setEncryptSalt(Base64.getEncoder().encodeToString(salt));
128 return builder.build();
131 private void updateDatastore(final @Nullable AaaEncryptServiceConfig expected) {
132 final var target = generateConfig(expected);
134 // Careful update of the datastore: we are coming from DTCL thread, so inherently 'expected' may already be out
135 // of date, either by user action, or our update from another node (in a cluster). We rely on transaction's
136 // read&put atomicity to do the right thing here.
137 final var iid = InstanceIdentifier.create(AaaEncryptServiceConfig.class);
138 final var tx = dataBroker.newReadWriteTransaction();
139 final var readFuture = tx.read(LogicalDatastoreType.CONFIGURATION, iid);
141 final AaaEncryptServiceConfig actual;
143 actual = readFuture.get().orElse(null);
144 } catch (InterruptedException | ExecutionException e) {
145 // Read failed: all we can do now is to disable the service and hope for an external recovery action -- like
146 // a restart of this component or a write to the datastore (which will trigger a retry).
148 LOG.error("Failed to read configuration, disabling service", e);
149 synchronized (this) {
155 if (!Objects.equals(actual, expected)) {
156 // Yup, there has been a race -- log that fact and bail out
158 LOG.debug(Markers.confidential(), "Skipping update on datastore mismatch: expected {} actual {}",
163 LOG.debug(Markers.confidential(), "Updating configuration to {}", target);
164 tx.put(LogicalDatastoreType.CONFIGURATION, iid, target);
165 Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
167 public void onFailure(final Throwable throwable) {
168 // Async update: we should get a new onDataTreeChanged() callback
169 LOG.warn("Configuration update failed, attempting to continue", throwable);
173 public void onSuccess(final CommitInfo result) {
174 LOG.info("Configuration update succeeded");
176 }, MoreExecutors.directExecutor());
180 private void disableInstance() {
181 if (instance != null) {
185 LOG.info("Encryption Service disabled");
189 private synchronized void updateInstance(final AaaEncryptServiceConfig newConfig) {
191 LOG.debug("Skipping instance update due to shutdown");
194 if (newConfig.equals(current)) {
195 LOG.debug("Skipping instance update due to equal configuration");
201 instance = factory.newInstance(FrameworkUtil.asDictionary(
202 AAAEncryptionServiceImpl.props(new EncryptServiceConfigImpl(newConfig))));
203 } catch (ComponentException e) {
204 LOG.error("Failed to start Encryption Service", e);
209 LOG.info("Encryption Service enabled");
212 private static boolean needKey(final AaaEncryptServiceConfig config) {
213 final var key = config.getEncryptKey();
214 return key == null || key.length() != config.requirePasswordLength();
217 private static boolean needSalt(final AaaEncryptServiceConfig config) {
218 final var salt = config.getEncryptSalt();
219 return salt == null || salt.isEmpty();