diff --git a/CredentialProvider.Microsoft/Resources.resx b/CredentialProvider.Microsoft/Resources.resx index 77816b90..3d9f58e2 100644 --- a/CredentialProvider.Microsoft/Resources.resx +++ b/CredentialProvider.Microsoft/Resources.resx @@ -467,4 +467,7 @@ Provide MSAL Cache Location {0} is not an Azure Artifacts feed. + + Unable to write token to credential cache. Exception: {0}, Message: {1} + \ No newline at end of file diff --git a/CredentialProvider.Microsoft/Util/EncryptedFile.cs b/CredentialProvider.Microsoft/Util/EncryptedFile.cs deleted file mode 100644 index 36101c5a..00000000 --- a/CredentialProvider.Microsoft/Util/EncryptedFile.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// -// Licensed under the MIT license. - -using System; -using System.IO; -using System.Security.Cryptography; - -namespace NuGetCredentialProvider.Util -{ - public class EncryptedFile - { - public static byte[] ReadFileBytes(string filePath, bool readUnencrypted = false) - { - try - { - return File.Exists(filePath) ? ProtectedData.Unprotect(File.ReadAllBytes(filePath), null, DataProtectionScope.CurrentUser) : null; - } - catch (NotSupportedException) - { - if (readUnencrypted) - { - return File.Exists(filePath) ? File.ReadAllBytes(filePath) : null; - } - - throw; - } - } - - public static void WriteFileBytes(string filePath, byte[] bytes, bool writeUnencrypted = false) - { - try - { - EnsureDirectoryExists(filePath); - - File.WriteAllBytes(filePath, ProtectedData.Protect(bytes, null, DataProtectionScope.CurrentUser)); - } - catch (NotSupportedException) - { - if (writeUnencrypted) - { - File.WriteAllBytes(filePath, bytes); - return; - } - - throw; - } - } - - private static void EnsureDirectoryExists(string filePath) - { - var directory = Path.GetDirectoryName(filePath); - - if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - } - } -} diff --git a/CredentialProvider.Microsoft/Util/EncryptedFileWithPermissions.cs b/CredentialProvider.Microsoft/Util/EncryptedFileWithPermissions.cs new file mode 100644 index 00000000..04e14034 --- /dev/null +++ b/CredentialProvider.Microsoft/Util/EncryptedFileWithPermissions.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft. All rights reserved. +// +// Licensed under the MIT license. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Security.AccessControl; +using System.Security.Cryptography; +using System.Security.Principal; +using Microsoft.Identity.Client.Extensions.Msal; +using Microsoft.Win32.SafeHandles; + +namespace NuGetCredentialProvider.Util +{ + public class EncryptedFileWithPermissions + { + #region Unix specific + + /// + /// Equivalent to calling open() with flags O_CREAT|O_WRONLY|O_TRUNC. O_TRUNC will truncate the file. + /// See https://man7.org/linux/man-pages/man2/open.2.html + /// + [DllImport("libc", EntryPoint = "creat", SetLastError = true)] + private static extern int PosixCreate([MarshalAs(UnmanagedType.LPStr)] string pathname, int mode); + + [DllImport("libc", EntryPoint = "chmod", SetLastError = true)] + private static extern int PosixChmod([MarshalAs(UnmanagedType.LPStr)] string pathname, int mode); + + #endregion + + public static byte[] ReadFileBytes(string filePath, bool readUnencrypted = false) + { + try + { + return File.Exists(filePath) ? ProtectedData.Unprotect(File.ReadAllBytes(filePath), null, DataProtectionScope.CurrentUser) : null; + } + catch (NotSupportedException) + { + if (readUnencrypted) + { + return File.Exists(filePath) ? File.ReadAllBytes(filePath) : null; + } + + throw; + } + } + + public static void WriteFileBytes(string filePath, byte[] bytes, bool writeUnencrypted = false) + { + try + { + EnsureDirectoryExists(filePath); + + WriteToNewFileWithOwnerRWPermissions(filePath, ProtectedData.Protect(bytes, null, DataProtectionScope.CurrentUser)); + } + catch (NotSupportedException) + { + if (writeUnencrypted) + { + WriteToNewFileWithOwnerRWPermissions(filePath, bytes); + return; + } + + throw; + } + } + + private static void EnsureDirectoryExists(string filePath) + { + var directory = Path.GetDirectoryName(filePath); + + if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + } + + /// + /// Based on https://stackoverflow.com/questions/45132081/file-permissions-on-linux-unix-with-net-core and on + /// https://github.com/NuGet/NuGet.Client/commit/d62db666c710bf95121fe8f5c6a6cbe01985456f and + /// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/b299b2581da87af50fde751e689f1bd4114516ce/src/client/Microsoft.Identity.Client.Extensions.Msal/Accessors/FileWithPermissions.cs + /// + private static void WriteToNewFileWithOwnerRWPermissions(string path, byte[] bytes) + { + + if (SharedUtilities.IsWindowsPlatform()) + { + WriteToNewFileWithOwnerRWPermissionsWindows(path, bytes); + } + else if (SharedUtilities.IsMacPlatform() || SharedUtilities.IsLinuxPlatform()) + { + WriteToNewFileWithOwnerRWPermissionsUnix(path, bytes); + } + else + { + throw new PlatformNotSupportedException(); + } + } + + private static void WriteToNewFileWithOwnerRWPermissionsUnix(string path, byte[] bytes) + { + int _0600 = Convert.ToInt32("600", 8); + + int fileDescriptor = PosixCreate(path, _0600); + + // if creat() fails, then try to use File.Create because it will throw a meaningful exception. + if (fileDescriptor == -1) + { + int posixCreateError = Marshal.GetLastWin32Error(); + using (File.Create(path)) + { + // File.Create() should have thrown an exception with an appropriate error message + } + File.Delete(path); + throw new InvalidOperationException($"libc creat() failed with last error code {posixCreateError}, but File.Create did not"); + } + + var safeFileHandle = new SafeFileHandle((IntPtr)fileDescriptor, ownsHandle: true); + using var fileStream = new FileStream(safeFileHandle, FileAccess.ReadWrite); + fileStream.Write(bytes, 0, bytes.Length); + } + +#pragma warning disable CA1416 // Validate platform compatibility + private static void WriteToNewFileWithOwnerRWPermissionsWindows(string filePath, byte[] bytes) + { + FileSecurity security = new(); + + var rights = FileSystemRights.Read | FileSystemRights.Write; + + security.AddAccessRule( + new FileSystemAccessRule( + WindowsIdentity.GetCurrent().Name, + rights, + InheritanceFlags.None, + PropagationFlags.NoPropagateInherit, + AccessControlType.Allow)); + + security.SetAccessRuleProtection(isProtected: true, preserveInheritance: false); + + FileStream fs = null; + + try + { +#if NET45_OR_GREATER + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + + fs = File.Create(filePath, bytes.Length, FileOptions.None, security); +#else + FileInfo info = new FileInfo(filePath); + fs = info.Create(FileMode.Create, rights, FileShare.Read, bytes.Length, FileOptions.None, security); +#endif + + fs.Write(bytes, 0, bytes.Length); + } + finally + { + fs?.Dispose(); + } + } +#pragma warning restore CA1416 // Validate platform compatibility + } +} diff --git a/CredentialProvider.Microsoft/Util/SessionTokenCache.cs b/CredentialProvider.Microsoft/Util/SessionTokenCache.cs index bfdfe784..b38c1756 100644 --- a/CredentialProvider.Microsoft/Util/SessionTokenCache.cs +++ b/CredentialProvider.Microsoft/Util/SessionTokenCache.cs @@ -221,12 +221,19 @@ private byte[] Serialize(Dictionary data) private byte[] ReadFileBytes() { - return EncryptedFile.ReadFileBytes(cacheFilePath, readUnencrypted: true); + return EncryptedFileWithPermissions.ReadFileBytes(cacheFilePath, readUnencrypted: true); } private void WriteFileBytes(byte[] bytes) { - EncryptedFile.WriteFileBytes(cacheFilePath, bytes, writeUnencrypted: true); + try + { + EncryptedFileWithPermissions.WriteFileBytes(cacheFilePath, bytes, writeUnencrypted: true); + } + catch(Exception e) + { + logger.Verbose(string.Format(Resources.SessionTokenCacheWriteFail, e.GetType(), e.Message)); + } } } }