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