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