'How to SAFELY invoke java keytool from C# code [closed]
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's 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);
}
}
Additional information on things I tried:
I am in .NET 4.6.2, and I know about
CommandLineBuilderExtension, but it's docs starts saying to not use it: This API supports the product infrastructure and is not intended to be used directly from your code.Xamarin related codebase seems to rely on whatever commandlinebuilderextension does
Looking the
CommandLineBuilder.cssource code, I can't tell how well it escapes (it includes a comment on code injection) and what a minimum version of it for only the purpose of the code above would look like.
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.
Bluntely 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();
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
