001/*
002 * Copyright 2019-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2019-2024 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-2024 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  // Indicates whether to allow interactively prompting for a passphrase if the
095  // specified file is encrypted and the key cannot be automatically obtained.
096  private final boolean allowPromptingForPassphrase;
097
098  // A list of passwords that will be tried as encryption keys if an encrypted
099  // password file is encountered.
100  @NotNull private final CopyOnWriteArrayList<char[]> encryptionPasswordCache;
101
102  // The print stream that should be used as standard output of an encrypted
103  // password file is encountered and it is necessary to prompt for the password
104  // used as the encryption key.
105  @NotNull private final PrintStream standardError;
106
107  // The print stream that should be used as standard output of an encrypted
108  // password file is encountered and it is necessary to prompt for the password
109  // used as the encryption key.
110  @NotNull private final PrintStream standardOutput;
111
112
113
114  /**
115   * Creates a new instance of this password file reader.  The JVM-default
116   * standard output and error streams will be used if it is necessary to
117   * interactively prompt the user for an encryption passphrase.
118   */
119  public PasswordFileReader()
120  {
121    this(true);
122  }
123
124
125
126  /**
127   * Creates a new instance of this password file reader.  The JVM-default
128   * standard output and error streams will be used if it is necessary to
129   * interactively prompt the user for an encryption passphrase.
130   *
131   * @param  allowPromptingForPassphrase
132   *              Indicates whether to allow interactively prompting the end
133   *              user for the encryption passphrase if the file is encrypted
134   *              and the key cannot be automatically obtained (for example,
135   *              from a Ping Identity server's encryption settings database).
136   */
137  public PasswordFileReader(final boolean allowPromptingForPassphrase)
138  {
139    this(System.out, System.err, allowPromptingForPassphrase);
140  }
141
142
143
144  /**
145   * Creates a new instance of this password file reader using the specified
146   * output and error streams if it is necessary to interactively prompt the
147   * user for an encryption passphrase.
148   *
149   * @param  standardOutput  The print stream that should be used as standard
150   *                         output if an encrypted password file is encountered
151   *                         and it is necessary to prompt for the password
152   *                         used as the encryption key.  This must not be
153   *                         {@code null}.
154   * @param  standardError   The print stream that should be used as standard
155   *                         error if an encrypted password file is encountered
156   *                         and it is necessary to prompt for the password
157   *                         used as the encryption key.  This must not be
158   *                         {@code null}.
159   */
160  public PasswordFileReader(@NotNull final PrintStream standardOutput,
161                            @NotNull final PrintStream standardError)
162  {
163    this(standardOutput, standardError, true);
164  }
165
166
167
168  /**
169   * Creates a new instance of this password file reader.
170   *
171   * @param  standardOutput
172   *              The print stream that should be used as standard output if an
173   *              encrypted password file is encountered and it is necessary to
174   *              prompt for the password used as the encryption key.  This must
175   *              not be {@code null}, but the provided stream will not be used
176   *              if the tool should not (or does not need to) prompt for an
177   *              encryption passphrase.
178   * @param  standardError
179   *              The print stream that should be used as standard error if an
180   *              encrypted password file is encountered and it is necessary to
181   *              prompt for the password used as the encryption key.  This must
182   *              not be {@code null}, but the provided stream will not be used
183   *              if the tool should not (or does not need to) prompt for an
184   *              encryption passphrase.
185   * @param  allowPromptingForPassphrase
186   *              Indicates whether to allow interactively prompting the end
187   *              user for the encryption passphrase if the file is encrypted
188   *              and the key cannot be automatically obtained (for example,
189   *              from a Ping Identity server's encryption settings database).
190   */
191  private PasswordFileReader(@NotNull final PrintStream standardOutput,
192                             @NotNull final PrintStream standardError,
193                             final boolean allowPromptingForPassphrase)
194  {
195    Validator.ensureNotNullWithMessage(standardOutput,
196         "PasswordFileReader.standardOutput must not be null.");
197    Validator.ensureNotNullWithMessage(standardError,
198         "PasswordFileReader.standardError must not be null.");
199
200    this.standardOutput = standardOutput;
201    this.standardError = standardError;
202    this.allowPromptingForPassphrase = allowPromptingForPassphrase;
203
204    encryptionPasswordCache = new CopyOnWriteArrayList<>();
205  }
206
207
208
209  /**
210   * Attempts to read a password from the specified file.
211   *
212   * @param  path  The path to the file from which the password should be read.
213   *               It must not be {@code null}, and the file must exist.
214   *
215   * @return  The characters that comprise the password read from the specified
216   *          file.
217   *
218   * @throws  IOException  If a problem is encountered while trying to read the
219   *                       password from the file.
220   *
221   * @throws  LDAPException  If the file does not exist, if it does not contain
222   *                         exactly one line, or if that line is empty.
223   */
224  @NotNull()
225  public char[] readPassword(@NotNull final String path)
226         throws IOException, LDAPException
227  {
228    return readPassword(new File(path));
229  }
230
231
232
233  /**
234   * Attempts to read a password from the specified file.
235   *
236   * @param  file  The path file from which the password should be read.  It
237   *               must not be {@code null}, and the file must exist.
238   *
239   * @return  The characters that comprise the password read from the specified
240   *          file.
241   *
242   * @throws  IOException  If a problem is encountered while trying to read the
243   *                       password from the file.
244   *
245   * @throws  LDAPException  If the file does not exist, if it does not contain
246   *                         exactly one line, or if that line is empty.
247   */
248  @NotNull()
249  public char[] readPassword(@NotNull final File file)
250         throws IOException, LDAPException
251  {
252    if (! file.exists())
253    {
254      throw new IOException(ERR_PW_FILE_READER_FILE_MISSING.get(
255           file.getAbsolutePath()));
256    }
257
258    if (! file.isFile())
259    {
260      throw new IOException(ERR_PW_FILE_READER_FILE_NOT_FILE.get(
261           file.getAbsolutePath()));
262    }
263
264    InputStream inputStream = new FileInputStream(file);
265    try
266    {
267      try
268      {
269        if (allowPromptingForPassphrase)
270        {
271          final ObjectPair<InputStream, char[]> encryptedFileData =
272               ToolUtils.getPossiblyPassphraseEncryptedInputStream(inputStream,
273                    encryptionPasswordCache, true,
274                    INFO_PW_FILE_READER_ENTER_PW_PROMPT
275                         .get(file.getAbsolutePath()),
276                    ERR_PW_FILE_READER_WRONG_PW.get(file.getAbsolutePath()),
277                    standardOutput, standardError);
278          inputStream = encryptedFileData.getFirst();
279
280          final char[] encryptionPassword = encryptedFileData.getSecond();
281          if (encryptionPassword != null)
282          {
283            synchronized (encryptionPasswordCache)
284            {
285              boolean passwordIsAlreadyCached = false;
286              for (final char[] cachedPassword : encryptionPasswordCache)
287              {
288                if (Arrays.equals(encryptionPassword, cachedPassword))
289                {
290                  passwordIsAlreadyCached = true;
291                  break;
292                }
293              }
294
295              if (!passwordIsAlreadyCached)
296              {
297                encryptionPasswordCache.add(encryptionPassword);
298              }
299            }
300          }
301        }
302        else
303        {
304          inputStream =
305               ToolUtils.getPossiblyPassphraseEncryptedInputStream(inputStream);
306        }
307      }
308      catch (final GeneralSecurityException e)
309      {
310        Debug.debugException(e);
311        throw new IOException(e);
312      }
313
314      inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream);
315
316      try (BufferedReader reader =
317                new BufferedReader(new InputStreamReader(inputStream)))
318      {
319        final String passwordLine = reader.readLine();
320        if (passwordLine == null)
321        {
322          throw new LDAPException(ResultCode.PARAM_ERROR,
323               ERR_PW_FILE_READER_FILE_EMPTY.get(file.getAbsolutePath()));
324        }
325
326        final String secondLine = reader.readLine();
327        if (secondLine != null)
328        {
329          throw new LDAPException(ResultCode.PARAM_ERROR,
330               ERR_PW_FILE_READER_FILE_HAS_MULTIPLE_LINES.get(
331               file.getAbsolutePath()));
332        }
333
334        if (passwordLine.isEmpty())
335        {
336          throw new LDAPException(ResultCode.PARAM_ERROR,
337               ERR_PW_FILE_READER_FILE_HAS_EMPTY_LINE.get(
338                    file.getAbsolutePath()));
339        }
340
341        return passwordLine.toCharArray();
342      }
343    }
344    finally
345    {
346      try
347      {
348
349        inputStream.close();
350      }
351      catch (final Exception e)
352      {
353        Debug.debugException(e);
354      }
355    }
356  }
357
358
359
360  /**
361   * Retrieves a list of the encryption passwords currently held in the cache.
362   *
363   * @return  A list of the encryption passwords currently held in the cache, or
364   *          an empty list if there are no cached passwords.
365   */
366  @NotNull()
367  public List<char[]> getCachedEncryptionPasswords()
368  {
369    final ArrayList<char[]> cacheCopy;
370    synchronized (encryptionPasswordCache)
371    {
372      cacheCopy = new ArrayList<>(encryptionPasswordCache.size());
373      for (final char[] cachedPassword : encryptionPasswordCache)
374      {
375        cacheCopy.add(Arrays.copyOf(cachedPassword, cachedPassword.length));
376      }
377    }
378
379    return Collections.unmodifiableList(cacheCopy);
380  }
381
382
383
384  /**
385   * Adds the provided password to the cache of passwords that will be tried as
386   * potential encryption keys if an encrypted password file is encountered.
387   *
388   * @param  encryptionPassword  A password to add to the cache of passwords
389   *                             that will be tried as potential encryption keys
390   *                             if an encrypted password file is encountered.
391   *                             It must not be {@code null} or empty.
392   */
393  public void addToEncryptionPasswordCache(
394                   @NotNull final String encryptionPassword)
395  {
396    addToEncryptionPasswordCache(encryptionPassword.toCharArray());
397  }
398
399
400
401  /**
402   * Adds the provided password to the cache of passwords that will be tried as
403   * potential encryption keys if an encrypted password file is encountered.
404   *
405   * @param  encryptionPassword  A password to add to the cache of passwords
406   *                             that will be tried as potential encryption keys
407   *                             if an encrypted password file is encountered.
408   *                             It must not be {@code null} or empty.
409   */
410  public void addToEncryptionPasswordCache(
411                   @NotNull final char[] encryptionPassword)
412  {
413    Validator.ensureNotNullWithMessage(encryptionPassword,
414         "PasswordFileReader.addToEncryptionPasswordCache.encryptionPassword " +
415              "must not be null or empty.");
416    Validator.ensureTrue((encryptionPassword.length > 0),
417         "PasswordFileReader.addToEncryptionPasswordCache.encryptionPassword " +
418              "must not be null or empty.");
419
420    synchronized (encryptionPasswordCache)
421    {
422      for (final char[] cachedPassword : encryptionPasswordCache)
423      {
424        if (Arrays.equals(cachedPassword, encryptionPassword))
425        {
426          return;
427        }
428      }
429
430      encryptionPasswordCache.add(encryptionPassword);
431    }
432  }
433
434
435
436  /**
437   * Clears the cache of passwords that will be tried as potential encryption
438   * keys if an encrypted password file is encountered.
439   *
440   * @param  zeroArrays  Indicates whether to zero out the contents of the
441   *                     cached passwords before clearing them.  If this is
442   *                     {@code true}, then all of the backing arrays for the
443   *                     cached passwords will be overwritten with all null
444   *                     characters to erase the original passwords from memory.
445   */
446  public void clearEncryptionPasswordCache(final boolean zeroArrays)
447  {
448    synchronized (encryptionPasswordCache)
449    {
450      if (zeroArrays)
451      {
452        for (final char[] cachedPassword : encryptionPasswordCache)
453        {
454          Arrays.fill(cachedPassword, '\u0000');
455        }
456      }
457
458      encryptionPasswordCache.clear();
459    }
460  }
461}