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  @NotNull 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  @NotNull 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  @NotNull 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(@NotNull final PrintStream standardOutput,
136                            @NotNull 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  @NotNull()
167  public char[] readPassword(@NotNull final String path)
168         throws IOException, LDAPException
169  {
170    return readPassword(new File(path));
171  }
172
173
174
175  /**
176   * Attempts to read a password from the specified file.
177   *
178   * @param  file  The path file from which the password should be read.  It
179   *               must not be {@code null}, and the file must exist.
180   *
181   * @return  The characters that comprise the password read from the specified
182   *          file.
183   *
184   * @throws  IOException  If a problem is encountered while trying to read the
185   *                       password from the file.
186   *
187   * @throws  LDAPException  If the file does not exist, if it does not contain
188   *                         exactly one line, or if that line is empty.
189   */
190  @NotNull()
191  public char[] readPassword(@NotNull final File file)
192         throws IOException, LDAPException
193  {
194    if (! file.exists())
195    {
196      throw new IOException(ERR_PW_FILE_READER_FILE_MISSING.get(
197           file.getAbsolutePath()));
198    }
199
200    if (! file.isFile())
201    {
202      throw new IOException(ERR_PW_FILE_READER_FILE_NOT_FILE.get(
203           file.getAbsolutePath()));
204    }
205
206    InputStream inputStream = new FileInputStream(file);
207    try
208    {
209      try
210      {
211        final ObjectPair<InputStream, char[]> encryptedFileData =
212             ToolUtils.getPossiblyPassphraseEncryptedInputStream(inputStream,
213                  encryptionPasswordCache, true,
214                  INFO_PW_FILE_READER_ENTER_PW_PROMPT
215                       .get(file.getAbsolutePath()),
216                  ERR_PW_FILE_READER_WRONG_PW.get(file.getAbsolutePath()),
217                  standardOutput, standardError);
218        inputStream = encryptedFileData.getFirst();
219
220        final char[] encryptionPassword = encryptedFileData.getSecond();
221        if (encryptionPassword != null)
222        {
223          synchronized (encryptionPasswordCache)
224          {
225            boolean passwordIsAlreadyCached = false;
226            for (final char[] cachedPassword : encryptionPasswordCache)
227            {
228              if (Arrays.equals(encryptionPassword, cachedPassword))
229              {
230                passwordIsAlreadyCached = true;
231                break;
232              }
233            }
234
235            if (!passwordIsAlreadyCached)
236            {
237              encryptionPasswordCache.add(encryptionPassword);
238            }
239          }
240        }
241      }
242      catch (final GeneralSecurityException e)
243      {
244        Debug.debugException(e);
245        throw new IOException(e);
246      }
247
248      inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream);
249
250      try (BufferedReader reader =
251                new BufferedReader(new InputStreamReader(inputStream)))
252      {
253        final String passwordLine = reader.readLine();
254        if (passwordLine == null)
255        {
256          throw new LDAPException(ResultCode.PARAM_ERROR,
257               ERR_PW_FILE_READER_FILE_EMPTY.get(file.getAbsolutePath()));
258        }
259
260        final String secondLine = reader.readLine();
261        if (secondLine != null)
262        {
263          throw new LDAPException(ResultCode.PARAM_ERROR,
264               ERR_PW_FILE_READER_FILE_HAS_MULTIPLE_LINES.get(
265               file.getAbsolutePath()));
266        }
267
268        if (passwordLine.isEmpty())
269        {
270          throw new LDAPException(ResultCode.PARAM_ERROR,
271               ERR_PW_FILE_READER_FILE_HAS_EMPTY_LINE.get(
272                    file.getAbsolutePath()));
273        }
274
275        return passwordLine.toCharArray();
276      }
277    }
278    finally
279    {
280      try
281      {
282
283        inputStream.close();
284      }
285      catch (final Exception e)
286      {
287        Debug.debugException(e);
288      }
289    }
290  }
291
292
293
294  /**
295   * Retrieves a list of the encryption passwords currently held in the cache.
296   *
297   * @return  A list of the encryption passwords currently held in the cache, or
298   *          an empty list if there are no cached passwords.
299   */
300  @NotNull()
301  public List<char[]> getCachedEncryptionPasswords()
302  {
303    final ArrayList<char[]> cacheCopy;
304    synchronized (encryptionPasswordCache)
305    {
306      cacheCopy = new ArrayList<>(encryptionPasswordCache.size());
307      for (final char[] cachedPassword : encryptionPasswordCache)
308      {
309        cacheCopy.add(Arrays.copyOf(cachedPassword, cachedPassword.length));
310      }
311    }
312
313    return Collections.unmodifiableList(cacheCopy);
314  }
315
316
317
318  /**
319   * Adds the provided password to the cache of passwords that will be tried as
320   * potential encryption keys if an encrypted password file is encountered.
321   *
322   * @param  encryptionPassword  A password to add to the cache of passwords
323   *                             that will be tried as potential encryption keys
324   *                             if an encrypted password file is encountered.
325   *                             It must not be {@code null} or empty.
326   */
327  public void addToEncryptionPasswordCache(
328                   @NotNull final String encryptionPassword)
329  {
330    addToEncryptionPasswordCache(encryptionPassword.toCharArray());
331  }
332
333
334
335  /**
336   * Adds the provided password to the cache of passwords that will be tried as
337   * potential encryption keys if an encrypted password file is encountered.
338   *
339   * @param  encryptionPassword  A password to add to the cache of passwords
340   *                             that will be tried as potential encryption keys
341   *                             if an encrypted password file is encountered.
342   *                             It must not be {@code null} or empty.
343   */
344  public void addToEncryptionPasswordCache(
345                   @NotNull final char[] encryptionPassword)
346  {
347    Validator.ensureNotNullWithMessage(encryptionPassword,
348         "PasswordFileReader.addToEncryptionPasswordCache.encryptionPassword " +
349              "must not be null or empty.");
350    Validator.ensureTrue((encryptionPassword.length > 0),
351         "PasswordFileReader.addToEncryptionPasswordCache.encryptionPassword " +
352              "must not be null or empty.");
353
354    synchronized (encryptionPasswordCache)
355    {
356      for (final char[] cachedPassword : encryptionPasswordCache)
357      {
358        if (Arrays.equals(cachedPassword, encryptionPassword))
359        {
360          return;
361        }
362      }
363
364      encryptionPasswordCache.add(encryptionPassword);
365    }
366  }
367
368
369
370  /**
371   * Clears the cache of passwords that will be tried as potential encryption
372   * keys if an encrypted password file is encountered.
373   *
374   * @param  zeroArrays  Indicates whether to zero out the contents of the
375   *                     cached passwords before clearing them.  If this is
376   *                     {@code true}, then all of the backing arrays for the
377   *                     cached passwords will be overwritten with all null
378   *                     characters to erase the original passwords from memory.
379   */
380  public void clearEncryptionPasswordCache(final boolean zeroArrays)
381  {
382    synchronized (encryptionPasswordCache)
383    {
384      if (zeroArrays)
385      {
386        for (final char[] cachedPassword : encryptionPasswordCache)
387        {
388          Arrays.fill(cachedPassword, '\u0000');
389        }
390      }
391
392      encryptionPasswordCache.clear();
393    }
394  }
395}