From 86d81296451a3e58b328898d1bf554c10f265a22 Mon Sep 17 00:00:00 2001 From: Thomas Fitzsimmons Date: Wed, 24 Jun 2026 18:19:42 -0400 Subject: [PATCH 1/7] 8366261: Provide utility methods for sun.security.util.Password Backport-of: 91417890a24450d7cb23d188df4f631304d41ed6 --- .../share/classes/java/io/Console.java | 76 ++++++++++++++++++- .../jdk/internal/misc/JavaIOAccess.java | 5 +- .../unix/native/libjava/Console_md.c | 19 ++++- .../windows/native/libjava/Console_md.c | 38 ++++++---- 4 files changed, 115 insertions(+), 23 deletions(-) diff --git a/src/java.base/share/classes/java/io/Console.java b/src/java.base/share/classes/java/io/Console.java index d427f7208b2..da7f0ff4b34 100644 --- a/src/java.base/share/classes/java/io/Console.java +++ b/src/java.base/share/classes/java/io/Console.java @@ -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; @@ -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> INSTANCE = new AtomicReference<>(); + private static Optional passwordConsole() { + Optional 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) { @@ -347,7 +394,9 @@ public char[] readPassword(String fmt, Object ... args) { throw ioe; } } - pw.println(); + if (!noNewLine) { + pw.println(); + } } } return passwd; @@ -411,6 +460,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. @@ -575,7 +630,7 @@ 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; @@ -588,10 +643,15 @@ public Charset charset() { // cons already exists when this method is called return cons.cs; } + public Optional 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(); @@ -615,4 +675,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(); } diff --git a/src/java.base/share/classes/jdk/internal/misc/JavaIOAccess.java b/src/java.base/share/classes/jdk/internal/misc/JavaIOAccess.java index b4e8926f1f7..9e8480d79e5 100644 --- a/src/java.base/share/classes/jdk/internal/misc/JavaIOAccess.java +++ b/src/java.base/share/classes/jdk/internal/misc/JavaIOAccess.java @@ -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 @@ -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 Optional passwordConsole(); + public char[] readPasswordNoNewLine(Console c); } diff --git a/src/java.base/unix/native/libjava/Console_md.c b/src/java.base/unix/native/libjava/Console_md.c index 829ceada934..e8b0a078930 100644 --- a/src/java.base/unix/native/libjava/Console_md.c +++ b/src/java.base/unix/native/libjava/Console_md.c @@ -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 @@ -32,10 +32,21 @@ #include #include -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 diff --git a/src/java.base/windows/native/libjava/Console_md.c b/src/java.base/windows/native/libjava/Console_md.c index 173b2ffeb4a..4825afd3147 100644 --- a/src/java.base/windows/native/libjava/Console_md.c +++ b/src/java.base/windows/native/libjava/Console_md.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2013, 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 @@ -31,23 +31,30 @@ #include #include -static HANDLE hStdOut = INVALID_HANDLE_VALUE; -static HANDLE hStdIn = INVALID_HANDLE_VALUE; -JNIEXPORT jboolean JNICALL -Java_java_io_Console_istty(JNIEnv *env, jclass cls) +JNIEXPORT jint JNICALL +Java_java_io_Console_ttyStatus(JNIEnv *env, jclass cls) { - if (hStdIn == INVALID_HANDLE_VALUE && - (hStdIn = GetStdHandle(STD_INPUT_HANDLE)) == INVALID_HANDLE_VALUE) { - return JNI_FALSE; + jint ret = 0; + HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); + HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE); + HANDLE hStdErr = GetStdHandle(STD_ERROR_HANDLE); + + if (hStdIn != INVALID_HANDLE_VALUE && + GetFileType(hStdIn) == FILE_TYPE_CHAR) { + ret |= java_io_Console_TTY_STDIN_MASK; + } + + if (hStdOut != INVALID_HANDLE_VALUE && + GetFileType(hStdOut) == FILE_TYPE_CHAR) { + ret |= java_io_Console_TTY_STDOUT_MASK; } - if (hStdOut == INVALID_HANDLE_VALUE && - (hStdOut = GetStdHandle(STD_OUTPUT_HANDLE)) == INVALID_HANDLE_VALUE) { - return JNI_FALSE; + + if (hStdErr != INVALID_HANDLE_VALUE && + GetFileType(hStdErr) == FILE_TYPE_CHAR) { + ret |= java_io_Console_TTY_STDERR_MASK; } - if (GetFileType(hStdIn) != FILE_TYPE_CHAR || - GetFileType(hStdOut) != FILE_TYPE_CHAR) - return JNI_FALSE; - return JNI_TRUE; + + return ret; } JNIEXPORT jstring JNICALL @@ -67,6 +74,7 @@ Java_java_io_Console_echo(JNIEnv *env, jclass cls, jboolean on) { DWORD fdwMode; jboolean old; + HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE); if (! GetConsoleMode(hStdIn, &fdwMode)) { JNU_ThrowIOExceptionWithLastError(env, "GetConsoleMode failed"); return !on; From 9e7ea5f17557c913310a14d58a7d90b5f2341147 Mon Sep 17 00:00:00 2001 From: Thomas Fitzsimmons Date: Wed, 24 Jun 2026 19:18:42 -0400 Subject: [PATCH 2/7] 8354469: Keytool exposes the password in plain text when command is piped using | grep Backport-of: a7a3a660e33fabc025ebe887f5605741be9ca8c3 --- .../share/classes/java/io/Console.java | 4 +- .../classes/sun/security/util/Password.java | 133 ++++++++++------ .../classes/sun/security/util/Resources.java | 4 + .../security/tools/keytool/EchoPassword.java | 149 ++++++++++++++++++ .../security/tools/keytool/SetInPassword.java | 44 ++++++ 5 files changed, 288 insertions(+), 46 deletions(-) create mode 100644 test/jdk/sun/security/tools/keytool/EchoPassword.java create mode 100644 test/jdk/sun/security/tools/keytool/SetInPassword.java diff --git a/src/java.base/share/classes/java/io/Console.java b/src/java.base/share/classes/java/io/Console.java index da7f0ff4b34..f9d58035ad4 100644 --- a/src/java.base/share/classes/java/io/Console.java +++ b/src/java.base/share/classes/java/io/Console.java @@ -382,7 +382,9 @@ private char[] readPassword0(boolean noNewLine, 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; diff --git a/src/java.base/share/classes/sun/security/util/Password.java b/src/java.base/share/classes/sun/security/util/Password.java index 5aca69b713b..2b2666c6fdc 100644 --- a/src/java.base/share/classes/sun/security/util/Password.java +++ b/src/java.base/share/classes/sun/security/util/Password.java @@ -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 @@ -33,7 +33,6 @@ /** * A utility class for reading passwords - * */ public class Password { /** Reads user password from given input stream. */ @@ -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; @@ -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; @@ -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) { @@ -127,35 +131,74 @@ 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 = c1.charset(); + } else { + c2 = SharedSecrets.getJavaIOAccess().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; } - 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; } diff --git a/src/java.base/share/classes/sun/security/util/Resources.java b/src/java.base/share/classes/sun/security/util/Resources.java index d1b0cae55e5..5cb695692e5 100644 --- a/src/java.base/share/classes/sun/security/util/Resources.java +++ b/src/java.base/share/classes/sun/security/util/Resources.java @@ -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"}, }; diff --git a/test/jdk/sun/security/tools/keytool/EchoPassword.java b/test/jdk/sun/security/tools/keytool/EchoPassword.java new file mode 100644 index 00000000000..7622ea9e8ea --- /dev/null +++ b/test/jdk/sun/security/tools/keytool/EchoPassword.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 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 + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8354469 + * @summary keytool password does not echo in multiple cases + * @library /java/awt/regtesthelpers + * @modules java.base/jdk.internal.util + * @build PassFailJFrame + * @compile -encoding UTF-8 EchoPassword.java + * @run main/manual/othervm EchoPassword + */ + +import javax.swing.JEditorPane; +import javax.swing.JLabel; +import javax.swing.event.HyperlinkEvent; + +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.io.File; +import java.nio.file.Path; + +public class EchoPassword { + + static JLabel label; + + public static void main(String[] args) throws Exception { + + var ks1 = "\"" + Path.of("8354469.ks1").toAbsolutePath() + "\""; + var ks2 = "\"" + Path.of("8354469.ks2").toAbsolutePath() + "\""; + var ks3 = "\"" + Path.of("8354469.ks3").toAbsolutePath() + "\""; + + final String keytool = "\"" + System.getProperty("java.home") + + File.separator + "bin" + File.separator + "keytool\""; + final String nonASCII = "äöäöäöäö"; + + final String[][] commands = { + // Input password from real Console + {"First command", keytool + " -keystore " + ks1 + + " -genkeypair -keyalg ec -dname cn=a -alias first"}, + // Input password from limited Console (when stdout is redirected) + {"Second command", keytool + " -keystore " + ks2 + + " -genkeypair -keyalg ec -dname cn=b -alias second | sort"}, + // Input password from System.in stream + {"Third command", "echo changeit| " + keytool + " -keystore " + ks1 + + " -genkeypair -keyalg ec -dname cn=c -alias third"}, + // Ensure limited Console does not write a newline to System.out + {"Fourth command", keytool + " -keystore " + ks1 + + " -exportcert -alias first | " + + keytool + " -printcert -rfc"}, + {"The password", nonASCII} + }; + + final String message = String.format(""" + Open a terminal or Windows Command Prompt window, perform + the following steps, and record the final result. Each time you + click a link to copy something, make sure the status line at the + bottom shows the link has been successfully clicked. +

Part I: Password Echoing Tests

+
    +
  1. Click Copy First Command to copy the + following command into the system clipboard. Paste it into the + terminal window and execute the command. +

    + %s +

    + When prompted, enter "changeit" and press Enter. When prompted + again, enter "changeit" again and press Enter. Verify that the + two password prompts show up on different lines, both + passwords are hidden, and a key pair is generated successfully. + +

  2. Click Copy Second Command to copy the + following command into the system clipboard. Paste it into the + terminal window and execute the command. +

    + %s +

    + When prompted, enter "changeit" and press Enter. When prompted + again, enter "changeit" again and press Enter. Verify that the + two password prompts show up on different lines, both + passwords are hidden, and a key pair is generated successfully. + +

  3. Click Copy Third Command to copy the + following command into the system clipboard. Paste it into the + terminal window and execute the command. +

    + %s +

    + You will see a prompt but you don't need to enter anything. + Verify that the password "changeit" is not shown in the command + output and a key pair is generated successfully. + +

  4. Click Copy Fourth Command to copy the + following command into the system clipboard. Paste it into the + terminal window and execute the command. +

    + %s +

    + When prompted, enter "changeit" and press Enter. Verify that the + password is hidden and a PEM certificate is correctly shown. +

+ Press "pass" if the behavior matches expectations; + otherwise, press "fail". + """, commands[0][1], commands[1][1], commands[2][1], commands[3][1], + commands[4][1]); + + PassFailJFrame.builder() + .instructions(message) + .rows(40).columns(100) + .hyperlinkListener(e -> { + if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + int pos = Integer.parseInt(e.getDescription().substring(1)); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents( + new StringSelection(commands[pos][1]), null); + label.setText(commands[pos][0] + " copied"); + if (e.getSource() instanceof JEditorPane ep) { + ep.getCaret().setVisible(false); + } + } + }) + .splitUIBottom(() -> { + label = new JLabel("Status"); + return label; + }) + .build() + .awaitAndCheck(); + } +} diff --git a/test/jdk/sun/security/tools/keytool/SetInPassword.java b/test/jdk/sun/security/tools/keytool/SetInPassword.java new file mode 100644 index 00000000000..ba6f06f898f --- /dev/null +++ b/test/jdk/sun/security/tools/keytool/SetInPassword.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 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 + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8354469 + * @summary ensure password can be read from user's System.in + * @library /test/lib + * @modules java.base/sun.security.tools.keytool + */ + +import jdk.test.lib.SecurityTools; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +public class SetInPassword { + public static void main(String[] args) throws Exception { + SecurityTools.keytool("-keystore ks -storepass changeit -genkeypair -alias a -dname CN=A -keyalg EC") + .shouldHaveExitValue(0); + System.setIn(new ByteArrayInputStream("changeit".getBytes(StandardCharsets.UTF_8))); + sun.security.tools.keytool.Main.main("-keystore ks -alias a -certreq".split(" ")); + } +} From f5dc333af8036c26b5b71abc3a35b2cd879d0325 Mon Sep 17 00:00:00 2001 From: Thomas Fitzsimmons Date: Thu, 25 Jun 2026 21:06:17 -0400 Subject: [PATCH 3/7] EchoPassword.java: Multiline string support is not available in 11 --- .../security/tools/keytool/EchoPassword.java | 102 +++++++++--------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/test/jdk/sun/security/tools/keytool/EchoPassword.java b/test/jdk/sun/security/tools/keytool/EchoPassword.java index 7622ea9e8ea..3cdd345ffe6 100644 --- a/test/jdk/sun/security/tools/keytool/EchoPassword.java +++ b/test/jdk/sun/security/tools/keytool/EchoPassword.java @@ -72,57 +72,57 @@ public static void main(String[] args) throws Exception { {"The password", nonASCII} }; - final String message = String.format(""" - Open a terminal or Windows Command Prompt window, perform - the following steps, and record the final result. Each time you - click a link to copy something, make sure the status line at the - bottom shows the link has been successfully clicked. -

Part I: Password Echoing Tests

-
    -
  1. Click Copy First Command to copy the - following command into the system clipboard. Paste it into the - terminal window and execute the command. -

    - %s -

    - When prompted, enter "changeit" and press Enter. When prompted - again, enter "changeit" again and press Enter. Verify that the - two password prompts show up on different lines, both - passwords are hidden, and a key pair is generated successfully. - -

  2. Click Copy Second Command to copy the - following command into the system clipboard. Paste it into the - terminal window and execute the command. -

    - %s -

    - When prompted, enter "changeit" and press Enter. When prompted - again, enter "changeit" again and press Enter. Verify that the - two password prompts show up on different lines, both - passwords are hidden, and a key pair is generated successfully. - -

  3. Click Copy Third Command to copy the - following command into the system clipboard. Paste it into the - terminal window and execute the command. -

    - %s -

    - You will see a prompt but you don't need to enter anything. - Verify that the password "changeit" is not shown in the command - output and a key pair is generated successfully. - -

  4. Click Copy Fourth Command to copy the - following command into the system clipboard. Paste it into the - terminal window and execute the command. -

    - %s -

    - When prompted, enter "changeit" and press Enter. Verify that the - password is hidden and a PEM certificate is correctly shown. -

- Press "pass" if the behavior matches expectations; - otherwise, press "fail". - """, commands[0][1], commands[1][1], commands[2][1], commands[3][1], + final String message = String.format( + "Open a terminal or Windows Command Prompt window, perform" + + "the following steps, and record the final result. Each time you" + + "click a link to copy something, make sure the status line at the" + + "bottom shows the link has been successfully clicked." + + "

Part I: Password Echoing Tests

" + + "
    " + + "
  1. Click Copy First Command to copy the" + + "following command into the system clipboard. Paste it into the" + + "terminal window and execute the command." + + "

    " + + "%s" + + "

    " + + "When prompted, enter \"changeit\" and press Enter. When prompted" + + "again, enter \"changeit\" again and press Enter. Verify that the" + + "two password prompts show up on different lines, both" + + "passwords are hidden, and a key pair is generated successfully." + + "" + + "

  2. Click Copy Second Command to copy the" + + "following command into the system clipboard. Paste it into the" + + "terminal window and execute the command." + + "

    " + + "%s" + + "

    " + + "When prompted, enter \"changeit\" and press Enter. When prompted" + + "again, enter \"changeit\" again and press Enter. Verify that the" + + "two password prompts show up on different lines, both" + + "passwords are hidden, and a key pair is generated successfully." + + "" + + "

  3. Click Copy Third Command to copy the" + + "following command into the system clipboard. Paste it into the" + + "terminal window and execute the command." + + "

    " + + "%s" + + "

    " + + "You will see a prompt but you don't need to enter anything." + + "Verify that the password \"changeit\" is not shown in the command" + + "output and a key pair is generated successfully." + + "" + + "

  4. Click Copy Fourth Command to copy the" + + "following command into the system clipboard. Paste it into the" + + "terminal window and execute the command." + + "

    " + + "%s" + + "

    " + + "When prompted, enter \"changeit\" and press Enter. Verify that the" + + "password is hidden and a PEM certificate is correctly shown." + + "

" + + "Press \"pass\" if the behavior matches expectations;" + + "otherwise, press \"fail\".", + commands[0][1], commands[1][1], commands[2][1], commands[3][1], commands[4][1]); PassFailJFrame.builder() From 73d7f52bf65fc258bfd2de79a6ced19f81300c76 Mon Sep 17 00:00:00 2001 From: Thomas Fitzsimmons Date: Thu, 25 Jun 2026 21:07:02 -0400 Subject: [PATCH 4/7] EchoPassword.java: hyperlinkListener support is not available in 11 --- test/jdk/sun/security/tools/keytool/EchoPassword.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/jdk/sun/security/tools/keytool/EchoPassword.java b/test/jdk/sun/security/tools/keytool/EchoPassword.java index 3cdd345ffe6..83f9b4c7ac2 100644 --- a/test/jdk/sun/security/tools/keytool/EchoPassword.java +++ b/test/jdk/sun/security/tools/keytool/EchoPassword.java @@ -128,17 +128,6 @@ public static void main(String[] args) throws Exception { PassFailJFrame.builder() .instructions(message) .rows(40).columns(100) - .hyperlinkListener(e -> { - if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { - int pos = Integer.parseInt(e.getDescription().substring(1)); - Toolkit.getDefaultToolkit().getSystemClipboard().setContents( - new StringSelection(commands[pos][1]), null); - label.setText(commands[pos][0] + " copied"); - if (e.getSource() instanceof JEditorPane ep) { - ep.getCaret().setVisible(false); - } - } - }) .splitUIBottom(() -> { label = new JLabel("Status"); return label; From c0b412586110004da243a4f5a03b0c824908e941 Mon Sep 17 00:00:00 2001 From: Thomas Fitzsimmons Date: Thu, 25 Jun 2026 21:07:58 -0400 Subject: [PATCH 5/7] Change JavaIOAccess.charset signature This gives sun.security.util.Password access to the password console's charset field. --- src/java.base/share/classes/java/io/Console.java | 4 ++-- .../share/classes/jdk/internal/misc/JavaIOAccess.java | 2 +- src/java.base/share/classes/sun/security/util/Password.java | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/java.base/share/classes/java/io/Console.java b/src/java.base/share/classes/java/io/Console.java index f9d58035ad4..bcebf5886f6 100644 --- a/src/java.base/share/classes/java/io/Console.java +++ b/src/java.base/share/classes/java/io/Console.java @@ -640,10 +640,10 @@ public Console console() { 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 passwordConsole() { return Console.passwordConsole(); diff --git a/src/java.base/share/classes/jdk/internal/misc/JavaIOAccess.java b/src/java.base/share/classes/jdk/internal/misc/JavaIOAccess.java index 9e8480d79e5..6341641dc90 100644 --- a/src/java.base/share/classes/jdk/internal/misc/JavaIOAccess.java +++ b/src/java.base/share/classes/jdk/internal/misc/JavaIOAccess.java @@ -31,7 +31,7 @@ public interface JavaIOAccess { public Console console(); - public Charset charset(); + public Charset charset(Console c); public Optional passwordConsole(); public char[] readPasswordNoNewLine(Console c); } diff --git a/src/java.base/share/classes/sun/security/util/Password.java b/src/java.base/share/classes/sun/security/util/Password.java index 2b2666c6fdc..21d233b113b 100644 --- a/src/java.base/share/classes/sun/security/util/Password.java +++ b/src/java.base/share/classes/sun/security/util/Password.java @@ -152,11 +152,12 @@ private static class ConsoleHolder { Charset charset; if (c1 != null) { c2 = null; - charset = c1.charset(); + charset = SharedSecrets.getJavaIOAccess().charset(c1); } else { c2 = SharedSecrets.getJavaIOAccess().passwordConsole() .orElse(null); - charset = (c2 != null) ? c2.charset() : null; + charset = (c2 != null) + ? SharedSecrets.getJavaIOAccess().charset(c2) : null; } enc = charset == null ? null : charset.newEncoder() .onMalformedInput(CodingErrorAction.REPLACE) From b1b3b9345b8e545bc7a4ba4bba4b927067e670b3 Mon Sep 17 00:00:00 2001 From: Thomas Fitzsimmons Date: Fri, 26 Jun 2026 11:51:05 -0400 Subject: [PATCH 6/7] EchoPassword.java: Change test instructions message for 11 Remove the "Copy" hyperlinks and instead just instruct the user to copy each command manually. Add spaces where necessary. --- .../security/tools/keytool/EchoPassword.java | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/test/jdk/sun/security/tools/keytool/EchoPassword.java b/test/jdk/sun/security/tools/keytool/EchoPassword.java index 83f9b4c7ac2..d00e3242b46 100644 --- a/test/jdk/sun/security/tools/keytool/EchoPassword.java +++ b/test/jdk/sun/security/tools/keytool/EchoPassword.java @@ -74,54 +74,52 @@ public static void main(String[] args) throws Exception { final String message = String.format( "Open a terminal or Windows Command Prompt window, perform" + - "the following steps, and record the final result. Each time you" + - "click a link to copy something, make sure the status line at the" + - "bottom shows the link has been successfully clicked." + + " the following steps, and record the final result." + "

Part I: Password Echoing Tests

" + "
    " + - "
  1. Click Copy First Command to copy the" + - "following command into the system clipboard. Paste it into the" + - "terminal window and execute the command." + + "
  2. Copy the" + + " following command into the system clipboard. Paste it into the" + + " terminal window and execute the command." + "

    " + "%s" + "

    " + "When prompted, enter \"changeit\" and press Enter. When prompted" + - "again, enter \"changeit\" again and press Enter. Verify that the" + - "two password prompts show up on different lines, both" + - "passwords are hidden, and a key pair is generated successfully." + + " again, enter \"changeit\" again and press Enter. Verify that the" + + " two password prompts show up on different lines, both" + + " passwords are hidden, and a key pair is generated successfully." + "" + - "

  3. Click Copy Second Command to copy the" + - "following command into the system clipboard. Paste it into the" + - "terminal window and execute the command." + + "
  4. Copy the" + + " following command into the system clipboard. Paste it into the" + + " terminal window and execute the command." + "

    " + "%s" + "

    " + "When prompted, enter \"changeit\" and press Enter. When prompted" + - "again, enter \"changeit\" again and press Enter. Verify that the" + - "two password prompts show up on different lines, both" + - "passwords are hidden, and a key pair is generated successfully." + + " again, enter \"changeit\" again and press Enter. Verify that the" + + " two password prompts show up on different lines, both" + + " passwords are hidden, and a key pair is generated successfully." + "" + - "

  5. Click Copy Third Command to copy the" + - "following command into the system clipboard. Paste it into the" + - "terminal window and execute the command." + + "
  6. Copy the" + + " following command into the system clipboard. Paste it into the" + + " terminal window and execute the command." + "

    " + "%s" + "

    " + "You will see a prompt but you don't need to enter anything." + - "Verify that the password \"changeit\" is not shown in the command" + - "output and a key pair is generated successfully." + + " Verify that the password \"changeit\" is not shown in the command" + + " output and a key pair is generated successfully." + "" + - "

  7. Click Copy Fourth Command to copy the" + - "following command into the system clipboard. Paste it into the" + - "terminal window and execute the command." + + "
  8. Copy the" + + " following command into the system clipboard. Paste it into the" + + " terminal window and execute the command." + "

    " + "%s" + "

    " + "When prompted, enter \"changeit\" and press Enter. Verify that the" + - "password is hidden and a PEM certificate is correctly shown." + + " password is hidden and a PEM certificate is correctly shown." + "

" + "Press \"pass\" if the behavior matches expectations;" + - "otherwise, press \"fail\".", + " otherwise, press \"fail\".", commands[0][1], commands[1][1], commands[2][1], commands[3][1], commands[4][1]); From 5146aa51548b951b92eb3c1f6a8770bdccded5c2 Mon Sep 17 00:00:00 2001 From: Thomas Fitzsimmons Date: Fri, 26 Jun 2026 11:55:29 -0400 Subject: [PATCH 7/7] EchoPassword.java: Remove unused status label --- test/jdk/sun/security/tools/keytool/EchoPassword.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/jdk/sun/security/tools/keytool/EchoPassword.java b/test/jdk/sun/security/tools/keytool/EchoPassword.java index d00e3242b46..a9dbd9014cf 100644 --- a/test/jdk/sun/security/tools/keytool/EchoPassword.java +++ b/test/jdk/sun/security/tools/keytool/EchoPassword.java @@ -33,7 +33,6 @@ */ import javax.swing.JEditorPane; -import javax.swing.JLabel; import javax.swing.event.HyperlinkEvent; import java.awt.Toolkit; @@ -43,8 +42,6 @@ public class EchoPassword { - static JLabel label; - public static void main(String[] args) throws Exception { var ks1 = "\"" + Path.of("8354469.ks1").toAbsolutePath() + "\""; @@ -126,10 +123,6 @@ public static void main(String[] args) throws Exception { PassFailJFrame.builder() .instructions(message) .rows(40).columns(100) - .splitUIBottom(() -> { - label = new JLabel("Status"); - return label; - }) .build() .awaitAndCheck(); }