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.cs
source 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();
}
CodePudding user response:
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.