Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 78 additions & 6 deletions src/java.base/share/classes/java/io/Console.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
package java.io;

import java.util.*;
import java.lang.annotation.Native;
import java.nio.charset.Charset;
import java.util.concurrent.atomic.AtomicReference;
import jdk.internal.misc.JavaIOAccess;
import jdk.internal.misc.SharedSecrets;
import sun.nio.cs.StreamDecoder;
Expand Down Expand Up @@ -308,6 +310,51 @@ public String readLine() {
* or {@code null} if an end of stream has been reached.
*/
public char[] readPassword(String fmt, Object ... args) {
return readPassword0(false, fmt, args);
}

// These two methods are intended for sun.security.util.Password, so tools like keytool can
// use Console even when standard output is redirected. The Password class should first
// check if `System.console()` returns a Console instance and use it if available. Otherwise,
// it should call this method to obtain a Console. This ensures only one Console
// instance exists in the Java runtime.
private static final AtomicReference<Optional<Console>> INSTANCE = new AtomicReference<>();
private static Optional<Console> passwordConsole() {
Optional<Console> result = INSTANCE.get();
if (result != null) {
return result;
}

synchronized (Console.class) {
result = INSTANCE.get();
if (result != null) {
return result;
}

// If there's already a proper console, throw an exception
if (System.console() != null) {
throw new IllegalStateException("Can't create a dedicated password " +
"console since a real console already exists");
}

// If stdin is NOT redirected, return an Optional containing a Console
// instance, otherwise an empty Optional.
result = isStdinTty() ?
Optional.of(
new Console()) :
Optional.empty();

INSTANCE.set(result);
return result;
}
}

// Dedicated entry for sun.security.util.Password when stdout is redirected.
private char[] readPasswordNoNewLine() {
return readPassword0(true, "");
}

private char[] readPassword0(boolean noNewLine, String fmt, Object ... args) {
char[] passwd = null;
synchronized (writeLock) {
synchronized(readLock) {
Expand Down Expand Up @@ -335,7 +382,9 @@ public char[] readPassword(String fmt, Object ... args) {
ioe.addSuppressed(x);
}
if (ioe != null) {
Arrays.fill(passwd, ' ');
if (passwd != null) {
Arrays.fill(passwd, ' ');
}
try {
if (reader instanceof LineReader) {
LineReader lr = (LineReader)reader;
Expand All @@ -347,7 +396,9 @@ public char[] readPassword(String fmt, Object ... args) {
throw ioe;
}
}
pw.println();
if (!noNewLine) {
pw.println();
}
}
}
return passwd;
Expand Down Expand Up @@ -411,6 +462,12 @@ public void flush() {
private boolean restoreEcho;
private boolean shutdownHookInstalled;
private static native String encoding();
@Native private static final int TTY_STDIN_MASK = 0x00000001;
@Native private static final int TTY_STDOUT_MASK = 0x00000002;
@Native private static final int TTY_STDERR_MASK = 0x00000004;
// ttyStatus() returns bit patterns above, a bit is set if the corresponding file
// descriptor is a character device
private static final int ttyStatus = ttyStatus();
/*
* Sets the console echo status to {@code on} and returns the previous
* console on/off status.
Expand Down Expand Up @@ -575,23 +632,28 @@ public int read(char cbuf[], int offset, int length)
static {
SharedSecrets.setJavaIOAccess(new JavaIOAccess() {
public Console console() {
if (istty()) {
if (isStdinTty() && isStdoutTty()) {
if (cons == null)
cons = new Console();
return cons;
}
return null;
}

public Charset charset() {
public Charset charset(Console c) {
// This method is called in sun.security.util.Password,
// cons already exists when this method is called
return cons.cs;
return c.cs;
}
public Optional<Console> passwordConsole() {
return Console.passwordConsole();
}
public char[] readPasswordNoNewLine(Console c) {
return c.readPasswordNoNewLine();
}
});
}
private static Console cons;
private static native boolean istty();
private Console() {
readLock = new Object();
writeLock = new Object();
Expand All @@ -615,4 +677,14 @@ private Console() {
cs));
rcb = new char[1024];
}
private static boolean isStdinTty() {
return (ttyStatus & TTY_STDIN_MASK) != 0;
}
private static boolean isStdoutTty() {
return (ttyStatus & TTY_STDOUT_MASK) != 0;
}
private static boolean isStderrTty() {
return (ttyStatus & TTY_STDERR_MASK) != 0;
}
private static native int ttyStatus();
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -27,8 +27,11 @@

import java.io.Console;
import java.nio.charset.Charset;
import java.util.Optional;

public interface JavaIOAccess {
public Console console();
public Charset charset();
public Charset charset(Console c);
public Optional<Console> passwordConsole();
public char[] readPasswordNoNewLine(Console c);
}
134 changes: 89 additions & 45 deletions src/java.base/share/classes/sun/security/util/Password.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2003, 2011, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -33,7 +33,6 @@

/**
* A utility class for reading passwords
*
*/
public class Password {
/** Reads user password from given input stream. */
Expand All @@ -50,29 +49,36 @@ public static char[] readPassword(InputStream in, boolean isEchoOn)

char[] consoleEntered = null;
byte[] consoleBytes = null;
char[] buf = null;

try {
// Use the new java.io.Console class
Console con = null;
if (!isEchoOn && in == System.in && ((con = System.console()) != null)) {
consoleEntered = con.readPassword();
// readPassword returns "" if you just print ENTER,
// to be compatible with old Password class, change to null
if (consoleEntered != null && consoleEntered.length == 0) {
return null;
if (!isEchoOn) {
if (in == System.in
&& ConsoleHolder.consoleIsAvailable()) {
consoleEntered = ConsoleHolder.readPassword();
// readPassword might return null. Stop now.
if (consoleEntered == null) {
return null;
}
consoleBytes = ConsoleHolder.convertToBytes(consoleEntered);
in = new ByteArrayInputStream(consoleBytes);
} else if (System.in.available() == 0) {
// This may be running in an IDE Run Window or in JShell,
// which acts like an interactive console and echoes the
// entered password. In this case, print a warning that
// the password might be echoed. If available() is not zero,
// it's more likely the input comes from a pipe, such as
// "echo password |" or "cat password_file |" where input
// will be silently consumed without echoing to the screen.
System.err.print(ResourcesMgr.getString
("warning.input.may.be.visible.on.screen"));
}
consoleBytes = convertToBytes(consoleEntered);
in = new ByteArrayInputStream(consoleBytes);
}

// Rest of the lines still necessary for KeyStoreLoginModule
// and when there is no console.

char[] lineBuffer;
char[] buf;
int i;

buf = lineBuffer = new char[128];
buf = new char[128];

int room = buf.length;
int offset = 0;
Expand Down Expand Up @@ -100,11 +106,11 @@ public static char[] readPassword(InputStream in, boolean isEchoOn)
/* fall through */
default:
if (--room < 0) {
char[] oldBuf = buf;
buf = new char[offset + 128];
room = buf.length - offset - 1;
System.arraycopy(lineBuffer, 0, buf, 0, offset);
Arrays.fill(lineBuffer, ' ');
lineBuffer = buf;
System.arraycopy(oldBuf, 0, buf, 0, offset);
Arrays.fill(oldBuf, ' ');
}
buf[offset++] = (char) c;
break;
Expand All @@ -117,8 +123,6 @@ public static char[] readPassword(InputStream in, boolean isEchoOn)

char[] ret = new char[offset];
System.arraycopy(buf, 0, ret, 0, offset);
Arrays.fill(buf, ' ');

return ret;
} finally {
if (consoleEntered != null) {
Expand All @@ -127,35 +131,75 @@ public static char[] readPassword(InputStream in, boolean isEchoOn)
if (consoleBytes != null) {
Arrays.fill(consoleBytes, (byte)0);
}
if (buf != null) {
Arrays.fill(buf, ' ');
}
}
}

/**
* Change a password read from Console.readPassword() into
* its original bytes.
*
* @param pass a char[]
* @return its byte[] format, similar to new String(pass).getBytes()
*/
private static byte[] convertToBytes(char[] pass) {
if (enc == null) {
synchronized (Password.class) {
enc = SharedSecrets.getJavaIOAccess()
.charset()
.newEncoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
// Everything on Console is inside this class.
private static class ConsoleHolder {

// primary console; may be null
private static final Console c1;
// secondary console (when stdout is redirected); may be null
private static final Console c2;
// encoder for c1 or c2
private static final CharsetEncoder enc;

static {
c1 = System.console();
Charset charset;
if (c1 != null) {
c2 = null;
charset = SharedSecrets.getJavaIOAccess().charset(c1);
} else {
c2 = SharedSecrets.getJavaIOAccess().passwordConsole()
.orElse(null);
charset = (c2 != null)
? SharedSecrets.getJavaIOAccess().charset(c2) : null;
}
enc = charset == null ? null : charset.newEncoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
}
byte[] ba = new byte[(int)(enc.maxBytesPerChar() * pass.length)];
ByteBuffer bb = ByteBuffer.wrap(ba);
synchronized (enc) {
enc.reset().encode(CharBuffer.wrap(pass), bb, true);

public static boolean consoleIsAvailable() {
return c1 != null || c2 != null;
}
if (bb.position() < ba.length) {
ba[bb.position()] = '\n';

public static char[] readPassword() {
assert consoleIsAvailable();
if (c1 != null) {
return c1.readPassword();
} else {
try {
return SharedSecrets.getJavaIOAccess()
.readPasswordNoNewLine(c2);
} finally {
System.err.println();
}
}
}

/**
* Convert a password read from console into its original bytes.
*
* @param pass a char[]
* @return its byte[] format, equivalent to new String(pass).getBytes()
* but String is immutable and cannot be cleaned up.
*/
public static byte[] convertToBytes(char[] pass) {
assert consoleIsAvailable();
byte[] ba = new byte[(int) (enc.maxBytesPerChar() * pass.length)];
ByteBuffer bb = ByteBuffer.wrap(ba);
synchronized (enc) {
enc.reset().encode(CharBuffer.wrap(pass), bb, true);
}
if (bb.remaining() > 0) {
bb.put((byte)'\n'); // will be recognized as a stop sign
}
return ba;
}
return ba;
}
private static volatile CharsetEncoder enc;
}
4 changes: 4 additions & 0 deletions src/java.base/share/classes/sun/security/util/Resources.java
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ public class Resources extends java.util.ListResourceBundle {
// sun.security.pkcs11.SunPKCS11
{"PKCS11.Token.providerName.Password.",
"PKCS11 Token [{0}] Password: "},

// sun.security.util.Password
{"warning.input.may.be.visible.on.screen",
"[WARNING: Input may be visible on screen]\u0020"},
};


Expand Down
19 changes: 15 additions & 4 deletions src/java.base/unix/native/libjava/Console_md.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -32,10 +32,21 @@
#include <unistd.h>
#include <termios.h>

JNIEXPORT jboolean JNICALL
Java_java_io_Console_istty(JNIEnv *env, jclass cls)
JNIEXPORT jint JNICALL
Java_java_io_Console_ttyStatus(JNIEnv *env, jclass cls)
{
return isatty(fileno(stdin)) && isatty(fileno(stdout));
jint ret = 0;

if (isatty(fileno(stdin))) {
ret |= java_io_Console_TTY_STDIN_MASK;
}
if (isatty(fileno(stdout))) {
ret |= java_io_Console_TTY_STDOUT_MASK;
}
if (isatty(fileno(stderr))) {
ret |= java_io_Console_TTY_STDERR_MASK;
}
return ret;
}

JNIEXPORT jstring JNICALL
Expand Down
Loading