[PATCH] contrib: add a pair of credential helpers for Mac OS X's keychain

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



This credential helper adds, searches, and removes entries from
the Mac OS X keychain. The C version links against the Security
framework and is probably the best choice for daily use.

A python version is also included primarily as a more readable
example and uses the /usr/bin/security CLI to access the keychain.

Tested with 10.6.8.

Signed-off-by: Jay Soffian <jaysoffian@xxxxxxxxx>
---
Here's a C version that no longer links to git. I also kept the original
Python version as an example. I decided not to call out to
'git credential-gitpass' as it was simple enough to manage /dev/tty
and there's no portability issues since this is OS X specific.

 contrib/credential-osxkeychain/Makefile            |   14 +
 .../git-credential-osxkeychain.c                   |  300 ++++++++++++++++++++
 .../git-credential-osxkeychain.py                  |  148 ++++++++++
 3 files changed, 462 insertions(+), 0 deletions(-)
 create mode 100644 contrib/credential-osxkeychain/Makefile
 create mode 100644 contrib/credential-osxkeychain/git-credential-osxkeychain.c
 create mode 100755 contrib/credential-osxkeychain/git-credential-osxkeychain.py

diff --git a/contrib/credential-osxkeychain/Makefile b/contrib/credential-osxkeychain/Makefile
new file mode 100644
index 0000000000..a0a7074cc6
--- /dev/null
+++ b/contrib/credential-osxkeychain/Makefile
@@ -0,0 +1,14 @@
+all:: git-credential-osxkeychain
+
+CC = gcc
+RM = rm -f
+CFLAGS = -O2 -Wall
+
+git-credential-osxkeychain: git-credential-osxkeychain.o
+	$(CC) -o $@ $< -Wl,-framework -Wl,Security
+
+git-credential-osxkeychain.o: git-credential-osxkeychain.c
+	$(CC) -c $(CFLAGS) $<
+
+clean:
+	$(RM) git-credential-osxkeychain git-credential-osxkeychain.o
diff --git a/contrib/credential-osxkeychain/git-credential-osxkeychain.c b/contrib/credential-osxkeychain/git-credential-osxkeychain.c
new file mode 100644
index 0000000000..2f611f7348
--- /dev/null
+++ b/contrib/credential-osxkeychain/git-credential-osxkeychain.c
@@ -0,0 +1,300 @@
+/* Copyright 2011 Jay Soffian. All rights reserved.
+ * FreeBSD License.
+ *
+ * A git credential helper that interfaces with the Mac OS X keychain
+ * via the Security framework.
+ */
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <termios.h>
+#include <Security/Security.h>
+
+static void die(const char *err, ...)
+{
+	char msg[4096];
+	va_list params;
+	va_start(params, err);
+	vsnprintf(msg, sizeof(msg), err, params);
+	fprintf(stderr, "%s\n", msg);
+	va_end(params);
+	exit(1);
+}
+
+void *xmalloc(size_t size)
+{
+	void *ret = malloc(size);
+	if (!ret)
+		die("Out of memory");
+	return ret;
+}
+
+void *xstrdup(const char *s1)
+{
+	void *ret = strdup(s1);
+	if (!ret)
+		die("Out of memory");
+	return ret;
+}
+
+void emit_user_pass(char *username, char *password)
+{
+	if (username)
+		printf("username=%s\n", username);
+	if (password)
+		printf("password=%s\n", password);
+}
+
+typedef enum { USERNAME, PASSWORD } prompt_type;
+
+void prompt(FILE *file, const char *what, const char *desc)
+{
+	if (desc)
+		fprintf(file, "%s for '%s': ", what, desc);
+	else
+		fprintf(file, "%s: ", what);
+}
+
+char *prompt_tty(prompt_type what, char *description)
+{
+	struct termios old;
+	struct termios new;
+	char buf[128];
+	int buf_len;
+	int fd = open("/dev/tty", O_RDWR|O_NOCTTY);
+	FILE *tty = fdopen(fd, "w+");
+	if (what == USERNAME) {
+		prompt(tty, "Username", description);
+	}
+	else {
+		prompt(tty, "Password", description);
+		tcgetattr(fd, &old);
+		memcpy(&new, &old, sizeof(struct termios));
+		new.c_lflag &= ~ECHO;
+		tcsetattr(fd, TCSADRAIN, &new);
+	}
+	if (!fgets(buf, sizeof(buf), tty)) {
+		fprintf(tty, "\n");
+		fclose(tty);
+		return NULL;
+	}
+	if (what == PASSWORD) {
+		tcsetattr(fd, TCSADRAIN, &old);
+		fprintf(tty, "\n");
+	}
+	fclose(tty);
+	buf_len = strlen(buf);
+	if (buf[buf_len-1] == '\n')
+		buf[buf_len-1] = '\0';
+	return xstrdup(buf);
+}
+
+char *username_from_keychain_item(SecKeychainItemRef item)
+{
+	OSStatus status;
+	SecKeychainAttributeList list;
+	SecKeychainAttribute attr;
+	list.count = 1;
+	list.attr = &attr;
+	attr.tag = kSecAccountItemAttr;
+	char *username;
+
+	status = SecKeychainItemCopyContent(item, NULL, &list, NULL, NULL);
+	if (status != noErr)
+		return NULL;
+	username = xmalloc(attr.length + 1);
+	strncpy(username, attr.data, attr.length);
+	username[attr.length] = '\0';
+	SecKeychainItemFreeContent(&list, NULL);
+	return username;
+}
+
+int find_internet_password(SecProtocolType protocol,
+			   char *hostname,
+			   char *username)
+{
+	void *password_buf;
+	UInt32 password_len;
+	OSStatus status;
+	char *password;
+	int free_username = 0;
+	SecKeychainItemRef item;
+
+	status = SecKeychainFindInternetPassword(
+			NULL,
+			strlen(hostname), hostname,
+			0, NULL,
+			username ? strlen(username) : 0, username,
+			0, NULL,
+			0,
+			protocol,
+			kSecAuthenticationTypeDefault,
+			&password_len, &password_buf,
+			&item);
+	if (status != noErr)
+		return -1;
+
+	password = xmalloc(password_len + 1);
+	strncpy(password, password_buf, password_len);
+	password[password_len] = '\0';
+	SecKeychainItemFreeContent(NULL, password_buf);
+	if (!username) {
+		username = username_from_keychain_item(item);
+		free_username = 1;
+	}
+	emit_user_pass(username, password);
+	if (free_username)
+		free(username);
+	free(password);
+	return 0;
+}
+
+void delete_internet_password(SecProtocolType protocol,
+			      char *hostname,
+			      char *username)
+{
+	OSStatus status;
+	SecKeychainItemRef item;
+
+	status = SecKeychainFindInternetPassword(
+			NULL,
+			strlen(hostname), hostname,
+			0, NULL,
+			username ? strlen(username) : 0, username,
+			0, NULL,
+			0,
+			protocol,
+			kSecAuthenticationTypeDefault,
+			0, NULL,
+			&item);
+	if (status != noErr)
+		return;
+	SecKeychainItemDelete(item);
+}
+
+void add_internet_password(SecProtocolType protocol,
+			   char *hostname,
+			   char *username,
+			   char *password,
+			   char *comment)
+{
+	const char *label_format = "%s (%s)";
+	char *label;
+	OSStatus status;
+	SecKeychainItemRef item;
+	SecKeychainAttributeList list;
+	SecKeychainAttribute attr;
+	list.count = 1;
+	list.attr = &attr;
+	status = SecKeychainAddInternetPassword(
+			NULL,
+			strlen(hostname), hostname,
+			0, NULL,
+			strlen(username), username,
+			0, NULL,
+			0,
+			protocol,
+			kSecAuthenticationTypeDefault,
+			strlen(password), password,
+			&item);
+	if (status != noErr)
+		return;
+
+	/* set the comment */
+	attr.tag = kSecCommentItemAttr;
+	attr.data = comment;
+	attr.length = strlen(comment);
+	SecKeychainItemModifyContent(item, &list, 0, NULL);
+
+	/* override the label */
+	label = xmalloc(strlen(hostname) + strlen(username) +
+			strlen(label_format));
+	sprintf(label, label_format, hostname, username);
+	attr.tag = kSecLabelItemAttr;
+	attr.data = label;
+	attr.length = strlen(label);
+	SecKeychainItemModifyContent(item, &list, 0, NULL);
+}
+
+int main(int argc, const char **argv)
+{
+	const char *usage =
+		"Usage: git credential-osxkeychain --unique=TOKEN [options]\n"
+		"Options:\n"
+		"    --description=DESCRIPTION\n"
+		"    --username=USERNAME\n"
+		"    --reject";
+	char *description = NULL, *username = NULL, *unique = NULL;
+	char *hostname, *password;
+	int i, free_username = 0, reject = 0;
+	SecProtocolType protocol = 0;
+
+	for (i = 1; i < argc; i++) {
+		const char *arg = argv[i];
+		if (!strncmp(arg, "--description=", 14)) {
+			description = (char *) arg + 14;
+		}
+		else if (!strncmp(arg, "--username=", 11)) {
+			username = (char *) arg + 11;
+		}
+		else if (!strncmp(arg, "--unique=", 9)) {
+			unique = (char *) arg + 9;
+		}
+		else if (!strcmp(arg, "--reject")) {
+			reject = 1;
+		}
+		else if (!strcmp(arg, "--help")) {
+			die(usage);
+		}
+		else
+			die("Unrecognized argument `%s'; try --help", arg);
+	}
+
+	if (!unique)
+		die("Must specify --unique=TOKEN; try --help");
+
+	hostname = strchr(unique, ':');
+	if (!hostname)
+		die("Invalid token `%s'", unique);
+	*hostname++ = '\0';
+
+	/* "GitHub for Mac" compatibility */
+	if (!strcmp(hostname, "github.com"))
+		hostname = "github.com/mac";
+
+	if (!strcmp(unique, "https")) {
+		protocol = kSecProtocolTypeHTTPS;
+	} else if (!strcmp(unique, "http")) {
+		protocol = kSecProtocolTypeHTTP;
+	}
+	else
+		die("Unrecognized protocol `%s'", unique);
+
+	/* if this is a rejection delete the existing creds */
+	if (reject) {
+		delete_internet_password(protocol, hostname, username);
+		return 0;
+	}
+
+	/* otherwise look for a matching keychain item */
+	if (!find_internet_password(protocol, hostname, username))
+		return 0;
+
+	/* no keychain item found, prompt the user and store the result */
+	if (!username) {
+		if (!(username = prompt_tty(USERNAME, description)))
+			return 0;
+		free_username = 1;
+	}
+	if (!(password = prompt_tty(PASSWORD, description)))
+		return 0;
+
+	add_internet_password(protocol, hostname, username, password,
+			      description ? description : "default");
+	emit_user_pass(username, password);
+	if (free_username)
+		free(username);
+	free(password);
+	return 0;
+}
diff --git a/contrib/credential-osxkeychain/git-credential-osxkeychain.py b/contrib/credential-osxkeychain/git-credential-osxkeychain.py
new file mode 100755
index 0000000000..ae5ec00d68
--- /dev/null
+++ b/contrib/credential-osxkeychain/git-credential-osxkeychain.py
@@ -0,0 +1,148 @@
+#!/usr/bin/python
+# Copyright 2011 Jay Soffian. All rights reserved.
+# FreeBSD License.
+"""
+A git credential helper that interfaces with the Mac OS X keychain via
+/usr/bin/security.
+"""
+
+import os
+import re
+import sys
+import termios
+from getpass import _raw_input
+from optparse import OptionParser
+from subprocess import Popen, PIPE
+
+USERNAME = 'USERNAME'
+PASSWORD = 'PASSWORD'
+PROMPTS = dict(USERNAME='Username', PASSWORD='Password')
+
+def prompt_tty(what, desc):
+    """Prompt on TTY for username or password with optional description"""
+    prompt = '%s%s: ' % (PROMPTS[what], " for '%s'" % desc if desc else '')
+    # Borrowed mostly from getpass.py
+    fd = os.open('/dev/tty', os.O_RDWR|os.O_NOCTTY)
+    tty = os.fdopen(fd, 'w+', 1)
+    if what == USERNAME:
+        return _raw_input(prompt, tty, tty)
+    old = termios.tcgetattr(fd) # a copy to save
+    new = old[:]
+    new[3] &= ~termios.ECHO  # 3 == 'lflags'
+    try:
+        termios.tcsetattr(fd, termios.TCSADRAIN, new)
+        return _raw_input(prompt, tty, tty)
+    finally:
+        termios.tcsetattr(fd, termios.TCSADRAIN, old)
+        tty.write('\n')
+
+def emit_user_pass(username, password):
+    if username:
+        print 'username=' + username
+    if password:
+        print 'password=' + password
+
+def make_security_args(command, protocol, hostname, username):
+    args = ['/usr/bin/security', command]
+    # tlfd is 'dflt' backwards - obvious /usr/bin/security bug
+    # but allows us to ignore matching saved web forms.
+    args.extend(['-t', 'tlfd'])
+    args.extend(['-r', protocol])
+    if hostname:
+        args.extend(['-s', hostname])
+    if username:
+        args.extend(['-a', username])
+    return args
+
+def find_internet_password(protocol, hostname, username):
+    args = make_security_args('find-internet-password',
+                              protocol, hostname, username)
+    args.append('-g') # asks for password on stderr
+    p = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
+    # grok stdout for username
+    out, err = p.communicate()
+    if p.returncode != 0:
+        return
+    for line in out.splitlines(): # pylint:disable-msg=E1103
+        m = re.search(r'^\s+"acct"<blob>=[^"]*"(.*)"$', line)
+        if m:
+            username = m.group(1)
+            break
+    # grok stderr for password
+    m = re.search(r'^password:[^"]*"(.*)"$', err)
+    if not m:
+        return
+    emit_user_pass(username, m.group(1))
+    return True
+
+def delete_internet_password(protocol, hostname, username):
+    args = make_security_args('delete-internet-password',
+                              protocol, hostname, username)
+    p = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
+    p.communicate()
+
+def add_internet_password(protocol, hostname, username, password):
+    # We do this over a pipe so that we can provide the password more
+    # securely than as an argument which would show up in ps output.
+    # Unfortunately this is possibly less robust since the security man
+    # page does not document how to quote arguments. Emprically it seems
+    # that using the double-quote, escaping \ and " works properly.
+    username = username.replace('\\', '\\\\').replace('"', '\\"')
+    password = password.replace('\\', '\\\\').replace('"', '\\"')
+    command = ' '.join([
+        'add-internet-password', '-U',
+        '-r', protocol,
+        '-s', hostname,
+        '-a "%s"' % username,
+        '-w "%s"' % password,
+        '-j default',
+        '-l "%s (%s)"' % (hostname, username),
+    ]) + '\n'
+    args = ['/usr/bin/security', '-i']
+    p = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
+    p.communicate(command)
+
+def main():
+    p = OptionParser()
+    p.add_option('--description')
+    p.add_option('--reject', action='store_true')
+    p.add_option('--unique', dest='token', help='REQUIRED OPTION')
+    p.add_option('--username')
+    opts, _ = p.parse_args()
+
+    if not opts.token:
+        p.error('--unique option required')
+    if not ':' in opts.token:
+        print >> sys.stderr, "Invalid token: '%s'" % opts.token
+        return 1
+    protocol, hostname = opts.token.split(':', 1)
+    if protocol not in ('http', 'https'):
+        print >> sys.stderr, "Unsupported protocol: '%s'" % protocol
+        return 1
+    if protocol == 'https':
+        protocol = 'htps'
+
+    # "GitHub for Mac" compatibility
+    if hostname == 'github.com':
+        hostname = 'github.com/mac'
+
+    # if this is a rejection delete the existing creds
+    if opts.reject:
+        delete_internet_password(protocol, hostname, opts.username)
+        return 0
+
+    # otherwise look for creds
+    if find_internet_password(protocol, hostname, opts.username):
+        return 0
+
+    # creds not found, so prompt the user then store the creds
+    username = opts.username
+    if username is None:
+        username = prompt_tty(USERNAME, opts.description)
+    password = prompt_tty(PASSWORD, opts.description)
+    add_internet_password(protocol, hostname, username, password)
+    emit_user_pass(username, password)
+    return 0
+
+if __name__ == '__main__':
+    sys.exit(main())
-- 
1.7.7.rc1.1.g011e1

--
To unsubscribe from this list: send the line "unsubscribe git" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html


[Index of Archives]     [Linux Kernel Development]     [Gcc Help]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [V4L]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Fedora Users]