Skip to content

Commit aa0d913

Browse files
committed
dpapi: add unit tests of DPAPI cred store
1 parent ea8f17e commit aa0d913

File tree

2 files changed

+128
-2
lines changed

2 files changed

+128
-2
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System;
2+
using Xunit;
3+
using Microsoft.Git.CredentialManager.Interop.Windows;
4+
using Microsoft.Git.CredentialManager.Tests.Objects;
5+
using System.IO;
6+
using System.Text;
7+
using System.Security.Cryptography;
8+
9+
namespace Microsoft.Git.CredentialManager.Tests.Interop.Windows
10+
{
11+
public class DpapiCredentialStoreTests
12+
{
13+
private const string TestStoreRoot = @"C:\dpapi_store";
14+
private const string TestNamespace = "git-test";
15+
16+
[PlatformFact(Platforms.Windows)]
17+
public void DpapiCredentialStore_AddOrUpdate_CreatesUTF8ProtectedFile()
18+
{
19+
var fs = new TestFileSystem();
20+
var store = new DpapiCredentialStore(fs, TestStoreRoot, TestNamespace);
21+
22+
string service = "https://example.com";
23+
const string userName = "john.doe";
24+
const string password = "letmein123"; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Fake credential")]
25+
26+
string expectedServiceSlug = Path.Combine(TestNamespace, "https", "example.com");
27+
string expectedFileName = $"{userName}.credential";
28+
string expectedFilePath = Path.Combine(TestStoreRoot, expectedServiceSlug, expectedFileName);
29+
30+
store.AddOrUpdate(service, userName, password);
31+
32+
Assert.True(fs.Directories.Contains(Path.Combine(TestStoreRoot, expectedServiceSlug)));
33+
Assert.True(fs.Files.TryGetValue(expectedFilePath, out byte[] data));
34+
35+
string contents = Encoding.UTF8.GetString(data);
36+
Assert.False(string.IsNullOrWhiteSpace(contents));
37+
38+
string[] lines = contents.Split(Environment.NewLine);
39+
40+
Assert.Equal(4, lines.Length);
41+
42+
byte[] cryptoData = Convert.FromBase64String(lines[0]);
43+
byte[] plainData = ProtectedData.Unprotect(cryptoData, null, DataProtectionScope.CurrentUser);
44+
string plainLine0 = Encoding.UTF8.GetString(plainData);
45+
46+
Assert.Equal(password, plainLine0);
47+
Assert.Equal($"service={service}", lines[1]);
48+
Assert.Equal($"account={userName}", lines[2]);
49+
Assert.True(string.IsNullOrWhiteSpace(lines[3]));
50+
}
51+
52+
[PlatformFact(Platforms.Windows)]
53+
public void DpapiCredentialStore_Get_KeyNotFound_ReturnsNull()
54+
{
55+
var fs = new TestFileSystem();
56+
var store = new DpapiCredentialStore(fs, TestStoreRoot, TestNamespace);
57+
58+
// Unique service; guaranteed not to exist!
59+
string service = Guid.NewGuid().ToString("N");
60+
61+
ICredential credential = store.Get(service, account: null);
62+
Assert.Null(credential);
63+
}
64+
65+
[PlatformFact(Platforms.Windows)]
66+
public void DpapiCredentialStore_Get_ReadProtectedFile()
67+
{
68+
var fs = new TestFileSystem();
69+
var store = new DpapiCredentialStore(fs, TestStoreRoot, TestNamespace);
70+
71+
string service = "https://example.com";
72+
const string userName = "john.doe";
73+
const string password = "letmein123"; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Fake credential")]
74+
75+
string serviceSlug = Path.Combine(TestNamespace, "https", "example.com");
76+
string fileName = $"{userName}.credential";
77+
string filePath = Path.Combine(TestStoreRoot, serviceSlug, fileName);
78+
79+
byte[] plainData = Encoding.UTF8.GetBytes(password);
80+
byte[] cryptoData = ProtectedData.Protect(plainData, null, DataProtectionScope.CurrentUser);
81+
string cryptoLine0 = Convert.ToBase64String(cryptoData);
82+
83+
var contents = new StringBuilder();
84+
contents.AppendLine(cryptoLine0);
85+
contents.AppendLine($"service={service}");
86+
contents.AppendLine($"account={userName}");
87+
contents.AppendLine();
88+
89+
byte[] data = Encoding.UTF8.GetBytes(contents.ToString());
90+
91+
fs.Directories.Add(Path.Combine(TestStoreRoot, serviceSlug));
92+
fs.Files[filePath] = data;
93+
94+
ICredential credential = store.Get(service, userName);
95+
96+
Assert.Equal(password, credential.Password);
97+
Assert.Equal(userName, credential.Account);
98+
}
99+
100+
[PlatformFact(Platforms.Windows)]
101+
public void DpapiCredentialStore_Remove_KeyNotFound_ReturnsFalse()
102+
{
103+
var fs = new TestFileSystem();
104+
var store = new DpapiCredentialStore(fs, TestStoreRoot, TestNamespace);
105+
106+
// Unique service; guaranteed not to exist!
107+
string service = Guid.NewGuid().ToString("N");
108+
109+
bool result = store.Remove(service, account: null);
110+
Assert.False(result);
111+
}
112+
}
113+
}

src/shared/TestInfrastructure/Objects/TestFileSystem.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ bool IFileSystem.FileExists(string path)
3737

3838
bool IFileSystem.DirectoryExists(string path)
3939
{
40-
return Directories.Contains(path);
40+
return Directories.Contains(TrimSlash(path));
4141
}
4242

4343
string IFileSystem.GetCurrentDirectory()
@@ -59,7 +59,7 @@ Stream IFileSystem.OpenFileStream(string path, FileMode fileMode, FileAccess fil
5959

6060
void IFileSystem.CreateDirectory(string path)
6161
{
62-
Directories.Add(path);
62+
Directories.Add(TrimSlash(path));
6363
}
6464

6565
void IFileSystem.DeleteFile(string path)
@@ -93,6 +93,19 @@ bool IsPatternMatch(string s, string p)
9393
}
9494

9595
#endregion
96+
97+
/// <summary>
98+
/// Trim trailing slashes from a path.
99+
/// </summary>
100+
public static string TrimSlash(string path)
101+
{
102+
if (path.Length > 0 && path[path.Length - 1] == Path.DirectorySeparatorChar)
103+
{
104+
return path.Substring(0, path.Length - 1);
105+
}
106+
107+
return path;
108+
}
96109
}
97110

98111
public class TestFileStream : MemoryStream

0 commit comments

Comments
 (0)