]>
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 H |
37 | static WCHAR *wusername, *password, *protocol, *host, *path, target[1024], |
38 | *password_expiry_utc; | |
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; |
a6253da0 EFL |
143 | |
144 | if (!CredEnumerateW(L"git:*", 0, &num_creds, &creds)) | |
145 | return; | |
146 | ||
147 | /* search for the first credential that matches username */ | |
148 | for (i = 0; i < num_creds; ++i) | |
cb626f8e | 149 | if (match_cred(creds[i], 0)) { |
8b2d219a | 150 | write_item("username", creds[i]->UserName, |
601e1e78 | 151 | creds[i]->UserName ? wcslen(creds[i]->UserName) : 0); |
8b2d219a KB |
152 | write_item("password", |
153 | (LPCWSTR)creds[i]->CredentialBlob, | |
154 | creds[i]->CredentialBlobSize / sizeof(WCHAR)); | |
488d9d52 H |
155 | for (int j = 0; j < creds[i]->AttributeCount; j++) { |
156 | attr = creds[i]->Attributes + j; | |
157 | if (!wcscmp(attr->Keyword, L"git_password_expiry_utc")) { | |
158 | write_item("password_expiry_utc", (LPCWSTR)attr->Value, | |
159 | attr->ValueSize / sizeof(WCHAR)); | |
160 | break; | |
161 | } | |
162 | } | |
a6253da0 EFL |
163 | break; |
164 | } | |
a6253da0 EFL |
165 | |
166 | CredFree(creds); | |
a6253da0 EFL |
167 | } |
168 | ||
169 | static void store_credential(void) | |
170 | { | |
171 | CREDENTIALW cred; | |
488d9d52 | 172 | CREDENTIAL_ATTRIBUTEW expiry_attr; |
a6253da0 EFL |
173 | |
174 | if (!wusername || !password) | |
175 | return; | |
176 | ||
a6253da0 EFL |
177 | cred.Flags = 0; |
178 | cred.Type = CRED_TYPE_GENERIC; | |
179 | cred.TargetName = target; | |
180 | cred.Comment = L"saved by git-credential-wincred"; | |
8b2d219a KB |
181 | cred.CredentialBlobSize = (wcslen(password)) * sizeof(WCHAR); |
182 | cred.CredentialBlob = (LPVOID)password; | |
a6253da0 | 183 | cred.Persist = CRED_PERSIST_LOCAL_MACHINE; |
8b2d219a KB |
184 | cred.AttributeCount = 0; |
185 | cred.Attributes = NULL; | |
488d9d52 H |
186 | if (password_expiry_utc != NULL) { |
187 | expiry_attr.Keyword = L"git_password_expiry_utc"; | |
188 | expiry_attr.Value = (LPVOID)password_expiry_utc; | |
189 | expiry_attr.ValueSize = (wcslen(password_expiry_utc)) * sizeof(WCHAR); | |
190 | expiry_attr.Flags = 0; | |
191 | cred.Attributes = &expiry_attr; | |
192 | cred.AttributeCount = 1; | |
193 | } | |
a6253da0 EFL |
194 | cred.TargetAlias = NULL; |
195 | cred.UserName = wusername; | |
196 | ||
a6253da0 EFL |
197 | if (!CredWriteW(&cred, 0)) |
198 | die("CredWrite failed"); | |
199 | } | |
200 | ||
201 | static void erase_credential(void) | |
202 | { | |
203 | CREDENTIALW **creds; | |
204 | DWORD num_creds; | |
205 | int i; | |
206 | ||
207 | if (!CredEnumerateW(L"git:*", 0, &num_creds, &creds)) | |
208 | return; | |
209 | ||
210 | for (i = 0; i < num_creds; ++i) { | |
cb626f8e | 211 | if (match_cred(creds[i], password != NULL)) |
a6253da0 EFL |
212 | CredDeleteW(creds[i]->TargetName, creds[i]->Type, 0); |
213 | } | |
214 | ||
215 | CredFree(creds); | |
216 | } | |
217 | ||
218 | static WCHAR *utf8_to_utf16_dup(const char *str) | |
219 | { | |
220 | int wlen = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); | |
221 | WCHAR *wstr = xmalloc(sizeof(WCHAR) * wlen); | |
222 | MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, wlen); | |
223 | return wstr; | |
224 | } | |
225 | ||
0a3a972c TB |
226 | #define KB (1024) |
227 | ||
a6253da0 EFL |
228 | static void read_credential(void) |
229 | { | |
0a3a972c TB |
230 | size_t alloc = 100 * KB; |
231 | char *buf = calloc(alloc, sizeof(*buf)); | |
a6253da0 | 232 | |
0a3a972c | 233 | while (fgets(buf, alloc, stdin)) { |
a6253da0 | 234 | char *v; |
0a3a972c TB |
235 | size_t len = strlen(buf); |
236 | int ends_in_newline = 0; | |
3b12f46a | 237 | /* strip trailing CR / LF */ |
0a3a972c TB |
238 | if (len && buf[len - 1] == '\n') { |
239 | buf[--len] = 0; | |
240 | ends_in_newline = 1; | |
241 | } | |
242 | if (len && buf[len - 1] == '\r') | |
3b12f46a | 243 | buf[--len] = 0; |
a6253da0 | 244 | |
0a3a972c TB |
245 | if (!ends_in_newline) |
246 | die("bad input: %s", buf); | |
247 | ||
3b12f46a | 248 | if (!*buf) |
a6253da0 | 249 | break; |
a6253da0 EFL |
250 | |
251 | v = strchr(buf, '='); | |
252 | if (!v) | |
253 | die("bad input: %s", buf); | |
254 | *v++ = '\0'; | |
255 | ||
256 | if (!strcmp(buf, "protocol")) | |
8b2d219a | 257 | protocol = utf8_to_utf16_dup(v); |
a6253da0 | 258 | else if (!strcmp(buf, "host")) |
8b2d219a | 259 | host = utf8_to_utf16_dup(v); |
a6253da0 | 260 | else if (!strcmp(buf, "path")) |
8b2d219a | 261 | path = utf8_to_utf16_dup(v); |
a6253da0 | 262 | else if (!strcmp(buf, "username")) { |
a6253da0 EFL |
263 | wusername = utf8_to_utf16_dup(v); |
264 | } else if (!strcmp(buf, "password")) | |
265 | password = utf8_to_utf16_dup(v); | |
488d9d52 H |
266 | else if (!strcmp(buf, "password_expiry_utc")) |
267 | password_expiry_utc = utf8_to_utf16_dup(v); | |
d6958049 MJC |
268 | /* |
269 | * Ignore other lines; we don't know what they mean, but | |
270 | * this future-proofs us when later versions of git do | |
271 | * learn new lines, and the helpers are updated to match. | |
272 | */ | |
a6253da0 | 273 | } |
0a3a972c TB |
274 | |
275 | free(buf); | |
a6253da0 EFL |
276 | } |
277 | ||
278 | int main(int argc, char *argv[]) | |
279 | { | |
280 | const char *usage = | |
c358ed75 | 281 | "usage: git credential-wincred <get|store|erase>\n"; |
a6253da0 EFL |
282 | |
283 | if (!argv[1]) | |
488d9d52 | 284 | die("%s", usage); |
a6253da0 EFL |
285 | |
286 | /* git use binary pipes to avoid CRLF-issues */ | |
287 | _setmode(_fileno(stdin), _O_BINARY); | |
288 | _setmode(_fileno(stdout), _O_BINARY); | |
289 | ||
290 | read_credential(); | |
291 | ||
a6253da0 EFL |
292 | if (!protocol || !(host || path)) |
293 | return 0; | |
294 | ||
295 | /* prepare 'target', the unique key for the credential */ | |
8b2d219a KB |
296 | wcscpy(target, L"git:"); |
297 | wcsncat(target, protocol, ARRAY_SIZE(target)); | |
298 | wcsncat(target, L"://", ARRAY_SIZE(target)); | |
299 | if (wusername) { | |
300 | wcsncat(target, wusername, ARRAY_SIZE(target)); | |
301 | wcsncat(target, L"@", ARRAY_SIZE(target)); | |
a6253da0 EFL |
302 | } |
303 | if (host) | |
8b2d219a | 304 | wcsncat(target, host, ARRAY_SIZE(target)); |
a6253da0 | 305 | if (path) { |
8b2d219a KB |
306 | wcsncat(target, L"/", ARRAY_SIZE(target)); |
307 | wcsncat(target, path, ARRAY_SIZE(target)); | |
a6253da0 EFL |
308 | } |
309 | ||
a6253da0 EFL |
310 | if (!strcmp(argv[1], "get")) |
311 | get_credential(); | |
312 | else if (!strcmp(argv[1], "store")) | |
313 | store_credential(); | |
314 | else if (!strcmp(argv[1], "erase")) | |
315 | erase_credential(); | |
316 | /* otherwise, ignore unknown action */ | |
317 | return 0; | |
318 | } |