LinuxCommunicator/Credentials.cs (520 lines of code) (raw):

//------------------------------------------------------------------------------ // <copyright file="Credentials.cs" company="Microsoft"> // Copyright (c) Microsoft Corporation. All rights reserved. // </copyright> //------------------------------------------------------------------------------ namespace Microsoft.Hpc.Communicators.LinuxCommunicator { #region Using directives using System; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Security; using System.Security.Cryptography; using System.Text; #endregion /// <summary> /// The Credentials class implements some common functionality for /// validating user credentials and creating logon tokens /// </summary> internal static class Credentials { #region PublicMethods public const int LOGON32_LOGON_INTERACTIVE = 2; public const int LOGON32_LOGON_NETWORK = 3; public const int LOGON32_LOGON_BATCH = 4; public const int LOGON32_LOGON_SERVICE = 5; public const int LOGON32_LOGON_UNLOCK = 7; public const int LOGON32_LOGON_NETWORK_CLEARTEXT = 8; public const int LOGON32_LOGON_NEW_CREDENTIALS = 9; public const int LOGON32_PROVIDER_DEFAULT = 0; public const int DEBUG_PROCESS = 0x00000001; public const int DEBUG_ONLY_THIS_PROCESS = 0x00000002; public const int CREATE_SUSPENDED = 0x00000004; public const int DETACHED_PROCESS = 0x00000008; public const int CREATE_NEW_CONSOLE = 0x00000010; public const int NORMAL_PRIORITY_CLASS = 0x00000020; public const int IDLE_PRIORITY_CLASS = 0x00000040; public const int HIGH_PRIORITY_CLASS = 0x00000080; public const int REALTIME_PRIORITY_CLASS = 0x00000100; public const int CREATE_NEW_PROCESS_GROUP = 0x00000200; public const int CREATE_UNICODE_ENVIRONMENT = 0x00000400; public const int CREATE_SEPARATE_WOW_VDM = 0x00000800; public const int CREATE_SHARED_WOW_VDM = 0x00001000; public const int CREATE_FORCEDOS = 0x00002000; public const int CREATE_DEFAULT_ERROR_MODE = 0x04000000; public const int CREATE_NO_WINDOW = 0x08000000; public const int CRED_MAX_USERNAME_LENGTH = (256 + 1 + 256); public const int CRED_MAX_PASSWORD_LENGTH = 256; internal static int ValidateCredentials(string username, string password, bool throwException) { try { using (SafeToken token = LogonUserAndGetToken(username, password)) { return 0; } } catch (Win32Exception exception) { if (throwException) { throw new System.Security.Authentication.AuthenticationException(exception.Message, exception); } return exception.ErrorCode; } } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Someone may use this function in the future")] internal static SafeToken GetTokenFromCredentials(string username, string password) { try { return LogonUserAndGetToken(username, password); } catch (Win32Exception exception) { throw new System.Security.Authentication.AuthenticationException(exception.Message, exception); } } static SafeToken LogonUserAndGetToken(string username, string password) { return SafeToken.LogonUser( username, password, LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT); } const CredentialNativeMethods.CREDUI_FLAGS DefaultCredUIFlags = CredentialNativeMethods.CREDUI_FLAGS.GENERIC_CREDENTIALS | CredentialNativeMethods.CREDUI_FLAGS.EXCLUDE_CERTIFICATES | CredentialNativeMethods.CREDUI_FLAGS.DO_NOT_PERSIST | CredentialNativeMethods.CREDUI_FLAGS.ALWAYS_SHOW_UI; /// <summary> /// Prompt for credentials without showing the "Save" checkbox /// </summary> /// <param name="target">Target to the credentials</param> /// <param name="username">receives user name</param> /// <param name="password">receives password associated</param> /// <param name="fConsole">Is this a console application</param> /// <param name="hwndParent">Parent window handle</param> [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Someone may use this function in the future")] internal static void PromptForCredentials(string target, ref string username, ref SecureString password, bool fConsole, IntPtr hwndParent) { bool fSave = false; PromptForCredentials(target, ref username, ref password, ref fSave, fConsole, hwndParent, DefaultCredUIFlags, null); } /// <summary> /// Prompt for credentials with default options. "Save" checkbox is shown. /// </summary> /// <param name="target">Target to the credentials</param> /// <param name="username">receives user name</param> /// <param name="password">receives password associated</param> /// <param name="fSave">receives if credentials are saved.</param> /// <param name="fConsole">Is this a console application</param> /// <param name="hwndParent">Parent window handle</param> internal static void PromptForCredentials(string target, ref string username, ref SecureString password, ref bool fSave, bool fConsole, IntPtr hwndParent) { CredentialNativeMethods.CREDUI_FLAGS flags = DefaultCredUIFlags | CredentialNativeMethods.CREDUI_FLAGS.SHOW_SAVE_CHECK_BOX; PromptForCredentials(target, ref username, ref password, ref fSave, fConsole, hwndParent, flags, null); } /// <summary> /// Prompt for credentials /// </summary> /// <param name="target">Target to the credentials</param> /// <param name="username">receives user name</param> /// <param name="password">receives password associated</param> /// <param name="fSave">receives if credentials are saved.</param> /// <param name="fConsole">Is this a console application</param> /// <param name="hwndParent">Parent window handle</param> /// <param name="flags">options for the window.</param> /// <param name="message">Message to end user included in the window.</param> internal static void PromptForCredentials(string target, ref string username, ref SecureString password, ref bool fSave, bool fConsole, IntPtr hwndParent, CredentialNativeMethods.CREDUI_FLAGS flags, string message) { CredentialNativeMethods.CREDUI_INFO info = new CredentialNativeMethods.CREDUI_INFO(); int result; int saveCredentials; StringBuilder user = new StringBuilder(username, CRED_MAX_USERNAME_LENGTH); StringBuilder pwd = new StringBuilder(CRED_MAX_PASSWORD_LENGTH); saveCredentials = 0; info.cbSize = Marshal.SizeOf(info); info.hwndParent = hwndParent; if (!string.IsNullOrEmpty(message)) { // Use default message text, when message is not specified. info.pszMessageText = message; } if (fConsole) { result = CredentialNativeMethods.CredUICmdLinePromptForCredentials(target, IntPtr.Zero, 0, user, CRED_MAX_USERNAME_LENGTH, pwd, CRED_MAX_PASSWORD_LENGTH, ref saveCredentials, flags); } else { if (hwndParent == new IntPtr(-1)) { throw new System.Security.Authentication.InvalidCredentialException(); } result = CredentialNativeMethods.CredUIPromptForCredentials(ref info, target, IntPtr.Zero, 0, user, CRED_MAX_USERNAME_LENGTH, pwd, CRED_MAX_PASSWORD_LENGTH, ref saveCredentials, flags); } if (result != 0) { throw new System.ComponentModel.Win32Exception(result); } fSave = (saveCredentials != 0); username = user.ToString(); password = new SecureString(); for (int i = 0; i < pwd.Length; i++) { password.AppendChar(pwd[i]); pwd[i] = ' '; // overwrite the cleartext password } password.MakeReadOnly(); } // // helper function to convert a SecureString password back to a cleartext string // [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal static string UnsecureString(SecureString secureString) { if (secureString == null) { return null; } string unsecureString; IntPtr ptr = Marshal.SecureStringToGlobalAllocUnicode(secureString); try { unsecureString = Marshal.PtrToStringUni(ptr); } finally { Marshal.ZeroFreeGlobalAllocUnicode(ptr); } return unsecureString; } #endregion #region Operate with local accounts internal static bool ExistsLocalAccount(string username) { IntPtr infoPtr = IntPtr.Zero; try { int errCode = CredentialNativeMethods.NetUserGetInfo(null, username, 1, out infoPtr); if (errCode == CredentialNativeMethods.NERR_Success) { return true; } else if (errCode == CredentialNativeMethods.NERR_UserNotFound) { return false; } throw new Exception("Error when checking whether the local account " + username + " exists. Error code: " + errCode); } finally { if (infoPtr != IntPtr.Zero) { try { Marshal.FreeHGlobal(infoPtr); } catch { } } } } /// <summary> /// In this function, we assume the caller has already checked that the user exists. /// Check it using ExistsLocalAccount method. /// </summary> /// <param name="username"></param> /// <param name="newPassword"></param> /// <param name="oldPassword"></param> internal static void SetLocalAccountPassword(string username, SecureString newPassword, SecureString oldPassword) { IntPtr newPlainPassword = IntPtr.Zero; IntPtr oldPlainPassword = IntPtr.Zero; try { if (newPassword == null) { newPlainPassword = Marshal.StringToCoTaskMemUni(String.Empty); } else { newPlainPassword = Marshal.SecureStringToCoTaskMemUnicode(newPassword); } if (oldPassword == null) { oldPlainPassword = Marshal.StringToCoTaskMemUni(String.Empty); } else { oldPlainPassword = Marshal.SecureStringToCoTaskMemUnicode(oldPassword); } // Note: the first parameter of NetUserChangePassword is domain name. If it // is NULL, the logon domain of the caller, instead of local computer, is used. // This is unlike NetUserAdd, NetUserGetInfo or NetUserSetInfo. uint errCode = CredentialNativeMethods.NetUserChangePassword( Environment.MachineName, username, oldPlainPassword, newPlainPassword); if (errCode != CredentialNativeMethods.NERR_Success) { switch (errCode) { case CredentialNativeMethods.NERR_UserNotFound: throw new ArgumentException("The local user " + username + " is not found."); case CredentialNativeMethods.NERR_PasswordTooShort: // If password is long enough but still you receive error // "NERR_PasswordTooShort" in a DOMAIN environment, then it is probably // an indication that the time span since the previous password change // operation is shorter than the allowed policy. It is a policy in AD to // have a minimum password age, for example, of 1 day. throw new ArgumentException("The password given is too short."); case CredentialNativeMethods.ERROR_INVALID_PASSWORD: throw new ArgumentException("The given old password is not valid."); default: throw new Exception("Error when trying to reset password for local account " + username + " exists. Error code: " + errCode); } } } finally { if (newPlainPassword != IntPtr.Zero) { Marshal.ZeroFreeCoTaskMemUnicode(newPlainPassword); } if (oldPlainPassword != IntPtr.Zero) { Marshal.ZeroFreeCoTaskMemUnicode(oldPlainPassword); } } } /// <summary> /// Set password for a specified local account regardless of old password /// In this function, we assume the caller has already checked that the user exists. /// Check it using ExistsLocalAccount method. /// </summary> /// <param name="username">local account name</param> /// <param name="password">new password for the specified local account</param> internal static void SetLocalAccountPassword(string username, SecureString password) { CredentialNativeMethods.USER_INFO_1003 info = new CredentialNativeMethods.USER_INFO_1003(); if (password == null) { info.sPassword = Marshal.StringToCoTaskMemUni(String.Empty); } else { info.sPassword = Marshal.SecureStringToCoTaskMemUnicode(password); } IntPtr infoPtr = IntPtr.Zero; try { infoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(info)); Marshal.StructureToPtr(info, infoPtr, false); uint errParam; int errCode = CredentialNativeMethods.NetUserSetInfo(null, username, /*information level of infoPtr =*/1003, infoPtr, out errParam); if (errCode != CredentialNativeMethods.NERR_Success) { switch (errCode) { case CredentialNativeMethods.NERR_UserNotFound: throw new ArgumentException("The local user " + username + " is not found."); default: throw new Exception("Error when setting password for local account " + username + ". Error code: " + errCode); } } } finally { if (info.sPassword != IntPtr.Zero) { Marshal.ZeroFreeCoTaskMemUnicode(info.sPassword); } if (infoPtr != IntPtr.Zero) { try { Marshal.FreeHGlobal(infoPtr); } catch { } } } } /// <summary> /// In this function, we assume the caller has already checked that the user does not exist. /// Check it using ExistsLocalAccount method /// </summary> /// <param name="username"></param> /// <param name="password"></param> /// <param name="isAdmin"></param> /// <param name="schedulerOnAzure"></param> internal static void AddLocalAccount(string username, string password, bool isAdmin, bool schedulerOnAzure) { // Call the SecureString version SecureString securePass = new SecureString(); foreach (char c in password.ToCharArray()) { securePass.AppendChar(c); } AddLocalAccount(username, securePass, isAdmin, schedulerOnAzure); } /// <summary> /// In this function, we assume the caller has already checked that the user does not exist. /// Check it using ExistsLocalAccount method /// </summary> /// <param name="username"></param> /// <param name="password"></param> /// <param name="isAdmin"></param> /// <param name="schedulerOnAzure"></param> internal static void AddLocalAccount(string username, SecureString password, bool isAdmin, bool schedulerOnAzure) { CredentialNativeMethods.USER_INFO_1 info = new CredentialNativeMethods.USER_INFO_1(); info.sUsername = username; if (password == null) { info.sPassword = Marshal.StringToCoTaskMemUni(String.Empty); } else { info.sPassword = Marshal.SecureStringToCoTaskMemUnicode(password); } info.uiPriv = CredentialNativeMethods.USER_PRIV_USER; info.sHome_Dir = null; info.sComment = null; info.sScript_Path = null; info.uiFlags = CredentialNativeMethods.UF_SCRIPT; // Check the string length to prevent memory overflow if (info.sUsername.Length > 128) { throw new ArgumentException("The user name is too long."); } if (password != null && password.Length > 512) { throw new ArgumentException("The password is too long."); } try { uint errParam = 0; int errCode = CredentialNativeMethods.NetUserAdd(null, 1, ref info, out errParam); if (errCode != CredentialNativeMethods.NERR_Success) { switch (errCode) { case CredentialNativeMethods.NERR_UserExists: throw new ArgumentException("The local user " + username + " already exists."); case CredentialNativeMethods.NERR_PasswordTooShort: throw new ArgumentException("The password given is too short."); default: throw new Exception("Error when trying to create local account " + username + " exists. Error code: " + errCode + ", sub-error: " + errParam); } } } finally { if (info.sPassword != IntPtr.Zero) { try { Marshal.ZeroFreeCoTaskMemUnicode(info.sPassword); } catch { } } } AddUserGroups(username, isAdmin, schedulerOnAzure); } /// <summary> /// Add a local account to related user and administrator groups /// </summary> /// <param name="username">local accout name</param> /// <param name="isAdmin">a flag indicating if the specified local account is admin</param> /// <param name="schedulerOnAzure">a flag indicating if scheduler is on on Azure</param> internal static void AddUserGroups(string username, bool isAdmin, bool schedulerOnAzure) { if (isAdmin) { AddUserGroup(username, "Administrators"); if (schedulerOnAzure) { AddUserGroup(username, "HPCAdminMirror"); } } AddUserGroup(username, "Users"); if (schedulerOnAzure) { AddUserGroup(username, "HPCUsers"); } } static void AddUserGroup(string username, string groupname) { CredentialNativeMethods.LOCALGROUP_MEMBERS_INFO_3 grpinfo; grpinfo.Domain = username; int errCode = 0; // add the user to the administrators group errCode = CredentialNativeMethods.NetLocalGroupAddMembers(null, groupname, 3, ref grpinfo, 1); if (errCode != 0 && errCode != CredentialNativeMethods.ERROR_MEMBER_IN_ALIAS) { throw new Exception("Error when trying to add local account " + username + " to group " + groupname + ". Error code:" + errCode); } } /// <summary> /// In this function, we assume the caller has already checked that the user exists. /// Check it using ExistsLocalAccount method. /// </summary> /// <param name="username"></param> internal static void DeleteLocalAccount(string username) { int errCode = CredentialNativeMethods.NetUserDel(null, username); if (errCode != CredentialNativeMethods.NERR_Success) { switch (errCode) { case CredentialNativeMethods.NERR_UserNotFound: throw new ArgumentException("The local user " + username + " is not found."); default: throw new Exception("Error when trying to delete local account " + username + " exists. Error code: " + errCode); } } } internal static string ToLocalAccount(string domainAccount) { if (string.IsNullOrWhiteSpace(domainAccount)) { throw new ArgumentNullException("domainAccount"); } string domainname = null; string username = null; if (domainAccount.Contains("\\")) { if (domainAccount.StartsWith("NT AUTHORITY\\", StringComparison.InvariantCultureIgnoreCase)) { return domainAccount; } try { string[] strs = domainAccount.Split(new char[] { '\\' }, StringSplitOptions.None); domainname = strs[0]; username = strs[1]; } catch { throw new ArgumentException(); } } else if (domainAccount.Contains("@")) { try { string[] strs = domainAccount.Split(new char[] { '@' }, StringSplitOptions.None); username = strs[0]; domainname = strs[1]; } catch { throw new ArgumentException(); } } else { return domainAccount; } return username; } /// <summary> /// Maximum number of characters in a user name /// </summary> const int MaxAccountNameLength = 20; /// <summary> /// Maximum number of characers from the /// domain user name to be used as the prefix /// of the generated user name /// </summary> const int MaxPrefixLength = 10; /// <summary> /// Maximum number of retries for hashing /// </summary> const int MaxHashRetryCount = 50; /// <summary> /// Delimiters valid in a user name /// </summary> private static readonly char[] delimiters = new char[] { '-', '_', ' ', '.', '#' }; /// <summary> /// Invalid Base64 characters for a user name string /// </summary> private static readonly char[] invalidBase64UserNameChars = new char[] { '+', '/', '=' }; /// <summary> /// Convert a domain account name into a unique local account name. /// </summary> /// <param name="domainAccount">domain account name</param> /// <returns>a unique local account name</returns> internal static string ToUniqueLocalAccount(string domainAccount) { if (string.IsNullOrWhiteSpace(domainAccount)) { throw new ArgumentNullException("domainAccount"); } string uniqueLocalAccount; if (domainAccount.StartsWith("NT AUTHORITY\\")) { uniqueLocalAccount = domainAccount; } else { // unique local account = Purged local name + suffix (hash code of domain account) string localname = Credentials.ToLocalAccount(domainAccount); // Remove all delimiters from the source string purged = RemoveChars(localname, delimiters); // We only take the first MaxPrefixLength characters from // the seed user name so as to guarantee at least // MaxPrefixLength characters from the hash. if (purged.Length > MaxPrefixLength) { purged = purged.Substring(0, MaxPrefixLength); } // The length is 10 or larger int suffixLength = MaxAccountNameLength - purged.Length; var builder = new StringBuilder(); builder.Append(purged); using (SHA256 sha256 = SHA256.Create()) { string suffix = localname; int retry = 0; do { sha256.ComputeHash(Encoding.Default.GetBytes(suffix)); byte[] hash = sha256.Hash; suffix = RemoveChars(Convert.ToBase64String(hash), invalidBase64UserNameChars); retry++; if (retry > MaxHashRetryCount) { throw new ApplicationException( "Failed to create a hash suffix to create a user name after maximum retries."); } } while (suffix.Length < suffixLength); builder.Append(suffix.Substring(0, suffixLength)); } uniqueLocalAccount = builder.ToString(); } return uniqueLocalAccount; } /// <summary> /// Remove characters from the given list in a string /// </summary> /// <param name="source">The string to purge</param> /// <param name="toRemove">The list of characters to remove</param> /// <returns>The purged string</returns> private static string RemoveChars(string source, char[] toRemove) { Debug.Assert( string.IsNullOrEmpty(source) == false, "Input string should not be null or empty."); Debug.Assert( toRemove != null, "The array of characters to remove cannot be null."); var builder = new StringBuilder(); for (int i = 0; i < source.Length; i++) { if (!toRemove.Any(d => d == source[i])) { builder.Append(source[i]); } } return builder.ToString(); } #endregion } }