如何从 C# 代码安全调用 java keytool

How to SAFELY invoke java keytool from C# code

我想在 C# 中创建一个 GUI,用于 运行 keytoolcmd.exe 幕后创建密钥库,包括密钥和证书数据。

然后输入数据需要

不幸的是,人们可能会在他们的密码中键入特殊字符,并且证书信息中也允许 space。

总的来说,我担心有人可能会在某处输入一些信息,这可能会导致灾难性的命令 运行ning 在幕后调用(例如 rm -rf *)。

有没有办法将带有输入信息的 java 属性文件传递给 keytool,或者有什么方法可以安全地转义作为字符串参数传递给的所有数据密钥工具?

我找不到 keytool 可以处理的任何类型的文件,即使是在单独的步骤中,也找不到可以消除此问题的文件。


这是不安全的代码(警告:这是不安全的!!):

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);
  }
}

示例用法为:

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


关于我尝试过的事情的附加信息:

现在我有一个非常严格的正则表达式,但我不知道这是否有问题:名字中包含非 A-Za-z0-9 字符的人,如果有人想在其中使用特殊字符他们的密码等等。理想情况下,如果有一种方法可以通过文件安全地传递参数,我会更喜欢。或者,在不依赖 Java keytool 的情况下,使用纯 C# 生成 Android 兼容密钥库的某种方法。


我直接从上面的 MSBuild 窃取了代码,我设法删掉了一些东西,并提出了类似下面的东西,它看起来至少是正确的,具有足够相似的有用功能。

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();
    }
}

然后重写的GetKeystoreGenerationCommand就变成了这个

    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();
    }

我相信,如果您不希望用户注入 shell 命令,直接调用 keytool 二进制文件而不是 cmd.exe 就可以解决问题。