3232import static java .util .Objects .requireNonNull ;
3333
3434/**
35- * Inspired by <a href="https://velvetcache.org/2023/03/26/a-peek-inside-pinentry/">A peek inside pinentry</a>
35+ * Inspired by <a href="https://velvetcache.org/2023/03/26/a-peek-inside-pinentry/">A peek inside pinentry</a>.
36+ * Also look at <a href="https://gorbe.io/posts/gnupg/pinentry/documentation/">Pinentry Documentation</a>.
37+ * Finally, source mirror is at <a href="https://github.com/gpg/pinentry">gpg/pinentry</a>.
3638 */
3739public class PinEntry {
3840 public enum Outcome {
@@ -43,59 +45,96 @@ public enum Outcome {
4345 FAILED ;
4446 }
4547
46- public record Result < T > (Outcome outcome , T payload ) {}
48+ public record Result (Outcome outcome , String payload ) {}
4749
4850 private final Logger logger = LoggerFactory .getLogger (getClass ());
4951 private final String cmd ;
5052 private final LinkedHashMap <String , String > commands ;
5153
54+ /**
55+ * Creates pin entry instance that will use the passed in cmd executable.
56+ */
5257 public PinEntry (String cmd ) {
5358 this .cmd = requireNonNull (cmd );
5459 this .commands = new LinkedHashMap <>();
5560 }
5661
62+ /**
63+ * Sets a "stable key handle" for caching purposes. Optional.
64+ */
5765 public PinEntry setKeyInfo (String keyInfo ) {
5866 requireNonNull (keyInfo );
59- commands .put ("SETKEYINFO" , keyInfo );
6067 commands .put ("OPTION" , "allow-external-password-cache" );
68+ commands .put ("SETKEYINFO" , keyInfo );
6169 return this ;
6270 }
6371
72+ /**
73+ * Sets the OK button label, by default "Ok".
74+ */
6475 public PinEntry setOk (String msg ) {
6576 requireNonNull (msg );
6677 commands .put ("SETOK" , msg );
6778 return this ;
6879 }
6980
81+ /**
82+ * Sets the CANCEL button label, by default "Cancel".
83+ */
7084 public PinEntry setCancel (String msg ) {
7185 requireNonNull (msg );
7286 commands .put ("SETCANCEL" , msg );
7387 return this ;
7488 }
7589
90+ /**
91+ * Sets the window title.
92+ */
7693 public PinEntry setTitle (String title ) {
7794 requireNonNull (title );
7895 commands .put ("SETTITLE" , title );
7996 return this ;
8097 }
8198
99+ /**
100+ * Sets additional test in window.
101+ */
82102 public PinEntry setDescription (String desc ) {
83103 requireNonNull (desc );
84104 commands .put ("SETDESC" , desc );
85105 return this ;
86106 }
87107
108+ /**
109+ * Sets the prompt.
110+ */
88111 public PinEntry setPrompt (String prompt ) {
89112 requireNonNull (prompt );
90113 commands .put ("SETPROMPT" , prompt );
91114 return this ;
92115 }
93116
117+ /**
118+ * If set, window will show "Error: xxx", usable for second attempt (ie "bad password").
119+ */
120+ public PinEntry setError (String error ) {
121+ requireNonNull (error );
122+ commands .put ("SETERROR" , error );
123+ return this ;
124+ }
125+
126+ /**
127+ * Usable with {@link #getPin()}, window will contain two input fields and will force user to type in same
128+ * input in both fields, ie to "confirm" the pin.
129+ */
94130 public PinEntry confirmPin () {
95- commands .put ("SETREPEAT" , cmd );
131+ commands .put ("SETREPEAT" , null );
96132 return this ;
97133 }
98134
135+ /**
136+ * Sets the window timeout, if no button pressed and timeout passes, Result will by {@link Outcome#TIMEOUT}.
137+ */
99138 public PinEntry setTimeout (Duration timeout ) {
100139 long seconds = timeout .toSeconds ();
101140 if (seconds < 0 ) {
@@ -105,22 +144,31 @@ public PinEntry setTimeout(Duration timeout) {
105144 return this ;
106145 }
107146
108- public Result <String > getPin () throws IOException {
147+ /**
148+ * Initiates a "get pin" dialogue with input field(s) using previously set options.
149+ */
150+ public Result getPin () throws IOException {
109151 commands .put ("GETPIN" , null );
110152 return execute ();
111153 }
112154
113- public Result <String > confirm () throws IOException {
155+ /**
156+ * Initiates a "confirmation" dialogue (no input) using previously set options.
157+ */
158+ public Result confirm () throws IOException {
114159 commands .put ("CONFIRM" , null );
115160 return execute ();
116161 }
117162
118- public Result <String > message () throws IOException {
163+ /**
164+ * Initiates a "message" dialogue (no input) using previously set options.
165+ */
166+ public Result message () throws IOException {
119167 commands .put ("MESSAGE" , null );
120168 return execute ();
121169 }
122170
123- private Result < String > execute () throws IOException {
171+ private Result execute () throws IOException {
124172 Process process = new ProcessBuilder (cmd ).start ();
125173 BufferedReader reader = process .inputReader ();
126174 BufferedWriter writer = process .outputWriter ();
@@ -141,20 +189,20 @@ private Result<String> execute() throws IOException {
141189 expectOK (reader );
142190 }
143191 }
144- Result < String > result = lastExpect (reader );
192+ Result result = lastExpect (reader );
145193 writer .write ("BYE" );
146194 writer .newLine ();
147195 writer .flush ();
148196 try {
149197 process .waitFor (5 , TimeUnit .SECONDS );
150198 int exitCode = process .exitValue ();
151199 if (exitCode != 0 ) {
152- return new Result <> (Outcome .FAILED , "Exit code: " + exitCode );
200+ return new Result (Outcome .FAILED , "Exit code: " + exitCode );
153201 } else {
154202 return result ;
155203 }
156204 } catch (Exception e ) {
157- return new Result <> (Outcome .FAILED , e .getMessage ());
205+ return new Result (Outcome .FAILED , e .getMessage ());
158206 }
159207 }
160208
@@ -166,7 +214,7 @@ private void expectOK(BufferedReader in) throws IOException {
166214 }
167215 }
168216
169- private Result < String > lastExpect (BufferedReader in ) throws IOException {
217+ private Result lastExpect (BufferedReader in ) throws IOException {
170218 while (true ) {
171219 String response = in .readLine ();
172220 logger .debug ("< {}" , response );
@@ -178,47 +226,49 @@ private Result<String> lastExpect(BufferedReader in) throws IOException {
178226 }
179227 if (response .startsWith ("ERR" )) {
180228 if (response .contains ("83886142" )) {
181- return new Result <> (Outcome .TIMEOUT , response );
229+ return new Result (Outcome .TIMEOUT , response );
182230 }
183231 if (response .contains ("83886179" )) {
184- return new Result <> (Outcome .CANCELED , response );
232+ return new Result (Outcome .CANCELED , response );
185233 }
186234 if (response .contains ("83886194" )) {
187- return new Result <> (Outcome .NOT_CONFIRMED , response );
235+ return new Result (Outcome .NOT_CONFIRMED , response );
188236 }
189237 }
190238 if (response .startsWith ("D" )) {
191- return new Result <> (Outcome .SUCCESS , response .substring (2 ));
239+ return new Result (Outcome .SUCCESS , response .substring (2 ));
192240 }
193241 if (response .startsWith ("OK" )) {
194- return new Result <> (Outcome .SUCCESS , response );
242+ return new Result (Outcome .SUCCESS , response );
195243 }
196244 }
197245 }
198246
199247 public static void main (String [] args ) throws IOException {
200- Result <String > pinResult = new PinEntry ("/usr/bin/pinentry-gnome3" )
201- .setTimeout (Duration .ofSeconds (5 ))
248+ // check what pinentry apps you have and replace the execName
249+ String cmd = "/usr/bin/pinentry-gnome3" ;
250+ Result pinResult = new PinEntry (cmd )
251+ .setTimeout (Duration .ofSeconds (15 ))
202252 .setKeyInfo ("maven:masterPassword" )
203253 .setTitle ("Maven Master Password" )
204254 .setDescription ("Please enter the Maven master password" )
205- .setPrompt ("Master password " )
206- .setOk ("Her you go!" )
255+ .setPrompt ("Password " )
256+ .setOk ("Here you go!" )
207257 .setCancel ("Uh oh, rather not" )
208258 // .confirmPin() (will not let you through if you cannot type same thing twice)
209259 .getPin ();
210260 if (pinResult .outcome () == Outcome .SUCCESS ) {
211- Result < String > confirmResult = new PinEntry ("/usr/bin/pinentry-gnome3" )
261+ Result confirmResult = new PinEntry (cmd )
212262 .setTitle ("Password confirmation" )
213- .setDescription ("Please confirm that the password you entered is correct " )
214- .setPrompt ("Is the password '" + pinResult .payload () + "' the one you want?" )
263+ .setPrompt ("Please confirm the password" )
264+ .setDescription ("Is the password '" + pinResult .payload () + "' the one you want?" )
215265 .confirm ();
216266 if (confirmResult .outcome () == Outcome .SUCCESS ) {
217- new PinEntry ("/usr/bin/pinentry-gnome3" )
267+ new PinEntry (cmd )
218268 .setTitle ("Password confirmed" )
219- .setDescription ("You confirmed your password" )
220269 .setPrompt ("The password '" + pinResult .payload () + "' is confirmed." )
221- .confirm ();
270+ .setDescription ("You confirmed your password" )
271+ .message ();
222272 } else {
223273 System .out .println (confirmResult );
224274 }
0 commit comments