Raw
1 /*
2 * A git credential helper that interface with Windows' Credential Manager
3 *
4 */
5 #include <windows.h>
6 #include <stdio.h>
7 #include <io.h>
8 #include <fcntl.h>
9 #include <wincred.h>
10
11 /* common helpers */
12
13 #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
14
15 #ifndef _MSC_VER
16 __attribute__((format (printf, 1, 2)))
17 #endif
18 static void die(const char *err, ...)
19 {
20 char msg[4096];
21 va_list params;
22 va_start(params, err);
23 vsnprintf(msg, sizeof(msg), err, params);
24 fprintf(stderr, "%s\n", msg);
25 va_end(params);
26 exit(1);
27 }
28
29 static void *xmalloc(size_t size)
30 {
31 void *ret = malloc(size);
32 if (!ret && !size)
33 ret = malloc(1);
34 if (!ret)
35 die("Out of memory");
36 return ret;
37 }
38
39 static WCHAR *wusername, *password, *protocol, *host, *path, target[1024],
40 *password_expiry_utc, *oauth_refresh_token;
41
42 static void target_append(const WCHAR *src)
43 {
44 size_t avail = ARRAY_SIZE(target) - wcslen(target) - 1; /* -1 for NUL */
45 if (avail < wcslen(src))
46 die("target buffer overflow");
47 wcsncat(target, src, avail);
48 }
49
50 static void write_item(const char *what, LPCWSTR wbuf, int wlen)
51 {
52 char *buf;
53
54 if (!wbuf || !wlen) {
55 printf("%s=\n", what);
56 return;
57 }
58
59 int len = WideCharToMultiByte(CP_UTF8, 0, wbuf, wlen, NULL, 0, NULL,
60 FALSE);
61 buf = xmalloc(len);
62
63 if (!WideCharToMultiByte(CP_UTF8, 0, wbuf, wlen, buf, len, NULL, FALSE))
64 die("WideCharToMultiByte failed!");
65
66 printf("%s=", what);
67 fwrite(buf, 1, len, stdout);
68 putchar('\n');
69 free(buf);
70 }
71
72 /*
73 * Match an (optional) expected string and a delimiter in the target string,
74 * consuming the matched text by updating the target pointer.
75 */
76
77 static LPCWSTR wcsstr_last(LPCWSTR str, LPCWSTR find)
78 {
79 LPCWSTR res = NULL, pos;
80 for (pos = wcsstr(str, find); pos; pos = wcsstr(pos + 1, find))
81 res = pos;
82 return res;
83 }
84
85 static int match_part_with_last(LPCWSTR *ptarget, LPCWSTR want, LPCWSTR delim, int last)
86 {
87 LPCWSTR delim_pos, start = *ptarget;
88 int len;
89
90 /* find start of delimiter (or end-of-string if delim is empty) */
91 if (*delim)
92 delim_pos = last ? wcsstr_last(start, delim) : wcsstr(start, delim);
93 else
94 delim_pos = start + wcslen(start);
95
96 /*
97 * match text up to delimiter, or end of string (e.g. the '/' after
98 * host is optional if not followed by a path)
99 */
100 if (delim_pos)
101 len = delim_pos - start;
102 else
103 len = wcslen(start);
104
105 /* update ptarget if we either found a delimiter or need a match */
106 if (delim_pos || want)
107 *ptarget = delim_pos ? delim_pos + wcslen(delim) : start + len;
108
109 return !want || (!wcsncmp(want, start, len) && !want[len]);
110 }
111
112 static int match_part(LPCWSTR *ptarget, LPCWSTR want, LPCWSTR delim)
113 {
114 return match_part_with_last(ptarget, want, delim, 0);
115 }
116
117 static int match_part_last(LPCWSTR *ptarget, LPCWSTR want, LPCWSTR delim)
118 {
119 return match_part_with_last(ptarget, want, delim, 1);
120 }
121
122 static int match_cred_password(const CREDENTIALW *cred) {
123 int ret;
124 size_t wlen = cred->CredentialBlobSize / sizeof(WCHAR);
125 WCHAR *cred_password = xmalloc((wlen + 1) * sizeof(WCHAR));
126 wcsncpy_s(cred_password, wlen + 1,
127 (LPCWSTR)cred->CredentialBlob, wlen);
128 ret = !wcscmp(cred_password, password);
129 free(cred_password);
130 return ret;
131 }
132
133 static int match_cred(const CREDENTIALW *cred, int match_password)
134 {
135 LPCWSTR target = cred->TargetName;
136 if (wusername && wcscmp(wusername, cred->UserName ? cred->UserName : L""))
137 return 0;
138
139 return match_part(&target, L"git", L":") &&
140 match_part(&target, protocol, L"://") &&
141 match_part_last(&target, wusername, L"@") &&
142 match_part(&target, host, L"/") &&
143 match_part(&target, path, L"") &&
144 (!match_password || match_cred_password(cred));
145 }
146
147 static void get_credential(void)
148 {
149 CREDENTIALW **creds;
150 DWORD num_creds;
151 int i;
152 CREDENTIAL_ATTRIBUTEW *attr;
153 WCHAR *secret;
154 WCHAR *line;
155 WCHAR *remaining_lines;
156 WCHAR *part;
157 WCHAR *remaining_parts;
158
159 if (!CredEnumerateW(L"git:*", 0, &num_creds, &creds))
160 return;
161
162 /* search for the first credential that matches username */
163 for (i = 0; i < num_creds; ++i)
164 if (match_cred(creds[i], 0)) {
165 write_item("username", creds[i]->UserName,
166 creds[i]->UserName ? wcslen(creds[i]->UserName) : 0);
167 if (creds[i]->CredentialBlobSize > 0) {
168 secret = xmalloc(creds[i]->CredentialBlobSize + sizeof(WCHAR));
169 wcsncpy_s(secret, creds[i]->CredentialBlobSize, (LPCWSTR)creds[i]->CredentialBlob, creds[i]->CredentialBlobSize / sizeof(WCHAR));
170 line = wcstok_s(secret, L"\r\n", &remaining_lines);
171 write_item("password", line, line ? wcslen(line) : 0);
172 while(line != NULL) {
173 part = wcstok_s(line, L"=", &remaining_parts);
174 if (!wcscmp(part, L"oauth_refresh_token")) {
175 write_item("oauth_refresh_token", remaining_parts, remaining_parts ? wcslen(remaining_parts) : 0);
176 }
177 line = wcstok_s(NULL, L"\r\n", &remaining_lines);
178 }
179 free(secret);
180 } else {
181 write_item("password",
182 (LPCWSTR)creds[i]->CredentialBlob,
183 creds[i]->CredentialBlobSize / sizeof(WCHAR));
184 }
185 for (int j = 0; j < creds[i]->AttributeCount; j++) {
186 attr = creds[i]->Attributes + j;
187 if (!wcscmp(attr->Keyword, L"git_password_expiry_utc")) {
188 write_item("password_expiry_utc", (LPCWSTR)attr->Value,
189 attr->ValueSize / sizeof(WCHAR));
190 break;
191 }
192 }
193 break;
194 }
195
196 CredFree(creds);
197 }
198
199 static void store_credential(void)
200 {
201 CREDENTIALW cred;
202 CREDENTIAL_ATTRIBUTEW expiry_attr;
203 WCHAR *secret;
204 int wlen;
205
206 if (!wusername || !password)
207 return;
208
209 if (oauth_refresh_token) {
210 wlen = _scwprintf(L"%s\r\noauth_refresh_token=%s", password, oauth_refresh_token);
211 secret = xmalloc((wlen + 1) * sizeof(WCHAR));
212 _snwprintf_s(secret, wlen + 1, wlen, L"%s\r\noauth_refresh_token=%s", password, oauth_refresh_token);
213 } else {
214 secret = _wcsdup(password);
215 }
216
217 cred.Flags = 0;
218 cred.Type = CRED_TYPE_GENERIC;
219 cred.TargetName = target;
220 cred.Comment = L"saved by git-credential-wincred";
221 cred.CredentialBlobSize = wcslen(secret) * sizeof(WCHAR);
222 cred.CredentialBlob = (LPVOID)_wcsdup(secret);
223 cred.Persist = CRED_PERSIST_LOCAL_MACHINE;
224 cred.AttributeCount = 0;
225 cred.Attributes = NULL;
226 if (password_expiry_utc != NULL) {
227 expiry_attr.Keyword = L"git_password_expiry_utc";
228 expiry_attr.Value = (LPVOID)password_expiry_utc;
229 expiry_attr.ValueSize = (wcslen(password_expiry_utc)) * sizeof(WCHAR);
230 expiry_attr.Flags = 0;
231 cred.Attributes = &expiry_attr;
232 cred.AttributeCount = 1;
233 }
234 cred.TargetAlias = NULL;
235 cred.UserName = wusername;
236
237 free(secret);
238
239 if (!CredWriteW(&cred, 0))
240 die("CredWrite failed");
241 }
242
243 static void erase_credential(void)
244 {
245 CREDENTIALW **creds;
246 DWORD num_creds;
247 int i;
248
249 if (!CredEnumerateW(L"git:*", 0, &num_creds, &creds))
250 return;
251
252 for (i = 0; i < num_creds; ++i) {
253 if (match_cred(creds[i], password != NULL))
254 CredDeleteW(creds[i]->TargetName, creds[i]->Type, 0);
255 }
256
257 CredFree(creds);
258 }
259
260 static WCHAR *utf8_to_utf16_dup(const char *str)
261 {
262 int wlen = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
263 WCHAR *wstr = xmalloc(sizeof(WCHAR) * wlen);
264 MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, wlen);
265 return wstr;
266 }
267
268 #define KB (1024)
269
270 static void read_credential(void)
271 {
272 size_t alloc = 100 * KB;
273 char *buf = calloc(alloc, sizeof(*buf));
274
275 while (fgets(buf, alloc, stdin)) {
276 char *v;
277 size_t len = strlen(buf);
278 int ends_in_newline = 0;
279 /* strip trailing CR / LF */
280 if (len && buf[len - 1] == '\n') {
281 buf[--len] = 0;
282 ends_in_newline = 1;
283 }
284 if (len && buf[len - 1] == '\r')
285 buf[--len] = 0;
286
287 if (!ends_in_newline)
288 die("bad input: %s", buf);
289
290 if (!*buf)
291 break;
292
293 v = strchr(buf, '=');
294 if (!v)
295 die("bad input: %s", buf);
296 *v++ = '\0';
297
298 if (!strcmp(buf, "protocol"))
299 protocol = utf8_to_utf16_dup(v);
300 else if (!strcmp(buf, "host"))
301 host = utf8_to_utf16_dup(v);
302 else if (!strcmp(buf, "path"))
303 path = utf8_to_utf16_dup(v);
304 else if (!strcmp(buf, "username")) {
305 wusername = utf8_to_utf16_dup(v);
306 } else if (!strcmp(buf, "password"))
307 password = utf8_to_utf16_dup(v);
308 else if (!strcmp(buf, "password_expiry_utc"))
309 password_expiry_utc = utf8_to_utf16_dup(v);
310 else if (!strcmp(buf, "oauth_refresh_token"))
311 oauth_refresh_token = utf8_to_utf16_dup(v);
312 /*
313 * Ignore other lines; we don't know what they mean, but
314 * this future-proofs us when later versions of git do
315 * learn new lines, and the helpers are updated to match.
316 */
317 }
318
319 free(buf);
320 }
321
322 int main(int argc, char *argv[])
323 {
324 const char *usage =
325 "usage: git credential-wincred <get|store|erase>\n";
326
327 if (!argv[1])
328 die("%s", usage);
329
330 /* git use binary pipes to avoid CRLF-issues */
331 _setmode(_fileno(stdin), _O_BINARY);
332 _setmode(_fileno(stdout), _O_BINARY);
333
334 read_credential();
335
336 if (!protocol || !(host || path))
337 return 0;
338
339 /* prepare 'target', the unique key for the credential */
340 wcscpy(target, L"git:");
341 target_append(protocol);
342 target_append(L"://");
343 if (wusername) {
344 target_append(wusername);
345 target_append(L"@");
346 }
347 if (host)
348 target_append(host);
349 if (path) {
350 target_append(L"/");
351 target_append(path);
352 }
353
354 if (!strcmp(argv[1], "get"))
355 get_credential();
356 else if (!strcmp(argv[1], "store"))
357 store_credential();
358 else if (!strcmp(argv[1], "erase"))
359 erase_credential();
360 /* otherwise, ignore unknown action */
361 return 0;
362 }