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