Skip to content

Commit 6ab201e

Browse files
authored
Read passwords from stdin with --enable-stdin-passwords (closes #7) (#357)
1 parent f383b56 commit 6ab201e

29 files changed

Lines changed: 1092 additions & 133 deletions

AGENTS.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,23 @@ jsignpdf-root/
2424
├── installcert/ # Certificate installer utility
2525
├── distribution/ # Packaging: ZIP assembly, Windows installer, docs
2626
└── website/ # Hugo documentation site (not a Maven module)
27+
└── docs/JSignPdf.adoc # ★ authoritative user guide (see below)
2728
```
2829

30+
## Key Documentation Artifacts
31+
32+
Keep these in sync with the code; user-facing features are not "done" until they land here too.
33+
34+
| File | Role | When to update |
35+
|---|---|---|
36+
| `website/docs/JSignPdf.adoc` | **Authoritative user guide.** Single source of truth consumed by both the Hugo site (`website/content/docs/guide/index.adoc` is regenerated from it by `website/prepare.sh`) and the Maven PDF build in `distribution/`. | Any new or changed user-visible feature: new CLI flags, new GUI panels, changed defaults, new keystore types, new exit codes, etc. Update the synopsis block, the relevant option table, and add a dedicated subsection if the feature has non-obvious usage. |
37+
| `distribution/doc/release-notes/<version>.md` | Release notes bundled with the artifact and used as the GitHub Release body. | Every release-worthy change. |
38+
| `README.md` | Top-level project landing page. Concise feature overview + pointers to the guide. | Only for high-signal changes (new feature categories, platform support, install paths). |
39+
| `jsignpdf/src/main/resources/net/sf/jsignpdf/translations/messages.properties` | Canonical English resource bundle (all others are Weblate-synced). CLI `--help` text comes from here. | Any new CLI option or GUI string. Do not hand-edit non-English `messages_*.properties` files. |
40+
| `design-doc/<version>-<topic>.md` | Design notes for larger changes. | Before implementing a non-trivial feature. |
41+
42+
When a PR touches a user-visible feature without updating `JSignPdf.adoc`, flag it as incomplete.
43+
2944
## Source Code Layout
3045

3146
All source is under `jsignpdf/src/main/java/net/sf/jsignpdf/`:

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,53 @@ Requires a **Java 21 (or newer) JRE** unless you use an installer that bundles i
3939

4040
Maven-Central-published artifacts (for embedding the signing engine in your own project) live under `com.github.kwart.jsign`.
4141

42+
## Reading passwords from standard input
43+
44+
Passing a password on the command line exposes it to every local process that can read
45+
`/proc/<pid>/cmdline` (or to shell history). To avoid that, JSignPdf can read password values
46+
from standard input (or from an interactive console) instead.
47+
48+
Opt in with `--enable-stdin-passwords` and use `-` as the value of any password option:
49+
50+
```bash
51+
# Keystore password, piped in
52+
printf '%s\n' "$KS_PASSWORD" \
53+
| java -jar JSignPdf.jar --enable-stdin-passwords \
54+
-kst PKCS12 -ksf keystore.p12 -ksp - -ka mykey \
55+
-d out/ input.pdf
56+
```
57+
58+
You can combine several password options in one invocation. When multiple options use `-`, the
59+
values are consumed from stdin in a **fixed canonical order** regardless of the order in which
60+
they appear on the command line:
61+
62+
1. `--keystore-password` (`-ksp`)
63+
2. `--key-password` (`-kp`)
64+
3. `--owner-password` (`-opwd`)
65+
4. `--user-password` (`-upwd`)
66+
5. `--tsa-cert-password` (`-tscp`)
67+
6. `--tsa-password` (`-tsp`)
68+
69+
```bash
70+
# Keystore password first, TSA password second — even though the CLI lists them in the other order.
71+
{ printf '%s\n' "$KS_PASSWORD"; printf '%s\n' "$TSA_PASSWORD"; } \
72+
| java -jar JSignPdf.jar --enable-stdin-passwords \
73+
-kst PKCS12 -ksf keystore.p12 -ksp - -ka mykey \
74+
-ts https://tsa.example/ -tsu tsauser -tsp - \
75+
-d out/ input.pdf
76+
```
77+
78+
Before blocking on each read, JSignPdf prints a progress line such as
79+
`[jsignpdf] Reading password for --keystore-password (1/2) from stdin...` to stderr so you can
80+
verify the order matches what your pipe is feeding. Use `-q` (`--quiet`) to suppress it.
81+
82+
When a console is attached (an interactive terminal), `-` switches to a prompted,
83+
no-echo read via `java.io.Console.readPassword`.
84+
85+
**Backwards compatibility:** without `--enable-stdin-passwords`, a literal `-` is still accepted
86+
as a password value exactly as before. JSignPdf will emit a one-time warning naming the flag, so
87+
a typo does not silently turn into a `-` password.
88+
4289
## Documentation
4390

4491
- User guide: <https://intoolswetrust.github.io/jsignpdf/>

design-doc/3.0.0-stdin-passwords.md

Lines changed: 285 additions & 0 deletions
Large diffs are not rendered by default.

distribution/doc/release-notes/3.0.0.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ packaging.
2525

2626
## Other changes
2727

28+
- **Read passwords from standard input** (#7). Any of the six password
29+
CLI options (`-ksp`, `-kp`, `-opwd`, `-upwd`, `-tscp`, `-tsp`) can now
30+
be given as `-` to read the value from stdin (or, in an interactive
31+
terminal, from the console with no-echo). Opt in with
32+
`--enable-stdin-passwords`; multiple passwords are consumed in a fixed
33+
canonical order printed on stderr before each read. Avoids leaking
34+
secrets to `/proc/<pid>/cmdline` and shell history.
2835
- "Reset Settings" action added to the JavaFX UI.
2936
- New and updated translations (Czech, German, French, Spanish, Slovak,
3037
Portuguese, Croatian, Simplified Chinese, Traditional Chinese, and

jsignpdf/src/main/java/net/sf/jsignpdf/Constants.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,9 @@ public class Constants {
289289
public static final String ARG_KS_PWD_LONG = "keystore-password";
290290
public static final String ARG_KS_PWD = "ksp";
291291

292+
public static final String ARG_ENABLE_STDIN_PWDS_LONG = "enable-stdin-passwords";
293+
public static final String STDIN_PWD_SENTINEL = "-";
294+
292295
public static final String ARG_KEY_PWD_LONG = "key-password";
293296
public static final String ARG_KEY_PWD = "kp";
294297

jsignpdf/src/main/java/net/sf/jsignpdf/SignerOptionsFromCmdLine.java

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131

3232
import static net.sf.jsignpdf.Constants.*;
3333

34+
import java.io.IOException;
35+
import java.io.PrintStream;
3436
import java.net.Proxy;
37+
import java.util.function.Consumer;
3538
import java.util.logging.Level;
3639
import java.util.logging.Logger;
3740

@@ -77,6 +80,19 @@ public class SignerOptionsFromCmdLine extends BasicSignerOptions {
7780

7881
private boolean gui;
7982

83+
private StdinPasswordReader passwordReader;
84+
private PrintStream warningOut;
85+
86+
/** Test seam: override the stdin password reader. */
87+
void setPasswordReader(StdinPasswordReader passwordReader) {
88+
this.passwordReader = passwordReader;
89+
}
90+
91+
/** Test seam: override the stream the stdin-sentinel warning is written to (default {@link System#err}). */
92+
void setWarningOut(PrintStream warningOut) {
93+
this.warningOut = warningOut;
94+
}
95+
8096
/**
8197
* Parses options provided as command line arguments.
8298
* @throws ParseException
@@ -127,14 +143,10 @@ public void loadCmdLine() throws ParseException {
127143
setKsType(line.getOptionValue(ARG_KS_TYPE));
128144
if (line.hasOption(ARG_KS_FILE))
129145
setKsFile(line.getOptionValue(ARG_KS_FILE));
130-
if (line.hasOption(ARG_KS_PWD))
131-
setKsPasswd(line.getOptionValue(ARG_KS_PWD));
132146
if (line.hasOption(ARG_KEY_ALIAS))
133147
setKeyAlias(line.getOptionValue(ARG_KEY_ALIAS));
134148
if (line.hasOption(ARG_KEY_INDEX))
135149
setKeyIndex(getInt(line.getParsedOptionValue(ARG_KEY_INDEX), getKeyIndex()));
136-
if (line.hasOption(ARG_KEY_PWD))
137-
setKeyPasswd(line.getOptionValue(ARG_KEY_PWD));
138150
if (line.hasOption(ARG_OUTPATH))
139151
setOutPath(line.getOptionValue(ARG_OUTPATH));
140152
if (line.hasOption(ARG_OPREFIX))
@@ -160,10 +172,6 @@ public void loadCmdLine() throws ParseException {
160172
setPdfEncryption(PDFEncryption.PASSWORD);
161173
if (line.hasOption(ARG_ENCRYPTION))
162174
setPdfEncryption(line.getOptionValue(ARG_ENCRYPTION));
163-
if (line.hasOption(ARG_PWD_OWNER))
164-
setPdfOwnerPwd(line.getOptionValue(ARG_PWD_OWNER));
165-
if (line.hasOption(ARG_PWD_USER))
166-
setPdfUserPwd(line.getOptionValue(ARG_PWD_USER));
167175
if (line.hasOption(ARG_ENC_CERT))
168176
setPdfEncryptionCertFile(line.getOptionValue(ARG_ENC_CERT));
169177
if (line.hasOption(ARG_RIGHT_PRINT))
@@ -215,13 +223,9 @@ public void loadCmdLine() throws ParseException {
215223
setTsaCertFileType(line.getOptionValue(ARG_TSA_CERT_FILE_TYPE));
216224
if (line.hasOption(ARG_TSA_CERT_FILE))
217225
setTsaCertFile(line.getOptionValue(ARG_TSA_CERT_FILE));
218-
if (line.hasOption(ARG_TSA_CERT_PWD))
219-
setTsaCertFilePwd(line.getOptionValue(ARG_TSA_CERT_PWD));
220226

221227
if (line.hasOption(ARG_TSA_USER))
222228
setTsaUser(line.getOptionValue(ARG_TSA_USER));
223-
if (line.hasOption(ARG_TSA_PWD))
224-
setTsaPasswd(line.getOptionValue(ARG_TSA_PWD));
225229
if (line.hasOption(ARG_TSA_POLICY_LONG))
226230
setTsaPolicy(line.getOptionValue(ARG_TSA_POLICY_LONG));
227231
if (line.hasOption(ARG_TSA_HASH_ALG))
@@ -245,6 +249,78 @@ public void loadCmdLine() throws ParseException {
245249
setInFile(files[0]);
246250
}
247251

252+
resolvePasswords(line);
253+
}
254+
255+
/**
256+
* Processes the six password options in a fixed canonical order, substituting values read from stdin
257+
* (or the interactive console) wherever the user gave the sentinel {@code "-"} together with
258+
* {@code --enable-stdin-passwords}. When the flag is missing, a {@code "-"} value is treated as a
259+
* literal password (backwards compatible) and a warning is written to stderr unless {@code -q} is set.
260+
* See {@code design-doc/3.0.0-stdin-passwords.md}.
261+
*/
262+
private void resolvePasswords(CommandLine line) throws ParseException {
263+
final boolean stdinEnabled = line.hasOption(ARG_ENABLE_STDIN_PWDS_LONG);
264+
final boolean quiet = line.hasOption(ARG_QUIET);
265+
266+
final String[][] slots = {
267+
{ ARG_KS_PWD, ARG_KS_PWD_LONG },
268+
{ ARG_KEY_PWD, ARG_KEY_PWD_LONG },
269+
{ ARG_PWD_OWNER, ARG_PWD_OWNER_LONG },
270+
{ ARG_PWD_USER, ARG_PWD_USER_LONG },
271+
{ ARG_TSA_CERT_PWD, ARG_TSA_CERT_PWD_LONG },
272+
{ ARG_TSA_PWD, ARG_TSA_PWD_LONG },
273+
};
274+
@SuppressWarnings("unchecked")
275+
final Consumer<String>[] setters = new Consumer[] {
276+
(Consumer<String>) this::setKsPasswd,
277+
(Consumer<String>) this::setKeyPasswd,
278+
(Consumer<String>) this::setPdfOwnerPwd,
279+
(Consumer<String>) this::setPdfUserPwd,
280+
(Consumer<String>) this::setTsaCertFilePwd,
281+
(Consumer<String>) this::setTsaPasswd,
282+
};
283+
284+
int total = 0;
285+
for (String[] slot : slots) {
286+
if (line.hasOption(slot[0]) && STDIN_PWD_SENTINEL.equals(line.getOptionValue(slot[0]))) {
287+
total++;
288+
}
289+
}
290+
291+
int index = 0;
292+
for (int i = 0; i < slots.length; i++) {
293+
final String shortArg = slots[i][0];
294+
final String longArg = slots[i][1];
295+
if (!line.hasOption(shortArg)) {
296+
continue;
297+
}
298+
final String raw = line.getOptionValue(shortArg);
299+
if (STDIN_PWD_SENTINEL.equals(raw)) {
300+
if (stdinEnabled) {
301+
index++;
302+
try {
303+
if (passwordReader == null) {
304+
passwordReader = StdinPasswordReader.systemDefault(quiet);
305+
}
306+
char[] chars = passwordReader.readNext(longArg, index, total);
307+
setters[i].accept(new String(chars));
308+
} catch (IOException ioe) {
309+
throw new ParseException(ioe.getMessage());
310+
}
311+
} else {
312+
if (!quiet) {
313+
PrintStream w = warningOut != null ? warningOut : System.err;
314+
w.println("[jsignpdf] Warning: --" + longArg + " value is '-'. Did you mean to pass --"
315+
+ ARG_ENABLE_STDIN_PWDS_LONG
316+
+ " to read it from stdin? Using '-' as the literal password.");
317+
}
318+
setters[i].accept(raw);
319+
}
320+
} else {
321+
setters[i].accept(raw);
322+
}
323+
}
248324
}
249325

250326
/**
@@ -303,6 +379,8 @@ private float getFloat(Object aVal, float aDefVal) {
303379
.withType(Number.class).withArgName("index").create(ARG_KEY_INDEX));
304380
OPTS.addOption(OptionBuilder.withLongOpt(ARG_KEY_PWD_LONG).withDescription(RES.get("hlp.keyPwd")).hasArg()
305381
.withArgName("password").create(ARG_KEY_PWD));
382+
OPTS.addOption(OptionBuilder.withLongOpt(ARG_ENABLE_STDIN_PWDS_LONG)
383+
.withDescription(RES.get("hlp.enableStdinPasswords")).create());
306384

307385
OPTS.addOption(OptionBuilder.withLongOpt(ARG_OUTPATH_LONG).withDescription(RES.get("hlp.outPath")).hasArg()
308386
.withArgName("path").create(ARG_OUTPATH));
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* The contents of this file are subject to the Mozilla Public License
3+
* Version 1.1 (the "License"); you may not use this file except in
4+
* compliance with the License. You may obtain a copy of the License at
5+
* http://www.mozilla.org/MPL/
6+
*
7+
* Software distributed under the License is distributed on an "AS IS"
8+
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
9+
* License for the specific language governing rights and limitations
10+
* under the License.
11+
*
12+
* The Original Code is 'JSignPdf, a free application for PDF signing'.
13+
*
14+
* The Initial Developer of the Original Code is Josef Cacek.
15+
* Portions created by Josef Cacek are Copyright (C) Josef Cacek. All Rights Reserved.
16+
*
17+
* Contributor(s): Josef Cacek.
18+
*
19+
* Alternatively, the contents of this file may be used under the terms
20+
* of the GNU Lesser General Public License, version 2.1 (the "LGPL License"), in which case the
21+
* provisions of LGPL License are applicable instead of those
22+
* above. If you wish to allow use of your version of this file only
23+
* under the terms of the LGPL License and not to allow others to use
24+
* your version of this file under the MPL, indicate your decision by
25+
* deleting the provisions above and replace them with the notice and
26+
* other provisions required by the LGPL License. If you do not delete
27+
* the provisions above, a recipient may use your version of this file
28+
* under either the MPL or the LGPL License.
29+
*/
30+
package net.sf.jsignpdf;
31+
32+
import java.io.BufferedReader;
33+
import java.io.Console;
34+
import java.io.EOFException;
35+
import java.io.IOException;
36+
import java.io.InputStreamReader;
37+
import java.io.PrintStream;
38+
39+
/**
40+
* Reads passwords from stdin (or the interactive console) for CLI password options that were
41+
* marked with the {@link Constants#STDIN_PWD_SENTINEL} sentinel.
42+
*
43+
* <p>See {@code design-doc/3.0.0-stdin-passwords.md}.
44+
*/
45+
final class StdinPasswordReader {
46+
47+
private static final String PROGRESS_PREFIX = "[jsignpdf] ";
48+
49+
private final BufferedReader reader;
50+
private final Console console;
51+
private final PrintStream progressOut;
52+
private final boolean quiet;
53+
54+
StdinPasswordReader(BufferedReader reader, Console console, PrintStream progressOut, boolean quiet) {
55+
this.reader = reader;
56+
this.console = console;
57+
this.progressOut = progressOut;
58+
this.quiet = quiet;
59+
}
60+
61+
/**
62+
* Default reader bound to {@link System#in}, {@link System#console()} and {@link System#err}.
63+
*/
64+
static StdinPasswordReader systemDefault(boolean quiet) {
65+
return new StdinPasswordReader(new BufferedReader(new InputStreamReader(System.in)), System.console(),
66+
System.err, quiet);
67+
}
68+
69+
/**
70+
* Reads the next password. Emits a progress line (unless quiet) before blocking on input.
71+
*
72+
* @param longOptionName the long option name (e.g. {@code keystore-password}), used in progress/error messages
73+
* @param index 1-based index of this read among the sentinel-marked options in the current invocation
74+
* @param total total count of sentinel-marked options in the current invocation
75+
* @return the password characters; never {@code null}
76+
* @throws IOException on EOF before a line is read, or on underlying I/O failure
77+
*/
78+
char[] readNext(String longOptionName, int index, int total) throws IOException {
79+
if (!quiet && progressOut != null) {
80+
progressOut.println(PROGRESS_PREFIX + "Reading password for --" + longOptionName + " (" + index + "/" + total
81+
+ ") from " + (console != null ? "console" : "stdin") + "...");
82+
}
83+
if (console != null) {
84+
char[] pwd = console.readPassword("Enter password for --" + longOptionName + ": ");
85+
if (pwd == null) {
86+
throw new EOFException(
87+
"Unexpected end of input while reading password for --" + longOptionName);
88+
}
89+
return pwd;
90+
}
91+
String line;
92+
try {
93+
line = reader.readLine();
94+
} catch (IOException e) {
95+
throw new IOException("Failed to read password for --" + longOptionName + ": " + e.getMessage(), e);
96+
}
97+
if (line == null) {
98+
throw new EOFException("Unexpected end of input while reading password for --" + longOptionName);
99+
}
100+
return line.toCharArray();
101+
}
102+
}

0 commit comments

Comments
 (0)