{"thread":{"id":"28353","subject":"[PATCH] contrib: add a credential helper for Mac OS X's keychain","startedAt":"2011-09-10T19:44:34Z","lastAt":"2011-09-14T22:23:16Z","messageCount":5,"participants":["Jay Soffian","John Szakmeister"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"175281","messageId":"1315683874-95583-1-git-send-email-jaysoffian@gmail.com","threadId":"28353","inReplyTo":null,"subject":"[PATCH] contrib: add a credential helper for Mac OS X's keychain","fromName":"Jay Soffian","fromEmail":"jaysoffian@gmail.com","sentAt":"2011-09-10T19:44:34Z","receivedAt":"2011-09-10T19:44:34Z","isPatch":true,"sender":{"key":"jaysoffian@gmail.com","avatar":"https://avatars.githubusercontent.com/u/155970?v=4"},"body":"A credential helper which uses /usr/bin/security to add, search,\nand remove entries from the Mac OS X keychain.\n\nTested with 10.6.8.\n\nSigned-off-by: Jay Soffian <jaysoffian@gmail.com>\n---\nThis is a quick script to explore the new credential API. A more robust\nimplementation would be to link to OS X's Security framework from C.\n\n contrib/credential/git-credential-osxkeychain |  148 +++++++++++++++++++++++++\n 1 files changed, 148 insertions(+), 0 deletions(-)\n create mode 100755 contrib/credential/git-credential-osxkeychain\n\ndiff --git a/contrib/credential/git-credential-osxkeychain b/contrib/credential/git-credential-osxkeychain\nnew file mode 100755\nindex 0000000000..ae5ec00d68\n--- /dev/null\n+++ b/contrib/credential/git-credential-osxkeychain\n@@ -0,0 +1,148 @@\n+#!/usr/bin/python\n+# Copyright 2011 Jay Soffian. All rights reserved.\n+# FreeBSD License.\n+\"\"\"\n+A git credential helper that interfaces with the Mac OS X keychain via\n+/usr/bin/security.\n+\"\"\"\n+\n+import os\n+import re\n+import sys\n+import termios\n+from getpass import _raw_input\n+from optparse import OptionParser\n+from subprocess import Popen, PIPE\n+\n+USERNAME = 'USERNAME'\n+PASSWORD = 'PASSWORD'\n+PROMPTS = dict(USERNAME='Username', PASSWORD='Password')\n+\n+def prompt_tty(what, desc):\n+    \"\"\"Prompt on TTY for username or password with optional description\"\"\"\n+    prompt = '%s%s: ' % (PROMPTS[what], \" for '%s'\" % desc if desc else '')\n+    # Borrowed mostly from getpass.py\n+    fd = os.open('/dev/tty', os.O_RDWR|os.O_NOCTTY)\n+    tty = os.fdopen(fd, 'w+', 1)\n+    if what == USERNAME:\n+        return _raw_input(prompt, tty, tty)\n+    old = termios.tcgetattr(fd) # a copy to save\n+    new = old[:]\n+    new[3] &= ~termios.ECHO  # 3 == 'lflags'\n+    try:\n+        termios.tcsetattr(fd, termios.TCSADRAIN, new)\n+        return _raw_input(prompt, tty, tty)\n+    finally:\n+        termios.tcsetattr(fd, termios.TCSADRAIN, old)\n+        tty.write('\\n')\n+\n+def emit_user_pass(username, password):\n+    if username:\n+        print 'username=' + username\n+    if password:\n+        print 'password=' + password\n+\n+def make_security_args(command, protocol, hostname, username):\n+    args = ['/usr/bin/security', command]\n+    # tlfd is 'dflt' backwards - obvious /usr/bin/security bug\n+    # but allows us to ignore matching saved web forms.\n+    args.extend(['-t', 'tlfd'])\n+    args.extend(['-r', protocol])\n+    if hostname:\n+        args.extend(['-s', hostname])\n+    if username:\n+        args.extend(['-a', username])\n+    return args\n+\n+def find_internet_password(protocol, hostname, username):\n+    args = make_security_args('find-internet-password',\n+                              protocol, hostname, username)\n+    args.append('-g') # asks for password on stderr\n+    p = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)\n+    # grok stdout for username\n+    out, err = p.communicate()\n+    if p.returncode != 0:\n+        return\n+    for line in out.splitlines(): # pylint:disable-msg=E1103\n+        m = re.search(r'^\\s+\"acct\"<blob>=[^\"]*\"(.*)\"$', line)\n+        if m:\n+            username = m.group(1)\n+            break\n+    # grok stderr for password\n+    m = re.search(r'^password:[^\"]*\"(.*)\"$', err)\n+    if not m:\n+        return\n+    emit_user_pass(username, m.group(1))\n+    return True\n+\n+def delete_internet_password(protocol, hostname, username):\n+    args = make_security_args('delete-internet-password',\n+                              protocol, hostname, username)\n+    p = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)\n+    p.communicate()\n+\n+def add_internet_password(protocol, hostname, username, password):\n+    # We do this over a pipe so that we can provide the password more\n+    # securely than as an argument which would show up in ps output.\n+    # Unfortunately this is possibly less robust since the security man\n+    # page does not document how to quote arguments. Emprically it seems\n+    # that using the double-quote, escaping \\ and \" works properly.\n+    username = username.replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"')\n+    password = password.replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"')\n+    command = ' '.join([\n+        'add-internet-password', '-U',\n+        '-r', protocol,\n+        '-s', hostname,\n+        '-a \"%s\"' % username,\n+        '-w \"%s\"' % password,\n+        '-j default',\n+        '-l \"%s (%s)\"' % (hostname, username),\n+    ]) + '\\n'\n+    args = ['/usr/bin/security', '-i']\n+    p = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)\n+    p.communicate(command)\n+\n+def main():\n+    p = OptionParser()\n+    p.add_option('--description')\n+    p.add_option('--reject', action='store_true')\n+    p.add_option('--unique', dest='token', help='REQUIRED OPTION')\n+    p.add_option('--username')\n+    opts, _ = p.parse_args()\n+\n+    if not opts.token:\n+        p.error('--unique option required')\n+    if not ':' in opts.token:\n+        print >> sys.stderr, \"Invalid token: '%s'\" % opts.token\n+        return 1\n+    protocol, hostname = opts.token.split(':', 1)\n+    if protocol not in ('http', 'https'):\n+        print >> sys.stderr, \"Unsupported protocol: '%s'\" % protocol\n+        return 1\n+    if protocol == 'https':\n+        protocol = 'htps'\n+\n+    # \"GitHub for Mac\" compatibility\n+    if hostname == 'github.com':\n+        hostname = 'github.com/mac'\n+\n+    # if this is a rejection delete the existing creds\n+    if opts.reject:\n+        delete_internet_password(protocol, hostname, opts.username)\n+        return 0\n+\n+    # otherwise look for creds\n+    if find_internet_password(protocol, hostname, opts.username):\n+        return 0\n+\n+    # creds not found, so prompt the user then store the creds\n+    username = opts.username\n+    if username is None:\n+        username = prompt_tty(USERNAME, opts.description)\n+    password = prompt_tty(PASSWORD, opts.description)\n+    add_internet_password(protocol, hostname, username, password)\n+    emit_user_pass(username, password)\n+    return 0\n+\n+if __name__ == '__main__':\n+    sys.exit(main())\n-- \n1.7.6.346.g5a895\n"},{"id":"175457","messageId":"CAEBDL5UprYM0=SgNQrsY8_aCGf+pxKc1NP0AFjJy8igzt3vDZQ@mail.gmail.com","threadId":"28353","inReplyTo":"1315683874-95583-1-git-send-email-jaysoffian@gmail.com","subject":"Re: [PATCH] contrib: add a credential helper for Mac OS X's keychain","fromName":"John Szakmeister","fromEmail":"john@szakmeister.net","sentAt":"2011-09-14T08:01:31Z","receivedAt":"2011-09-14T08:01:31Z","isPatch":true,"sender":{"key":"john@szakmeister.net","avatar":"https://avatars.githubusercontent.com/u/448087?v=4"},"body":"On Sat, Sep 10, 2011 at 3:44 PM, Jay Soffian <jaysoffian@gmail.com> wrote:\n> A credential helper which uses /usr/bin/security to add, search,\n> and remove entries from the Mac OS X keychain.\n>\n> Tested with 10.6.8.\n>\n> Signed-off-by: Jay Soffian <jaysoffian@gmail.com>\n> ---\n> This is a quick script to explore the new credential API. A more robust\n> implementation would be to link to OS X's Security framework from C.\n[snip]\n> +def add_internet_password(protocol, hostname, username, password):\n> +    # We do this over a pipe so that we can provide the password more\n> +    # securely than as an argument which would show up in ps output.\n> +    # Unfortunately this is possibly less robust since the security man\n> +    # page does not document how to quote arguments. Emprically it seems\n> +    # that using the double-quote, escaping \\ and \" works properly.\n\nI'm not sure this comment is correct... it looks like you're passing\nthe password on the command line...\n\n> +    username = username.replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"')\n> +    password = password.replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"')\n> +    command = ' '.join([\n> +        'add-internet-password', '-U',\n> +        '-r', protocol,\n> +        '-s', hostname,\n> +        '-a \"%s\"' % username,\n> +        '-w \"%s\"' % password,\n\n...right here. :-(\n\n-John\n"},{"id":"175463","messageId":"CAEBDL5X9jic1GVNWdWVcvSfWELTu79oE2eEiPHEsxS0hV+ZigA@mail.gmail.com","threadId":"28353","inReplyTo":"1315683874-95583-1-git-send-email-jaysoffian@gmail.com","subject":"Re: [PATCH] contrib: add a credential helper for Mac OS X's keychain","fromName":"John Szakmeister","fromEmail":"john@szakmeister.net","sentAt":"2011-09-14T10:24:39Z","receivedAt":"2011-09-14T10:24:39Z","isPatch":true,"sender":{"key":"john@szakmeister.net","avatar":"https://avatars.githubusercontent.com/u/448087?v=4"},"body":"On Sat, Sep 10, 2011 at 3:44 PM, Jay Soffian <jaysoffian@gmail.com> wrote:\n> A credential helper which uses /usr/bin/security to add, search,\n> and remove entries from the Mac OS X keychain.\n>\n> Tested with 10.6.8.\n>\n> Signed-off-by: Jay Soffian <jaysoffian@gmail.com>\n> ---\n> This is a quick script to explore the new credential API. A more robust\n> implementation would be to link to OS X's Security framework from C.\n\nI'm pretty close with a version of this that is written in C.  I've\ndone it as a completely separate build from git, although it probably\nwouldn't be hard to make it build in the contrib area.\n\nIf no one else picks it up, I may look at doing a version against the\nsecrets api for Linux too.\n\n-John\n"},{"id":"175472","messageId":"CAG+J_Dw-vf7FtyT-vPpj-LHBo0rCBJi39bHh=8vWjc52QBMM2A@mail.gmail.com","threadId":"28353","inReplyTo":"CAEBDL5UprYM0=SgNQrsY8_aCGf+pxKc1NP0AFjJy8igzt3vDZQ@mail.gmail.com","subject":"Re: [PATCH] contrib: add a credential helper for Mac OS X's keychain","fromName":"Jay Soffian","fromEmail":"jaysoffian@gmail.com","sentAt":"2011-09-14T13:31:42Z","receivedAt":"2011-09-14T13:31:42Z","isPatch":true,"sender":{"key":"jaysoffian@gmail.com","avatar":"https://avatars.githubusercontent.com/u/155970?v=4"},"body":"On Wed, Sep 14, 2011 at 4:01 AM, John Szakmeister <john@szakmeister.net> wrote:\n>> +def add_internet_password(protocol, hostname, username, password):\n>> +    # We do this over a pipe so that we can provide the password more\n>> +    # securely than as an argument which would show up in ps output.\n>> +    # Unfortunately this is possibly less robust since the security man\n>> +    # page does not document how to quote arguments. Emprically it seems\n>> +    # that using the double-quote, escaping \\ and \" works properly.\n>\n> I'm not sure this comment is correct... it looks like you're passing\n> the password on the command line...\n>\n>> +    username = username.replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"')\n>> +    password = password.replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"')\n>> +    command = ' '.join([\n>> +        'add-internet-password', '-U',\n>> +        '-r', protocol,\n>> +        '-s', hostname,\n>> +        '-a \"%s\"' % username,\n>> +        '-w \"%s\"' % password,\n>\n> ...right here. :-(\n\nNope, you snipped out too much context. That command is turned into a\nstring and then sent to /usr/bin/security on its stdin. It is\nabsolutely not passed on the command-line.\n\nj.\n"},{"id":"175517","messageId":"CAEBDL5UJLLzpSsop9OzLuTyohZ4y854js-DfDM3dY+5DtDgx9Q@mail.gmail.com","threadId":"28353","inReplyTo":"CAG+J_Dw-vf7FtyT-vPpj-LHBo0rCBJi39bHh=8vWjc52QBMM2A@mail.gmail.com","subject":"Re: [PATCH] contrib: add a credential helper for Mac OS X's keychain","fromName":"John Szakmeister","fromEmail":"john@szakmeister.net","sentAt":"2011-09-14T22:23:16Z","receivedAt":"2011-09-14T22:23:16Z","isPatch":true,"sender":{"key":"john@szakmeister.net","avatar":"https://avatars.githubusercontent.com/u/448087?v=4"},"body":"On Wed, Sep 14, 2011 at 9:31 AM, Jay Soffian <jaysoffian@gmail.com> wrote:\n[snip]\n> Nope, you snipped out too much context. That command is turned into a\n> string and then sent to /usr/bin/security on its stdin. It is\n> absolutely not passed on the command-line.\n\nBah.  I see it now.  Thanks.\n\n-John\n"}]}