001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2006-2008 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2016 ForgeRock AS.
016 */
017package org.opends.server.extensions;
018
019import static org.opends.messages.ExtensionMessages.*;
020import static org.opends.server.util.StaticUtils.*;
021
022import java.io.BufferedReader;
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.FileReader;
026import java.io.IOException;
027import java.security.KeyStore;
028import java.security.KeyStoreException;
029import java.util.Enumeration;
030import java.util.List;
031
032import javax.net.ssl.KeyManager;
033import javax.net.ssl.KeyManagerFactory;
034
035import org.forgerock.i18n.LocalizableMessage;
036import org.forgerock.i18n.slf4j.LocalizedLogger;
037import org.forgerock.opendj.config.server.ConfigChangeResult;
038import org.forgerock.opendj.config.server.ConfigException;
039import org.forgerock.opendj.ldap.ResultCode;
040import org.opends.server.admin.server.ConfigurationChangeListener;
041import org.opends.server.admin.std.server.FileBasedKeyManagerProviderCfg;
042import org.opends.server.api.KeyManagerProvider;
043import org.opends.server.core.DirectoryServer;
044import org.forgerock.opendj.ldap.DN;
045import org.opends.server.types.DirectoryException;
046import org.opends.server.types.InitializationException;
047
048/**
049 * This class defines a key manager provider that will access keys stored in a
050 * file located on the Directory Server filesystem.
051 */
052public class FileBasedKeyManagerProvider
053       extends KeyManagerProvider<FileBasedKeyManagerProviderCfg>
054       implements ConfigurationChangeListener<FileBasedKeyManagerProviderCfg>
055{
056  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
057
058  /** The DN of the configuration entry for this key manager provider. */
059  private DN configEntryDN;
060  /** The configuration for this key manager provider. */
061  private FileBasedKeyManagerProviderCfg currentConfig;
062
063  /** The PIN needed to access the keystore. */
064  private char[] keyStorePIN;
065  /** The path to the key store backing file. */
066  private String keyStoreFile;
067  /** The key store type to use. */
068  private String keyStoreType;
069
070  /**
071   * Creates a new instance of this file-based key manager provider.  The
072   * <CODE>initializeKeyManagerProvider</CODE> method must be called on the
073   * resulting object before it may be used.
074   */
075  public FileBasedKeyManagerProvider()
076  {
077    // No implementation is required.
078  }
079
080  @Override
081  public void initializeKeyManagerProvider(
082      FileBasedKeyManagerProviderCfg configuration)
083      throws ConfigException, InitializationException {
084    // Store the DN of the configuration entry and register as a change listener
085    currentConfig = configuration;
086    configEntryDN = configuration.dn();
087    configuration.addFileBasedChangeListener(this);
088
089    final ConfigChangeResult ccr = new ConfigChangeResult();
090    keyStoreFile = getKeyStoreFile(configuration, configEntryDN, ccr);
091    keyStoreType = getKeyStoreType(configuration, configEntryDN, ccr);
092    keyStorePIN = getKeyStorePIN(configuration, configEntryDN, ccr);
093    if (!ccr.getMessages().isEmpty()) {
094      throw new InitializationException(ccr.getMessages().get(0));
095    }
096  }
097
098  /** Performs any finalization that may be necessary for this key manager provider. */
099  @Override
100  public void finalizeKeyManagerProvider()
101  {
102    currentConfig.removeFileBasedChangeListener(this);
103  }
104
105  @Override
106  public boolean containsKeyWithAlias(String alias) {
107    try {
108      KeyStore keyStore = getKeystore();
109      Enumeration<String> aliases = keyStore.aliases();
110      while (aliases.hasMoreElements()) {
111        String theAlias = aliases.nextElement();
112        if (alias.equals(theAlias) && keyStore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) {
113          return true;
114        }
115      }
116    }
117    catch (DirectoryException | KeyStoreException e) {
118    }
119
120    return false;
121  }
122
123  private KeyStore getKeystore() throws DirectoryException
124  {
125    try
126    {
127      KeyStore keyStore = KeyStore.getInstance(keyStoreType);
128
129      try (FileInputStream inputStream = new FileInputStream(getFileForPath(keyStoreFile)))
130      {
131        keyStore.load(inputStream, keyStorePIN);
132      }
133      return keyStore;
134    }
135    catch (Exception e)
136    {
137      logger.traceException(e);
138
139      LocalizableMessage message = ERR_FILE_KEYMANAGER_CANNOT_LOAD.get(
140              keyStoreFile, getExceptionMessage(e));
141      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
142    }
143  }
144
145  @Override
146  public KeyManager[] getKeyManagers() throws DirectoryException
147  {
148    KeyStore keyStore = getKeystore();
149
150    try
151    {
152      if (! findOneKeyEntry(keyStore))
153      {
154        // Troubleshooting message to let now of possible config error
155        logger.error(ERR_NO_KEY_ENTRY_IN_KEYSTORE, keyStoreFile);
156      }
157
158      String keyManagerAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
159      KeyManagerFactory keyManagerFactory =
160           KeyManagerFactory.getInstance(keyManagerAlgorithm);
161      keyManagerFactory.init(keyStore, keyStorePIN);
162      return keyManagerFactory.getKeyManagers();
163    }
164    catch (Exception e)
165    {
166      logger.traceException(e);
167
168      LocalizableMessage message = ERR_FILE_KEYMANAGER_CANNOT_CREATE_FACTORY.get(
169          keyStoreFile, getExceptionMessage(e));
170      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
171    }
172  }
173
174  @Override
175  public boolean containsAtLeastOneKey()
176  {
177    try
178    {
179      return findOneKeyEntry(getKeystore());
180    }
181    catch (Exception e) {
182      logger.traceException(e);
183      return false;
184    }
185  }
186
187  private boolean findOneKeyEntry(KeyStore keyStore) throws KeyStoreException
188  {
189    Enumeration<String> aliases = keyStore.aliases();
190    while (aliases.hasMoreElements())
191    {
192      String alias = aliases.nextElement();
193      if (keyStore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class))
194      {
195        return true;
196      }
197    }
198    return false;
199  }
200
201  @Override
202  public boolean isConfigurationAcceptable(
203                        FileBasedKeyManagerProviderCfg configuration,
204                        List<LocalizableMessage> unacceptableReasons)
205  {
206    return isConfigurationChangeAcceptable(configuration, unacceptableReasons);
207  }
208
209  @Override
210  public boolean isConfigurationChangeAcceptable(
211                      FileBasedKeyManagerProviderCfg configuration,
212                      List<LocalizableMessage> unacceptableReasons)
213  {
214    int startSize = unacceptableReasons.size();
215    DN cfgEntryDN = configuration.dn();
216
217    final ConfigChangeResult ccr = new ConfigChangeResult();
218    getKeyStoreFile(configuration, cfgEntryDN, ccr);
219    getKeyStoreType(configuration, cfgEntryDN, ccr);
220    getKeyStorePIN(configuration, cfgEntryDN, ccr);
221    unacceptableReasons.addAll(ccr.getMessages());
222
223    return startSize == unacceptableReasons.size();
224  }
225
226  @Override
227  public ConfigChangeResult applyConfigurationChange(
228                                 FileBasedKeyManagerProviderCfg configuration)
229  {
230    final ConfigChangeResult ccr = new ConfigChangeResult();
231    String newKeyStoreFile = getKeyStoreFile(configuration, configEntryDN, ccr);
232    String newKeyStoreType = getKeyStoreType(configuration, configEntryDN, ccr);
233    char[] newPIN = getKeyStorePIN(configuration, configEntryDN, ccr);
234
235    if (ccr.getResultCode() == ResultCode.SUCCESS)
236    {
237      currentConfig = configuration;
238      keyStorePIN   = newPIN;
239      keyStoreFile  = newKeyStoreFile;
240      keyStoreType  = newKeyStoreType;
241    }
242
243    return ccr;
244  }
245
246  /** Get the path to the key store file. */
247  private String getKeyStoreFile(FileBasedKeyManagerProviderCfg configuration, DN cfgEntryDN,
248      final ConfigChangeResult ccr)
249  {
250    String keyStoreFile = configuration.getKeyStoreFile();
251    try
252    {
253      File f = getFileForPath(keyStoreFile);
254      if (!f.exists() || !f.isFile())
255      {
256        ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
257        ccr.addMessage(ERR_FILE_KEYMANAGER_NO_SUCH_FILE.get(keyStoreFile, cfgEntryDN));
258      }
259    }
260    catch (Exception e)
261    {
262      logger.traceException(e);
263
264      ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
265      ccr.addMessage(ERR_FILE_KEYMANAGER_CANNOT_DETERMINE_FILE.get(cfgEntryDN, getExceptionMessage(e)));
266    }
267    return keyStoreFile;
268  }
269
270  /** Get the keystore type. If none is specified, then use the default type. */
271  private String getKeyStoreType(FileBasedKeyManagerProviderCfg configuration, DN cfgEntryDN,
272      final ConfigChangeResult ccr)
273  {
274    if (configuration.getKeyStoreType() != null)
275    {
276      try
277      {
278        KeyStore.getInstance(configuration.getKeyStoreType());
279        return configuration.getKeyStoreType();
280      }
281      catch (KeyStoreException kse)
282      {
283        logger.traceException(kse);
284
285        ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
286        ccr.addMessage(ERR_FILE_KEYMANAGER_INVALID_TYPE.get(
287            configuration.getKeyStoreType(), cfgEntryDN, getExceptionMessage(kse)));
288      }
289    }
290    return KeyStore.getDefaultType();
291  }
292
293  /**
294   * Get the PIN needed to access the contents of the keystore file.
295   * <p>
296   * We will offer several places to look for the PIN, and we will do so in the following order:
297   * <ol>
298   * <li>In a specified Java property</li>
299   * <li>In a specified environment variable</li>
300   * <li>In a specified file on the server filesystem</li>
301   * <li>As the value of a configuration attribute.</li>
302   * <ol>
303   * In any case, the PIN must be in the clear.
304   * <p>
305   * It is acceptable to have no PIN (OPENDJ-18)
306   */
307  private char[] getKeyStorePIN(FileBasedKeyManagerProviderCfg configuration, DN cfgEntryDN,
308      final ConfigChangeResult ccr)
309  {
310    if (configuration.getKeyStorePinProperty() != null)
311    {
312      String propertyName = configuration.getKeyStorePinProperty();
313      String pinStr = System.getProperty(propertyName);
314
315      if (pinStr == null)
316      {
317        ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
318        ccr.addMessage(ERR_FILE_KEYMANAGER_PIN_PROPERTY_NOT_SET.get(propertyName, cfgEntryDN));
319      }
320      else
321      {
322        return pinStr.toCharArray();
323      }
324    }
325    else if (configuration.getKeyStorePinEnvironmentVariable() != null)
326    {
327      String enVarName = configuration.getKeyStorePinEnvironmentVariable();
328      String pinStr    = System.getenv(enVarName);
329
330      if (pinStr == null)
331      {
332        ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
333        ccr.addMessage(ERR_FILE_KEYMANAGER_PIN_ENVAR_NOT_SET.get(enVarName, cfgEntryDN));
334      }
335      else
336      {
337        return pinStr.toCharArray();
338      }
339    }
340    else if (configuration.getKeyStorePinFile() != null)
341    {
342      String fileName = configuration.getKeyStorePinFile();
343      File   pinFile  = getFileForPath(fileName);
344
345      if (!pinFile.exists())
346      {
347        ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
348        ccr.addMessage(ERR_FILE_KEYMANAGER_PIN_NO_SUCH_FILE.get(fileName, cfgEntryDN));
349      }
350      else
351      {
352        String pinStr = readPinFromFile(pinFile, fileName, ccr);
353        if (pinStr == null)
354        {
355          ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
356          ccr.addMessage(ERR_FILE_KEYMANAGER_PIN_FILE_EMPTY.get(fileName, cfgEntryDN));
357        }
358        else
359        {
360          return pinStr.toCharArray();
361        }
362      }
363    }
364    else if (configuration.getKeyStorePin() != null)
365    {
366      return configuration.getKeyStorePin().toCharArray();
367    }
368    return null;
369  }
370
371  private String readPinFromFile(File pinFile, String fileName, ConfigChangeResult ccr)
372  {
373    try (BufferedReader br = new BufferedReader(new FileReader(pinFile)))
374    {
375      return br.readLine();
376    }
377    catch (IOException ioe)
378    {
379      ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
380      ccr.addMessage(ERR_FILE_KEYMANAGER_PIN_FILE_CANNOT_READ.get(fileName, configEntryDN, getExceptionMessage(ioe)));
381      return null;
382    }
383  }
384}