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));
+ }
}
}
}