-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add android keystore authenticator for OSX and Windows
- Loading branch information
Showing
4 changed files
with
371 additions
and
0 deletions.
There are no files selected for viewing
201 changes: 201 additions & 0 deletions
201
Editor/BuildPipelines/AndroidKeystoreAuthenticatorOSX.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
11
Editor/BuildPipelines/AndroidKeystoreAuthenticatorOSX.cs.meta
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
148 changes: 148 additions & 0 deletions
148
Editor/BuildPipelines/AndroidKeystoreAuthenticatorWindows.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
11
Editor/BuildPipelines/AndroidKeystoreAuthenticatorWindows.cs.meta
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.