using System; using System.IO; using UnityEditor; using UnityEditor.Build; using UnityEngine; #if UNITY_EDITOR_OSX /// /// 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. /// public class AndroidKeystoreAuthenticator : IPreprocessBuild, IPostprocessBuild { const string KeychainServiceName = "Android Keystore"; public int callbackOrder { get { return 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; } /// /// Manage a password, adding, updating and loading it from the macOS Keychain /// as necessary. /// /// Display name to identify password to the user /// Unique name to store the password in the Keychain /// The current password (if any) /// The loaded password or null if the password doesn't exist /// in the Keychain or matches the current password 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; } /// /// Add a password to the Keychain, updating it if it already exists /// /// Name of the password in the keychain (user account) /// The password to save 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); } } /// /// Call the security command line tool to set and get macOS Keychain passwords. /// /// The command to execute. /// Arguments passed to the command. /// The standard output of the command. /// The standard error of the command. /// The exit code of the command. 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; } /// /// Call the security command line tool to set and get macOS Keychain passwords. /// /// The command to execute. /// Arguments passed to the command. /// Data to write to the command's standard input. /// The standard output of the command. /// The standard error of the command. /// The exit code of the command. 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