threads / patch / 28353

patchcontrib: add a credential helper for Mac OS X's keychain

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

## tl;dr

5 messages between Sep 10, 2011 and Sep 14, 2011. Diffs are folded; open one to read it.

replies: 4people: 2as markdown or json

Jay Soffian· Sep 10, 2011, 19:44 UTC · lore

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
Show changes to contrib/credential/git-credential-osxkeychain +148 −0
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
John Szakmeister· Sep 14, 2011, 08:01 UTC · re: Jay Soffian · lore

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

On Sat, Sep 10, 2011 at 3:44 PM, Jay Soffian <jaysoffian@gmail.com> wrote:
Show 9 quoted lines
> 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.
[snip]
Show 6 quoted lines
> +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.

I'm not sure this comment is correct... it looks like you're passing the password on the command line...

Show 8 quoted lines
> +    username = username.replace('\\', '\\\\').replace('"', '\\"')
> +    password = password.replace('\\', '\\\\').replace('"', '\\"')
> +    command = ' '.join([
> +        'add-internet-password', '-U',
> +        '-r', protocol,
> +        '-s', hostname,
> +        '-a "%s"' % username,
> +        '-w "%s"' % password,
...right here. :-(
-John
Jay Soffian· Sep 14, 2011, 13:31 UTC · re: John Szakmeister · lore

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

On Wed, Sep 14, 2011 at 4:01 AM, John Szakmeister <john@szakmeister.net> wrote:
Show 20 quoted lines
>> +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.
>
> I'm not sure this comment is correct... it looks like you're passing
> the password on the command line...
>
>> +    username = username.replace('\\', '\\\\').replace('"', '\\"')
>> +    password = password.replace('\\', '\\\\').replace('"', '\\"')
>> +    command = ' '.join([
>> +        'add-internet-password', '-U',
>> +        '-r', protocol,
>> +        '-s', hostname,
>> +        '-a "%s"' % username,
>> +        '-w "%s"' % password,
>
> ...right here. :-(

Nope, you snipped out too much context. That command is turned into a string and then sent to /usr/bin/security on its stdin. It is absolutely not passed on the command-line.

j.
John Szakmeister· Sep 14, 2011, 22:23 UTC · re: Jay Soffian · lore

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

On Wed, Sep 14, 2011 at 9:31 AM, Jay Soffian <jaysoffian@gmail.com> wrote: [snip]

> Nope, you snipped out too much context. That command is turned into a
> string and then sent to /usr/bin/security on its stdin. It is
> absolutely not passed on the command-line.
Bah.  I see it now.  Thanks.
-John
John Szakmeister· Sep 14, 2011, 10:24 UTC · re: Jay Soffian · lore

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

On Sat, Sep 10, 2011 at 3:44 PM, Jay Soffian <jaysoffian@gmail.com> wrote:
Show 9 quoted lines
> 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.

I'm pretty close with a version of this that is written in C. I've done it as a completely separate build from git, although it probably wouldn't be hard to make it build in the contrib area.

If no one else picks it up, I may look at doing a version against the secrets api for Linux too.

-John

← back to recent threads