Skip to content
Open
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
28 changes: 23 additions & 5 deletions src/java.base/share/classes/java/io/Console.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

package java.io;

import java.lang.annotation.Native;
import java.util.*;
import java.nio.charset.Charset;
import jdk.internal.access.JavaIOAccess;
Expand Down Expand Up @@ -545,15 +546,20 @@ public Charset charset() {
* @since 22
*/
public boolean isTerminal() {
return istty;
return true;
}

private static UnsupportedOperationException newUnsupportedOperationException() {
return new UnsupportedOperationException(
"Console class itself does not provide implementation");
}

private static final boolean istty = istty();
@Native static final int TTY_STDIN_MASK = 0x00000001;
@Native static final int TTY_STDOUT_MASK = 0x00000002;
@Native 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();
private static final Charset STDIN_CHARSET =
Charset.forName(System.getProperty("stdin.encoding"), UTF_8.INSTANCE);
private static final Charset STDOUT_CHARSET =
Expand All @@ -565,6 +571,9 @@ private static UnsupportedOperationException newUnsupportedOperationException()
public Console console() {
return cons;
}
public boolean isStdinTty() {
return Console.isStdinTty();
}
});
}

Expand All @@ -586,7 +595,7 @@ private static Console instantiateConsole() {

for (var jcp : ServiceLoader.load(ModuleLayer.boot(), JdkConsoleProvider.class)) {
if (consModName.equals(jcp.getClass().getModule().getName())) {
var jc = jcp.console(istty, STDIN_CHARSET, STDOUT_CHARSET);
var jc = jcp.console(isStdinTty() && isStdoutTty(), STDIN_CHARSET, STDOUT_CHARSET);
if (jc != null) {
c = new ProxyingConsole(jc);
}
Expand All @@ -597,12 +606,21 @@ private static Console instantiateConsole() {
}

// If not found, default to built-in Console
if (istty && c == null) {
if (isStdinTty() && isStdoutTty() && c == null) {
c = new ProxyingConsole(new JdkConsoleImpl(STDIN_CHARSET, STDOUT_CHARSET));
}

return c;
}

private static native boolean istty();
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, 2022, 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 @@ -29,4 +29,5 @@

public interface JavaIOAccess {
Console console();
boolean isStdinTty();
}
47 changes: 45 additions & 2 deletions src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@
import java.util.Formatter;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;

import jdk.internal.access.SharedSecrets;
import jdk.internal.util.StaticProperty;
import sun.nio.cs.StreamDecoder;
import sun.nio.cs.StreamEncoder;
import sun.nio.cs.UTF_8;

/**
* JdkConsole implementation based on the platform's TTY.
Expand Down Expand Up @@ -103,6 +106,42 @@ public String readLine() {

@Override
public char[] readPassword(Locale locale, String format, Object ... args) {
return readPassword0(false, locale, format, args);
}

// These two methods are intended for sun.security.util.Password, so tools like keytool can
// use JdkConsoleImpl 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 JdkConsoleImpl. This ensures only one Console
// instance exists in the Java runtime.
private static final StableValue<Optional<JdkConsoleImpl>> INSTANCE = StableValue.of();
public static Optional<JdkConsoleImpl> passwordConsole() {
return INSTANCE.orElseSet(() -> {
// 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 JdkConsoleImpl
// instance, otherwise an empty Optional.
return SharedSecrets.getJavaIOAccess().isStdinTty() ?
Optional.of(
new JdkConsoleImpl(
Charset.forName(System.getProperty("stdin.encoding"), UTF_8.INSTANCE),
Charset.forName(System.getProperty("stdout.encoding"), UTF_8.INSTANCE))) :
Optional.empty();
});
}

// Dedicated entry for sun.security.util.Password when stdout is redirected.
// This method strictly avoids producing any output by using noNewLine = true
// and an empty format string.
public char[] readPasswordNoNewLine() {
return readPassword0(true, Locale.getDefault(Locale.Category.FORMAT), "");
}

private char[] readPassword0(boolean noNewLine, Locale locale, String format, Object ... args) {
char[] passwd = null;
synchronized (writeLock) {
synchronized(readLock) {
Expand Down Expand Up @@ -135,7 +174,9 @@ public char[] readPassword(Locale locale, String format, Object ... args) {
ioe.addSuppressed(x);
}
if (ioe != null) {
Arrays.fill(passwd, ' ');
if (passwd != null) {
Arrays.fill(passwd, ' ');
}
try {
if (reader instanceof LineReader lr) {
lr.zeroOut();
Expand All @@ -146,7 +187,9 @@ public char[] readPassword(Locale locale, String format, Object ... args) {
throw ioe;
}
}
pw.println();
if (!noNewLine) {
pw.println();
}
}
}
return passwd;
Expand Down
134 changes: 88 additions & 46 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, 2022, 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 @@ -29,11 +29,12 @@
import java.nio.*;
import java.nio.charset.*;
import java.util.Arrays;

import jdk.internal.access.SharedSecrets;
import jdk.internal.io.JdkConsoleImpl;

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

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

try {
// Only use Console if `in` is the initial System.in
Console con;
if (!isEchoOn &&
in == SharedSecrets.getJavaLangAccess().initialSystemIn() &&
((con = System.console()) != null)) {
consoleEntered = con.readPassword();
// readPassword returns "" if you just press ENTER with the built-in Console,
// to be compatible with old Password class, change to null
if (consoleEntered == null || consoleEntered.length == 0) {
return null;
if (!isEchoOn) {
if (in == SharedSecrets.getJavaLangAccess().initialSystemIn()
&& 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;

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

int room = buf.length;
int offset = 0;
Expand Down Expand Up @@ -101,11 +108,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 @@ -118,8 +125,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 @@ -128,35 +133,72 @@ 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 = System.console()
.charset()
.newEncoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
// Everything on Console or JdkConsoleImpl 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 JdkConsoleImpl c2;
// encoder for c1 or c2
private static final CharsetEncoder enc;

static {
c1 = System.console();
Charset charset;
if (c1 != null) {
c2 = null;
charset = c1.charset();
} else {
c2 = JdkConsoleImpl.passwordConsole().orElse(null);
charset = (c2 != null) ? c2.charset() : 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;
}

public static char[] readPassword() {
assert consoleIsAvailable();
if (c1 != null) {
return c1.readPassword();
} else {
try {
return c2.readPasswordNoNewLine();
} finally {
System.err.println();
}
}
}
if (bb.position() < ba.length) {
ba[bb.position()] = '\n';

/**
* 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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,6 @@ line.number.expected.expect.found.actual.=line {0}: expected [{1}], found [{2}]

# sun.security.pkcs11.SunPKCS11
PKCS11.Token.providerName.Password.=PKCS11 Token [{0}] Password:\u0020

# sun.security.util.Password
warning.input.may.be.visible.on.screen=[WARNING: Input may be visible on screen]\u0020
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, 2024, 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 @@ -31,8 +31,19 @@
#include <stdlib.h>
#include <unistd.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;
}
Loading