/
SecretServiceCollection.cs
370 lines (309 loc) · 14.1 KB
/
SecretServiceCollection.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using static GitCredentialManager.Interop.Linux.Native.Gobject;
using static GitCredentialManager.Interop.Linux.Native.Glib;
using static GitCredentialManager.Interop.Linux.Native.Libsecret;
using static GitCredentialManager.Interop.Linux.Native.Libsecret.SecretSchemaAttributeType;
using static GitCredentialManager.Interop.Linux.Native.Libsecret.SecretSchemaFlags;
namespace GitCredentialManager.Interop.Linux
{
public class SecretServiceCollection : ICredentialStore
{
private const string SchemaName = "com.microsoft.GitCredentialManager";
private const string ServiceAttributeName = "service";
private const string AccountAttributeName = "account";
private const string PlainTextContentType = "plain/text";
private readonly string _namespace;
#region Constructors
/// <summary>
/// Open the default secret collection for the current user.
/// </summary>
/// <param name="namespace">Optional namespace to scope credential operations.</param>
/// <returns>Default secret collection.</returns>
public SecretServiceCollection(string @namespace)
{
PlatformUtils.EnsureLinux();
_namespace = @namespace;
}
#endregion
#region ICredentialStore
public IList<string> GetAccounts(string service)
{
return Enumerate(service, null).Select(x => x.Account).Distinct().ToList();
}
public ICredential Get(string service, string account)
{
return Enumerate(service, account).FirstOrDefault();
}
private unsafe IEnumerable<ICredential> Enumerate(string service, string account)
{
GHashTable* queryAttrs = null;
GList* results = null;
GError* error = null;
try
{
SecretService* secService = GetSecretService();
queryAttrs = CreateSearchQuery(service, account);
SecretSchema schema = GetSchema();
// Execute search query and return all results
results = secret_service_search_sync(
secService,
ref schema,
queryAttrs,
SecretSearchFlags.SECRET_SEARCH_UNLOCK,
IntPtr.Zero,
out error);
if (error != null)
{
int code = error->code;
string message = Marshal.PtrToStringAuto(error->message)!;
throw new InteropException("Failed to search for credentials", code, new Exception(message));
}
var credentials = new List<ICredential>();
GList* itemPtr = results;
while (itemPtr != null && itemPtr->data != IntPtr.Zero)
{
SecretItem* item = (SecretItem*) itemPtr->data;
// Although we've unlocked the collection during the search call,
// an item can also be individually locked within a collection.
// If the item is locked we should try and unlock it.
if (secret_item_get_locked(item))
{
var toUnlockList = new GList
{
data = (IntPtr) item,
next = IntPtr.Zero,
prev = IntPtr.Zero
};
int numUnlocked = secret_service_unlock_sync(
secService,
&toUnlockList,
IntPtr.Zero,
out _,
out error
);
if (numUnlocked != 1)
{
throw new InteropException("Failed to unlock item", numUnlocked);
}
}
credentials.Add(CreateCredentialFromItem(item));
itemPtr = (GList*)itemPtr->next;
}
return credentials;
}
finally
{
if (queryAttrs != null) g_hash_table_destroy(queryAttrs);
if (error != null) g_error_free(error);
if (results != null) g_list_free_full(results, g_object_unref);
}
}
public unsafe void AddOrUpdate(string service, string account, string secret)
{
GHashTable* attributes = null;
SecretValue* secretValue = null;
GError *error = null;
// If there is an existing credential that matches the same account and password
// then don't bother writing out anything because they're the same!
ICredential existingCred = Get(service, account);
if (existingCred != null &&
StringComparer.Ordinal.Equals(existingCred.Account, account) &&
StringComparer.Ordinal.Equals(existingCred.Password, secret))
{
return;
}
try
{
SecretService* secService = GetSecretService();
// Create attributes for the key and user
attributes = g_hash_table_new_full(g_str_hash, g_str_equal,
Marshal.FreeHGlobal, Marshal.FreeHGlobal);
string fullServiceName = CreateServiceName(service);
IntPtr serviceKeyPtr = Marshal.StringToHGlobalAnsi(ServiceAttributeName);
IntPtr serviceValuePtr = Marshal.StringToHGlobalAnsi(fullServiceName);
g_hash_table_insert(attributes, serviceKeyPtr, serviceValuePtr);
if (!string.IsNullOrWhiteSpace(account))
{
IntPtr accountKeyPtr = Marshal.StringToHGlobalAnsi(AccountAttributeName);
IntPtr accountValuePtr = Marshal.StringToHGlobalAnsi(account);
g_hash_table_insert(attributes, accountKeyPtr, accountValuePtr);
}
// Create the secret value object from the secret string
byte[] secretBytes = Encoding.UTF8.GetBytes(secret);
secretValue = secret_value_new(secretBytes, secretBytes.Length, PlainTextContentType);
SecretSchema schema = GetSchema();
// Store the secret with the associated attributes
bool result = secret_service_store_sync(
secService,
ref schema,
attributes,
null,
fullServiceName, // Use full service name as label
secretValue,
IntPtr.Zero,
out error);
if (error != null)
{
int code = error->code;
string message = Marshal.PtrToStringAuto(error->message)!;
throw new InteropException("Failed to store credentials", code, new Exception(message));
}
if (!result)
{
throw new InteropException("Failed to store credentials", -1);
}
}
finally
{
if (attributes != null) g_hash_table_destroy(attributes);
if (secretValue != null) secret_value_unref(secretValue);
if (error != null) g_error_free(error);
}
}
public unsafe bool Remove(string service, string account)
{
GHashTable* attributes = null;
GError* error = null;
try
{
SecretService* secService = GetSecretService();
// Create search query
attributes = CreateSearchQuery(service, account);
SecretSchema schema = GetSchema();
// Erase the secret with the specified key
bool result = secret_service_clear_sync(
secService,
ref schema,
attributes,
IntPtr.Zero,
out error);
if (error != null)
{
int code = error->code;
string message = Marshal.PtrToStringAuto(error->message)!;
throw new InteropException("Failed to erase credentials", code, new Exception(message));
}
return result;
}
finally
{
if (attributes != null) g_hash_table_destroy(attributes);
if (error != null) g_error_free(error);
}
}
#endregion
private unsafe GHashTable* CreateSearchQuery(string service, string account)
{
// Build search query
GHashTable* queryAttrs = g_hash_table_new_full(
g_str_hash, g_str_equal,
Marshal.FreeHGlobal, Marshal.FreeHGlobal);
// If we've be given a service then filter on the service attribute
if (!string.IsNullOrWhiteSpace(service))
{
string fullServiceName = CreateServiceName(service);
IntPtr keyPtr = Marshal.StringToHGlobalAnsi(ServiceAttributeName);
IntPtr valuePtr = Marshal.StringToHGlobalAnsi(fullServiceName);
g_hash_table_insert(queryAttrs, keyPtr, valuePtr);
}
// If we've be given a username then filter on the account attribute
if (!string.IsNullOrWhiteSpace(account))
{
IntPtr keyPtr = Marshal.StringToHGlobalAnsi(AccountAttributeName);
IntPtr valuePtr = Marshal.StringToHGlobalAnsi(account);
g_hash_table_insert(queryAttrs, keyPtr, valuePtr);
}
return queryAttrs;
}
private static unsafe ICredential CreateCredentialFromItem(SecretItem* item)
{
GHashTable* secretAttrs = null;
IntPtr serviceKeyPtr = IntPtr.Zero;
IntPtr accountKeyPtr = IntPtr.Zero;
SecretValue* value = null;
IntPtr passwordPtr = IntPtr.Zero;
GError* error = null;
try
{
secretAttrs = secret_item_get_attributes(item);
// Extract the service attribute
serviceKeyPtr = Marshal.StringToHGlobalAnsi(ServiceAttributeName);
IntPtr serviceValuePtr = g_hash_table_lookup(secretAttrs, serviceKeyPtr);
string service = Marshal.PtrToStringAuto(serviceValuePtr);
// Extract the account attribute
accountKeyPtr = Marshal.StringToHGlobalAnsi(AccountAttributeName);
IntPtr accountValuePtr = g_hash_table_lookup(secretAttrs, accountKeyPtr);
string account = Marshal.PtrToStringAuto(accountValuePtr);
// Load the secret value
secret_item_load_secret_sync(item, IntPtr.Zero, out error);
value = secret_item_get_secret(item);
if (value == null)
{
throw new InteropException("Failed to load secret", -1);
}
// Extract the secret/password
passwordPtr = secret_value_get(value, out int passwordLength);
string password = Marshal.PtrToStringAuto(passwordPtr, passwordLength);
return new SecretServiceCredential(service, account, password);
}
finally
{
if (secretAttrs != null) g_hash_table_unref(secretAttrs);
if (accountKeyPtr != IntPtr.Zero) Marshal.FreeHGlobal(accountKeyPtr);
if (serviceKeyPtr != IntPtr.Zero) Marshal.FreeHGlobal(serviceKeyPtr);
if (value != null) secret_value_unref(value);
if (error != null) g_error_free(error);
}
}
private string CreateServiceName(string service)
{
var sb = new StringBuilder();
if (!string.IsNullOrWhiteSpace(_namespace))
{
sb.AppendFormat("{0}:", _namespace);
}
sb.Append(service);
return sb.ToString();
}
private static unsafe SecretService* GetSecretService()
{
// Get a handle to the default secret service, open a session,
// and load all collections
SecretService* service = secret_service_get_sync(
SecretServiceFlags.SECRET_SERVICE_OPEN_SESSION | SecretServiceFlags.SECRET_SERVICE_LOAD_COLLECTIONS,
IntPtr.Zero, out GError* error);
if (error != null)
{
int code = error->code;
string message = Marshal.PtrToStringAuto(error->message)!;
g_error_free(error);
throw new InteropException("Failed to open secret service session", code, new Exception(message));
}
return service;
}
private static SecretSchema GetSchema()
{
var schema = new SecretSchema
{
name = SchemaName,
flags = SECRET_SCHEMA_DONT_MATCH_NAME,
attributes = new SecretSchemaAttribute[32]
};
schema.attributes[0] = new SecretSchemaAttribute
{
name = ServiceAttributeName,
type = SECRET_SCHEMA_ATTRIBUTE_STRING
};
schema.attributes[1] = new SecretSchemaAttribute
{
name = AccountAttributeName,
type = SECRET_SCHEMA_ATTRIBUTE_STRING
};
return schema;
}
}
}