1
0
Fork 0
mirror of https://github.com/git/git.git synced 2024-05-30 00:56:08 +02:00

Merge branch 'ba/osxkeychain-updates'

Update osxkeychain backend with features required for the recent
credential subsystem.

* ba/osxkeychain-updates:
  osxkeychain: store new attributes
  osxkeychain: erase matching passwords only
  osxkeychain: erase all matching credentials
  osxkeychain: replace deprecated SecKeychain API
This commit is contained in:
Junio C Hamano 2024-04-16 14:50:30 -07:00
commit 51c15ac1b6
2 changed files with 309 additions and 68 deletions

View File

@ -8,7 +8,8 @@ CFLAGS = -g -O2 -Wall
-include ../../../config.mak -include ../../../config.mak
git-credential-osxkeychain: git-credential-osxkeychain.o git-credential-osxkeychain: git-credential-osxkeychain.o
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) -Wl,-framework -Wl,Security $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) \
-framework Security -framework CoreFoundation
git-credential-osxkeychain.o: git-credential-osxkeychain.c git-credential-osxkeychain.o: git-credential-osxkeychain.c
$(CC) -c $(CFLAGS) $< $(CC) -c $(CFLAGS) $<

View File

@ -3,14 +3,51 @@
#include <stdlib.h> #include <stdlib.h>
#include <Security/Security.h> #include <Security/Security.h>
static SecProtocolType protocol; #define ENCODING kCFStringEncodingUTF8
static char *host; static CFStringRef protocol; /* Stores constant strings - not memory managed */
static char *path; static CFStringRef host;
static char *username; static CFNumberRef port;
static char *password; static CFStringRef path;
static UInt16 port; static CFStringRef username;
static CFDataRef password;
static CFDataRef password_expiry_utc;
static CFDataRef oauth_refresh_token;
__attribute__((format (printf, 1, 2))) static void clear_credential(void)
{
if (host) {
CFRelease(host);
host = NULL;
}
if (port) {
CFRelease(port);
port = NULL;
}
if (path) {
CFRelease(path);
path = NULL;
}
if (username) {
CFRelease(username);
username = NULL;
}
if (password) {
CFRelease(password);
password = NULL;
}
if (password_expiry_utc) {
CFRelease(password_expiry_utc);
password_expiry_utc = NULL;
}
if (oauth_refresh_token) {
CFRelease(oauth_refresh_token);
oauth_refresh_token = NULL;
}
}
#define STRING_WITH_LENGTH(s) s, sizeof(s) - 1
__attribute__((format (printf, 1, 2), __noreturn__))
static void die(const char *err, ...) static void die(const char *err, ...)
{ {
char msg[4096]; char msg[4096];
@ -19,70 +56,199 @@ static void die(const char *err, ...)
vsnprintf(msg, sizeof(msg), err, params); vsnprintf(msg, sizeof(msg), err, params);
fprintf(stderr, "%s\n", msg); fprintf(stderr, "%s\n", msg);
va_end(params); va_end(params);
clear_credential();
exit(1); exit(1);
} }
static void *xstrdup(const char *s1) static void *xmalloc(size_t len)
{ {
void *ret = strdup(s1); void *ret = malloc(len);
if (!ret) if (!ret)
die("Out of memory"); die("Out of memory");
return ret; return ret;
} }
#define KEYCHAIN_ITEM(x) (x ? strlen(x) : 0), x static CFDictionaryRef create_dictionary(CFAllocatorRef allocator, ...)
#define KEYCHAIN_ARGS \ {
NULL, /* default keychain */ \ va_list args;
KEYCHAIN_ITEM(host), \ const void *key;
0, NULL, /* account domain */ \ CFMutableDictionaryRef result;
KEYCHAIN_ITEM(username), \
KEYCHAIN_ITEM(path), \
port, \
protocol, \
kSecAuthenticationTypeDefault
static void write_item(const char *what, const char *buf, int len) result = CFDictionaryCreateMutable(allocator,
0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
va_start(args, allocator);
while ((key = va_arg(args, const void *)) != NULL) {
const void *value;
value = va_arg(args, const void *);
if (value)
CFDictionarySetValue(result, key, value);
}
va_end(args);
return result;
}
#define CREATE_SEC_ATTRIBUTES(...) \
create_dictionary(kCFAllocatorDefault, \
kSecClass, kSecClassInternetPassword, \
kSecAttrServer, host, \
kSecAttrAccount, username, \
kSecAttrPath, path, \
kSecAttrPort, port, \
kSecAttrProtocol, protocol, \
kSecAttrAuthenticationType, \
kSecAttrAuthenticationTypeDefault, \
__VA_ARGS__);
static void write_item(const char *what, const char *buf, size_t len)
{ {
printf("%s=", what); printf("%s=", what);
fwrite(buf, 1, len, stdout); fwrite(buf, 1, len, stdout);
putchar('\n'); putchar('\n');
} }
static void find_username_in_item(SecKeychainItemRef item) static void find_username_in_item(CFDictionaryRef item)
{ {
SecKeychainAttributeList list; CFStringRef account_ref;
SecKeychainAttribute attr; char *username_buf;
CFIndex buffer_len;
list.count = 1; account_ref = CFDictionaryGetValue(item, kSecAttrAccount);
list.attr = &attr; if (!account_ref)
attr.tag = kSecAccountItemAttr; {
write_item("username", "", 0);
if (SecKeychainItemCopyContent(item, NULL, &list, NULL, NULL))
return; return;
}
write_item("username", attr.data, attr.length); username_buf = (char *)CFStringGetCStringPtr(account_ref, ENCODING);
SecKeychainItemFreeContent(&list, NULL); if (username_buf)
{
write_item("username", username_buf, strlen(username_buf));
return;
}
/* If we can't get a CString pointer then
* we need to allocate our own buffer */
buffer_len = CFStringGetMaximumSizeForEncoding(
CFStringGetLength(account_ref), ENCODING) + 1;
username_buf = xmalloc(buffer_len);
if (CFStringGetCString(account_ref,
username_buf,
buffer_len,
ENCODING)) {
write_item("username", username_buf, buffer_len - 1);
}
free(username_buf);
} }
static void find_internet_password(void) static OSStatus find_internet_password(void)
{ {
void *buf; CFDictionaryRef attrs;
UInt32 len; CFDictionaryRef item;
SecKeychainItemRef item; CFDataRef data;
OSStatus result;
if (SecKeychainFindInternetPassword(KEYCHAIN_ARGS, &len, &buf, &item)) attrs = CREATE_SEC_ATTRIBUTES(kSecMatchLimit, kSecMatchLimitOne,
return; kSecReturnAttributes, kCFBooleanTrue,
kSecReturnData, kCFBooleanTrue,
NULL);
result = SecItemCopyMatching(attrs, (CFTypeRef *)&item);
if (result) {
goto out;
}
write_item("password", buf, len); data = CFDictionaryGetValue(item, kSecValueData);
write_item("password",
(const char *)CFDataGetBytePtr(data),
CFDataGetLength(data));
if (!username) if (!username)
find_username_in_item(item); find_username_in_item(item);
SecKeychainItemFreeContent(NULL, buf); CFRelease(item);
out:
CFRelease(attrs);
/* We consider not found to not be an error */
if (result == errSecItemNotFound)
result = errSecSuccess;
return result;
} }
static void delete_internet_password(void) static OSStatus delete_ref(const void *itemRef)
{ {
SecKeychainItemRef item; CFArrayRef item_ref_list;
CFDictionaryRef delete_query;
OSStatus result;
item_ref_list = CFArrayCreate(kCFAllocatorDefault,
&itemRef,
1,
&kCFTypeArrayCallBacks);
delete_query = create_dictionary(kCFAllocatorDefault,
kSecClass, kSecClassInternetPassword,
kSecMatchItemList, item_ref_list,
NULL);
if (password) {
/* We only want to delete items with a matching password */
CFIndex capacity;
CFMutableDictionaryRef query;
CFDataRef data;
capacity = CFDictionaryGetCount(delete_query) + 1;
query = CFDictionaryCreateMutableCopy(kCFAllocatorDefault,
capacity,
delete_query);
CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue);
result = SecItemCopyMatching(query, (CFTypeRef *)&data);
if (!result) {
CFDataRef kc_password;
const UInt8 *raw_data;
const UInt8 *line;
/* Don't match appended metadata */
raw_data = CFDataGetBytePtr(data);
line = memchr(raw_data, '\n', CFDataGetLength(data));
if (line)
kc_password = CFDataCreateWithBytesNoCopy(
kCFAllocatorDefault,
raw_data,
line - raw_data,
kCFAllocatorNull);
else
kc_password = data;
if (CFEqual(kc_password, password))
result = SecItemDelete(delete_query);
if (line)
CFRelease(kc_password);
CFRelease(data);
}
CFRelease(query);
} else {
result = SecItemDelete(delete_query);
}
CFRelease(delete_query);
CFRelease(item_ref_list);
return result;
}
static OSStatus delete_internet_password(void)
{
CFDictionaryRef attrs;
CFArrayRef refs;
OSStatus result;
/* /*
* Require at least a protocol and host for removal, which is what git * Require at least a protocol and host for removal, which is what git
@ -90,25 +256,69 @@ static void delete_internet_password(void)
* Keychain manager. * Keychain manager.
*/ */
if (!protocol || !host) if (!protocol || !host)
return; return -1;
if (SecKeychainFindInternetPassword(KEYCHAIN_ARGS, 0, NULL, &item)) attrs = CREATE_SEC_ATTRIBUTES(kSecMatchLimit, kSecMatchLimitAll,
return; kSecReturnRef, kCFBooleanTrue,
NULL);
result = SecItemCopyMatching(attrs, (CFTypeRef *)&refs);
CFRelease(attrs);
SecKeychainItemDelete(item); if (!result) {
for (CFIndex i = 0; !result && i < CFArrayGetCount(refs); i++)
result = delete_ref(CFArrayGetValueAtIndex(refs, i));
CFRelease(refs);
}
/* We consider not found to not be an error */
if (result == errSecItemNotFound)
result = errSecSuccess;
return result;
} }
static void add_internet_password(void) static OSStatus add_internet_password(void)
{ {
CFMutableDataRef data;
CFDictionaryRef attrs;
OSStatus result;
/* Only store complete credentials */ /* Only store complete credentials */
if (!protocol || !host || !username || !password) if (!protocol || !host || !username || !password)
return; return -1;
if (SecKeychainAddInternetPassword( data = CFDataCreateMutableCopy(kCFAllocatorDefault, 0, password);
KEYCHAIN_ARGS, if (password_expiry_utc) {
KEYCHAIN_ITEM(password), CFDataAppendBytes(data,
NULL)) (const UInt8 *)STRING_WITH_LENGTH("\npassword_expiry_utc="));
return; CFDataAppendBytes(data,
CFDataGetBytePtr(password_expiry_utc),
CFDataGetLength(password_expiry_utc));
}
if (oauth_refresh_token) {
CFDataAppendBytes(data,
(const UInt8 *)STRING_WITH_LENGTH("\noauth_refresh_token="));
CFDataAppendBytes(data,
CFDataGetBytePtr(oauth_refresh_token),
CFDataGetLength(oauth_refresh_token));
}
attrs = CREATE_SEC_ATTRIBUTES(kSecValueData, data,
NULL);
result = SecItemAdd(attrs, NULL);
if (result == errSecDuplicateItem) {
CFDictionaryRef query;
query = CREATE_SEC_ATTRIBUTES(NULL);
result = SecItemUpdate(query, attrs);
CFRelease(query);
}
CFRelease(data);
CFRelease(attrs);
return result;
} }
static void read_credential(void) static void read_credential(void)
@ -131,36 +341,60 @@ static void read_credential(void)
if (!strcmp(buf, "protocol")) { if (!strcmp(buf, "protocol")) {
if (!strcmp(v, "imap")) if (!strcmp(v, "imap"))
protocol = kSecProtocolTypeIMAP; protocol = kSecAttrProtocolIMAP;
else if (!strcmp(v, "imaps")) else if (!strcmp(v, "imaps"))
protocol = kSecProtocolTypeIMAPS; protocol = kSecAttrProtocolIMAPS;
else if (!strcmp(v, "ftp")) else if (!strcmp(v, "ftp"))
protocol = kSecProtocolTypeFTP; protocol = kSecAttrProtocolFTP;
else if (!strcmp(v, "ftps")) else if (!strcmp(v, "ftps"))
protocol = kSecProtocolTypeFTPS; protocol = kSecAttrProtocolFTPS;
else if (!strcmp(v, "https")) else if (!strcmp(v, "https"))
protocol = kSecProtocolTypeHTTPS; protocol = kSecAttrProtocolHTTPS;
else if (!strcmp(v, "http")) else if (!strcmp(v, "http"))
protocol = kSecProtocolTypeHTTP; protocol = kSecAttrProtocolHTTP;
else if (!strcmp(v, "smtp")) else if (!strcmp(v, "smtp"))
protocol = kSecProtocolTypeSMTP; protocol = kSecAttrProtocolSMTP;
else /* we don't yet handle other protocols */ else {
/* we don't yet handle other protocols */
clear_credential();
exit(0); exit(0);
}
} }
else if (!strcmp(buf, "host")) { else if (!strcmp(buf, "host")) {
char *colon = strchr(v, ':'); char *colon = strchr(v, ':');
if (colon) { if (colon) {
UInt16 port_i;
*colon++ = '\0'; *colon++ = '\0';
port = atoi(colon); port_i = atoi(colon);
port = CFNumberCreate(kCFAllocatorDefault,
kCFNumberShortType,
&port_i);
} }
host = xstrdup(v); host = CFStringCreateWithCString(kCFAllocatorDefault,
v,
ENCODING);
} }
else if (!strcmp(buf, "path")) else if (!strcmp(buf, "path"))
path = xstrdup(v); path = CFStringCreateWithCString(kCFAllocatorDefault,
v,
ENCODING);
else if (!strcmp(buf, "username")) else if (!strcmp(buf, "username"))
username = xstrdup(v); username = CFStringCreateWithCString(
kCFAllocatorDefault,
v,
ENCODING);
else if (!strcmp(buf, "password")) else if (!strcmp(buf, "password"))
password = xstrdup(v); password = CFDataCreate(kCFAllocatorDefault,
(UInt8 *)v,
strlen(v));
else if (!strcmp(buf, "password_expiry_utc"))
password_expiry_utc = CFDataCreate(kCFAllocatorDefault,
(UInt8 *)v,
strlen(v));
else if (!strcmp(buf, "oauth_refresh_token"))
oauth_refresh_token = CFDataCreate(kCFAllocatorDefault,
(UInt8 *)v,
strlen(v));
/* /*
* Ignore other lines; we don't know what they mean, but * Ignore other lines; we don't know what they mean, but
* this future-proofs us when later versions of git do * this future-proofs us when later versions of git do
@ -173,6 +407,7 @@ static void read_credential(void)
int main(int argc, const char **argv) int main(int argc, const char **argv)
{ {
OSStatus result = 0;
const char *usage = const char *usage =
"usage: git credential-osxkeychain <get|store|erase>"; "usage: git credential-osxkeychain <get|store|erase>";
@ -182,12 +417,17 @@ int main(int argc, const char **argv)
read_credential(); read_credential();
if (!strcmp(argv[1], "get")) if (!strcmp(argv[1], "get"))
find_internet_password(); result = find_internet_password();
else if (!strcmp(argv[1], "store")) else if (!strcmp(argv[1], "store"))
add_internet_password(); result = add_internet_password();
else if (!strcmp(argv[1], "erase")) else if (!strcmp(argv[1], "erase"))
delete_internet_password(); result = delete_internet_password();
/* otherwise, ignore unknown action */ /* otherwise, ignore unknown action */
if (result)
die("failed to %s: %d", argv[1], (int)result);
clear_credential();
return 0; return 0;
} }