/* You may freely copy, distribute, modify and use this class as long as the original author attribution remains intact. See message below. Copyright (C) 2003 Christian Pesch. All Rights Reserved. */ package slash.metamusic.util; import java.io.*; import java.util.Arrays; /** * Helper to read passwords from a {@link PushbackInputStream}. */ public class PasswordReader { /** * Reads chars from in until an end-of-line sequence (EOL) or end-of-file (EOF) is encountered, * and then returns the data as a char[]. * <p/> * The EOL sequence may be any of the standard formats: '\n' (unix), '\r' (mac), "\r\n" (dos). * The EOL sequence is always completely read off the stream but is never included in the result. * <i>Note:</i> this means that the result will never contain the chars '\n' or '\r'. * In order to guarantee reading thru but not beyond the EOL sequence for all formats (unix, mac, dos), * this method requires that a PushbackReader and not a more general Reader be supplied. * <p/> * The code is secure: no Strings are used, only char arrays, * and all such arrays other than the result are guaranteed to be blanked out after last use to ensure privacy. * Thus, this method is suitable for reading in sensitive information such as passwords. * <p/> * This method never returns null; if no data before the EOL or EOF is read, a zero-length char[] is returned. * <p/> * * @throws IllegalArgumentException if in == null * @throws IOException if an I/O problem occurs * @see <a href="http://java.sun.com/j2se/1.4.2/docs/guide/security/jce/JCERefGuide.html#PBEEx">Password based encryption code examples from JCE documentation</a> */ public static char[] readLineSecure(PushbackReader in) throws IllegalArgumentException, IOException { if (in == null) throw new IllegalArgumentException("in == null"); char[] buffer = null; try { buffer = new char[128]; int offset = 0; loop: while (true) { int c = in.read(); switch (c) { case -1: case '\n': break loop; case '\r': int c2 = in.read(); if ((c2 != '\n') && (c2 != -1)) in.unread(c2); // guarantees that mac & dos line end sequences are completely read thru but not beyond break loop; default: buffer = checkBuffer(buffer, offset); buffer[offset++] = (char) c; break; } } char[] result = new char[offset]; System.arraycopy(buffer, 0, result, 0, offset); return result; } finally { eraseChars(buffer); } } /** * Checks if buffer is sufficiently large to store an element at an index == offset. * If it is, then buffer is simply returned. * If it is not, then a new char[] of more than sufficient size is created and initialized with buffer's current elements and returned; * the original supplied buffer is guaranteed to be blanked out upon method return in this case. * <p/> * * @throws IllegalArgumentException if buffer == null; offset < 0 */ public static char[] checkBuffer(char[] buffer, int offset) throws IllegalArgumentException { if (buffer == null) throw new IllegalArgumentException("buffer == null"); if (offset < 0) throw new IllegalArgumentException("offset = " + offset + " is < 0"); if (offset < buffer.length) return buffer; else { try { char[] bufferNew = new char[offset + 128]; System.arraycopy(buffer, 0, bufferNew, 0, buffer.length); return bufferNew; } finally { eraseChars(buffer); } } } /** * If buffer is not null, fills buffer with space (' ') chars. */ public static void eraseChars(char[] buffer) { if (buffer != null) Arrays.fill(buffer, ' '); } /** * Reads and returns some sensitive piece of information (e.g. a password) * from the console (i.e. System.in and System.out) in a secure manner. * <p/> * For top security, all console input is masked out while the user types in the password. * Once the user presses enter, the password is read via a call to {@link #readLineSecure readLineSecure(pr)}, * using a PushbackReader that wraps System.in. * <p/> * This method never returns null. * <p/> * * @throws IOException if an I/O problem occurs * @throws InterruptedException if the calling thread is interrupted while it is waiting at some point * @see <a href="http://java.sun.com/features/2002/09/pword_mask.html">Password masking in console</a> */ public static char[] readConsoleSecure(String prompt) throws IOException, InterruptedException { // start a separate thread which will mask out all chars typed on System.in by overwriting them using System.out: StreamMasker masker = new StreamMasker(System.out, prompt); Thread threadMasking = new Thread(masker); threadMasking.start(); // Goal: block this current thread (allowing masker to mask all user input) // while the user is in the middle of typing the password. // This may be achieved by trying to read just the first byte from System.in, // since reading from System.in blocks until it detects that an enter has been pressed. // Wrap System.in with a PushbackReader because this byte will be unread below. PushbackReader pr = new PushbackReader(new InputStreamReader(System.in)); int c = pr.read(); // When current thread gets here, the block on reading System.in is over (e.g. the user pressed enter, or some error occurred?) // signal threadMasking to stop and wait till it is dead: masker.stop(); threadMasking.join(); // check for stream errors: if (c == -1) throw new IOException("end-of-file was detected in System.in without any data being read"); if (System.out.checkError()) throw new IOException("an I/O problem was detected in System.out"); // pushback the first byte and supply the now unaltered stream to readLineSecure which will return the complete password: pr.unread(c); return readLineSecure(pr); } /** * Masks an InputStream by overwriting blank chars to the PrintStream corresponding to its output. * A typical application is for password input masking. * <p/> * * @see <a href="http://java.sun.com/features/2002/09/pword_mask.html">Password masking in console</a> */ public static class StreamMasker implements Runnable { private static final String TEN_BLANKS = repeatChars(' ', 10); private final PrintStream out; private final String promptOverwrite; private volatile boolean doMasking; // MUST be volatile to ensure update by one thread is instantly visible to other threads /** * Constructor. * <p/> * * @throws IllegalArgumentException if out == null; prompt == null; prompt contains the char '\r' or '\n' */ public StreamMasker(PrintStream out, String prompt) throws IllegalArgumentException { if (out == null) throw new IllegalArgumentException("out == null"); if (prompt == null) throw new IllegalArgumentException("prompt == null"); if (prompt.indexOf('\r') != -1) throw new IllegalArgumentException("prompt contains the char '\\r'"); if (prompt.indexOf('\n') != -1) throw new IllegalArgumentException("prompt contains the char '\\n'"); this.out = out; String setCursorToStart = repeatChars('\b', prompt.length() + TEN_BLANKS.length()); this.promptOverwrite = setCursorToStart + // sets cursor back to beginning of line: prompt + // writes prompt (critical: this reduces visual flicker in the prompt text that otherwise occurs if simply write blanks here) TEN_BLANKS + // writes 10 blanks beyond the prompt to mask out any input; go 10, not 1, spaces beyond end of prompt to handle the (hopefully rare) case that input occurred at a rapid rate setCursorToStart + // sets cursor back to beginning of line: prompt; // writes prompt again; the cursor will now be positioned immediately after prompt (critical: overwriting only works if all input text starts here) } /** * Returns a String of the specified length which consists of entirely of the char c. * <p/> * * @throws IllegalArgumentException if length < 0 */ private static String repeatChars(char c, int length) throws IllegalArgumentException { if (length < 0) throw new IllegalArgumentException("length = " + length + " is < 0"); StringBuffer sb = new StringBuffer(length); for (int i = 0; i < length; i++) { sb.append(c); } return sb.toString(); } /** * Repeatedly overwrites the current line of out with prompt followed by blanks. * This effectively masks any chars coming on out, as long as the masking occurs fast enough. * <p/> * To help ensure that masking occurs when system is in heavy use, the calling thread will have its priority * boosted to the max for the duration of the call (with its original priority restored upon return). * Interrupting the calling thread will eventually result in an exit from this method, * and the interrupted status of the calling thread will be set to true. * <p/> * * @throws RuntimeException if an error in the masking process is detected */ public void run() throws RuntimeException { int priorityOriginal = Thread.currentThread().getPriority(); Thread.currentThread().setPriority(Thread.MAX_PRIORITY); try { doMasking = true; // do this assignment here and NOT at variable declaration line to allow this instance to be restarted if desired while (doMasking) { out.print(promptOverwrite); // call checkError, which first flushes out, and then lets us confirm that everything was written correctly: if (out.checkError()) throw new RuntimeException("an I/O problem was detected in out"); // should be an IOException, but that would break method contract // limit the masking rate to fairly share the cpu; interruption breaks the loop try { Thread.sleep(1); // have experimentally found that sometimes see chars for a brief bit unless set this to its min value } catch (InterruptedException ie) { Thread.currentThread().interrupt(); // resets the interrupted status, which is typically lost when an InterruptedException is thrown, as per our method contract; see Lea, "Concurrent Programming in Java Second Edition", p. 171 return; // return, NOT break, since now want to skip the lines below where write bunch of blanks since typically the user will not have pressed enter yet } } // erase any prompt that may have been spuriously written on the NEXT line after the user pressed enter out.print('\r'); for (int i = 0; i < promptOverwrite.length(); i++) out.print(' '); out.print('\r'); } finally { Thread.currentThread().setPriority(priorityOriginal); } } /** * Signals any thread executing run to stop masking and exit run. */ public void stop() { doMasking = false; } } }