Skip to content

Commit a7a3a66

Browse files
committed
8354469: Keytool exposes the password in plain text when command is piped using | grep
Reviewed-by: mullan, smarks, naoto, hchao
1 parent c9cbd31 commit a7a3a66

File tree

6 files changed

+323
-48
lines changed

6 files changed

+323
-48
lines changed

src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,9 @@ private char[] readPassword0(boolean noNewLine, Locale locale, String format, Ob
174174
ioe.addSuppressed(x);
175175
}
176176
if (ioe != null) {
177-
Arrays.fill(passwd, ' ');
177+
if (passwd != null) {
178+
Arrays.fill(passwd, ' ');
179+
}
178180
try {
179181
if (reader instanceof LineReader lr) {
180182
lr.zeroOut();

src/java.base/share/classes/sun/security/util/Password.java

Lines changed: 88 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -29,11 +29,12 @@
2929
import java.nio.*;
3030
import java.nio.charset.*;
3131
import java.util.Arrays;
32+
3233
import jdk.internal.access.SharedSecrets;
34+
import jdk.internal.io.JdkConsoleImpl;
3335

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

5152
char[] consoleEntered = null;
5253
byte[] consoleBytes = null;
54+
char[] buf = null;
5355

5456
try {
5557
// Only use Console if `in` is the initial System.in
56-
Console con;
57-
if (!isEchoOn &&
58-
in == SharedSecrets.getJavaLangAccess().initialSystemIn() &&
59-
((con = System.console()) != null)) {
60-
consoleEntered = con.readPassword();
61-
// readPassword returns "" if you just press ENTER with the built-in Console,
62-
// to be compatible with old Password class, change to null
63-
if (consoleEntered == null || consoleEntered.length == 0) {
64-
return null;
58+
if (!isEchoOn) {
59+
if (in == SharedSecrets.getJavaLangAccess().initialSystemIn()
60+
&& ConsoleHolder.consoleIsAvailable()) {
61+
consoleEntered = ConsoleHolder.readPassword();
62+
// readPassword might return null. Stop now.
63+
if (consoleEntered == null) {
64+
return null;
65+
}
66+
consoleBytes = ConsoleHolder.convertToBytes(consoleEntered);
67+
in = new ByteArrayInputStream(consoleBytes);
68+
} else if (System.in.available() == 0) {
69+
// This may be running in an IDE Run Window or in JShell,
70+
// which acts like an interactive console and echoes the
71+
// entered password. In this case, print a warning that
72+
// the password might be echoed. If available() is not zero,
73+
// it's more likely the input comes from a pipe, such as
74+
// "echo password |" or "cat password_file |" where input
75+
// will be silently consumed without echoing to the screen.
76+
System.err.print(ResourcesMgr.getString
77+
("warning.input.may.be.visible.on.screen"));
6578
}
66-
consoleBytes = convertToBytes(consoleEntered);
67-
in = new ByteArrayInputStream(consoleBytes);
6879
}
6980

7081
// Rest of the lines still necessary for KeyStoreLoginModule
7182
// and when there is no console.
72-
73-
char[] lineBuffer;
74-
char[] buf;
75-
76-
buf = lineBuffer = new char[128];
83+
buf = new char[128];
7784

7885
int room = buf.length;
7986
int offset = 0;
@@ -101,11 +108,11 @@ public static char[] readPassword(InputStream in, boolean isEchoOn)
101108
/* fall through */
102109
default:
103110
if (--room < 0) {
111+
char[] oldBuf = buf;
104112
buf = new char[offset + 128];
105113
room = buf.length - offset - 1;
106-
System.arraycopy(lineBuffer, 0, buf, 0, offset);
107-
Arrays.fill(lineBuffer, ' ');
108-
lineBuffer = buf;
114+
System.arraycopy(oldBuf, 0, buf, 0, offset);
115+
Arrays.fill(oldBuf, ' ');
109116
}
110117
buf[offset++] = (char) c;
111118
break;
@@ -118,8 +125,6 @@ public static char[] readPassword(InputStream in, boolean isEchoOn)
118125

119126
char[] ret = new char[offset];
120127
System.arraycopy(buf, 0, ret, 0, offset);
121-
Arrays.fill(buf, ' ');
122-
123128
return ret;
124129
} finally {
125130
if (consoleEntered != null) {
@@ -128,35 +133,72 @@ public static char[] readPassword(InputStream in, boolean isEchoOn)
128133
if (consoleBytes != null) {
129134
Arrays.fill(consoleBytes, (byte)0);
130135
}
136+
if (buf != null) {
137+
Arrays.fill(buf, ' ');
138+
}
131139
}
132140
}
133141

134-
/**
135-
* Change a password read from Console.readPassword() into
136-
* its original bytes.
137-
*
138-
* @param pass a char[]
139-
* @return its byte[] format, similar to new String(pass).getBytes()
140-
*/
141-
private static byte[] convertToBytes(char[] pass) {
142-
if (enc == null) {
143-
synchronized (Password.class) {
144-
enc = System.console()
145-
.charset()
146-
.newEncoder()
147-
.onMalformedInput(CodingErrorAction.REPLACE)
148-
.onUnmappableCharacter(CodingErrorAction.REPLACE);
142+
// Everything on Console or JdkConsoleImpl is inside this class.
143+
private static class ConsoleHolder {
144+
145+
// primary console; may be null
146+
private static final Console c1;
147+
// secondary console (when stdout is redirected); may be null
148+
private static final JdkConsoleImpl c2;
149+
// encoder for c1 or c2
150+
private static final CharsetEncoder enc;
151+
152+
static {
153+
c1 = System.console();
154+
Charset charset;
155+
if (c1 != null) {
156+
c2 = null;
157+
charset = c1.charset();
158+
} else {
159+
c2 = JdkConsoleImpl.passwordConsole().orElse(null);
160+
charset = (c2 != null) ? c2.charset() : null;
149161
}
162+
enc = charset == null ? null : charset.newEncoder()
163+
.onMalformedInput(CodingErrorAction.REPLACE)
164+
.onUnmappableCharacter(CodingErrorAction.REPLACE);
150165
}
151-
byte[] ba = new byte[(int)(enc.maxBytesPerChar() * pass.length)];
152-
ByteBuffer bb = ByteBuffer.wrap(ba);
153-
synchronized (enc) {
154-
enc.reset().encode(CharBuffer.wrap(pass), bb, true);
166+
167+
public static boolean consoleIsAvailable() {
168+
return c1 != null || c2 != null;
169+
}
170+
171+
public static char[] readPassword() {
172+
assert consoleIsAvailable();
173+
if (c1 != null) {
174+
return c1.readPassword();
175+
} else {
176+
try {
177+
return c2.readPasswordNoNewLine();
178+
} finally {
179+
System.err.println();
180+
}
181+
}
155182
}
156-
if (bb.position() < ba.length) {
157-
ba[bb.position()] = '\n';
183+
184+
/**
185+
* Convert a password read from console into its original bytes.
186+
*
187+
* @param pass a char[]
188+
* @return its byte[] format, equivalent to new String(pass).getBytes()
189+
* but String is immutable and cannot be cleaned up.
190+
*/
191+
public static byte[] convertToBytes(char[] pass) {
192+
assert consoleIsAvailable();
193+
byte[] ba = new byte[(int) (enc.maxBytesPerChar() * pass.length)];
194+
ByteBuffer bb = ByteBuffer.wrap(ba);
195+
synchronized (enc) {
196+
enc.reset().encode(CharBuffer.wrap(pass), bb, true);
197+
}
198+
if (bb.remaining() > 0) {
199+
bb.put((byte)'\n'); // will be recognized as a stop sign
200+
}
201+
return ba;
158202
}
159-
return ba;
160203
}
161-
private static volatile CharsetEncoder enc;
162204
}

src/java.base/share/classes/sun/security/util/resources/security.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,6 @@ line.number.expected.expect.found.actual.=line {0}: expected [{1}], found [{2}]
7474

7575
# sun.security.pkcs11.SunPKCS11
7676
PKCS11.Token.providerName.Password.=PKCS11 Token [{0}] Password:\u0020
77+
78+
# sun.security.util.Password
79+
warning.input.may.be.visible.on.screen=[WARNING: Input may be visible on screen]\u0020
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/*
25+
* @test
26+
* @bug 8354469
27+
* @summary keytool password does not echo in multiple cases
28+
* @library /java/awt/regtesthelpers
29+
* @modules java.base/jdk.internal.util
30+
* @build PassFailJFrame
31+
* @run main/manual/othervm EchoPassword
32+
*/
33+
34+
import jdk.internal.util.OperatingSystem;
35+
36+
import javax.swing.JEditorPane;
37+
import javax.swing.JLabel;
38+
import javax.swing.event.HyperlinkEvent;
39+
40+
import java.awt.Toolkit;
41+
import java.awt.datatransfer.StringSelection;
42+
import java.io.File;
43+
import java.nio.file.Path;
44+
45+
public class EchoPassword {
46+
47+
static JLabel label;
48+
49+
public static void main(String[] args) throws Exception {
50+
51+
var ks1 = "\"" + Path.of("8354469.ks1").toAbsolutePath() + "\"";
52+
var ks2 = "\"" + Path.of("8354469.ks2").toAbsolutePath() + "\"";
53+
var ks3 = "\"" + Path.of("8354469.ks3").toAbsolutePath() + "\"";
54+
55+
final String keytool = "\"" + System.getProperty("java.home")
56+
+ File.separator + "bin" + File.separator + "keytool\"";
57+
final String nonASCII = "äöäöäöäö";
58+
59+
final String[][] commands = {
60+
// Input password from real Console
61+
{"First command", keytool + " -keystore " + ks1
62+
+ " -genkeypair -keyalg ec -dname cn=a -alias first"},
63+
// Input password from limited Console (when stdout is redirected)
64+
{"Second command", keytool + " -keystore " + ks2
65+
+ " -genkeypair -keyalg ec -dname cn=b -alias second | sort"},
66+
// Input password from System.in stream
67+
{"Third command", "echo changeit| " + keytool + " -keystore " + ks1
68+
+ " -genkeypair -keyalg ec -dname cn=c -alias third"},
69+
// Ensure limited Console does not write a newline to System.out
70+
{"Fourth command", keytool + " -keystore " + ks1
71+
+ " -exportcert -alias first | "
72+
+ keytool + " -printcert -rfc"},
73+
// Non-ASCII password from System.in
74+
{"Fifth command", "("
75+
// Solution 2 of https://stackoverflow.com/a/29747723
76+
+ (OperatingSystem.isWindows()
77+
? ("echo " + nonASCII + "^&echo " + nonASCII + "^&rem.")
78+
: ("echo " + nonASCII + "; echo " + nonASCII))
79+
+ ") | " + keytool + " -keystore " + ks3
80+
+ " -genkeypair -alias a -keyalg ec -dname cn=a"},
81+
// Non-ASCII password from Console
82+
{"Sixth command", keytool + " -keystore " + ks3
83+
+ " -exportcert -alias a -rfc"},
84+
{"The password", nonASCII}
85+
};
86+
87+
final String message = String.format("""
88+
<html>Open a terminal or Windows Command Prompt window, perform
89+
the following steps, and record the final result. Each time you
90+
click a link to copy something, make sure the status line at the
91+
bottom shows the link has been successfully clicked.
92+
<h3>Part I: Password Echoing Tests</h3>
93+
<ol>
94+
<li>Click <a href='c0'>Copy First Command</a> to copy the
95+
following command into the system clipboard. Paste it into the
96+
terminal window and execute the command.
97+
<p><code>
98+
%s
99+
</code><p>
100+
When prompted, enter "changeit" and press Enter. When prompted
101+
again, enter "changeit" again and press Enter. Verify that the
102+
two password prompts show up on different lines, both
103+
passwords are hidden, and a key pair is generated successfully.
104+
105+
<li>Click <a href='c1'>Copy Second Command</a> to copy the
106+
following command into the system clipboard. Paste it into the
107+
terminal window and execute the command.
108+
<p><code>
109+
%s
110+
</code><p>
111+
When prompted, enter "changeit" and press Enter. When prompted
112+
again, enter "changeit" again and press Enter. Verify that the
113+
two password prompts show up on different lines, both
114+
passwords are hidden, and a key pair is generated successfully.
115+
116+
<li>Click <a href='c2'>Copy Third Command</a> to copy the
117+
following command into the system clipboard. Paste it into the
118+
terminal window and execute the command.
119+
<p><code>
120+
%s
121+
</code><p>
122+
You will see a prompt but you don't need to enter anything.
123+
Verify that the password "changeit" is not shown in the command
124+
output and a key pair is generated successfully.
125+
126+
<li>Click <a href='c3'>Copy Fourth Command</a> to copy the
127+
following command into the system clipboard. Paste it into the
128+
terminal window and execute the command.
129+
<p><code>
130+
%s
131+
</code><p>
132+
When prompted, enter "changeit" and press Enter. Verify that the
133+
password is hidden and a PEM certificate is correctly shown.
134+
</ol>
135+
<h3>Part II: Interoperability on Non-ASCII Passwords</h3>
136+
<ol>
137+
<li>Click <a href='c4'>Copy Fifth Command</a> to copy the
138+
following command into the system clipboard. Paste it into the
139+
terminal window and execute the command.
140+
<p><code>
141+
%s
142+
</code><p>
143+
Verify that a key pair is generated successfully.
144+
145+
<li>Click <a href='c5'>Copy Sixth Command</a> to copy the
146+
following command into the system clipboard. Paste it into the
147+
terminal window and execute the command.
148+
<p><code>
149+
%s
150+
</code><p>
151+
When prompted, click <a href='c6'>Copy Password</a> to copy the
152+
password. Paste it into the terminal window and press Enter.
153+
Verify that the password is hidden and a PEM certificate is
154+
correctly shown.
155+
</ol>
156+
Press "pass" if the behavior matches expectations;
157+
otherwise, press "fail".
158+
""", commands[0][1], commands[1][1], commands[2][1], commands[3][1],
159+
commands[4][1], commands[5][1], commands[6][1]);
160+
161+
PassFailJFrame.builder()
162+
.instructions(message)
163+
.rows(40).columns(100)
164+
.hyperlinkListener(e -> {
165+
if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
166+
int pos = Integer.parseInt(e.getDescription().substring(1));
167+
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(
168+
new StringSelection(commands[pos][1]), null);
169+
label.setText(commands[pos][0] + " copied");
170+
if (e.getSource() instanceof JEditorPane ep) {
171+
ep.getCaret().setVisible(false);
172+
}
173+
}
174+
})
175+
.splitUIBottom(() -> {
176+
label = new JLabel("Status");
177+
return label;
178+
})
179+
.build()
180+
.awaitAndCheck();
181+
}
182+
}

0 commit comments

Comments
 (0)