Skip to content

Commit 32654f4

Browse files
Fix pass credential store to support .gpg-id in subdirectories (issue #2263)
Co-authored-by: marekzmyslowski <1062877+marekzmyslowski@users.noreply.github.com>
1 parent 29dd825 commit 32654f4

3 files changed

Lines changed: 80 additions & 22 deletions

File tree

src/shared/Core.Tests/Interop/Posix/GnuPassCredentialStoreTests.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,40 @@ public void GnuPassCredentialStore_Remove_NotFound_ReturnsFalse()
8686
Assert.False(result);
8787
}
8888

89+
[PosixFact]
90+
public void GnuPassCredentialStore_ReadWriteDelete_GpgIdInSubdirectory()
91+
{
92+
var fs = new TestFileSystem();
93+
var gpg = new TestGpg(fs);
94+
string storeRoot = InitializePasswordStoreWithGpgIdInSubdirectory(fs, gpg, TestNamespace);
95+
96+
var collection = new GpgPassCredentialStore(fs, gpg, storeRoot, TestNamespace);
97+
98+
// Create a service that is guaranteed to be unique
99+
string uniqueGuid = Guid.NewGuid().ToString("N");
100+
string service = $"https://example.com/{uniqueGuid}";
101+
const string userName = "john.doe";
102+
const string password = "letmein123"; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Fake credential")]
103+
104+
try
105+
{
106+
// Write
107+
collection.AddOrUpdate(service, userName, password);
108+
109+
// Read
110+
ICredential outCredential = collection.Get(service, userName);
111+
112+
Assert.NotNull(outCredential);
113+
Assert.Equal(userName, outCredential.Account);
114+
Assert.Equal(password, outCredential.Password);
115+
}
116+
finally
117+
{
118+
// Ensure we clean up after ourselves even in case of 'get' failures
119+
collection.Remove(service, userName);
120+
}
121+
}
122+
89123
private static string InitializePasswordStore(TestFileSystem fs, TestGpg gpg)
90124
{
91125
string homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
@@ -102,5 +136,27 @@ private static string InitializePasswordStore(TestFileSystem fs, TestGpg gpg)
102136

103137
return storePath;
104138
}
139+
140+
private static string InitializePasswordStoreWithGpgIdInSubdirectory(TestFileSystem fs, TestGpg gpg, string subdirectory)
141+
{
142+
string homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
143+
string storePath = Path.Combine(homePath, ".password-store");
144+
string userId = "gcm-test@example.com";
145+
146+
// Place .gpg-id only in the namespace subdirectory (not the store root),
147+
// simulating a pass store where the root has no .gpg-id but submodules do.
148+
string subDirPath = Path.Combine(storePath, subdirectory);
149+
string gpgIdPath = Path.Combine(subDirPath, ".gpg-id");
150+
151+
// Ensure we have a GPG key for use with testing
152+
gpg.GenerateKeys(userId);
153+
154+
// Init the password store with .gpg-id only in the subdirectory
155+
fs.Directories.Add(storePath);
156+
fs.Directories.Add(subDirPath);
157+
fs.Files[gpgIdPath] = Encoding.UTF8.GetBytes(userId);
158+
159+
return storePath;
160+
}
105161
}
106162
}

src/shared/Core/CredentialStore.cs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -291,18 +291,6 @@ private void ValidateGpgPass(out string storeRoot, out string execPath)
291291
storeRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".password-store");
292292
}
293293

294-
// Check we have a GPG ID to sign credential files with
295-
string gpgIdFile = Path.Combine(storeRoot, ".gpg-id");
296-
if (!_context.FileSystem.FileExists(gpgIdFile))
297-
{
298-
var format =
299-
"Password store has not been initialized at '{0}'; run `pass init <gpg-id>` to initialize the store.";
300-
var message = string.Format(format, storeRoot);
301-
_context.Trace2.WriteError(message);
302-
throw new Exception(message + Environment.NewLine +
303-
$"See {Constants.HelpUrls.GcmCredentialStores} for more information."
304-
);
305-
}
306294
}
307295

308296
private void ValidateCredentialCache(out string options)

src/shared/Core/Interop/Posix/GpgPassCredentialStore.cs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,33 @@ public GpgPassCredentialStore(IFileSystem fileSystem, IGpg gpg, string storeRoot
2121

2222
protected override string CredentialFileExtension => ".gpg";
2323

24-
private string GetGpgId()
24+
private string GetGpgId(string credentialPath)
2525
{
26-
string gpgIdPath = Path.Combine(StoreRoot, ".gpg-id");
27-
if (!FileSystem.FileExists(gpgIdPath))
26+
// Walk up from the credential's directory to the store root, looking for a .gpg-id file.
27+
// This mimics the behavior of pass, which uses the nearest .gpg-id in the directory hierarchy.
28+
string dir = Path.GetDirectoryName(credentialPath);
29+
while (dir != null)
2830
{
29-
throw new Exception($"Cannot find GPG ID in '{gpgIdPath}'; password store has not been initialized");
30-
}
31+
string gpgIdPath = Path.Combine(dir, ".gpg-id");
32+
if (FileSystem.FileExists(gpgIdPath))
33+
{
34+
using (var stream = FileSystem.OpenFileStream(gpgIdPath, FileMode.Open, FileAccess.Read, FileShare.Read))
35+
using (var reader = new StreamReader(stream))
36+
{
37+
return reader.ReadLine();
38+
}
39+
}
3140

32-
using (var stream = FileSystem.OpenFileStream(gpgIdPath, FileMode.Open, FileAccess.Read, FileShare.Read))
33-
using (var reader = new StreamReader(stream))
34-
{
35-
return reader.ReadLine();
41+
// Stop after checking the store root
42+
if (FileSystem.IsSamePath(dir, StoreRoot))
43+
{
44+
break;
45+
}
46+
47+
dir = Path.GetDirectoryName(dir);
3648
}
49+
50+
throw new Exception($"Cannot find GPG ID in password store at '{StoreRoot}'; run `pass init <gpg-id>` to initialize the store.");
3751
}
3852

3953
protected override bool TryDeserializeCredential(string path, out FileCredential credential)
@@ -68,7 +82,7 @@ protected override bool TryDeserializeCredential(string path, out FileCredential
6882

6983
protected override void SerializeCredential(FileCredential credential)
7084
{
71-
string gpgId = GetGpgId();
85+
string gpgId = GetGpgId(credential.FullPath);
7286

7387
var sb = new StringBuilder(credential.Password);
7488
sb.AppendFormat("{1}service={0}{1}", credential.Service, Environment.NewLine);

0 commit comments

Comments
 (0)