001/*
002 * Copyright 2013-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2013-2017 UnboundID Corp.
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.lang.reflect.Method;
027import java.util.Arrays;
028import java.util.concurrent.atomic.AtomicBoolean;
029
030import com.unboundid.ldap.sdk.LDAPException;
031import com.unboundid.ldap.sdk.ResultCode;
032
033import static com.unboundid.util.UtilityMessages.*;
034
035
036
037/**
038 * This class provides a mechanism for reading a password from the command line
039 * in a way that attempts to prevent it from being displayed.  If it is
040 * available (i.e., Java SE 6 or later), the
041 * {@code java.io.Console.readPassword} method will be used to accomplish this.
042 * For Java SE 5 clients, a more primitive approach must be taken, which
043 * requires flooding standard output with backspace characters using a
044 * high-priority thread.  This has only a limited effectiveness, but it is the
045 * best option available for older Java versions.
046 */
047@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
048public final class PasswordReader
049       extends Thread
050{
051  /**
052   * The input stream from which to read the password.  This should only be set
053   * when running unit tests.
054   */
055  private static volatile BufferedReader TEST_READER = null;
056
057
058
059  // Indicates whether a request has been made for the backspace thread to
060  // stop running.
061  private final AtomicBoolean stopRequested;
062
063  // An object that will be used to wait for the reader thread to be started.
064  private final Object startMutex;
065
066
067
068  /**
069   * Creates a new instance of this password reader thread.
070   */
071  private PasswordReader()
072  {
073    startMutex = new Object();
074    stopRequested = new AtomicBoolean(false);
075
076    setName("Password Reader Thread");
077    setDaemon(true);
078    setPriority(Thread.MAX_PRIORITY);
079  }
080
081
082
083  /**
084   * Reads a password from the console.
085   *
086   * @return  The characters that comprise the password that was read.
087   *
088   * @throws  LDAPException  If a problem is encountered while trying to read
089   *                         the password.
090   */
091  public static byte[] readPassword()
092         throws LDAPException
093  {
094    // If an input stream is available, then read the password from it.
095    final BufferedReader testReader = TEST_READER;
096    if (testReader != null)
097    {
098      try
099      {
100        return StaticUtils.getBytes(testReader.readLine());
101      }
102      catch (final Exception e)
103      {
104        Debug.debugException(e);
105        throw new LDAPException(ResultCode.LOCAL_ERROR,
106             ERR_PW_READER_FAILURE.get(StaticUtils.getExceptionMessage(e)),
107             e);
108      }
109    }
110
111
112    // Try to use the Java SE 6 approach first.
113    try
114    {
115      final Method consoleMethod = System.class.getMethod("console");
116      final Object consoleObject = consoleMethod.invoke(null);
117
118      final Method readPasswordMethod =
119        consoleObject.getClass().getMethod("readPassword");
120      final char[] pwChars = (char[]) readPasswordMethod.invoke(consoleObject);
121
122      final ByteStringBuffer buffer = new ByteStringBuffer();
123      buffer.append(pwChars);
124      Arrays.fill(pwChars, '\u0000');
125      final byte[] pwBytes = buffer.toByteArray();
126      buffer.clear(true);
127      return pwBytes;
128    }
129    catch (final Exception e)
130    {
131      Debug.debugException(e);
132    }
133
134    // Fall back to the an approach that should work with Java SE 5.
135    try
136    {
137      final PasswordReader r = new PasswordReader();
138      try
139      {
140        synchronized (r.startMutex)
141        {
142          r.start();
143          r.startMutex.wait();
144        }
145
146        // NOTE:  0x0A is '\n' and 0x0D is '\r'.
147        final ByteStringBuffer buffer = new ByteStringBuffer();
148        while (true)
149        {
150          final int byteRead = System.in.read();
151          if ((byteRead < 0) || (byteRead == 0x0A))
152          {
153            // This is the end of the value, as indicated by a UNIX line
154            // terminator sequence.
155            break;
156          }
157          else if (byteRead == 0x0D)
158          {
159            final int nextCharacter = System.in.read();
160            if ((nextCharacter < 0) || (byteRead == 0x0A))
161            {
162              // This is the end of the value as indicated by a Windows line
163              // terminator sequence.
164              break;
165            }
166            else
167            {
168              buffer.append((byte) byteRead);
169              buffer.append((byte) nextCharacter);
170            }
171          }
172          else
173          {
174            buffer.append((byte) byteRead);
175          }
176        }
177
178        final byte[] pwBytes = buffer.toByteArray();
179        buffer.clear(true);
180        return pwBytes;
181      }
182      finally
183      {
184        r.stopRequested.set(true);
185      }
186    }
187    catch (final Exception e)
188    {
189      Debug.debugException(e);
190
191      if (e instanceof InterruptedException)
192      {
193        Thread.currentThread().interrupt();
194      }
195
196      throw new LDAPException(ResultCode.LOCAL_ERROR,
197           ERR_PW_READER_FAILURE.get(StaticUtils.getExceptionMessage(e)),
198           e);
199    }
200  }
201
202
203
204  /**
205   * Repeatedly sends backspace and space characters to standard output in an
206   * attempt to try to hide what the user enters.
207   */
208  @Override()
209  public void run()
210  {
211    synchronized (startMutex)
212    {
213      startMutex.notifyAll();
214    }
215
216    while (! stopRequested.get())
217    {
218      System.out.print("\u0008 ");
219      yield();
220    }
221  }
222
223
224
225  /**
226   * Specifies the input stream from which to read the password.  This should
227   * only be set when running unit tests.
228   *
229   * @param  reader  The input stream from which to read the password.  It may
230   *                 be {@code null} to obtain the password from the normal
231   *                 means.
232   */
233  static void setTestReader(final BufferedReader reader)
234  {
235    TEST_READER = reader;
236  }
237}