Search code examples
c#.netcmdcode-injectionkeytool

How to safely invoke Java keytool from C# code


I want to create a GUI in C# that will be used to run keytool on cmd.exe behind the scenes to create a keystore, including a key, and certificate data.

Input data then requires

  • Keystore path
  • Password
  • Key alias
  • Key password
  • Validity
  • Certificate info (cn, ou, o, l, st and c)

Unfortunately, people may type special characters in their passwords and also space is allowed in the certificate info.

Overall, I am worried someone may input some information somewhere that can result in a disastrous command running behind the scenes once this is called (like rm -rf *).

Is there a way to pass a Java properties file with the input information to keytool or is there any way that I can safely escape all the data that is passed as string parameters to keytool?

I could not find any type of file that keytool could take, even in separate steps, that would eliminate this issue.

Here is the unsafe code (warning: it's unsafe!):

using System;
using System.IO;
using System.Diagnostics;
using System.Collections.Generic;
using System.Text.RegularExpressions;

public class AndroidKeystoreCertificateData
{
  public string FirstAndLastName;
  public string OrganizationalUnit;
  public string OrganizationName;
  public string CityOrLocality;
  public string StateOrProvince;
  public string CountryCode;
}

public class AndroidKeystoreData : AndroidKeystoreCertificateData
{
  public string KeystorePath;
  public string Password;
  public string KeyAlias;
  public string KeyPassword;
  public int ValidityInYears;
}

internal class AndroidUtils
{

  private static bool RunCommand(string command, string working_dir, bool show_window = true)
  {
    using (Process proc = new Process
    {
      StartInfo =
      {
        UseShellExecute = false,
        FileName = "cmd.exe",
        Arguments = command,
        CreateNoWindow = !show_window,
        WorkingDirectory = working_dir
      }
    })
    {
      try
      {
        proc.Start();
        proc.WaitForExit();
        return true;
      }
      catch
      {
        return false;
      }
    }
    return false;
  }
  
  private static string FilterString(string st)
  {
    return Regex.Replace(st, @"[^\w\d _]", "").Trim();
  }

  public static string GetKeystoreCertificateInputString(AndroidKeystoreCertificateData data)
  {
    string strCN = FilterString(data.FirstAndLastName);
    string strOU = FilterString(data.OrganizationalUnit);
    string strO = FilterString(data.OrganizationName);
    string strL = FilterString(data.CityOrLocality);
    string cnST = FilterString(data.StateOrProvince);
    string cnC = FilterString(data.CountryCode);

    string cert = "\"";
    if (!string.IsNullOrEmpty(strCN)) cert += "cn=" + strCN + ", ";
    if (!string.IsNullOrEmpty(strOU)) cert += "ou=" + strOU + ", ";
    if (!string.IsNullOrEmpty(strO))  cert += "o=" + strO + ", ";
    if (!string.IsNullOrEmpty(strL))  cert += "l=" + strL + ", ";
    if (!string.IsNullOrEmpty(cnST))  cert += "st=" + cnST + ", ";
    if (!string.IsNullOrEmpty(cnC))   cert += "c=" + cnC + "\"";

    if (cert.Length > 2) return cert;

    return string.Empty;
  }
  
  private static string GetKeytoolPath()
  {
    string javaHome = Environment.GetEnvironmentVariable("JAVA_HOME", EnvironmentVariableTarget.User);
    return Path.Combine(javaHome, "bin\\keytool");
  }

  private static string GetKeystoreGenerationCommand(AndroidKeystoreData d)
  {
    string cert = GetKeystoreCertificateInputString(d);
    string keytool = GetKeytoolPath();
    string days = (d.ValidityInYears * 365).ToString();

    string dname = "-dname \"cn=" + d.KeyAlias + "\"";
    if (!string.IsNullOrEmpty(cert)) dname = "-dname " + cert;

    string cmd = "echo y | " + keytool + " -genkeypair " + dname + 
        " -alias " + d.KeyAlias + " -keypass " + d.KeyPassword +
        " -keystore " + d.KeystorePath + " -storepass " + d.Password + " -validity " + days;

    return cmd;
  }
  
  public static bool RunGenerateKeystore(AndroidKeystoreData d)
  {
    string cmd = GetKeystoreGenerationCommand(d);
    string wdir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);

    return RunCommand(cmd, wdir, false);
  }
}

An example usage would be:

using System;

class MainClass
{
  static void Main(string[] args)
  {
    AndroidKeystoreData d = new AndroidKeystoreData();

    d.KeystorePath = "keystorepath";
    d.Password = "pass";
    d.KeyAlias = "key0";
    d.KeyPassword = "pass";

    d.ValidityInYears = 25*365;

    d.FirstAndLastName = "self";
    d.OrganizationalUnit = "my ou";
    d.OrganizationName = "my o";
    d.CityOrLocality = "my city";
    d.StateOrProvince = "my state";
    d.CountryCode = "cc";

    AndroidUtils.RunGenerateKeystore(d);
  }
}

repository | zip file


Additional information on things I tried:

For now I have a very restrictive regex going on but I don't know if this may be problematic: people with non A-Za-z0-9 characters in their names, if someone wants to use special characters in their passwords and so on. Ideally if there's a way to pass parameters safely through a file, I would prefer. Or alternatively, some way to generate an Android compatible keystore in pure C# without relying in Java keytool.

Bluntly stealing code from MSBuild above I managed to cut things out and come up with something like below, which seems like about right as minimum with a similar enough functionality to be useful.

using System;
using System.Text;
using System.Text.RegularExpressions;

namespace AndroidSignTool
{
    public class CommandArgumentsBuilder
    {
        private StringBuilder Cmd { get; } = new StringBuilder();

        private readonly Regex DefinitelyNeedQuotes = new Regex(@"^[a-z\\/:0-9\._\-+=]*$", RegexOptions.None);

        private readonly Regex AllowedUnquoted = new Regex(@"[|><\s,;""]+", RegexOptions.IgnoreCase);

        private bool IsQuotingRequired(string parameter)
        {
            bool isQuotingRequired = false;

            if (parameter != null)
            {
                bool hasAllUnquotedCharacters = AllowedUnquoted.IsMatch(parameter);
                bool hasSomeQuotedCharacters = DefinitelyNeedQuotes.IsMatch(parameter);

                isQuotingRequired = !hasAllUnquotedCharacters;
                isQuotingRequired = isQuotingRequired || hasSomeQuotedCharacters;
            }

            return isQuotingRequired;
        }

        private void AppendTextWithQuoting(string unquotedTextToAppend)
        {
            if (string.IsNullOrEmpty(unquotedTextToAppend))
                return;


            bool addQuotes = IsQuotingRequired(unquotedTextToAppend);

            if (addQuotes)
            {
                Cmd.Append('"');
            }

            // Count the number of quotes
            int literalQuotes = 0;
            for (int i = 0; i < unquotedTextToAppend.Length; i++)
            {
                if (unquotedTextToAppend[i] == '"')
                {
                    literalQuotes++;
                }
            }
            if (literalQuotes > 0)
            {
                // Replace any \" sequences with \\"
                unquotedTextToAppend = unquotedTextToAppend.Replace("\\\"", "\\\\\"");
                // Now replace any " with \"
                unquotedTextToAppend = unquotedTextToAppend.Replace("\"", "\\\"");
            }

            Cmd.Append(unquotedTextToAppend);

            // Be careful any trailing slash doesn't escape the quote we're about to add
            if (addQuotes && unquotedTextToAppend.EndsWith("\\", StringComparison.Ordinal))
            {
                Cmd.Append('\\');
            }

            if (addQuotes)
            {
                Cmd.Append('"');
            }
        }

        public CommandArgumentsBuilder()
        {
        }

        public void AppendSwitch(string switchName)
        {
            if (string.IsNullOrEmpty(switchName))
                return;

            if (Cmd.Length != 0 && Cmd[Cmd.Length - 1] != ' ')
            {
                Cmd.Append(' ');
            }

            Cmd.Append(switchName);
        }

        public void AppendSwitchIfNotNull(string switchName, string parameter)
        {
            if (string.IsNullOrEmpty(switchName) || string.IsNullOrEmpty(parameter))
                return;

            AppendSwitch(switchName);
            AppendTextWithQuoting(parameter);
        }

        public override string ToString() => Cmd.ToString();
    }
}

then the rewritten GetKeystoreGenerationCommand becomes this

public static string GetKeystoreGenerationCommand(AndroidKeystoreData d)
{
    string cert = GetKeystoreCertificateInputString(d);
    string keytool =  "%JAVA_HOME%\\bin\\keytool" ;// GetKeytoolPath();
    string days = (d.ValidityInYears * 365).ToString();

    if (!string.IsNullOrEmpty(cert)) cert = d.KeyAlias;

    var cmd = new CommandArgumentsBuilder();

    cmd.AppendSwitch("echo y | " + keytool);
    cmd.AppendSwitch("-genkeypair");

    cmd.AppendSwitchIfNotNull("-dname", cert);

    cmd.AppendSwitchIfNotNull("-alias", d.KeyAlias);
    cmd.AppendSwitchIfNotNull("-keypass", d.KeyPassword);
    cmd.AppendSwitchIfNotNull("-storepass", d.Password);
    cmd.AppendSwitchIfNotNull("-keystore", d.KeystorePath);
    cmd.AppendSwitchIfNotNull("-validity", days);

    return cmd.ToString();
}

Solution

  • I believe that invoking the keytool binary directly instead of cmd.exe would do the trick if you don't want the user to inject shell commands.