001/*
002 * Copyright 2019-2020 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2019-2020 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2019-2020 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.util;
037
038
039
040import java.io.BufferedReader;
041import java.io.File;
042import java.io.FileInputStream;
043import java.io.InputStream;
044import java.io.IOException;
045import java.io.InputStreamReader;
046import java.io.PrintStream;
047import java.security.GeneralSecurityException;
048import java.util.ArrayList;
049import java.util.Arrays;
050import java.util.Collections;
051import java.util.List;
052import java.util.concurrent.CopyOnWriteArrayList;
053
054import com.unboundid.ldap.sdk.LDAPException;
055import com.unboundid.ldap.sdk.ResultCode;
056import com.unboundid.ldap.sdk.unboundidds.tools.ToolUtils;
057
058import static com.unboundid.util.UtilityMessages.*;
059
060
061
062/**
063 * This class provides a mechanism for reading a password from a file.  Password
064 * files must contain exactly one line, which must be non-empty, and the entire
065 * content of that line will be used as the password.
066 * <BR><BR>
067 * The contents of the file may have optionally been encrypted with the
068 * {@link PassphraseEncryptedOutputStream}, and may have optionally been
069 * compressed with the {@code GZIPOutputStream}.  If the data is both compressed
070 * and encrypted, then it must have been compressed before it was encrypted, so
071 * that it is necessary to decrypt the data before it can be decompressed.
072 * <BR><BR>
073 * If the file is encrypted, then the encryption key may be obtained in one of
074 * the following ways:
075 * <UL>
076 *   <LI>If this code is running in a tool that is part of a Ping Identity
077 *       Directory Server installation (or a related product like the Directory
078 *       Proxy Server or Data Synchronization Server, or an alternately branded
079 *       version of these products, like the Alcatel-Lucent or Nokia 8661
080 *       versions), and the file was encrypted with a key from that server's
081 *       encryption settings database, then the tool will try to get the
082 *       key from the corresponding encryption settings definition.  In many
083 *       cases, this may not require any interaction from the user at all.</LI>
084 *   <LI>The reader maintains a cache of passwords that have been previously
085 *       used.  If the same password is used to encrypt multiple files, it may
086 *       only need to be requested once from the user.  The caller can also
087 *       manually add passwords to this cache if they are known in advance.</LI>
088 *   <LI>The user can be interactively prompted for the password.</LI>
089 * </UL>
090 */
091@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
092public final class PasswordFileReader
093{
094  // A list of passwords that will be tried as encryption keys if an encrypted
095  // password file is encountered.
096  private final CopyOnWriteArrayList<char[]> encryptionPasswordCache;
097
098  // The print stream that should be used as standard output of an encrypted
099  // password file is encountered and it is necessary to prompt for the password
100  // used as the encryption key.
101  private final PrintStream standardError;
102
103  // The print stream that should be used as standard output of an encrypted
104  // password file is encountered and it is necessary to prompt for the password
105  // used as the encryption key.
106  private final PrintStream standardOutput;
107
108
109
110  /**
111   * Creates a new instance of this password file reader.  The JVM-default
112   * standard output and error streams will be used.
113   */
114  public PasswordFileReader()
115  {
116    this(System.out, System.err);
117  }
118
119
120
121  /**
122   * Creates a new instance of this password file reader.
123   *
124   * @param  standardOutput  The print stream that should be used as standard
125   *                         output if an encrypted password file is encountered
126   *                         and it is necessary to prompt for the password
127   *                         used as the encryption key.  This must not be
128   *                         {@code null}.
129   * @param  standardError   The print stream that should be used as standard
130   *                         error if an encrypted password file is encountered
131   *                         and it is necessary to prompt for the password
132   *                         used as the encryption key.  This must not be
133   *                         {@code null}.
134   */
135  public PasswordFileReader(final PrintStream standardOutput,
136                            final PrintStream standardError)
137  {
138    Validator.ensureNotNullWithMessage(standardOutput,
139         "PasswordFileReader.standardOutput must not be null.");
140    Validator.ensureNotNullWithMessage(standardError,
141         "PasswordFileReader.standardError must not be null.");
142
143    this.standardOutput = standardOutput;
144    this.standardError = standardError;
145
146    encryptionPasswordCache = new CopyOnWriteArrayList<>();
147  }
148
149
150
151  /**
152   * Attempts to read a password from the specified file.
153   *
154   * @param  path  The path to the file from which the password should be read.
155   *               It must not be {@code null}, and the file must exist.
156   *
157   * @return  The characters that comprise the password read from the specified
158   *          file.
159   *
160   * @throws  IOException  If a problem is encountered while trying to read the
161   *                       password from the file.
162   *
163   * @throws  LDAPException  If the file does not exist, if it does not contain
164   *                         exactly one line, or if that line is empty.
165   */
166  public char[] readPassword(final String path)
167         throws IOException, LDAPException
168  {
169    return readPassword(new File(path));
170  }
171
172
173
174  /**
175   * Attempts to read a password from the specified file.
176   *
177   * @param  file  The path file from which the password should be read.  It
178   *               must not be {@code null}, and the file must exist.
179   *
180   * @return  The characters that comprise the password read from the specified
181   *          file.
182   *
183   * @throws  IOException  If a problem is encountered while trying to read the
184   *                       password from the file.
185   *
186   * @throws  LDAPException  If the file does not exist, if it does not contain
187   *                         exactly one line, or if that line is empty.
188   */
189  public char[] readPassword(final File file)
190         throws IOException, LDAPException
191  {
192    if (! file.exists())
193    {
194      throw new IOException(ERR_PW_FILE_READER_FILE_MISSING.get(
195           file.getAbsolutePath()));
196    }
197
198    if (! file.isFile())
199    {
200      throw new IOException(ERR_PW_FILE_READER_FILE_NOT_FILE.get(
201           file.getAbsolutePath()));
202    }
203
204    InputStream inputStream = new FileInputStream(file);
205    try
206    {
207      try
208      {
209        final ObjectPair<InputStream, char[]> encryptedFileData =
210             ToolUtils.getPossiblyPassphraseEncryptedInputStream(inputStream,
211                  encryptionPasswordCache, true,
212                  INFO_PW_FILE_READER_ENTER_PW_PROMPT
213                       .get(file.getAbsolutePath()),
214                  ERR_PW_FILE_READER_WRONG_PW.get(file.getAbsolutePath()),
215                  standardOutput, standardError);
216        inputStream = encryptedFileData.getFirst();
217
218        final char[] encryptionPassword = encryptedFileData.getSecond();
219        if (encryptionPassword != null)
220        {
221          synchronized (encryptionPasswordCache)
222          {
223            boolean passwordIsAlreadyCached = false;
224            for (final char[] cachedPassword : encryptionPasswordCache)
225            {
226              if (Arrays.equals(encryptionPassword, cachedPassword))
227              {
228                passwordIsAlreadyCached = true;
229                break;
230              }
231            }
232
233            if (!passwordIsAlreadyCached)
234            {
235              encryptionPasswordCache.add(encryptionPassword);
236            }
237          }
238        }
239      }
240      catch (final GeneralSecurityException e)
241      {
242        Debug.debugException(e);
243        throw new IOException(e);
244      }
245
246      inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream);
247
248      try (BufferedReader reader =
249                new BufferedReader(new InputStreamReader(inputStream)))
250      {
251        final String passwordLine = reader.readLine();
252        if (passwordLine == null)
253        {
254          throw new LDAPException(ResultCode.PARAM_ERROR,
255               ERR_PW_FILE_READER_FILE_EMPTY.get(file.getAbsolutePath()));
256        }
257
258        final String secondLine = reader.readLine();
259        if (secondLine != null)
260        {
261          throw new LDAPException(ResultCode.PARAM_ERROR,
262               ERR_PW_FILE_READER_FILE_HAS_MULTIPLE_LINES.get(
263               file.getAbsolutePath()));
264        }
265
266        if (passwordLine.isEmpty())
267        {
268          throw new LDAPException(ResultCode.PARAM_ERROR,
269               ERR_PW_FILE_READER_FILE_HAS_EMPTY_LINE.get(
270                    file.getAbsolutePath()));
271        }
272
273        return passwordLine.toCharArray();
274      }
275    }
276    finally
277    {
278      try
279      {
280
281        inputStream.close();
282      }
283      catch (final Exception e)
284      {
285        Debug.debugException(e);
286      }
287    }
288  }
289
290
291
292  /**
293   * Retrieves a list of the encryption passwords currently held in the cache.
294   *
295   * @return  A list of the encryption passwords currently held in the cache, or
296   *          an empty list if there are no cached passwords.
297   */
298  public List<char[]> getCachedEncryptionPasswords()
299  {
300    final ArrayList<char[]> cacheCopy;
301    synchronized (encryptionPasswordCache)
302    {
303      cacheCopy = new ArrayList<>(encryptionPasswordCache.size());
304      for (final char[] cachedPassword : encryptionPasswordCache)
305      {
306        cacheCopy.add(Arrays.copyOf(cachedPassword, cachedPassword.length));
307      }
308    }
309
310    return Collections.unmodifiableList(cacheCopy);
311  }
312
313
314
315  /**
316   * Adds the provided password to the cache of passwords that will be tried as
317   * potential encryption keys if an encrypted password file is encountered.
318   *
319   * @param  encryptionPassword  A password to add to the cache of passwords
320   *                             that will be tried as potential encryption keys
321   *                             if an encrypted password file is encountered.
322   *                             It must not be {@code null} or empty.
323   */
324  public void addToEncryptionPasswordCache(final String encryptionPassword)
325  {
326    addToEncryptionPasswordCache(encryptionPassword.toCharArray());
327  }
328
329
330
331  /**
332   * Adds the provided password to the cache of passwords that will be tried as
333   * potential encryption keys if an encrypted password file is encountered.
334   *
335   * @param  encryptionPassword  A password to add to the cache of passwords
336   *                             that will be tried as potential encryption keys
337   *                             if an encrypted password file is encountered.
338   *                             It must not be {@code null} or empty.
339   */
340  public void addToEncryptionPasswordCache(final char[] encryptionPassword)
341  {
342    Validator.ensureNotNullWithMessage(encryptionPassword,
343         "PasswordFileReader.addToEncryptionPasswordCache.encryptionPassword " +
344              "must not be null or empty.");
345    Validator.ensureTrue((encryptionPassword.length > 0),
346         "PasswordFileReader.addToEncryptionPasswordCache.encryptionPassword " +
347              "must not be null or empty.");
348
349    synchronized (encryptionPasswordCache)
350    {
351      for (final char[] cachedPassword : encryptionPasswordCache)
352      {
353        if (Arrays.equals(cachedPassword, encryptionPassword))
354        {
355          return;
356        }
357      }
358
359      encryptionPasswordCache.add(encryptionPassword);
360    }
361  }
362
363
364
365  /**
366   * Clears the cache of passwords that will be tried as potential encryption
367   * keys if an encrypted password file is encountered.
368   *
369   * @param  zeroArrays  Indicates whether to zero out the contents of the
370   *                     cached passwords before clearing them.  If this is
371   *                     {@code true}, then all of the backing arrays for the
372   *                     cached passwords will be overwritten with all null
373   *                     characters to erase the original passwords from memory.
374   */
375  public void clearEncryptionPasswordCache(final boolean zeroArrays)
376  {
377    synchronized (encryptionPasswordCache)
378    {
379      if (zeroArrays)
380      {
381        for (final char[] cachedPassword : encryptionPasswordCache)
382        {
383          Arrays.fill(cachedPassword, '\u0000');
384        }
385      }
386
387      encryptionPasswordCache.clear();
388    }
389  }
390}