[PATCH] contrib: add a credential helper for Mac OS X's keychain

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

 



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@xxxxxxxxx>
---
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

--
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]