-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
330 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
229 changes: 229 additions & 0 deletions
229
src/main/java/org/codehaus/plexus/components/secdispatcher/PinEntry.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
/* | ||
* Licensed to the Apache Software Foundation (ASF) under one | ||
* or more contributor license agreements. See the NOTICE file | ||
* distributed with this work for additional information | ||
* regarding copyright ownership. The ASF licenses this file | ||
* to you under the Apache License, Version 2.0 (the | ||
* "License"); you may not use this file except in compliance | ||
* with the License. You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
package org.codehaus.plexus.components.secdispatcher; | ||
|
||
import java.io.BufferedReader; | ||
import java.io.BufferedWriter; | ||
import java.io.IOException; | ||
import java.time.Duration; | ||
import java.util.LinkedHashMap; | ||
import java.util.Map; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import static java.util.Objects.requireNonNull; | ||
|
||
/** | ||
* Inspired by <a href="https://velvetcache.org/2023/03/26/a-peek-inside-pinentry/">A peek inside pinentry</a> | ||
*/ | ||
public class PinEntry { | ||
public enum Outcome { | ||
SUCCESS, | ||
TIMEOUT, | ||
NOT_CONFIRMED, | ||
CANCELED, | ||
FAILED; | ||
} | ||
|
||
public record Result<T>(Outcome outcome, T payload) {} | ||
|
||
private final Logger logger = LoggerFactory.getLogger(getClass()); | ||
private final String cmd; | ||
private final LinkedHashMap<String, String> commands; | ||
|
||
public PinEntry(String cmd) { | ||
this.cmd = requireNonNull(cmd); | ||
this.commands = new LinkedHashMap<>(); | ||
} | ||
|
||
public PinEntry setKeyInfo(String keyInfo) { | ||
requireNonNull(keyInfo); | ||
commands.put("SETKEYINFO", keyInfo); | ||
commands.put("OPTION", "allow-external-password-cache"); | ||
return this; | ||
} | ||
|
||
public PinEntry setOk(String msg) { | ||
requireNonNull(msg); | ||
commands.put("SETOK", msg); | ||
return this; | ||
} | ||
|
||
public PinEntry setCancel(String msg) { | ||
requireNonNull(msg); | ||
commands.put("SETCANCEL", msg); | ||
return this; | ||
} | ||
|
||
public PinEntry setTitle(String title) { | ||
requireNonNull(title); | ||
commands.put("SETTITLE", title); | ||
return this; | ||
} | ||
|
||
public PinEntry setDescription(String desc) { | ||
requireNonNull(desc); | ||
commands.put("SETDESC", desc); | ||
return this; | ||
} | ||
|
||
public PinEntry setPrompt(String prompt) { | ||
requireNonNull(prompt); | ||
commands.put("SETPROMPT", prompt); | ||
return this; | ||
} | ||
|
||
public PinEntry confirmPin() { | ||
commands.put("SETREPEAT", cmd); | ||
return this; | ||
} | ||
|
||
public PinEntry setTimeout(Duration timeout) { | ||
long seconds = timeout.toSeconds(); | ||
if (seconds < 0) { | ||
throw new IllegalArgumentException("Set timeout is 0 seconds"); | ||
} | ||
commands.put("SETTIMEOUT", String.valueOf(seconds)); | ||
return this; | ||
} | ||
|
||
public Result<String> getPin() throws IOException { | ||
commands.put("GETPIN", null); | ||
return execute(); | ||
} | ||
|
||
public Result<String> confirm() throws IOException { | ||
commands.put("CONFIRM", null); | ||
return execute(); | ||
} | ||
|
||
public Result<String> message() throws IOException { | ||
commands.put("MESSAGE", null); | ||
return execute(); | ||
} | ||
|
||
private Result<String> execute() throws IOException { | ||
Process process = new ProcessBuilder(cmd).start(); | ||
BufferedReader reader = process.inputReader(); | ||
BufferedWriter writer = process.outputWriter(); | ||
expectOK(process.inputReader()); | ||
Map.Entry<String, String> lastEntry = commands.entrySet().iterator().next(); | ||
for (Map.Entry<String, String> entry : commands.entrySet()) { | ||
String cmd; | ||
if (entry.getValue() != null) { | ||
cmd = entry.getKey() + " " + entry.getValue(); | ||
} else { | ||
cmd = entry.getKey(); | ||
} | ||
logger.debug("> {}", cmd); | ||
writer.write(cmd); | ||
writer.newLine(); | ||
writer.flush(); | ||
if (entry != lastEntry) { | ||
expectOK(reader); | ||
} | ||
} | ||
Result<String> result = lastExpect(reader); | ||
writer.write("BYE"); | ||
writer.newLine(); | ||
writer.flush(); | ||
try { | ||
process.waitFor(5, TimeUnit.SECONDS); | ||
int exitCode = process.exitValue(); | ||
if (exitCode != 0) { | ||
return new Result<>(Outcome.FAILED, "Exit code: " + exitCode); | ||
} else { | ||
return result; | ||
} | ||
} catch (Exception e) { | ||
return new Result<>(Outcome.FAILED, e.getMessage()); | ||
} | ||
} | ||
|
||
private void expectOK(BufferedReader in) throws IOException { | ||
String response = in.readLine(); | ||
logger.debug("< {}", response); | ||
if (!response.startsWith("OK")) { | ||
throw new IOException("Expected OK but got this instead: " + response); | ||
} | ||
} | ||
|
||
private Result<String> lastExpect(BufferedReader in) throws IOException { | ||
while (true) { | ||
String response = in.readLine(); | ||
logger.debug("< {}", response); | ||
if (response.startsWith("#")) { | ||
continue; | ||
} | ||
if (response.startsWith("S")) { | ||
continue; | ||
} | ||
if (response.startsWith("ERR")) { | ||
if (response.contains("83886142")) { | ||
return new Result<>(Outcome.TIMEOUT, response); | ||
} | ||
if (response.contains("83886179")) { | ||
return new Result<>(Outcome.CANCELED, response); | ||
} | ||
if (response.contains("83886194")) { | ||
return new Result<>(Outcome.NOT_CONFIRMED, response); | ||
} | ||
} | ||
if (response.startsWith("D")) { | ||
return new Result<>(Outcome.SUCCESS, response.substring(2)); | ||
} | ||
if (response.startsWith("OK")) { | ||
return new Result<>(Outcome.SUCCESS, response); | ||
} | ||
} | ||
} | ||
|
||
public static void main(String[] args) throws IOException { | ||
Result<String> pinResult = new PinEntry("/usr/bin/pinentry-gnome3") | ||
.setTimeout(Duration.ofSeconds(5)) | ||
.setKeyInfo("maven:masterPassword") | ||
.setTitle("Maven Master Password") | ||
.setDescription("Please enter the Maven master password") | ||
.setPrompt("Master password") | ||
.setOk("Her you go!") | ||
.setCancel("Uh oh, rather not") | ||
// .confirmPin() (will not let you through if you cannot type same thing twice) | ||
.getPin(); | ||
if (pinResult.outcome() == Outcome.SUCCESS) { | ||
Result<String> confirmResult = new PinEntry("/usr/bin/pinentry-gnome3") | ||
.setTitle("Password confirmation") | ||
.setDescription("Please confirm that the password you entered is correct") | ||
.setPrompt("Is the password '" + pinResult.payload() + "' the one you want?") | ||
.confirm(); | ||
if (confirmResult.outcome() == Outcome.SUCCESS) { | ||
new PinEntry("/usr/bin/pinentry-gnome3") | ||
.setTitle("Password confirmed") | ||
.setDescription("You confirmed your password") | ||
.setPrompt("The password '" + pinResult.payload() + "' is confirmed.") | ||
.confirm(); | ||
} else { | ||
System.out.println(confirmResult); | ||
} | ||
} else { | ||
System.out.println(pinResult); | ||
} | ||
} | ||
} |
80 changes: 80 additions & 0 deletions
80
...a/org/codehaus/plexus/components/secdispatcher/internal/sources/PinEntryMasterSource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
/* | ||
* Licensed to the Apache Software Foundation (ASF) under one | ||
* or more contributor license agreements. See the NOTICE file | ||
* distributed with this work for additional information | ||
* regarding copyright ownership. The ASF licenses this file | ||
* to you under the Apache License, Version 2.0 (the | ||
* "License"); you may not use this file except in compliance | ||
* with the License. You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
package org.codehaus.plexus.components.secdispatcher.internal.sources; | ||
|
||
import javax.inject.Named; | ||
import javax.inject.Singleton; | ||
|
||
import java.io.IOException; | ||
import java.time.Duration; | ||
import java.util.Optional; | ||
|
||
import org.codehaus.plexus.components.secdispatcher.MasterSource; | ||
import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta; | ||
import org.codehaus.plexus.components.secdispatcher.PinEntry; | ||
import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; | ||
|
||
/** | ||
* Inspired by <a href="https://velvetcache.org/2023/03/26/a-peek-inside-pinentry/">A peek inside pinentry</a> | ||
*/ | ||
@Singleton | ||
@Named(PinEntryMasterSource.NAME) | ||
public class PinEntryMasterSource extends PrefixMasterSourceSupport implements MasterSource, MasterSourceMeta { | ||
public static final String NAME = "pinentry-prompt"; | ||
|
||
public PinEntryMasterSource() { | ||
super(NAME + ":"); | ||
} | ||
|
||
@Override | ||
public String description() { | ||
return "Secure PinEntry prompt"; | ||
} | ||
|
||
@Override | ||
public Optional<String> configTemplate() { | ||
return Optional.of(NAME + ":" + "$pinentryPath"); | ||
} | ||
|
||
@Override | ||
public String doHandle(String s) throws SecDispatcherException { | ||
try { | ||
PinEntry.Result<String> result = new PinEntry(s) | ||
.setTimeout(Duration.ofSeconds(30)) | ||
.setKeyInfo("maven:masterPassword") | ||
.setTitle("Maven Master Password") | ||
.setDescription("Please enter the Maven master password") | ||
.setPrompt("Maven master password") | ||
.setOk("Ok") | ||
.setCancel("Cancel") | ||
.getPin(); | ||
if (result.outcome() == PinEntry.Outcome.SUCCESS) { | ||
return result.payload(); | ||
} else if (result.outcome() == PinEntry.Outcome.CANCELED) { | ||
throw new SecDispatcherException("User canceled the operation"); | ||
} else if (result.outcome() == PinEntry.Outcome.TIMEOUT) { | ||
throw new SecDispatcherException("Timeout"); | ||
} else { | ||
throw new SecDispatcherException("Failure: " + result.payload()); | ||
} | ||
} catch (IOException e) { | ||
throw new SecDispatcherException("Could not collect the password", e); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters