From: Jay Soffian <jaysoffian@gmail.com>
To: git@vger.kernel.org
Cc: Jay Soffian <jaysoffian@gmail.com>,
Junio C Hamano <junio@kernel.org>, Jeff King <peff@peff.net>
Subject: [PATCH] contrib: add a credential helper for Mac OS X's keychain
Date: Sat, 10 Sep 2011 15:44:34 -0400 [thread overview]
Message-ID: <1315683874-95583-1-git-send-email-jaysoffian@gmail.com> (raw)
A credential helper which uses /usr/bin/security to add, search,
and remove entries from the Mac OS X keychain.
Tested with 10.6.8.
Signed-off-by: Jay Soffian <jaysoffian@gmail.com>
---
This is a quick script to explore the new credential API. A more robust
implementation would be to link to OS X's Security framework from C.
contrib/credential/git-credential-osxkeychain | 148 +++++++++++++++++++++++++
1 files changed, 148 insertions(+), 0 deletions(-)
create mode 100755 contrib/credential/git-credential-osxkeychain
diff --git a/contrib/credential/git-credential-osxkeychain b/contrib/credential/git-credential-osxkeychain
new file mode 100755
index 0000000000..ae5ec00d68
--- /dev/null
+++ b/contrib/credential/git-credential-osxkeychain
@@ -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.6.346.g5a895
next reply other threads:[~2011-09-10 19:44 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2011-09-10 19:44 Jay Soffian [this message]
2011-09-14 8:01 ` [PATCH] contrib: add a credential helper for Mac OS X's keychain John Szakmeister
2011-09-14 13:31 ` Jay Soffian
2011-09-14 22:23 ` John Szakmeister
2011-09-14 10:24 ` John Szakmeister
-- strict thread matches above, loose matches on Subject: below --
2011-09-14 17:58 Jay Soffian
2011-09-14 18:19 ` Jay Soffian
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=1315683874-95583-1-git-send-email-jaysoffian@gmail.com \
--to=jaysoffian@gmail.com \
--cc=git@vger.kernel.org \
--cc=junio@kernel.org \
--cc=peff@peff.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.