Skip to content

Commit

Permalink
[Xamarin.Android.Build.Tasks] EnvVar & File support for Signing APKs (#…
Browse files Browse the repository at this point in the history
…3522)

Fixes: #3513

Both `jarsigner` and `apksigner` provide a way to use both files and
environment variables for the store and key passwords.

For `jarsigner` you have to suffix the parameter switch with either
`:env` or `:file` to use those options.  For `apksigner` you have to
prefix the value with either `:env`, `:file` or `:pass`.

We currently only support raw passwords.

This commit adds support for using both `env:` and `file:` for signing.
When providing values for the MSBuild properties such as
`$(AndroidSigningStorePass)` and `$(AndroidSigningKeyPass)` all they
need to do is prefix the value with `env:` or `file:` to use the
alternative parameters.

	/p:AndroidSigningKeyPass=env:MyPasswordEnvVar
	/p:AndroidSigningKeyPass=file:PathToPasswordFile

This will stop passwords appearing in build logs etc.
  • Loading branch information
dellis1972 authored and jonpryor committed Aug 24, 2019
1 parent ed597fb commit 04a4308
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 16 deletions.
38 changes: 38 additions & 0 deletions Documentation/guides/BuildProcess.md
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,15 @@ server, the following MSBuild properties can be used:
the value entered when `keytool` asks **Enter key password for
$(AndroidSigningKeyAlias)**.
You can use the raw password here, however if you want to hide your password in logs
you can use a prefix of env: or file: to point it to an Environment variable or
a file. For example
```
env:<PasswordEnvironentVariable>
file:<PasswordFile>
```
- **AndroidSigningKeyStore** &ndash; Specifies the filename of the
keystore file created by `keytool`. This corresponds to the value
provided to the **keytool -keystore** option.
Expand All @@ -1082,6 +1091,15 @@ server, the following MSBuild properties can be used:
`keytool` when creating the keystore file and asked **Enter
keystore password:**.
You can use the raw password here, however if you want to hide your password in logs
you can use a prefix of env: or file: to point it to an Environment variable or
a file. For example
```
env:<PasswordEnvironentVariable>
file:<PasswordFile>
```
For example, consider the following `keytool` invocation:
```shell
Expand Down Expand Up @@ -1111,6 +1129,26 @@ To use the keystore generated above, use the property group:
</PropertyGroup>
```
To use an environment variable to store your password you can do the following
```xml
<PropertyGroup>
<AndroidSigningStorePass>env:SomeEnvironmentVariableWithThePassword</AndroidSigningStorePass>
<AndroidSigningKeyPass>env:SomeEnvironmentVariableWithThePassword</AndroidSigningKeyPass>
</PropertyGroup>
```
to use a file you can do the following
To use an environment variable to store your password you can do the following
```xml
<PropertyGroup>
<AndroidSigningStorePass>file:SomeFileWithThePassword</AndroidSigningStorePass>
<AndroidSigningKeyPass>file:SomeFileWithThePassword</AndroidSigningKeyPass>
</PropertyGroup>
```
<a name="Build_Actions" />
## Build Actions
Expand Down
34 changes: 32 additions & 2 deletions src/Xamarin.Android.Build.Tasks/Tasks/AndroidApkSigner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,27 @@ public class AndroidApkSigner : JavaToolTask
[Required]
public string KeyAlias { get; set; }

/// <summary>
/// The Password for the Key.
/// You can use the raw password here, however if you want to hide your password in logs
/// you can use a preview of env: or file: to point it to an Environment variable or
/// a file.
///
/// env:<PasswordEnvironentVariable>
/// file:<PasswordFile>
/// </summary>
[Required]
public string KeyPass { get; set; }

/// <summary>
/// The Password for the Keystore.
/// You can use the raw password here, however if you want to hide your password in logs
/// you can use a preview of env: or file: to point it to an Environment variable or
/// a file.
///
/// env:<PasswordEnvironentVariable>
/// file:<PasswordFile>
/// </summary>
[Required]
public string StorePass { get; set; }

Expand All @@ -41,6 +59,18 @@ public override bool Execute ()
return base.Execute ();
}

void AddStorePass (CommandLineBuilder cmd, string cmdLineSwitch, string value)
{
if (value.StartsWith ("env:", StringComparison.Ordinal)) {
cmd.AppendSwitchIfNotNull ($"{cmdLineSwitch} ", value);
}
else if (value.StartsWith ("file:", StringComparison.Ordinal)) {
cmd.AppendSwitchIfNotNull ($"{cmdLineSwitch} file:", value.Replace ("file:", string.Empty));
} else {
cmd.AppendSwitchIfNotNull ($"{cmdLineSwitch} pass:", value);
}
}

protected override string GenerateCommandLineCommands ()
{
var cmd = new CommandLineBuilder ();
Expand All @@ -59,9 +89,9 @@ protected override string GenerateCommandLineCommands ()
cmd.AppendSwitchIfNotNull ("-jar ", ApkSignerJar);
cmd.AppendSwitch ("sign");
cmd.AppendSwitchIfNotNull ("--ks ", KeyStore);
cmd.AppendSwitchIfNotNull ("--ks-pass pass:", StorePass);
AddStorePass (cmd, "--ks-pass", StorePass);
cmd.AppendSwitchIfNotNull ("--ks-key-alias ", KeyAlias);
cmd.AppendSwitchIfNotNull ("--key-pass pass:", KeyPass);
AddStorePass (cmd, "--key-pass", KeyPass);
cmd.AppendSwitchIfNotNull ("--min-sdk-version ", minSdk.ToString ());
cmd.AppendSwitchIfNotNull ("--max-sdk-version ", maxSdk.ToString ());

Expand Down
35 changes: 33 additions & 2 deletions src/Xamarin.Android.Build.Tasks/Tasks/AndroidSignPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,27 @@ public class AndroidSignPackage : AndroidToolTask
[Required]
public string KeyAlias { get; set; }

/// <summary>
/// The Password for the Key.
/// You can use the raw password here, however if you want to hide your password in logs
/// you can use a preview of env: or file: to point it to an Environment variable or
/// a file.
///
/// env:<PasswordEnvironentVariable>
/// file:<PasswordFile>
/// </summary>
[Required]
public string KeyPass { get; set; }

/// <summary>
/// The Password for the Keystore.
/// You can use the raw password here, however if you want to hide your password in logs
/// you can use a preview of env: or file: to point it to an Environment variable or
/// a file.
///
/// env:<PasswordEnvironentVariable>
/// file:<PasswordFile>
/// </summary>
[Required]
public string StorePass { get; set; }

Expand All @@ -48,6 +66,19 @@ public class AndroidSignPackage : AndroidToolTask

protected override string DefaultErrorCode => "ANDJS0000";

void AddStorePass (CommandLineBuilder cmd, string cmdLineSwitch, string value)
{
string pass = value.Replace ("env:", string.Empty).Replace ("file:", string.Empty);
if (value.StartsWith ("env:", StringComparison.Ordinal)) {
cmd.AppendSwitchIfNotNull ($"{cmdLineSwitch}:env ", pass);
}
else if (value.StartsWith ("file:", StringComparison.Ordinal)) {
cmd.AppendSwitchIfNotNull ($"{cmdLineSwitch}:file ", pass);
} else {
cmd.AppendSwitchIfNotNull ($"{cmdLineSwitch} ", pass);
}
}

protected override string GenerateCommandLineCommands ()
{
var fileName = Path.GetFileNameWithoutExtension (UnsignedApk);
Expand All @@ -57,8 +88,8 @@ protected override string GenerateCommandLineCommands ()
cmd.AppendSwitchIfNotNull ("-tsa ", TimestampAuthorityUrl);
cmd.AppendSwitchIfNotNull ("-tsacert ", TimestampAuthorityCertificateAlias);
cmd.AppendSwitchIfNotNull ("-keystore ", KeyStore);
cmd.AppendSwitchIfNotNull ("-storepass ", StorePass);
cmd.AppendSwitchIfNotNull ("-keypass ", KeyPass);
AddStorePass (cmd, "-storepass", StorePass);
AddStorePass (cmd, "-keypass", KeyPass);
cmd.AppendSwitchIfNotNull ("-digestalg ", DigestAlgorithm);
cmd.AppendSwitchIfNotNull ("-sigalg ", SigningAlgorithm);
cmd.AppendSwitchIfNotNull ("-signedjar ", Path.Combine (SignedApkDirectory, $"{fileName}{FileSuffix}{extension}" ));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1670,20 +1670,27 @@ public void BuildLibraryWhichUsesResources ([Values (false, true)] bool isReleas

#pragma warning disable 414
static object [] AndroidStoreKeyTests = new object [] {
// isRelease, AndroidKeyStore, ExpectedResult
new object[] { false , "False" , "debug.keystore"},
new object[] { true , "False" , "debug.keystore"},
new object[] { false , "True" , "-keystore test.keystore"},
new object[] { true , "True" , "-keystore test.keystore"},
new object[] { false , "" , "debug.keystore"},
new object[] { true , "" , "debug.keystore"},
// isRelease, AndroidKeyStore, password, ExpectedResult
new object[] { false , "False" , "android", "debug.keystore"},
new object[] { true , "False" , "android", "debug.keystore"},
new object[] { false , "True" , "android", "-keystore test.keystore"},
new object[] { true , "True" , "android", "-keystore test.keystore"},
new object[] { false , "" , "android", "debug.keystore"},
new object[] { true , "" , "android", "debug.keystore"},
new object[] { false , "True" , "env:android", "-keystore test.keystore"},
new object[] { true , "True" , "env:android", "-keystore test.keystore"},
new object[] { false , "True" , "file:android", "-keystore test.keystore"},
new object[] { true , "True" , "file:android", "-keystore test.keystore"},
};
#pragma warning restore 414

[Test]
[TestCaseSource (nameof (AndroidStoreKeyTests))]
public void TestAndroidStoreKey (bool isRelease, string androidKeyStore, string expected)
public void TestAndroidStoreKey (bool isRelease, string androidKeyStore, string password, string expected)
{
string path = Path.Combine ("temp", TestName);
string storepassfile = Path.Combine (Root, path, "storepass.txt");
string keypassfile = Path.Combine (Root, path, "keypass.txt");
byte [] data;
using (var stream = typeof (XamarinAndroidCommonProject).Assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Base.test.keystore")) {
data = new byte [stream.Length];
Expand All @@ -1692,17 +1699,35 @@ public void TestAndroidStoreKey (bool isRelease, string androidKeyStore, string
var proj = new XamarinAndroidApplicationProject () {
IsRelease = isRelease
};
Dictionary<string, string> envVar = new Dictionary<string, string> ();
if (password.StartsWith ("env:", StringComparison.Ordinal)) {
envVar.Add ("_MYPASSWORD", password.Replace ("env:", string.Empty));
proj.SetProperty ("AndroidSigningStorePass", "env:_MYPASSWORD");
proj.SetProperty ("AndroidSigningKeyPass", "env:_MYPASSWORD");
} else if (password.StartsWith ("file:", StringComparison.Ordinal)) {
proj.SetProperty ("AndroidSigningStorePass", $"file:{storepassfile}");
proj.SetProperty ("AndroidSigningKeyPass", $"file:{keypassfile}");
} else {
proj.SetProperty ("AndroidSigningStorePass", password);
proj.SetProperty ("AndroidSigningKeyPass", password);
}
proj.SetProperty ("AndroidKeyStore", androidKeyStore);
proj.SetProperty ("AndroidSigningKeyStore", "test.keystore");
proj.SetProperty ("AndroidSigningStorePass", "android");
proj.SetProperty ("AndroidSigningKeyAlias", "mykey");
proj.SetProperty ("AndroidSigningKeyPass", "android");
proj.OtherBuildItems.Add (new BuildItem (BuildActions.None, "test.keystore") {
BinaryContent = () => data
});
using (var b = CreateApkBuilder (Path.Combine ("temp", TestName), false, false)) {
proj.OtherBuildItems.Add (new BuildItem (BuildActions.None, "storepass.txt") {
TextContent = () => password.Replace ("file:", string.Empty),
Encoding = Encoding.ASCII,
});
proj.OtherBuildItems.Add (new BuildItem (BuildActions.None, "keypass.txt") {
TextContent = () => password.Replace ("file:", string.Empty),
Encoding = Encoding.ASCII,
});
using (var b = CreateApkBuilder (path, false, false)) {
b.Verbosity = LoggerVerbosity.Diagnostic;
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");
Assert.IsTrue (b.Build (proj, environmentVariables: envVar), "Build should have succeeded.");
StringAssertEx.Contains (expected, b.LastBuildOutput,
"The Wrong keystore was used to sign the apk");
}
Expand Down

0 comments on commit 04a4308

Please sign in to comment.