using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Net; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; using GitHub.Runner.Common.Util; using Newtonsoft.Json; using System.IO; using System.Runtime.Serialization; using System.Security.Cryptography; using GitHub.Runner.Sdk; namespace GitHub.Runner.Common { // The purpose of this class is to store user's credential during runner configuration and retrive the credential back at runtime. #if OS_WINDOWS [ServiceLocator(Default = typeof(WindowsRunnerCredentialStore))] #elif OS_OSX [ServiceLocator(Default = typeof(MacOSRunnerCredentialStore))] #else [ServiceLocator(Default = typeof(LinuxRunnerCredentialStore))] #endif public interface IRunnerCredentialStore : IRunnerService { NetworkCredential Write(string target, string username, string password); // throw exception when target not found from cred store NetworkCredential Read(string target); // throw exception when target not found from cred store void Delete(string target); } #if OS_WINDOWS // Windows credential store is per user. // This is a limitation for user configure the runner run as windows service, when user's current login account is different with the service run as account. // Ex: I login the box as domain\admin, configure the runner as windows service and run as domian\buildserver // domain\buildserver won't read the stored credential from domain\admin's windows credential store. // To workaround this limitation. // Anytime we try to save a credential: // 1. store it into current user's windows credential store // 2. use DP-API do a machine level encrypt and store the encrypted content on disk. // At the first time we try to read the credential: // 1. read from current user's windows credential store, delete the DP-API encrypted backup content on disk if the windows credential store read succeed. // 2. if credential not found in current user's windows credential store, read from the DP-API encrypted backup content on disk, // write the credential back the current user's windows credential store and delete the backup on disk. public sealed class WindowsRunnerCredentialStore : RunnerService, IRunnerCredentialStore { private string _credStoreFile; private Dictionary _credStore; public override void Initialize(IHostContext hostContext) { base.Initialize(hostContext); _credStoreFile = hostContext.GetConfigFile(WellKnownConfigFile.CredentialStore); if (File.Exists(_credStoreFile)) { _credStore = IOUtil.LoadObject>(_credStoreFile); } else { _credStore = new Dictionary(StringComparer.OrdinalIgnoreCase); } } public NetworkCredential Write(string target, string username, string password) { Trace.Entering(); ArgUtil.NotNullOrEmpty(target, nameof(target)); ArgUtil.NotNullOrEmpty(username, nameof(username)); ArgUtil.NotNullOrEmpty(password, nameof(password)); // save to .credential_store file first, then Windows credential store string usernameBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(username)); string passwordBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(password)); // Base64Username:Base64Password -> DP-API machine level encrypt -> Base64Encoding string encryptedUsernamePassword = Convert.ToBase64String(ProtectedData.Protect(Encoding.UTF8.GetBytes($"{usernameBase64}:{passwordBase64}"), null, DataProtectionScope.LocalMachine)); Trace.Info($"Credentials for '{target}' written to credential store file."); _credStore[target] = encryptedUsernamePassword; // save to .credential_store file SyncCredentialStoreFile(); // save to Windows Credential Store return WriteInternal(target, username, password); } public NetworkCredential Read(string target) { Trace.Entering(); ArgUtil.NotNullOrEmpty(target, nameof(target)); IntPtr credPtr = IntPtr.Zero; try { if (CredRead(target, CredentialType.Generic, 0, out credPtr)) { Credential credStruct = (Credential)Marshal.PtrToStructure(credPtr, typeof(Credential)); int passwordLength = (int)credStruct.CredentialBlobSize; string password = passwordLength > 0 ? Marshal.PtrToStringUni(credStruct.CredentialBlob, passwordLength / sizeof(char)) : String.Empty; string username = Marshal.PtrToStringUni(credStruct.UserName); Trace.Info($"Credentials for '{target}' read from windows credential store."); // delete from .credential_store file since we are able to read it from windows credential store if (_credStore.Remove(target)) { Trace.Info($"Delete credentials for '{target}' from credential store file."); SyncCredentialStoreFile(); } return new NetworkCredential(username, password); } else { // Can't read from Windows Credential Store, fail back to .credential_store file if (_credStore.ContainsKey(target) && !string.IsNullOrEmpty(_credStore[target])) { Trace.Info($"Credentials for '{target}' read from credential store file."); // Base64Decode -> DP-API machine level decrypt -> Base64Username:Base64Password -> Base64Decode string decryptedUsernamePassword = Encoding.UTF8.GetString(ProtectedData.Unprotect(Convert.FromBase64String(_credStore[target]), null, DataProtectionScope.LocalMachine)); string[] credential = decryptedUsernamePassword.Split(':'); if (credential.Length == 2 && !string.IsNullOrEmpty(credential[0]) && !string.IsNullOrEmpty(credential[1])) { string username = Encoding.UTF8.GetString(Convert.FromBase64String(credential[0])); string password = Encoding.UTF8.GetString(Convert.FromBase64String(credential[1])); // store back to windows credential store for current user NetworkCredential creds = WriteInternal(target, username, password); // delete from .credential_store file since we are able to write the credential to windows credential store for current user. if (_credStore.Remove(target)) { Trace.Info($"Delete credentials for '{target}' from credential store file."); SyncCredentialStoreFile(); } return creds; } else { throw new ArgumentOutOfRangeException(nameof(decryptedUsernamePassword)); } } throw new Win32Exception(Marshal.GetLastWin32Error(), $"CredRead throw an error for '{target}'"); } } finally { if (credPtr != IntPtr.Zero) { CredFree(credPtr); } } } public void Delete(string target) { Trace.Entering(); ArgUtil.NotNullOrEmpty(target, nameof(target)); // remove from .credential_store file if (_credStore.Remove(target)) { Trace.Info($"Delete credentials for '{target}' from credential store file."); SyncCredentialStoreFile(); } // remove from windows credential store if (!CredDelete(target, CredentialType.Generic, 0)) { throw new Win32Exception(Marshal.GetLastWin32Error(), $"Failed to delete credentials for {target}"); } else { Trace.Info($"Credentials for '{target}' deleted from windows credential store."); } } private NetworkCredential WriteInternal(string target, string username, string password) { // save to Windows Credential Store Credential credential = new Credential() { Type = CredentialType.Generic, Persist = (UInt32)CredentialPersist.LocalMachine, TargetName = Marshal.StringToCoTaskMemUni(target), UserName = Marshal.StringToCoTaskMemUni(username), CredentialBlob = Marshal.StringToCoTaskMemUni(password), CredentialBlobSize = (UInt32)Encoding.Unicode.GetByteCount(password), AttributeCount = 0, Comment = IntPtr.Zero, Attributes = IntPtr.Zero, TargetAlias = IntPtr.Zero }; try { if (CredWrite(ref credential, 0)) { Trace.Info($"Credentials for '{target}' written to windows credential store."); return new NetworkCredential(username, password); } else { int error = Marshal.GetLastWin32Error(); throw new Win32Exception(error, "Failed to write credentials"); } } finally { if (credential.CredentialBlob != IntPtr.Zero) { Marshal.FreeCoTaskMem(credential.CredentialBlob); } if (credential.TargetName != IntPtr.Zero) { Marshal.FreeCoTaskMem(credential.TargetName); } if (credential.UserName != IntPtr.Zero) { Marshal.FreeCoTaskMem(credential.UserName); } } } private void SyncCredentialStoreFile() { Trace.Info("Sync in-memory credential store with credential store file."); // delete the cred store file first anyway, since it's a readonly file. IOUtil.DeleteFile(_credStoreFile); // delete cred store file when all creds gone if (_credStore.Count == 0) { return; } else { IOUtil.SaveObject(_credStore, _credStoreFile); File.SetAttributes(_credStoreFile, File.GetAttributes(_credStoreFile) | FileAttributes.Hidden); } } [DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern bool CredDelete(string target, CredentialType type, int reservedFlag); [DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern bool CredRead(string target, CredentialType type, int reservedFlag, out IntPtr CredentialPtr); [DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern bool CredWrite([In] ref Credential userCredential, [In] UInt32 flags); [DllImport("Advapi32.dll", EntryPoint = "CredFree", SetLastError = true)] internal static extern bool CredFree([In] IntPtr cred); internal enum CredentialPersist : UInt32 { Session = 0x01, LocalMachine = 0x02 } internal enum CredentialType : uint { Generic = 0x01, DomainPassword = 0x02, DomainCertificate = 0x03 } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] internal struct Credential { public UInt32 Flags; public CredentialType Type; public IntPtr TargetName; public IntPtr Comment; public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten; public UInt32 CredentialBlobSize; public IntPtr CredentialBlob; public UInt32 Persist; public UInt32 AttributeCount; public IntPtr Attributes; public IntPtr TargetAlias; public IntPtr UserName; } } #elif OS_OSX public sealed class MacOSRunnerCredentialStore : RunnerService, IRunnerCredentialStore { private const string _osxRunnerCredStoreKeyChainName = "_GITHUB_ACTIONS_RUNNER_CREDSTORE_INTERNAL_"; // Keychain requires a password, but this is not intended to add security private const string _osxRunnerCredStoreKeyChainPassword = "C46F23C36AF94B72B1EAEE32C68670A0"; private string _securityUtil; private string _runnerCredStoreKeyChain; public override void Initialize(IHostContext hostContext) { base.Initialize(hostContext); _securityUtil = WhichUtil.Which("security", true, Trace); _runnerCredStoreKeyChain = hostContext.GetConfigFile(WellKnownConfigFile.CredentialStore); // Create osx key chain if it doesn't exists. if (!File.Exists(_runnerCredStoreKeyChain)) { List securityOut = new List(); List securityError = new List(); object outputLock = new object(); using (var p = HostContext.CreateService()) { p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout) { if (!string.IsNullOrEmpty(stdout.Data)) { lock (outputLock) { securityOut.Add(stdout.Data); } } }; p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr) { if (!string.IsNullOrEmpty(stderr.Data)) { lock (outputLock) { securityError.Add(stderr.Data); } } }; // make sure the 'security' has access to the key so we won't get prompt at runtime. int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root), fileName: _securityUtil, arguments: $"create-keychain -p {_osxRunnerCredStoreKeyChainPassword} \"{_runnerCredStoreKeyChain}\"", environment: null, cancellationToken: CancellationToken.None).GetAwaiter().GetResult(); if (exitCode == 0) { Trace.Info($"Successfully create-keychain for {_runnerCredStoreKeyChain}"); } else { if (securityOut.Count > 0) { Trace.Error(string.Join(Environment.NewLine, securityOut)); } if (securityError.Count > 0) { Trace.Error(string.Join(Environment.NewLine, securityError)); } throw new InvalidOperationException($"'security create-keychain' failed with exit code {exitCode}."); } } } else { // Try unlock and lock the keychain, make sure it's still in good stage UnlockKeyChain(); LockKeyChain(); } } public NetworkCredential Write(string target, string username, string password) { Trace.Entering(); ArgUtil.NotNullOrEmpty(target, nameof(target)); ArgUtil.NotNullOrEmpty(username, nameof(username)); ArgUtil.NotNullOrEmpty(password, nameof(password)); try { UnlockKeyChain(); // base64encode username + ':' + base64encode password // OSX keychain requires you provide -s target and -a username to retrieve password // So, we will trade both username and password as 'secret' store into keychain string usernameBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(username)); string passwordBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(password)); string secretForKeyChain = $"{usernameBase64}:{passwordBase64}"; List securityOut = new List(); List securityError = new List(); object outputLock = new object(); using (var p = HostContext.CreateService()) { p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout) { if (!string.IsNullOrEmpty(stdout.Data)) { lock (outputLock) { securityOut.Add(stdout.Data); } } }; p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr) { if (!string.IsNullOrEmpty(stderr.Data)) { lock (outputLock) { securityError.Add(stderr.Data); } } }; // make sure the 'security' has access to the key so we won't get prompt at runtime. int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root), fileName: _securityUtil, arguments: $"add-generic-password -s {target} -a GITHUBACTIONSRUNNER -w {secretForKeyChain} -T \"{_securityUtil}\" \"{_runnerCredStoreKeyChain}\"", environment: null, cancellationToken: CancellationToken.None).GetAwaiter().GetResult(); if (exitCode == 0) { Trace.Info($"Successfully add-generic-password for {target} (GITHUBACTIONSRUNNER)"); } else { if (securityOut.Count > 0) { Trace.Error(string.Join(Environment.NewLine, securityOut)); } if (securityError.Count > 0) { Trace.Error(string.Join(Environment.NewLine, securityError)); } throw new InvalidOperationException($"'security add-generic-password' failed with exit code {exitCode}."); } } return new NetworkCredential(username, password); } finally { LockKeyChain(); } } public NetworkCredential Read(string target) { Trace.Entering(); ArgUtil.NotNullOrEmpty(target, nameof(target)); try { UnlockKeyChain(); string username; string password; List securityOut = new List(); List securityError = new List(); object outputLock = new object(); using (var p = HostContext.CreateService()) { p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout) { if (!string.IsNullOrEmpty(stdout.Data)) { lock (outputLock) { securityOut.Add(stdout.Data); } } }; p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr) { if (!string.IsNullOrEmpty(stderr.Data)) { lock (outputLock) { securityError.Add(stderr.Data); } } }; int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root), fileName: _securityUtil, arguments: $"find-generic-password -s {target} -a GITHUBACTIONSRUNNER -w -g \"{_runnerCredStoreKeyChain}\"", environment: null, cancellationToken: CancellationToken.None).GetAwaiter().GetResult(); if (exitCode == 0) { string keyChainSecret = securityOut.First(); string[] secrets = keyChainSecret.Split(':'); if (secrets.Length == 2 && !string.IsNullOrEmpty(secrets[0]) && !string.IsNullOrEmpty(secrets[1])) { Trace.Info($"Successfully find-generic-password for {target} (GITHUBACTIONSRUNNER)"); username = Encoding.UTF8.GetString(Convert.FromBase64String(secrets[0])); password = Encoding.UTF8.GetString(Convert.FromBase64String(secrets[1])); return new NetworkCredential(username, password); } else { throw new ArgumentOutOfRangeException(nameof(keyChainSecret)); } } else { if (securityOut.Count > 0) { Trace.Error(string.Join(Environment.NewLine, securityOut)); } if (securityError.Count > 0) { Trace.Error(string.Join(Environment.NewLine, securityError)); } throw new InvalidOperationException($"'security find-generic-password' failed with exit code {exitCode}."); } } } finally { LockKeyChain(); } } public void Delete(string target) { Trace.Entering(); ArgUtil.NotNullOrEmpty(target, nameof(target)); try { UnlockKeyChain(); List securityOut = new List(); List securityError = new List(); object outputLock = new object(); using (var p = HostContext.CreateService()) { p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout) { if (!string.IsNullOrEmpty(stdout.Data)) { lock (outputLock) { securityOut.Add(stdout.Data); } } }; p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr) { if (!string.IsNullOrEmpty(stderr.Data)) { lock (outputLock) { securityError.Add(stderr.Data); } } }; int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root), fileName: _securityUtil, arguments: $"delete-generic-password -s {target} -a GITHUBACTIONSRUNNER \"{_runnerCredStoreKeyChain}\"", environment: null, cancellationToken: CancellationToken.None).GetAwaiter().GetResult(); if (exitCode == 0) { Trace.Info($"Successfully delete-generic-password for {target} (GITHUBACTIONSRUNNER)"); } else { if (securityOut.Count > 0) { Trace.Error(string.Join(Environment.NewLine, securityOut)); } if (securityError.Count > 0) { Trace.Error(string.Join(Environment.NewLine, securityError)); } throw new InvalidOperationException($"'security delete-generic-password' failed with exit code {exitCode}."); } } } finally { LockKeyChain(); } } private void UnlockKeyChain() { Trace.Entering(); ArgUtil.NotNullOrEmpty(_securityUtil, nameof(_securityUtil)); ArgUtil.NotNullOrEmpty(_runnerCredStoreKeyChain, nameof(_runnerCredStoreKeyChain)); List securityOut = new List(); List securityError = new List(); object outputLock = new object(); using (var p = HostContext.CreateService()) { p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout) { if (!string.IsNullOrEmpty(stdout.Data)) { lock (outputLock) { securityOut.Add(stdout.Data); } } }; p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr) { if (!string.IsNullOrEmpty(stderr.Data)) { lock (outputLock) { securityError.Add(stderr.Data); } } }; // make sure the 'security' has access to the key so we won't get prompt at runtime. int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root), fileName: _securityUtil, arguments: $"unlock-keychain -p {_osxRunnerCredStoreKeyChainPassword} \"{_runnerCredStoreKeyChain}\"", environment: null, cancellationToken: CancellationToken.None).GetAwaiter().GetResult(); if (exitCode == 0) { Trace.Info($"Successfully unlock-keychain for {_runnerCredStoreKeyChain}"); } else { if (securityOut.Count > 0) { Trace.Error(string.Join(Environment.NewLine, securityOut)); } if (securityError.Count > 0) { Trace.Error(string.Join(Environment.NewLine, securityError)); } throw new InvalidOperationException($"'security unlock-keychain' failed with exit code {exitCode}."); } } } private void LockKeyChain() { Trace.Entering(); ArgUtil.NotNullOrEmpty(_securityUtil, nameof(_securityUtil)); ArgUtil.NotNullOrEmpty(_runnerCredStoreKeyChain, nameof(_runnerCredStoreKeyChain)); List securityOut = new List(); List securityError = new List(); object outputLock = new object(); using (var p = HostContext.CreateService()) { p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout) { if (!string.IsNullOrEmpty(stdout.Data)) { lock (outputLock) { securityOut.Add(stdout.Data); } } }; p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr) { if (!string.IsNullOrEmpty(stderr.Data)) { lock (outputLock) { securityError.Add(stderr.Data); } } }; // make sure the 'security' has access to the key so we won't get prompt at runtime. int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root), fileName: _securityUtil, arguments: $"lock-keychain \"{_runnerCredStoreKeyChain}\"", environment: null, cancellationToken: CancellationToken.None).GetAwaiter().GetResult(); if (exitCode == 0) { Trace.Info($"Successfully lock-keychain for {_runnerCredStoreKeyChain}"); } else { if (securityOut.Count > 0) { Trace.Error(string.Join(Environment.NewLine, securityOut)); } if (securityError.Count > 0) { Trace.Error(string.Join(Environment.NewLine, securityError)); } throw new InvalidOperationException($"'security lock-keychain' failed with exit code {exitCode}."); } } } } #else public sealed class LinuxRunnerCredentialStore : RunnerService, IRunnerCredentialStore { // 'ghrunner' 128 bits iv private readonly byte[] iv = new byte[] { 0x67, 0x68, 0x72, 0x75, 0x6e, 0x6e, 0x65, 0x72, 0x67, 0x68, 0x72, 0x75, 0x6e, 0x6e, 0x65, 0x72 }; // 256 bits key private byte[] _symmetricKey; private string _credStoreFile; private Dictionary _credStore; public override void Initialize(IHostContext hostContext) { base.Initialize(hostContext); _credStoreFile = hostContext.GetConfigFile(WellKnownConfigFile.CredentialStore); if (File.Exists(_credStoreFile)) { _credStore = IOUtil.LoadObject>(_credStoreFile); } else { _credStore = new Dictionary(StringComparer.OrdinalIgnoreCase); } string machineId; if (File.Exists("/etc/machine-id")) { // try use machine-id as encryption key // this helps avoid accidental information disclosure, but isn't intended for true security machineId = File.ReadAllLines("/etc/machine-id").FirstOrDefault(); Trace.Info($"machine-id length {machineId?.Length ?? 0}."); // machine-id doesn't exist or machine-id is not 256 bits if (string.IsNullOrEmpty(machineId) || machineId.Length != 32) { Trace.Warning("Can not get valid machine id from '/etc/machine-id'."); machineId = "43e7fe5da07740cf914b90f1dac51c2a"; } } else { // /etc/machine-id not exist Trace.Warning("/etc/machine-id doesn't exist."); machineId = "43e7fe5da07740cf914b90f1dac51c2a"; } List keyBuilder = new List(); foreach (var c in machineId) { keyBuilder.Add(Convert.ToByte(c)); } _symmetricKey = keyBuilder.ToArray(); } public NetworkCredential Write(string target, string username, string password) { Trace.Entering(); ArgUtil.NotNullOrEmpty(target, nameof(target)); ArgUtil.NotNullOrEmpty(username, nameof(username)); ArgUtil.NotNullOrEmpty(password, nameof(password)); Trace.Info($"Store credential for '{target}' to cred store."); Credential cred = new Credential(username, Encrypt(password)); _credStore[target] = cred; SyncCredentialStoreFile(); return new NetworkCredential(username, password); } public NetworkCredential Read(string target) { Trace.Entering(); ArgUtil.NotNullOrEmpty(target, nameof(target)); Trace.Info($"Read credential for '{target}' from cred store."); if (_credStore.ContainsKey(target)) { Credential cred = _credStore[target]; if (!string.IsNullOrEmpty(cred.UserName) && !string.IsNullOrEmpty(cred.Password)) { Trace.Info($"Return credential for '{target}' from cred store."); return new NetworkCredential(cred.UserName, Decrypt(cred.Password)); } } throw new KeyNotFoundException(target); } public void Delete(string target) { Trace.Entering(); ArgUtil.NotNullOrEmpty(target, nameof(target)); if (_credStore.ContainsKey(target)) { Trace.Info($"Delete credential for '{target}' from cred store."); _credStore.Remove(target); SyncCredentialStoreFile(); } else { throw new KeyNotFoundException(target); } } private void SyncCredentialStoreFile() { Trace.Entering(); Trace.Info("Sync in-memory credential store with credential store file."); // delete cred store file when all creds gone if (_credStore.Count == 0) { IOUtil.DeleteFile(_credStoreFile); return; } if (!File.Exists(_credStoreFile)) { CreateCredentialStoreFile(); } IOUtil.SaveObject(_credStore, _credStoreFile); } private string Encrypt(string secret) { using (Aes aes = Aes.Create()) { aes.Key = _symmetricKey; aes.IV = iv; // Create a decrytor to perform the stream transform. ICryptoTransform encryptor = aes.CreateEncryptor(); // Create the streams used for encryption. using (MemoryStream msEncrypt = new MemoryStream()) { using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) { using (StreamWriter swEncrypt = new StreamWriter(csEncrypt)) { swEncrypt.Write(secret); } return Convert.ToBase64String(msEncrypt.ToArray()); } } } } private string Decrypt(string encryptedText) { using (Aes aes = Aes.Create()) { aes.Key = _symmetricKey; aes.IV = iv; // Create a decrytor to perform the stream transform. ICryptoTransform decryptor = aes.CreateDecryptor(); // Create the streams used for decryption. using (MemoryStream msDecrypt = new MemoryStream(Convert.FromBase64String(encryptedText))) { using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) { using (StreamReader srDecrypt = new StreamReader(csDecrypt)) { // Read the decrypted bytes from the decrypting stream and place them in a string. return srDecrypt.ReadToEnd(); } } } } } private void CreateCredentialStoreFile() { File.WriteAllText(_credStoreFile, ""); File.SetAttributes(_credStoreFile, File.GetAttributes(_credStoreFile) | FileAttributes.Hidden); // Try to lock down the .credentials_store file to the owner/group var chmodPath = WhichUtil.Which("chmod", trace: Trace); if (!String.IsNullOrEmpty(chmodPath)) { var arguments = $"600 {new FileInfo(_credStoreFile).FullName}"; using (var invoker = HostContext.CreateService()) { var exitCode = invoker.ExecuteAsync(HostContext.GetDirectory(WellKnownDirectory.Root), chmodPath, arguments, null, default(CancellationToken)).GetAwaiter().GetResult(); if (exitCode == 0) { Trace.Info("Successfully set permissions for credentials store file {0}", _credStoreFile); } else { Trace.Warning("Unable to successfully set permissions for credentials store file {0}. Received exit code {1} from {2}", _credStoreFile, exitCode, chmodPath); } } } else { Trace.Warning("Unable to locate chmod to set permissions for credentials store file {0}.", _credStoreFile); } } } [DataContract] internal class Credential { public Credential() { } public Credential(string userName, string password) { UserName = userName; Password = password; } [DataMember(IsRequired = true)] public string UserName { get; set; } [DataMember(IsRequired = true)] public string Password { get; set; } } #endif }