Raw
1 #include <stdio.h>
2 #include <string.h>
3 #include <stdlib.h>
4 #include <Security/Security.h>
5 #include "git-compat-util.h"
6 #include "strbuf.h"
7 #include "wrapper.h"
8
9 #define ENCODING kCFStringEncodingUTF8
10 static CFStringRef protocol; /* Stores constant strings - not memory managed */
11 static CFStringRef host;
12 static CFNumberRef port;
13 static CFStringRef path;
14 static CFStringRef username;
15 static CFDataRef password;
16 static CFDataRef password_expiry_utc;
17 static CFDataRef oauth_refresh_token;
18 static char *state_seen;
19
20 static void clear_credential(void)
21 {
22 if (host) {
23 CFRelease(host);
24 host = NULL;
25 }
26 if (port) {
27 CFRelease(port);
28 port = NULL;
29 }
30 if (path) {
31 CFRelease(path);
32 path = NULL;
33 }
34 if (username) {
35 CFRelease(username);
36 username = NULL;
37 }
38 if (password) {
39 CFRelease(password);
40 password = NULL;
41 }
42 if (password_expiry_utc) {
43 CFRelease(password_expiry_utc);
44 password_expiry_utc = NULL;
45 }
46 if (oauth_refresh_token) {
47 CFRelease(oauth_refresh_token);
48 oauth_refresh_token = NULL;
49 }
50 }
51
52 #define STRING_WITH_LENGTH(s) s, sizeof(s) - 1
53
54 static CFDictionaryRef create_dictionary(CFAllocatorRef allocator, ...)
55 {
56 va_list args;
57 const void *key;
58 CFMutableDictionaryRef result;
59
60 result = CFDictionaryCreateMutable(allocator,
61 0,
62 &kCFTypeDictionaryKeyCallBacks,
63 &kCFTypeDictionaryValueCallBacks);
64
65
66 va_start(args, allocator);
67 while ((key = va_arg(args, const void *)) != NULL) {
68 const void *value;
69 value = va_arg(args, const void *);
70 if (value)
71 CFDictionarySetValue(result, key, value);
72 }
73 va_end(args);
74
75 return result;
76 }
77
78 #define CREATE_SEC_ATTRIBUTES(...) \
79 create_dictionary(kCFAllocatorDefault, \
80 kSecClass, kSecClassInternetPassword, \
81 kSecAttrServer, host, \
82 kSecAttrAccount, username, \
83 kSecAttrPath, path, \
84 kSecAttrPort, port, \
85 kSecAttrProtocol, protocol, \
86 kSecAttrAuthenticationType, \
87 kSecAttrAuthenticationTypeDefault, \
88 __VA_ARGS__);
89
90 static void write_item(const char *what, const char *buf, size_t len)
91 {
92 printf("%s=", what);
93 fwrite(buf, 1, len, stdout);
94 putchar('\n');
95 }
96
97 static void write_item_strbuf(struct strbuf *sb, const char *what, const char *buf, int n)
98 {
99 char s[32];
100
101 xsnprintf(s, sizeof(s), "__%s=", what);
102 strbuf_add(sb, s, strlen(s));
103 strbuf_add(sb, buf, n);
104 }
105
106 static void write_item_strbuf_cfstring(struct strbuf *sb, const char *what, CFStringRef ref)
107 {
108 char *buf;
109 int len;
110
111 if (!ref)
112 return;
113 len = CFStringGetMaximumSizeForEncoding(CFStringGetLength(ref), ENCODING) + 1;
114 buf = xmalloc(len);
115 if (CFStringGetCString(ref, buf, len, ENCODING))
116 write_item_strbuf(sb, what, buf, strlen(buf));
117 free(buf);
118 }
119
120 static void write_item_strbuf_cfnumber(struct strbuf *sb, const char *what, CFNumberRef ref)
121 {
122 short n;
123 char buf[32];
124
125 if (!ref)
126 return;
127 if (!CFNumberGetValue(ref, kCFNumberShortType, &n))
128 return;
129 xsnprintf(buf, sizeof(buf), "%d", n);
130 write_item_strbuf(sb, what, buf, strlen(buf));
131 }
132
133 static void write_item_strbuf_cfdata(struct strbuf *sb, const char *what, CFDataRef ref)
134 {
135 char *buf;
136 int len;
137
138 if (!ref)
139 return;
140 buf = (char *)CFDataGetBytePtr(ref);
141 if (!buf || strlen(buf) == 0)
142 return;
143 len = CFDataGetLength(ref);
144 write_item_strbuf(sb, what, buf, len);
145 }
146
147 static void encode_state_seen(struct strbuf *sb)
148 {
149 strbuf_add(sb, "osxkeychain:seen=", strlen("osxkeychain:seen="));
150 write_item_strbuf_cfstring(sb, "host", host);
151 write_item_strbuf_cfnumber(sb, "port", port);
152 write_item_strbuf_cfstring(sb, "path", path);
153 write_item_strbuf_cfstring(sb, "username", username);
154 write_item_strbuf_cfdata(sb, "password", password);
155 }
156
157 static void find_username_in_item(CFDictionaryRef item)
158 {
159 CFStringRef account_ref;
160 char *username_buf;
161 CFIndex buffer_len;
162
163 account_ref = CFDictionaryGetValue(item, kSecAttrAccount);
164 if (!account_ref)
165 {
166 write_item("username", "", 0);
167 return;
168 }
169 username = CFStringCreateCopy(kCFAllocatorDefault, account_ref);
170
171 username_buf = (char *)CFStringGetCStringPtr(account_ref, ENCODING);
172 if (username_buf)
173 {
174 write_item("username", username_buf, strlen(username_buf));
175 return;
176 }
177
178 /* If we can't get a CString pointer then
179 * we need to allocate our own buffer */
180 buffer_len = CFStringGetMaximumSizeForEncoding(
181 CFStringGetLength(account_ref), ENCODING) + 1;
182 username_buf = xmalloc(buffer_len);
183 if (CFStringGetCString(account_ref,
184 username_buf,
185 buffer_len,
186 ENCODING)) {
187 write_item("username", username_buf, strlen(username_buf));
188 }
189 free(username_buf);
190 }
191
192 static OSStatus find_internet_password(void)
193 {
194 CFDictionaryRef attrs;
195 CFDictionaryRef item;
196 CFDataRef data;
197 OSStatus result;
198
199 attrs = CREATE_SEC_ATTRIBUTES(kSecMatchLimit, kSecMatchLimitOne,
200 kSecReturnAttributes, kCFBooleanTrue,
201 kSecReturnData, kCFBooleanTrue,
202 NULL);
203 result = SecItemCopyMatching(attrs, (CFTypeRef *)&item);
204 if (result) {
205 goto out;
206 }
207
208 data = CFDictionaryGetValue(item, kSecValueData);
209 password = CFDataCreateCopy(kCFAllocatorDefault, data);
210
211 write_item("password",
212 (const char *)CFDataGetBytePtr(data),
213 CFDataGetLength(data));
214 if (!username)
215 find_username_in_item(item);
216
217 CFRelease(item);
218
219 write_item("capability[]", "state", strlen("state"));
220 {
221 struct strbuf sb;
222
223 strbuf_init(&sb, 1024);
224 encode_state_seen(&sb);
225 write_item("state[]", sb.buf, strlen(sb.buf));
226 strbuf_release(&sb);
227 }
228
229 out:
230 CFRelease(attrs);
231
232 /* We consider not found to not be an error */
233 if (result == errSecItemNotFound)
234 result = errSecSuccess;
235
236 return result;
237 }
238
239 static OSStatus delete_ref(const void *itemRef)
240 {
241 CFArrayRef item_ref_list;
242 CFDictionaryRef delete_query;
243 OSStatus result;
244
245 item_ref_list = CFArrayCreate(kCFAllocatorDefault,
246 &itemRef,
247 1,
248 &kCFTypeArrayCallBacks);
249 delete_query = create_dictionary(kCFAllocatorDefault,
250 kSecClass, kSecClassInternetPassword,
251 kSecMatchItemList, item_ref_list,
252 NULL);
253
254 if (password) {
255 /* We only want to delete items with a matching password */
256 CFIndex capacity;
257 CFMutableDictionaryRef query;
258 CFDataRef data;
259
260 capacity = CFDictionaryGetCount(delete_query) + 1;
261 query = CFDictionaryCreateMutableCopy(kCFAllocatorDefault,
262 capacity,
263 delete_query);
264 CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue);
265 result = SecItemCopyMatching(query, (CFTypeRef *)&data);
266 if (!result) {
267 CFDataRef kc_password;
268 const UInt8 *raw_data;
269 const UInt8 *line;
270
271 /* Don't match appended metadata */
272 raw_data = CFDataGetBytePtr(data);
273 line = memchr(raw_data, '\n', CFDataGetLength(data));
274 if (line)
275 kc_password = CFDataCreateWithBytesNoCopy(
276 kCFAllocatorDefault,
277 raw_data,
278 line - raw_data,
279 kCFAllocatorNull);
280 else
281 kc_password = data;
282
283 if (CFEqual(kc_password, password))
284 result = SecItemDelete(delete_query);
285
286 if (line)
287 CFRelease(kc_password);
288 CFRelease(data);
289 }
290
291 CFRelease(query);
292 } else {
293 result = SecItemDelete(delete_query);
294 }
295
296 CFRelease(delete_query);
297 CFRelease(item_ref_list);
298
299 return result;
300 }
301
302 static OSStatus delete_internet_password(void)
303 {
304 CFDictionaryRef attrs;
305 CFArrayRef refs;
306 OSStatus result;
307
308 /*
309 * Require at least a protocol and host for removal, which is what git
310 * will give us; if you want to do something more fancy, use the
311 * Keychain manager.
312 */
313 if (!protocol || !host)
314 return -1;
315
316 attrs = CREATE_SEC_ATTRIBUTES(kSecMatchLimit, kSecMatchLimitAll,
317 kSecReturnRef, kCFBooleanTrue,
318 NULL);
319 result = SecItemCopyMatching(attrs, (CFTypeRef *)&refs);
320 CFRelease(attrs);
321
322 if (!result) {
323 for (CFIndex i = 0; !result && i < CFArrayGetCount(refs); i++)
324 result = delete_ref(CFArrayGetValueAtIndex(refs, i));
325
326 CFRelease(refs);
327 }
328
329 /* We consider not found to not be an error */
330 if (result == errSecItemNotFound)
331 result = errSecSuccess;
332
333 return result;
334 }
335
336 static OSStatus add_internet_password(void)
337 {
338 CFMutableDataRef data;
339 CFDictionaryRef attrs;
340 OSStatus result;
341
342 /* Only store complete credentials */
343 if (!protocol || !host || !username || !password)
344 return -1;
345
346 if (state_seen) {
347 struct strbuf sb;
348
349 strbuf_init(&sb, 1024);
350 encode_state_seen(&sb);
351 if (!strcmp(state_seen, sb.buf)) {
352 strbuf_release(&sb);
353 return errSecSuccess;
354 }
355 strbuf_release(&sb);
356 }
357
358 data = CFDataCreateMutableCopy(kCFAllocatorDefault, 0, password);
359 if (password_expiry_utc) {
360 CFDataAppendBytes(data,
361 (const UInt8 *)STRING_WITH_LENGTH("\npassword_expiry_utc="));
362 CFDataAppendBytes(data,
363 CFDataGetBytePtr(password_expiry_utc),
364 CFDataGetLength(password_expiry_utc));
365 }
366 if (oauth_refresh_token) {
367 CFDataAppendBytes(data,
368 (const UInt8 *)STRING_WITH_LENGTH("\noauth_refresh_token="));
369 CFDataAppendBytes(data,
370 CFDataGetBytePtr(oauth_refresh_token),
371 CFDataGetLength(oauth_refresh_token));
372 }
373
374 attrs = CREATE_SEC_ATTRIBUTES(kSecValueData, data,
375 NULL);
376
377 result = SecItemAdd(attrs, NULL);
378 if (result == errSecDuplicateItem) {
379 CFDictionaryRef query;
380 query = CREATE_SEC_ATTRIBUTES(NULL);
381 result = SecItemUpdate(query, attrs);
382 CFRelease(query);
383 }
384
385 CFRelease(data);
386 CFRelease(attrs);
387
388 return result;
389 }
390
391 static void read_credential(void)
392 {
393 char *buf = NULL;
394 size_t alloc;
395 ssize_t line_len;
396
397 while ((line_len = getline(&buf, &alloc, stdin)) > 0) {
398 char *v;
399
400 if (!strcmp(buf, "\n"))
401 break;
402 buf[line_len-1] = '\0';
403
404 v = strchr(buf, '=');
405 if (!v)
406 die("bad input: %s", buf);
407 *v++ = '\0';
408
409 if (!strcmp(buf, "protocol")) {
410 if (!strcmp(v, "imap"))
411 protocol = kSecAttrProtocolIMAP;
412 else if (!strcmp(v, "imaps"))
413 protocol = kSecAttrProtocolIMAPS;
414 else if (!strcmp(v, "ftp"))
415 protocol = kSecAttrProtocolFTP;
416 else if (!strcmp(v, "ftps"))
417 protocol = kSecAttrProtocolFTPS;
418 else if (!strcmp(v, "https"))
419 protocol = kSecAttrProtocolHTTPS;
420 else if (!strcmp(v, "http"))
421 protocol = kSecAttrProtocolHTTP;
422 else if (!strcmp(v, "smtp"))
423 protocol = kSecAttrProtocolSMTP;
424 else {
425 /* we don't yet handle other protocols */
426 clear_credential();
427 exit(0);
428 }
429 }
430 else if (!strcmp(buf, "host")) {
431 char *colon = strchr(v, ':');
432 if (colon) {
433 UInt16 port_i;
434 *colon++ = '\0';
435 port_i = atoi(colon);
436 port = CFNumberCreate(kCFAllocatorDefault,
437 kCFNumberShortType,
438 &port_i);
439 }
440 host = CFStringCreateWithCString(kCFAllocatorDefault,
441 v,
442 ENCODING);
443 }
444 else if (!strcmp(buf, "path"))
445 path = CFStringCreateWithCString(kCFAllocatorDefault,
446 v,
447 ENCODING);
448 else if (!strcmp(buf, "username"))
449 username = CFStringCreateWithCString(
450 kCFAllocatorDefault,
451 v,
452 ENCODING);
453 else if (!strcmp(buf, "password"))
454 password = CFDataCreate(kCFAllocatorDefault,
455 (UInt8 *)v,
456 strlen(v));
457 else if (!strcmp(buf, "password_expiry_utc"))
458 password_expiry_utc = CFDataCreate(kCFAllocatorDefault,
459 (UInt8 *)v,
460 strlen(v));
461 else if (!strcmp(buf, "oauth_refresh_token"))
462 oauth_refresh_token = CFDataCreate(kCFAllocatorDefault,
463 (UInt8 *)v,
464 strlen(v));
465 else if (!strcmp(buf, "state[]")) {
466 int len = strlen("osxkeychain:seen=");
467 if (!strncmp(v, "osxkeychain:seen=", len))
468 state_seen = xstrdup(v);
469 }
470 /*
471 * Ignore other lines; we don't know what they mean, but
472 * this future-proofs us when later versions of git do
473 * learn new lines, and the helpers are updated to match.
474 */
475 }
476
477 free(buf);
478 }
479
480 int main(int argc, const char **argv)
481 {
482 OSStatus result = 0;
483 const char *usage =
484 "usage: git credential-osxkeychain <get|store|erase>";
485
486 if (argc < 2 || !*argv[1])
487 die("%s", usage);
488
489 if (open(argv[0], O_RDONLY | O_EXLOCK) == -1)
490 die("failed to lock %s", argv[0]);
491
492 read_credential();
493
494 if (!strcmp(argv[1], "get"))
495 result = find_internet_password();
496 else if (!strcmp(argv[1], "store"))
497 result = add_internet_password();
498 else if (!strcmp(argv[1], "erase"))
499 result = delete_internet_password();
500 /* otherwise, ignore unknown action */
501
502 if (result)
503 die("failed to %s: %d", argv[1], (int)result);
504
505 clear_credential();
506
507 if (state_seen)
508 free(state_seen);
509
510 return 0;
511 }