This one pretty much sucks. Mem-leaks and a sketchy deletion-filter. Currently uses "::" as an attribute-separator, but this is not robust without encoding if the attribute values themselves contains "::". Since the Windows port of Git expects binary pipes, we need to make sure the helper-end also sets up binary pipes. Side-step CRLF-issue in test to make it pass. Signed-off-by: Erik Faye-Lund <kusmabite@xxxxxxxxx> --- Here's a hacky and not-quite-as-cool-as-I'd-hope credential-helper for Windows. The good news is that it Works For Me(tm), but the bad news is that it sucks. The code is provided for discussion only so far. I'm not really sure how to make it less sucky in some regards, part of this I blame on lacking documentation of the credential-helper prococol :P Here's some things I can't find documented: 1) Encoding of usernames. I'm assuming this is supposed to be UTF-8, because SecKeychainFindInternetPassword which is used by the OSX-helper is documented to take accountName as UTF-8. 2) Encoding of passwords. I'm assuming UTF-8, as mixing encodings here would be insane :P 3) How to match credentials for look-up when not supplying all attributes. I've implemented a kind-of dodgy matching; I use a string that packs all supplied attributes. This seems to work, but I'm a bit worried about what happens when people try to mix credentials saved with credential.useHttpPath=true with those saved with credential.useHttpPath=false. 4) How to match credentials for deletion when not supplying all attributes. It seems from the test-suite that a credential is expected to be deleted even if the username is not given. But what happens in such a case when there's two different credentials for one domain, with different username? The OSX-helper seems to only delete one of them (but I'm not entirely sure which one, due to lacking documentation from Apple's side). The OSX-helper also have a comment saying "Require at least a protocol and host for removal, which is what git will give us; if you want to do something more fancy, use the Keychain manager". This comment puzzles me a bit; won't git also give us path in case of credential.useHttpPath=true? 5) How overwriting credentials work. If there's a credential where all attributes except for the password match, will "store" just overwrite it? Should it complain? I'm guessing that a lot of these questions fall in the "who cares" category. For instance, git probably never tries to create a credential it already has stored. But at least saying "This cannot happen" in the documentation makes it a lot easier to write a credential-helper. Thoughts? contrib/credential/wincred/Makefile | 8 + .../credential/wincred/git-credential-wincred.c | 320 ++++++++++++++++++++ t/lib-credential.sh | 4 + 3 files changed, 332 insertions(+), 0 deletions(-) create mode 100644 contrib/credential/wincred/Makefile create mode 100644 contrib/credential/wincred/git-credential-wincred.c diff --git a/contrib/credential/wincred/Makefile b/contrib/credential/wincred/Makefile new file mode 100644 index 0000000..b4f098f --- /dev/null +++ b/contrib/credential/wincred/Makefile @@ -0,0 +1,8 @@ +all: git-credential-wincred.exe + +CC = gcc +RM = rm -f +CFLAGS = -O2 -Wall + +git-credential-wincred.exe : git-credential-wincred.c + $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@ diff --git a/contrib/credential/wincred/git-credential-wincred.c b/contrib/credential/wincred/git-credential-wincred.c new file mode 100644 index 0000000..760a3dc --- /dev/null +++ b/contrib/credential/wincred/git-credential-wincred.c @@ -0,0 +1,320 @@ +/* + * A git credential helper that interface with Windows' Credential Manager + * + */ +#include <windows.h> +#include <stdio.h> +#include <io.h> +#include <fcntl.h> + +/* common helpers */ + +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); +} + +static void *xmalloc(size_t size) +{ + void *ret = malloc(size); + if (!ret && !size) + ret = malloc(1); + if (!ret) + die("Out of memory"); + return ret; +} + +static char *xstrdup(const char *str) +{ + char *ret = strdup(str); + if (!ret) + die("Out of memory"); + return ret; +} + +/* MinGW doesn't have wincred.h, so we need to define stuff */ + +typedef struct _CREDENTIAL_ATTRIBUTE { + LPWSTR Keyword; + DWORD Flags; + DWORD ValueSize; + LPBYTE Value; +} CREDENTIAL_ATTRIBUTE, *PCREDENTIAL_ATTRIBUTE; + +typedef struct _CREDENTIALW { + DWORD Flags; + DWORD Type; + LPWSTR TargetName; + LPWSTR Comment; + FILETIME LastWritten; + DWORD CredentialBlobSize; + LPBYTE CredentialBlob; + DWORD Persist; + DWORD AttributeCount; + PCREDENTIAL_ATTRIBUTE Attributes; + LPWSTR TargetAlias; + LPWSTR UserName; +} CREDENTIALW, *PCREDENTIALW; + +#define CRED_TYPE_GENERIC 1 +#define CRED_PERSIST_LOCAL_MACHINE 2 +#define CRED_PACK_GENERIC_CREDENTIALS 4 + +typedef BOOL (WINAPI *CredWriteWT)(PCREDENTIALW, DWORD); +typedef BOOL (WINAPI *CredUnPackAuthenticationBufferWT)(DWORD, PVOID, DWORD, + LPWSTR, DWORD *, LPWSTR, DWORD *, LPWSTR, DWORD *); +typedef BOOL (WINAPI *CredEnumerateWT)(LPCWSTR, DWORD, DWORD *, + PCREDENTIALW **); +typedef BOOL (WINAPI *CredPackAuthenticationBufferWT)(DWORD, LPWSTR, LPWSTR, + PBYTE, DWORD *); +typedef VOID (WINAPI *CredFreeT)(PVOID); +typedef BOOL (WINAPI *CredDeleteWT)(LPCWSTR, DWORD, DWORD); + +static HMODULE advapi, credui; +static CredWriteWT CredWriteW; +static CredUnPackAuthenticationBufferWT CredUnPackAuthenticationBufferW; +static CredEnumerateWT CredEnumerateW; +static CredPackAuthenticationBufferWT CredPackAuthenticationBufferW; +static CredFreeT CredFree; +static CredDeleteWT CredDeleteW; + +static void load_cred_funcs(void) +{ + /* load DLLs */ + advapi = LoadLibrary("advapi32.dll"); + credui = LoadLibrary("credui.dll"); + if (!advapi || !credui) + die("failed to load DLLs"); + + /* get function pointers */ + CredWriteW = (CredWriteWT)GetProcAddress(advapi, "CredWriteW"); + CredUnPackAuthenticationBufferW = (CredUnPackAuthenticationBufferWT) + GetProcAddress(credui, "CredUnPackAuthenticationBufferW"); + CredEnumerateW = (CredEnumerateWT)GetProcAddress(advapi, + "CredEnumerateW"); + CredPackAuthenticationBufferW = (CredPackAuthenticationBufferWT) + GetProcAddress(credui, "CredPackAuthenticationBufferW"); + CredFree = (CredFreeT)GetProcAddress(advapi, "CredFree"); + CredDeleteW = (CredDeleteWT)GetProcAddress(advapi, "CredDeleteW"); + if (!CredWriteW || !CredUnPackAuthenticationBufferW || + !CredEnumerateW || !CredPackAuthenticationBufferW || !CredFree || + !CredDeleteW) + die("failed to load functions"); +} + +static char target_buf[1024]; +static char *protocol, *host, *path, *username; +static WCHAR *wusername, *password, *target; + +static void write_item(const char *what, WCHAR *wbuf) +{ + char *buf; + int len = WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, NULL, 0, NULL, + FALSE); + buf = xmalloc(len); + + if (!WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, buf, len, NULL, FALSE)) + die("WideCharToMultiByte failed!"); + + printf("%s=", what); + fwrite(buf, 1, len - 1, stdout); + putchar('\n'); + free(buf); +} + +static void get_credential(void) +{ + WCHAR *user_buf, *pass_buf; + DWORD user_buf_size = 0, pass_buf_size = 0; + CREDENTIALW **creds, *cred; + DWORD num_creds; + WCHAR temp[4096]; + + wcscpy(temp, target); + wcscat(temp, L"*"); + + if (!CredEnumerateW(temp, 0, &num_creds, &creds)) + return; + + if (!wusername) { + /* no username was specified, just pick the first one */ + cred = creds[0]; + } else { + /* search for the first credential that matches username */ + int i; + cred = NULL; + for (i = 0; i < num_creds; ++i) + if (!wcscmp(wusername, creds[i]->UserName)) { + cred = creds[i]; + break; + } + if (!cred) + return; + } + + CredUnPackAuthenticationBufferW(0, cred->CredentialBlob, + cred->CredentialBlobSize, NULL, &user_buf_size, NULL, NULL, + NULL, &pass_buf_size); + + user_buf = xmalloc(user_buf_size * sizeof(WCHAR)); + pass_buf = xmalloc(pass_buf_size * sizeof(WCHAR)); + + if (!CredUnPackAuthenticationBufferW(0, cred->CredentialBlob, + cred->CredentialBlobSize, user_buf, &user_buf_size, NULL, NULL, + pass_buf, &pass_buf_size)) + die("CredUnPackAuthenticationBuffer failed"); + + CredFree(creds); + + /* zero-terminate (sizes include zero-termination) */ + user_buf[user_buf_size - 1] = L'\0'; + pass_buf[pass_buf_size - 1] = L'\0'; + + write_item("username", user_buf); + write_item("password", pass_buf); + + free(user_buf); + free(pass_buf); +} + +static void store_credential(void) +{ + CREDENTIALW cred; + BYTE *auth_buf; + DWORD auth_buf_size = 0; + + if (!wusername || !password) + return; + + /* query buffer size */ + CredPackAuthenticationBufferW(0, wusername, password, + NULL, &auth_buf_size); + + auth_buf = xmalloc(auth_buf_size); + + if (!CredPackAuthenticationBufferW(0, wusername, password, + auth_buf, &auth_buf_size)) + die("CredPackAuthenticationBuffer failed"); + + cred.Flags = 0; + cred.Type = CRED_TYPE_GENERIC; + cred.TargetName = target; + cred.Comment = L"saved by git-credential-wincred"; + cred.CredentialBlobSize = auth_buf_size; + cred.CredentialBlob = auth_buf; + cred.Persist = CRED_PERSIST_LOCAL_MACHINE; + cred.AttributeCount = 0; + cred.Attributes = NULL; + cred.TargetAlias = NULL; + cred.UserName = wusername; + if (!CredWriteW(&cred, 0)) + die("CredWrite failed"); +} + +static void erase_credential(void) +{ + WCHAR temp[4096]; + CREDENTIALW **creds; + DWORD num_creds; + int i; + + wcscpy(temp, target); + wcscat(temp, L"*"); + + if (!CredEnumerateW(temp, 0, &num_creds, &creds)) + return; + + for (i = 0; i < num_creds; ++i) + CredDeleteW(creds[i]->TargetName, creds[i]->Type, 0); + + CredFree(creds); +} + +static WCHAR *utf8_to_utf16_dup(const char *str) +{ + int wlen = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + WCHAR *wstr = xmalloc(sizeof(WCHAR) * wlen); + MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, wlen); + return wstr; +} + +static void read_credential(void) +{ + char buf[1024]; + + while (fgets(buf, sizeof(buf), stdin)) { + char *v; + + if (!strcmp(buf, "\n")) + break; + buf[strlen(buf)-1] = '\0'; + + v = strchr(buf, '='); + if (!v) + die("bad input: %s", buf); + *v++ = '\0'; + + if (!strcmp(buf, "protocol")) + protocol = xstrdup(v); + else if (!strcmp(buf, "host")) + host = xstrdup(v); + else if (!strcmp(buf, "path")) + path = xstrdup(v); + else if (!strcmp(buf, "username")) { + username = xstrdup(v); + wusername = utf8_to_utf16_dup(v); + } else if (!strcmp(buf, "password")) + password = utf8_to_utf16_dup(v); + } +} + +int main(int argc, char *argv[]) +{ + const char *usage = + "Usage: git credential-wincred <get|store|erase>\n"; + + if (!argv[1]) + die(usage); + + /* git use binary pipes to avoid CRLF-issues */ + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); + + read_credential(); + + load_cred_funcs(); + + /* prepare 'target', the unique key for the credential */ + if (!protocol || !host) + return 0; + + strncpy(target_buf, "git::protocol=", sizeof(target_buf)); + strncat(target_buf, protocol, sizeof(target_buf)); + strncat(target_buf, "::host=", sizeof(target_buf)); + strncat(target_buf, host, sizeof(target_buf)); + if (username) { + strncat(target_buf, "::user=", sizeof(target_buf)); + strncat(target_buf, username, sizeof(target_buf)); + } + if (path) { + strncat(target_buf, "::path=", sizeof(target_buf)); + strncat(target_buf, host, sizeof(target_buf)); + } + + target = utf8_to_utf16_dup(target_buf); + + if (!strcmp(argv[1], "get")) + get_credential(); + else if (!strcmp(argv[1], "store")) + store_credential(); + else if (!strcmp(argv[1], "erase")) + erase_credential(); + /* otherwise, ignore unknown action */ + return 0; +} diff --git a/t/lib-credential.sh b/t/lib-credential.sh index 4a37cd7..d30ae8a 100755 --- a/t/lib-credential.sh +++ b/t/lib-credential.sh @@ -8,6 +8,10 @@ check() { read_chunk >expect-stdout && read_chunk >expect-stderr && test-credential "$@" <stdin >stdout 2>stderr && + if test_have_prereq MINGW + then + dos2unix -q stderr + fi && test_cmp expect-stdout stdout && test_cmp expect-stderr stderr } -- 1.7.9 -- 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