Skip to content

Commit

Permalink
Add android keystore authenticator for OSX and Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
magicjar committed Nov 1, 2021
1 parent 52cb3cb commit a2520df
Show file tree
Hide file tree
Showing 4 changed files with 371 additions and 0 deletions.
201 changes: 201 additions & 0 deletions Editor/BuildPipelines/AndroidKeystoreAuthenticatorOSX.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
#if UNITY_EDITOR_OSX

// https://gist.github.com/sttz/7428deda13722519389ef5b8d91dee66#file-androidkeystoreauthenticator-cs

using System;
using System.IO;
using UnityEditor;
using UnityEditor.Build;
using UnityEngine;

/// <summary>
/// Unity clears Android Keystore and Key Alias passwords when it exits,
/// likely for security reasons.
///
/// This script uses the macOS Keychain to store the passwords securely
/// on your system. The passwords are stored per Keystore and Key Alias,
/// so you only have to enter them once accross all your Unity projects.
///
/// Enter the passwords once like normal in Unity's Player Settings and
/// make a build. On successive builds, the passwords are loaded from the
/// Keychain and you don't have to enter them again.
///
/// To update the passwords, enter the new ones in the Player Settings and
/// then make a build. Use Keychain Access to delete saved passwords,
/// search for "Android Keystore" in the login Keychain.
/// </summary>
public class AndroidKeystoreAuthenticatorOSX : IPreprocessBuild, IPostprocessBuild
{
const string KeychainServiceName = "Android Keystore";

public int callbackOrder => 0;

public void OnPreprocessBuild(BuildTarget target, string path)
{
var keystorePath = PlayerSettings.Android.keystoreName;
if (string.IsNullOrEmpty(keystorePath)
|| !File.Exists(keystorePath)
|| string.IsNullOrEmpty(PlayerSettings.Android.keyaliasName)) {
// Unity will warn the user that the Keystore hasn't been configured yet
return;
}

// Used to only warn once if both Keystore and Key Alias passwords are missing
var hasWarned = false;

var pwd = ManagePassword("Keystore", keystorePath, PlayerSettings.Android.keystorePass, ref hasWarned);
if (pwd != null) {
PlayerSettings.Android.keystorePass = pwd;
}

var keyName = keystorePath + "#" + PlayerSettings.Android.keyaliasName;
pwd = ManagePassword("Key Alias", keyName, PlayerSettings.Android.keyaliasPass, ref hasWarned);
if (pwd != null) {
PlayerSettings.Android.keyaliasPass = pwd;
}
}

public void OnPostprocessBuild(BuildTarget target, string path)
{
// Remove passwords after build
// Note that this is not reliable as it won't be called if the build is cancelled
// In this case Unity will clear the password when it exits
PlayerSettings.Android.keystorePass = null;
PlayerSettings.Android.keyaliasPass = null;
}

/// <summary>
/// Manage a password, adding, updating and loading it from the macOS Keychain
/// as necessary.
/// </summary>
/// <param name="name">Display name to identify password to the user</param>
/// <param name="keychainName">Unique name to store the password in the Keychain</param>
/// <param name="currentPassword">The current password (if any)</param>
/// <returns>The loaded password or null if the password doesn't exist
/// in the Keychain or matches the current password</returns>
string ManagePassword(string name, string keychainName, string currentPassword, ref bool hasWarned)
{
var command = string.Format(
"find-generic-password -a '{0}' -s '{1}' -w",
keychainName, KeychainServiceName
);
string output, error;
var code = Execute("security", command, out output, out error);
if (code != 0) {
if (string.IsNullOrEmpty(currentPassword)) {
if (!hasWarned) {
// Password not saved or set, prompt the user to do so
EditorUtility.DisplayDialog(
"Android Keystore Authenticator",
"The " + name + " password could not found in the Keychain.\n\n"
+ "Set it once in Unity's Player Settings and it will "
+ "be remembered in the Keychain for successive builds.",
"Ok"
);
hasWarned = true;
}
return null;
} else {
// Password set but not yet saved, store it in Keychain
Debug.Log("Adding " + name + " password to Keychain.");
AddPassword(keychainName, currentPassword);
return null;
}
} else if (currentPassword != output) {
if (!string.IsNullOrEmpty(currentPassword)) {
// Password in Keychain differs, update it
Debug.Log("Updating " + name + " password in Keychain.");
AddPassword(keychainName, currentPassword);
return null;
} else {
// Set password from Keychain
return output;
}
}

// Keychain password and Player Settins password match
return null;
}

/// <summary>
/// Add a password to the Keychain, updating it if it already exists
/// </summary>
/// <param name="keychainName">Name of the password in the keychain (user account)</param>
/// <param name="password">The password to save</param>
static void AddPassword(string keychainName, string password)
{
// We use the interactive mode of security here that allows us to
// pipe the command to stdin and thus avoid having the password
// exposed in the process table.
var command = string.Format(
"add-generic-password -U -a '{0}' -s '{1}' -w '{2}'\n",
keychainName, KeychainServiceName, password
);
string output, error;
var code = Execute("security", "-i", command, out output, out error);
if (code != 0) {
Debug.LogError("Failed to store password in Keychain: " + error);
}
}

/// <summary>
/// Call the security command line tool to set and get macOS Keychain passwords.
/// </summary>
/// <param name="command">The command to execute.</param>
/// <param name="arguments">Arguments passed to the command.</param>
/// <param name="output">The standard output of the command.</param>
/// <param name="error">The standard error of the command.</param>
/// <returns>The exit code of the command.</returns>
static int Execute(string command, string arguments, out string output, out string error)
{
var proc = new System.Diagnostics.Process();
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardError = true;
proc.StartInfo.RedirectStandardOutput = true;
proc.StartInfo.FileName = command;
proc.StartInfo.Arguments = arguments;

proc.Start();
proc.WaitForExit();

output = proc.StandardOutput.ReadToEnd();
error = proc.StandardError.ReadToEnd();
return proc.ExitCode;
}

/// <summary>
/// Call the security command line tool to set and get macOS Keychain passwords.
/// </summary>
/// <param name="command">The command to execute.</param>
/// <param name="arguments">Arguments passed to the command.</param>
/// <param name="input">Data to write to the command's standard input.</param>
/// <param name="output">The standard output of the command.</param>
/// <param name="error">The standard error of the command.</param>
/// <returns>The exit code of the command.</returns>
static int Execute(string command, string arguments, string input, out string output, out string error)
{
var proc = new System.Diagnostics.Process();
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardInput = true;
proc.StartInfo.RedirectStandardError = true;
proc.StartInfo.RedirectStandardOutput = true;
proc.StartInfo.FileName = command;
proc.StartInfo.Arguments = arguments;

proc.Start();

// Unity's old Mono runtime writes a BOM to the input stream,
// tripping up the command. Ceate a new writer with an encoding
// that has BOM disabled.
var writer = new StreamWriter(proc.StandardInput.BaseStream, new System.Text.UTF8Encoding(false));
writer.Write(input);
writer.Close();

proc.WaitForExit();

output = proc.StandardOutput.ReadToEnd();
error = proc.StandardError.ReadToEnd();
return proc.ExitCode;
}
}
#endif
11 changes: 11 additions & 0 deletions Editor/BuildPipelines/AndroidKeystoreAuthenticatorOSX.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

148 changes: 148 additions & 0 deletions Editor/BuildPipelines/AndroidKeystoreAuthenticatorWindows.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#if UNITY_EDITOR_WIN

using System;
using System.IO;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEngine;
using System.Text;
using System.Reflection;

public class AndroidKeystoreAuthenticatorWindows : IPreprocessBuildWithReport, IPostprocessBuildWithReport
{
public int callbackOrder => 0;

public void OnPreprocessBuild(BuildReport report)
{
var keystorePath = PlayerSettings.applicationIdentifier + "#Keystore";
if (string.IsNullOrEmpty(keystorePath) || !File.Exists(PlayerSettings.Android.keystoreName) || string.IsNullOrEmpty(PlayerSettings.Android.keyaliasName))
{
// Unity will warn the user that the Keystore hasn't been configured yet
return;
}

// Used to only warn once if both Keystore and Key Alias passwords are missing
var hasWarned = false;

var pwd = ManagePassword("Keystore", keystorePath, PlayerSettings.Android.keystorePass, ref hasWarned);
if (pwd != null)
{
PlayerSettings.Android.keystorePass = pwd;
}

var keyName = PlayerSettings.applicationIdentifier + "#Keyalias";
pwd = ManagePassword("Key Alias", keyName, PlayerSettings.Android.keyaliasPass, ref hasWarned);
if (pwd != null)
{
PlayerSettings.Android.keyaliasPass = pwd;
}
}

public void OnPostprocessBuild(BuildReport report)
{
// Remove passwords after build
// Note that this is not reliable as it won't be called if the build is cancelled
// In this case Unity will clear the password when it exits
PlayerSettings.Android.keystorePass = null;
PlayerSettings.Android.keyaliasPass = null;
}

static string ManagePassword(string name, string keyName, string currentPassword, ref bool hasWarned)
{
string output;
var code = Execute(keyName, "", "-load", out output);
Debug.Log("Output " + output + " Code " + code);
if (code != 0)
{
if (string.IsNullOrEmpty(currentPassword))
{
if (!hasWarned)
{
// Password not saved or set, prompt the user to do so
EditorUtility.DisplayDialog(
"Android Keystore Authenticator",
"The " + name + " password could not found in the EditorPrefs.\n\n"
+ "Set it once in Unity's Player Settings and it will "
+ "be remembered in the EditorPrefs for successive builds.",
"Ok"
);
hasWarned = true;
}
return null;
}
else
{
// Password set but not yet saved, store it in EditorPrefs
Debug.Log("Adding " + name + " password to EditorPrefs.");
AddPassword(keyName, currentPassword);
return null;
}
}
else if (currentPassword != output)
{
if (!string.IsNullOrEmpty(currentPassword))
{
// Password in EditorPrefs differs, update it
Debug.Log("Updating " + name + " password in EditorPrefs.");
AddPassword(keyName, currentPassword);
return null;
}
else
{
// Set password from EditorPrefs
return output;
}
}

// EditorPrefs password and Player Settins password match
return null;
}

static void AddPassword(string keyName, string password)
{
// We use the interactive mode of security here that allows us to
// pipe the command to stdin and thus avoid having the password
// exposed in the process table.
string output;
var code = Execute(keyName, password, "-store", out output);
if (code != 0)
{
Debug.LogError("Failed to store password in EditorPrefs");
}
}

static int Execute(string key, string value, string arguments, out string output)
{
string codeBase = Assembly.GetExecutingAssembly().CodeBase;

if (arguments == "-store")
{
EditorPrefs.SetString(key, Encode(value));
}

try
{
output = Decode(EditorPrefs.GetString(key, ""));
return 0;
}
catch (System.Exception)
{
output = "";
return 1;
}
}

static string Encode(string inputText)
{
byte[] bytesToEncode = Encoding.UTF8.GetBytes(inputText);
return Convert.ToBase64String(bytesToEncode);
}

static string Decode(string encodedText)
{
byte[] decodedBytes = Convert.FromBase64String(encodedText);
return Encoding.UTF8.GetString(decodedBytes);
}
}
#endif
11 changes: 11 additions & 0 deletions Editor/BuildPipelines/AndroidKeystoreAuthenticatorWindows.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit a2520df

Please sign in to comment.