The eleventh batch
[alt-git.git] / contrib / credential / osxkeychain / git-credential-osxkeychain.c
blob1c8310d7fefca066ee5da304bc895cf43be063f2
1 #include <stdio.h>
2 #include <string.h>
3 #include <stdlib.h>
4 #include <Security/Security.h>
6 #define ENCODING kCFStringEncodingUTF8
7 static CFStringRef protocol; /* Stores constant strings - not memory managed */
8 static CFStringRef host;
9 static CFNumberRef port;
10 static CFStringRef path;
11 static CFStringRef username;
12 static CFDataRef password;
13 static CFDataRef password_expiry_utc;
14 static CFDataRef oauth_refresh_token;
15 static int state_seen;
17 static void clear_credential(void)
19 if (host) {
20 CFRelease(host);
21 host = NULL;
23 if (port) {
24 CFRelease(port);
25 port = NULL;
27 if (path) {
28 CFRelease(path);
29 path = NULL;
31 if (username) {
32 CFRelease(username);
33 username = NULL;
35 if (password) {
36 CFRelease(password);
37 password = NULL;
39 if (password_expiry_utc) {
40 CFRelease(password_expiry_utc);
41 password_expiry_utc = NULL;
43 if (oauth_refresh_token) {
44 CFRelease(oauth_refresh_token);
45 oauth_refresh_token = NULL;
49 #define STRING_WITH_LENGTH(s) s, sizeof(s) - 1
51 __attribute__((format (printf, 1, 2), __noreturn__))
52 static void die(const char *err, ...)
54 char msg[4096];
55 va_list params;
56 va_start(params, err);
57 vsnprintf(msg, sizeof(msg), err, params);
58 fprintf(stderr, "%s\n", msg);
59 va_end(params);
60 clear_credential();
61 exit(1);
64 static void *xmalloc(size_t len)
66 void *ret = malloc(len);
67 if (!ret)
68 die("Out of memory");
69 return ret;
72 static CFDictionaryRef create_dictionary(CFAllocatorRef allocator, ...)
74 va_list args;
75 const void *key;
76 CFMutableDictionaryRef result;
78 result = CFDictionaryCreateMutable(allocator,
80 &kCFTypeDictionaryKeyCallBacks,
81 &kCFTypeDictionaryValueCallBacks);
84 va_start(args, allocator);
85 while ((key = va_arg(args, const void *)) != NULL) {
86 const void *value;
87 value = va_arg(args, const void *);
88 if (value)
89 CFDictionarySetValue(result, key, value);
91 va_end(args);
93 return result;
96 #define CREATE_SEC_ATTRIBUTES(...) \
97 create_dictionary(kCFAllocatorDefault, \
98 kSecClass, kSecClassInternetPassword, \
99 kSecAttrServer, host, \
100 kSecAttrAccount, username, \
101 kSecAttrPath, path, \
102 kSecAttrPort, port, \
103 kSecAttrProtocol, protocol, \
104 kSecAttrAuthenticationType, \
105 kSecAttrAuthenticationTypeDefault, \
106 __VA_ARGS__);
108 static void write_item(const char *what, const char *buf, size_t len)
110 printf("%s=", what);
111 fwrite(buf, 1, len, stdout);
112 putchar('\n');
115 static void find_username_in_item(CFDictionaryRef item)
117 CFStringRef account_ref;
118 char *username_buf;
119 CFIndex buffer_len;
121 account_ref = CFDictionaryGetValue(item, kSecAttrAccount);
122 if (!account_ref)
124 write_item("username", "", 0);
125 return;
128 username_buf = (char *)CFStringGetCStringPtr(account_ref, ENCODING);
129 if (username_buf)
131 write_item("username", username_buf, strlen(username_buf));
132 return;
135 /* If we can't get a CString pointer then
136 * we need to allocate our own buffer */
137 buffer_len = CFStringGetMaximumSizeForEncoding(
138 CFStringGetLength(account_ref), ENCODING) + 1;
139 username_buf = xmalloc(buffer_len);
140 if (CFStringGetCString(account_ref,
141 username_buf,
142 buffer_len,
143 ENCODING)) {
144 write_item("username", username_buf, strlen(username_buf));
146 free(username_buf);
149 static OSStatus find_internet_password(void)
151 CFDictionaryRef attrs;
152 CFDictionaryRef item;
153 CFDataRef data;
154 OSStatus result;
156 attrs = CREATE_SEC_ATTRIBUTES(kSecMatchLimit, kSecMatchLimitOne,
157 kSecReturnAttributes, kCFBooleanTrue,
158 kSecReturnData, kCFBooleanTrue,
159 NULL);
160 result = SecItemCopyMatching(attrs, (CFTypeRef *)&item);
161 if (result) {
162 goto out;
165 data = CFDictionaryGetValue(item, kSecValueData);
167 write_item("password",
168 (const char *)CFDataGetBytePtr(data),
169 CFDataGetLength(data));
170 if (!username)
171 find_username_in_item(item);
173 CFRelease(item);
175 write_item("capability[]", "state", strlen("state"));
176 write_item("state[]", "osxkeychain:seen=1", strlen("osxkeychain:seen=1"));
178 out:
179 CFRelease(attrs);
181 /* We consider not found to not be an error */
182 if (result == errSecItemNotFound)
183 result = errSecSuccess;
185 return result;
188 static OSStatus delete_ref(const void *itemRef)
190 CFArrayRef item_ref_list;
191 CFDictionaryRef delete_query;
192 OSStatus result;
194 item_ref_list = CFArrayCreate(kCFAllocatorDefault,
195 &itemRef,
197 &kCFTypeArrayCallBacks);
198 delete_query = create_dictionary(kCFAllocatorDefault,
199 kSecClass, kSecClassInternetPassword,
200 kSecMatchItemList, item_ref_list,
201 NULL);
203 if (password) {
204 /* We only want to delete items with a matching password */
205 CFIndex capacity;
206 CFMutableDictionaryRef query;
207 CFDataRef data;
209 capacity = CFDictionaryGetCount(delete_query) + 1;
210 query = CFDictionaryCreateMutableCopy(kCFAllocatorDefault,
211 capacity,
212 delete_query);
213 CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue);
214 result = SecItemCopyMatching(query, (CFTypeRef *)&data);
215 if (!result) {
216 CFDataRef kc_password;
217 const UInt8 *raw_data;
218 const UInt8 *line;
220 /* Don't match appended metadata */
221 raw_data = CFDataGetBytePtr(data);
222 line = memchr(raw_data, '\n', CFDataGetLength(data));
223 if (line)
224 kc_password = CFDataCreateWithBytesNoCopy(
225 kCFAllocatorDefault,
226 raw_data,
227 line - raw_data,
228 kCFAllocatorNull);
229 else
230 kc_password = data;
232 if (CFEqual(kc_password, password))
233 result = SecItemDelete(delete_query);
235 if (line)
236 CFRelease(kc_password);
237 CFRelease(data);
240 CFRelease(query);
241 } else {
242 result = SecItemDelete(delete_query);
245 CFRelease(delete_query);
246 CFRelease(item_ref_list);
248 return result;
251 static OSStatus delete_internet_password(void)
253 CFDictionaryRef attrs;
254 CFArrayRef refs;
255 OSStatus result;
258 * Require at least a protocol and host for removal, which is what git
259 * will give us; if you want to do something more fancy, use the
260 * Keychain manager.
262 if (!protocol || !host)
263 return -1;
265 attrs = CREATE_SEC_ATTRIBUTES(kSecMatchLimit, kSecMatchLimitAll,
266 kSecReturnRef, kCFBooleanTrue,
267 NULL);
268 result = SecItemCopyMatching(attrs, (CFTypeRef *)&refs);
269 CFRelease(attrs);
271 if (!result) {
272 for (CFIndex i = 0; !result && i < CFArrayGetCount(refs); i++)
273 result = delete_ref(CFArrayGetValueAtIndex(refs, i));
275 CFRelease(refs);
278 /* We consider not found to not be an error */
279 if (result == errSecItemNotFound)
280 result = errSecSuccess;
282 return result;
285 static OSStatus add_internet_password(void)
287 CFMutableDataRef data;
288 CFDictionaryRef attrs;
289 OSStatus result;
291 if (state_seen)
292 return errSecSuccess;
294 /* Only store complete credentials */
295 if (!protocol || !host || !username || !password)
296 return -1;
298 data = CFDataCreateMutableCopy(kCFAllocatorDefault, 0, password);
299 if (password_expiry_utc) {
300 CFDataAppendBytes(data,
301 (const UInt8 *)STRING_WITH_LENGTH("\npassword_expiry_utc="));
302 CFDataAppendBytes(data,
303 CFDataGetBytePtr(password_expiry_utc),
304 CFDataGetLength(password_expiry_utc));
306 if (oauth_refresh_token) {
307 CFDataAppendBytes(data,
308 (const UInt8 *)STRING_WITH_LENGTH("\noauth_refresh_token="));
309 CFDataAppendBytes(data,
310 CFDataGetBytePtr(oauth_refresh_token),
311 CFDataGetLength(oauth_refresh_token));
314 attrs = CREATE_SEC_ATTRIBUTES(kSecValueData, data,
315 NULL);
317 result = SecItemAdd(attrs, NULL);
318 if (result == errSecDuplicateItem) {
319 CFDictionaryRef query;
320 query = CREATE_SEC_ATTRIBUTES(NULL);
321 result = SecItemUpdate(query, attrs);
322 CFRelease(query);
325 CFRelease(data);
326 CFRelease(attrs);
328 return result;
331 static void read_credential(void)
333 char *buf = NULL;
334 size_t alloc;
335 ssize_t line_len;
337 while ((line_len = getline(&buf, &alloc, stdin)) > 0) {
338 char *v;
340 if (!strcmp(buf, "\n"))
341 break;
342 buf[line_len-1] = '\0';
344 v = strchr(buf, '=');
345 if (!v)
346 die("bad input: %s", buf);
347 *v++ = '\0';
349 if (!strcmp(buf, "protocol")) {
350 if (!strcmp(v, "imap"))
351 protocol = kSecAttrProtocolIMAP;
352 else if (!strcmp(v, "imaps"))
353 protocol = kSecAttrProtocolIMAPS;
354 else if (!strcmp(v, "ftp"))
355 protocol = kSecAttrProtocolFTP;
356 else if (!strcmp(v, "ftps"))
357 protocol = kSecAttrProtocolFTPS;
358 else if (!strcmp(v, "https"))
359 protocol = kSecAttrProtocolHTTPS;
360 else if (!strcmp(v, "http"))
361 protocol = kSecAttrProtocolHTTP;
362 else if (!strcmp(v, "smtp"))
363 protocol = kSecAttrProtocolSMTP;
364 else {
365 /* we don't yet handle other protocols */
366 clear_credential();
367 exit(0);
370 else if (!strcmp(buf, "host")) {
371 char *colon = strchr(v, ':');
372 if (colon) {
373 UInt16 port_i;
374 *colon++ = '\0';
375 port_i = atoi(colon);
376 port = CFNumberCreate(kCFAllocatorDefault,
377 kCFNumberShortType,
378 &port_i);
380 host = CFStringCreateWithCString(kCFAllocatorDefault,
382 ENCODING);
384 else if (!strcmp(buf, "path"))
385 path = CFStringCreateWithCString(kCFAllocatorDefault,
387 ENCODING);
388 else if (!strcmp(buf, "username"))
389 username = CFStringCreateWithCString(
390 kCFAllocatorDefault,
392 ENCODING);
393 else if (!strcmp(buf, "password"))
394 password = CFDataCreate(kCFAllocatorDefault,
395 (UInt8 *)v,
396 strlen(v));
397 else if (!strcmp(buf, "password_expiry_utc"))
398 password_expiry_utc = CFDataCreate(kCFAllocatorDefault,
399 (UInt8 *)v,
400 strlen(v));
401 else if (!strcmp(buf, "oauth_refresh_token"))
402 oauth_refresh_token = CFDataCreate(kCFAllocatorDefault,
403 (UInt8 *)v,
404 strlen(v));
405 else if (!strcmp(buf, "state[]")) {
406 if (!strcmp(v, "osxkeychain:seen=1"))
407 state_seen = 1;
410 * Ignore other lines; we don't know what they mean, but
411 * this future-proofs us when later versions of git do
412 * learn new lines, and the helpers are updated to match.
416 free(buf);
419 int main(int argc, const char **argv)
421 OSStatus result = 0;
422 const char *usage =
423 "usage: git credential-osxkeychain <get|store|erase>";
425 if (!argv[1])
426 die("%s", usage);
428 if (open(argv[0], O_RDONLY | O_EXLOCK) == -1)
429 die("failed to lock %s", argv[0]);
431 read_credential();
433 if (!strcmp(argv[1], "get"))
434 result = find_internet_password();
435 else if (!strcmp(argv[1], "store"))
436 result = add_internet_password();
437 else if (!strcmp(argv[1], "erase"))
438 result = delete_internet_password();
439 /* otherwise, ignore unknown action */
441 if (result)
442 die("failed to %s: %d", argv[1], (int)result);
444 clear_credential();
446 return 0;