Skip to content

Commit 29f16b3

Browse files
Fix GetGpgId: walk up from credential path to StoreRoot per GNU Pass behaviour
Co-authored-by: marekzmyslowski <1062877+marekzmyslowski@users.noreply.github.com>
1 parent 25b7027 commit 29f16b3

2 files changed

Lines changed: 71 additions & 9 deletions

File tree

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,56 @@ public void GnuPassCredentialStore_ReadWriteDelete_GpgIdInSubdirectory()
120120
}
121121
}
122122

123+
[PosixFact]
124+
public void GnuPassCredentialStore_WriteCredential_MultipleGpgIds_UsesNearestGpgId()
125+
{
126+
// Verify that when two subdirectories each have their own .gpg-id, encrypting a credential
127+
// under one subdirectory uses that subdirectory's GPG identity, not the other one.
128+
var fs = new TestFileSystem();
129+
var gpg = new TestGpg(fs);
130+
131+
string homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
132+
string storePath = Path.Combine(homePath, ".password-store");
133+
134+
const string personalUserId = "personal@example.com";
135+
const string workUserId = "work@example.com";
136+
137+
// Only register the personal key; if the wrong (work) key is picked, EncryptFile will throw.
138+
gpg.GenerateKeys(personalUserId);
139+
140+
string personalSubDir = Path.Combine(storePath, "personal");
141+
string workSubDir = Path.Combine(storePath, "work");
142+
143+
fs.Directories.Add(storePath);
144+
fs.Directories.Add(personalSubDir);
145+
fs.Directories.Add(workSubDir);
146+
fs.Files[Path.Combine(personalSubDir, ".gpg-id")] = Encoding.UTF8.GetBytes(personalUserId);
147+
fs.Files[Path.Combine(workSubDir, ".gpg-id")] = Encoding.UTF8.GetBytes(workUserId);
148+
149+
// Use "personal" namespace so credentials are stored under storePath/personal/...
150+
var collection = new GpgPassCredentialStore(fs, gpg, storePath, "personal");
151+
152+
string service = $"https://example.com/{Guid.NewGuid():N}";
153+
const string userName = "john.doe";
154+
const string password = "letmein123"; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Fake credential")]
155+
156+
try
157+
{
158+
// Write - should pick personal/.gpg-id (personalUserId), not work/.gpg-id (workUserId)
159+
collection.AddOrUpdate(service, userName, password);
160+
161+
ICredential outCredential = collection.Get(service, userName);
162+
163+
Assert.NotNull(outCredential);
164+
Assert.Equal(userName, outCredential.Account);
165+
Assert.Equal(password, outCredential.Password);
166+
}
167+
finally
168+
{
169+
collection.Remove(service, userName);
170+
}
171+
}
172+
123173
private static string InitializePasswordStore(TestFileSystem fs, TestGpg gpg)
124174
{
125175
string homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);

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

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,30 @@ 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 credentialFullPath)
2525
{
26-
// Search for a .gpg-id file anywhere under the store root.
27-
// This handles configurations where .gpg-id is in a subdirectory
28-
// (e.g., a git submodule) rather than the store root itself.
29-
foreach (string gpgIdPath in FileSystem.EnumerateFiles(StoreRoot, ".gpg-id"))
26+
// Walk up from the credential's directory to the store root, looking for a .gpg-id file.
27+
// This mimics the behaviour of GNU Pass, which uses the nearest .gpg-id in the directory hierarchy.
28+
string dir = Path.GetDirectoryName(credentialFullPath);
29+
while (dir != null)
3030
{
31-
using (var stream = FileSystem.OpenFileStream(gpgIdPath, FileMode.Open, FileAccess.Read, FileShare.Read))
32-
using (var reader = new StreamReader(stream))
31+
string gpgIdPath = Path.Combine(dir, ".gpg-id");
32+
if (FileSystem.FileExists(gpgIdPath))
3333
{
34-
return reader.ReadLine();
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+
}
3539
}
40+
41+
// Stop after checking the store root
42+
if (FileSystem.IsSamePath(dir, StoreRoot))
43+
{
44+
break;
45+
}
46+
47+
dir = Path.GetDirectoryName(dir);
3648
}
3749

3850
throw new Exception($"Cannot find GPG ID in password store at '{StoreRoot}'; run `pass init <gpg-id>` to initialize the store.");
@@ -70,7 +82,7 @@ protected override bool TryDeserializeCredential(string path, out FileCredential
7082

7183
protected override void SerializeCredential(FileCredential credential)
7284
{
73-
string gpgId = GetGpgId();
85+
string gpgId = GetGpgId(credential.FullPath);
7486

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

0 commit comments

Comments
 (0)