Calling GnuPG in Java via a Runtime Process to encrypt and decrypt files - Decrypt always hangs

10,117

Solution 1

I forget how you handle it in Java, there are 100 methods for that. But I was stuck with decrypt command itself, it was very helpful, though you didn't need all those quotes and if you wish to decrypt a large file, it goes like this:

gpg --passphrase-fd 0 --output yourfile.txt --decrypt /encryptedfile.txt.gpg/ 0</passwrdfile.txt

Solution 2

This may or may not be the problem (in the decrypt function)

    BufferedReader gpgOutput = new BufferedReader(new InputStreamReader(gpgProcess.getInputStream()));
    BufferedReader gpgErrorOutput = new BufferedReader(new InputStreamReader(gpgProcess.getInputStream()));
    BufferedWriter gpgInput = new BufferedWriter(new OutputStreamWriter(gpgProcess.getOutputStream()));

You are wrapping the result of getInputStream twice. Obviously gpgErrorOutput should be wrapping the error stream, not the input stream.

Solution 3

Have you tried to run that command from command-line, not from Java code? There can be an issue with 'for your eyes only' option, when GnuPG will wait for console output.

Solution 4

I stumbled upon this thread today because I was having the exact same issue as far as the program hanging. Cameron's thread from above contains the solution, which is that you have to be draining the inputStream from your process. If you don't, the stream fills up and hangs. Simply adding

String line = null;
while ( (line = gpgOutput.readLine()) != null ) {
     System.out.println(line);
}

Before checking the exitValue fixed it for me.

Share:
10,117

Related videos on Youtube

framauro13
Author by

framauro13

I've been primarily a Java Developer for almost 8 years in Indianapolis, IN. I'm currently branching into mobile and graphics development (OpenGL), mainly as a hobby.

Updated on June 04, 2022

Comments

  • framauro13
    framauro13 almost 2 years

    NOTE: Coming back to this later as I've been unable to find a working solution. Draining the input streams manually instead of using BufferedReaders doesn't seem to help as the inputStream.read() method permanently blocks the program. I placed the gpg call in a batch file, and called the batch file from Java to only get the same result. Once gpg is called with the decrypt option, the input stream seems to become inaccessible, blocking the entire program. I'll have to come back to this when I have more time to focus on the task. In the mean time, I'll have to get decryption working by some other means (probably BouncyCastle).

    The last option to probably try is to call cmd.exe, and write the command through the input stream generated by that process...

    I appreciate the assistance on this issue.


    I've been working on this problem for a couple days and haven't made any progress, so I thought I'd turn to the exeprtise here for some help.

    I am creating a simple program that will call GnuPG via a Java runtime process. It needs to be able to encrypt and decrypt files. Encryption works, but I'm having some problems decrypting files. Whenever I try to decrypt a file, the process hangs.exitValue() always throws it's IllegalThreadStateException and the program chugs along as if it's still waiting. The code for these methods is attached below. The ultimate goal of the program is to decrypt the file, and parse it's contents in Java.

    I've tried three approaches to getting the gpgDecrypt method to work. The first approach involved removing the passphrase-fd option and writing the passphrase to gpg via the gpgOutput stream in the catch block, assuming it was prompting for the passphrase like it would via the command line. This didn't work, so I put the passphrase in a file and added the -passphrase-fd option. In this case, the program repeats infinitely. If I write anything via the gpgOutput stream the program will complete. The Exit value printed will have a value of 2, and the result variable will be blank.

    The third option is BouncyCastle, but I'm having problems getting it to recognize my private key (which is probably a separate post all together).

    The keys I'm using to encrypt and decrypt are 4096-bit RSA keys, generated by GnuPG. In both cases using the passphrase and the passphrase file, I've tried piping the output to a file via > myFile.txt, but it doesn't seem to make any difference.

    Here are the gpgEncrypt, gpgDecrypt and getStreamText methods. I posted both since the encrypt works, and I can't see any glaring differences between how I'm executing and handling the process between the encrypt and decrypt methods. getStreamText just reads the contents of the streams and returns a string.

    EDIT: Quick note, Windows environment. If I copy the decrypt command output, it works via the console just fine. So I know the command is valid.


    public boolean gpgEncrypt(String file, String recipient, String outputFile){
        boolean success = true;
        StringBuilder gpgCommand = new StringBuilder("gpg --recipient \"");
        gpgCommand.append(recipient).append("\" --output \"").append(outputFile).append("\" --yes --encrypt \"");
        gpgCommand.append(file).append("\"");
    
        System.out.println("ENCRYPT COMMAND: " + gpgCommand);
        try {
            Process gpgProcess = Runtime.getRuntime().exec(gpgCommand.toString());
    
            BufferedReader gpgOutput = new BufferedReader(new InputStreamReader(gpgProcess.getInputStream()));
            BufferedWriter gpgInput = new BufferedWriter(new OutputStreamWriter(gpgProcess.getOutputStream()));
            BufferedReader gpgErrorOutput = new BufferedReader(new InputStreamReader(gpgProcess.getErrorStream()));
    
            boolean executing = true;
    
            while(executing){
                try{
                    int exitValue = gpgProcess.exitValue();
    
                    if(gpgErrorOutput.ready()){
                        String error = getStreamText(gpgErrorOutput);
                        System.err.println(error);
                        success = false;
                        break;
                    }else if(gpgOutput.ready()){
                        System.out.println(getStreamText(gpgOutput));
                    }
    
                    executing = false;
                }catch(Exception e){
                    //The process is not yet ready to exit.  Take a break and try again.
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e1) {
                        System.err.println("This thread has insomnia: " + e1.getMessage());
                    }
                }
            }
        } catch (IOException e) {
            System.err.println("Error running GPG via runtime: " + e.getMessage());
            success = false;
        }
    
        return success;
    }
    
    public String gpgDecrypt(String file, String passphraseFile){
        String result = null;
        StringBuilder command = new StringBuilder("gpg --passphrase-fd 0 --decrypt \"");
        command.append(file).append("\" 0<\"").append(passphraseFile).append("\"");             
        System.out.println("DECRYPT COMMAND: " + command.toString());
        try {
    
            Process gpgProcess = Runtime.getRuntime().exec(command.toString());
    
            BufferedReader gpgOutput = new BufferedReader(new InputStreamReader(gpgProcess.getInputStream()));
            BufferedReader gpgErrorOutput = new BufferedReader(new InputStreamReader(gpgProcess.getErrorStream()));
            BufferedWriter gpgInput = new BufferedWriter(new OutputStreamWriter(gpgProcess.getOutputStream()));
    
            boolean executing = true;
    
            while(executing){
                try{
                    if(gpgErrorOutput.ready()){
                        result = getStreamText(gpgErrorOutput);
                        System.err.println(result);
                        break;
                    }else if(gpgOutput.ready()){
                        result = getStreamText(gpgOutput);
                    }
    
                    int exitValue = gpgProcess.exitValue();
                    System.out.println("EXIT: " + exitValue);
    
                    executing = false;
                }catch(IllegalThreadStateException e){
                    System.out.println("Not yet ready.  Stream status: " + gpgOutput.ready() + ", error: " + gpgErrorOutput.ready());
    
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e1) {
                        System.err.println("This thread has insomnia: " + e1.getMessage());
                    }
                }
            }
        } catch (IOException e) {
            System.err.println("Unable to execute GPG decrypt command via command line: " + e.getMessage());
        }
    
        return result;
    }
    
    private String getStreamText(BufferedReader reader) throws IOException{
        StringBuilder result = new StringBuilder();
        try{
            while(reader.ready()){
                result.append(reader.readLine());
                if(reader.ready()){
                    result.append("\n");
                }
            }
        }catch(IOException ioe){
            System.err.println("Error while reading the stream: " + ioe.getMessage());
            throw ioe;
        }
        return result.toString();
    }
    
    • SyntaxT3rr0r
      SyntaxT3rr0r over 13 years
      I've done a lot of batch/script calls from Java. I can tell you one thing: trying to build a String containing spacing characters is a recipe for disaster. Do NEVER EVER call the Runtime.getRuntime().exec(String) method. What you SHOULD do is split your String and call the Runtime.getRuntime().exec(String[]) method. Note that I'm not saying it shall solve the issue you have here: all I'm saying is you will suffer at one point or another if you keep invoking batch/scripts the way you currently do.
  • Matthew Wilson
    Matthew Wilson over 13 years
    Also have you read javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html ? A reliable use of Runtime.exec needs Threads reading the output of the program.
  • Cameron Skinner
    Cameron Skinner over 13 years
    You also need to drain the streams as @Matthew mentioned. It could be that the stdout or stderr buffers are filling up which will cause the process to hang.
  • framauro13
    framauro13 over 13 years
    @Matthew I will multithread this eventually, but I wanted to get a single file to work first. Will try spawning a new thread.
  • framauro13
    framauro13 over 13 years
    @Cameron The getStreamText method should read from the streams if/when they are ready. However, I am calling exitValue() first before checking... I'll try moving the output stream checks in front immediately after executing the process to see if that is causing a problem. The files I am decrypting are only a few kilobytes, but it could still be a problem. I'll try moving that block of code in front of the exit value check... maybe they're filling up too fast.
  • Cameron Skinner
    Cameron Skinner over 13 years
    Yes, it's the exitValue() call before the drain that will cause problems. The Apache commons-io package has classes that will pump streams for you so you don't need to do your own threading.
  • framauro13
    framauro13 over 13 years
    Updated the code to check the streams for content before calling exitValue(). Added a break in the error check to end the loop if an error was detected. Both ready() methods still return false, and the process continues to loop indefinitely.
  • Matthew Wilson
    Matthew Wilson over 13 years
    Try adding logging from the threads which are draining stdout and stderr, to see if you are getting unexpected errors.
  • Cameron Skinner
    Cameron Skinner over 13 years
    You're going to need to add the drains. It's possible that your code can hit both the ready calls before the sub-process has even started in which case they'll return false and you have the same problem as before. Basically, you always need to drain the streams even when you think you're doing something simple.
  • framauro13
    framauro13 over 13 years
    I hate to do it, but I think I've got to abandon this approach and look at doing this by another means (BouncyCastle) as it's eaten up way too much time already. I added a method that didn't use a buffered reader, just read the bytes from the input stream. When inputStream.read(bytes) is called, the program stops responding. The read call blocks the program and keeps it from continuing to execute. I even tried wrapping the gpg call in a batch file, and calling that from Java, only to get the same results. Encrypt works fine, but decrypt just doesn't seem to want to work.
  • framauro13
    framauro13 over 13 years
    The commands generated by the Java application run fine when copied from the console and pasted onto the command line, but calling gpg with the decrypt option seems to completely "plug up" the input stream. I'll have to dig into it further when I have more time to focus on this task.
  • Cameron Skinner
    Cameron Skinner over 13 years
    You should also be aware that Reader.ready() does not do what it looks like you think it does. In your getStreamText method you check Reader.ready() then call readLine() - the ready call does not guarantee that readLine will not block. It only guarantees that there is at least one byte to be read, but readLine() may attempt to read an arbitrary number of bytes which could still result in blocking.

Related