diff --git a/README.md b/README.md index 5ddcc329..dc0964c7 100644 --- a/README.md +++ b/README.md @@ -75,3 +75,11 @@ Find a password for the `service` in the keychain. `service` - The string service name. Yields the string password, or `null` if an entry for the given service and account was not found. + +### findCredentials(service) + +Find all accounts and password for the `service` in the keychain. + +`service` - The string service name. + +Yields an array of `{ account: 'foo', password: 'bar' }`. diff --git a/binding.gyp b/binding.gyp index 292ce68e..bc6b1ed5 100644 --- a/binding.gyp +++ b/binding.gyp @@ -7,6 +7,7 @@ 'src/async.cc', 'src/main.cc', 'src/keytar.h', + 'src/credentials.h', ], 'conditions': [ ['OS=="mac"', { diff --git a/lib/keytar.js b/lib/keytar.js index 350b184c..e3d4e470 100644 --- a/lib/keytar.js +++ b/lib/keytar.js @@ -49,5 +49,11 @@ module.exports = { checkRequired(service, 'Service') return callbackPromise(callback => keytar.findPassword(service, callback)) + }, + + findCredentials: function (service) { + checkRequired(service, 'Service') + + return callbackPromise(callback => keytar.findCredentials(service, callback)) } } diff --git a/spec/keytar-spec.js b/spec/keytar-spec.js index 4e81c584..b69fe75d 100644 --- a/spec/keytar-spec.js +++ b/spec/keytar-spec.js @@ -3,6 +3,7 @@ var keytar = require('../') describe("keytar", function() { var service = 'keytar tests' + var service2 = 'other tests' var account = 'buster' var password = 'secret' var account2 = 'buster2' @@ -11,11 +12,14 @@ describe("keytar", function() { beforeEach(async function() { await keytar.deletePassword(service, account), await keytar.deletePassword(service, account2) + await keytar.deletePassword(service2, account) + }) afterEach(async function() { await keytar.deletePassword(service, account), await keytar.deletePassword(service, account2) + await keytar.deletePassword(service2, account) }) describe("setPassword/getPassword(service, account)", function() { @@ -68,4 +72,28 @@ describe("keytar", function() { assert.equal(await keytar.findPassword(service), null) }) }) + + describe('findCredentials(service)', function() { + it('yields an array of the credentials', async function() { + await keytar.setPassword(service, account, password) + await keytar.setPassword(service, account2, password2) + await keytar.setPassword(service2, account, password) + + const found = await keytar.findCredentials(service) + const sorted = found.sort(function(a, b) { + return a.account.localeCompare(b.account) + }) + + assert.deepEqual([{account: account, password: password}, {account: account2, password: password2}], sorted) + }); + + it('returns an empty array when no credentials are found', async function() { + const accounts = await keytar.findCredentials(service) + assert.deepEqual([], accounts) + }) + + afterEach(async function() { + await keytar.deletePassword(service2, account) + }) + }); }) diff --git a/src/async.cc b/src/async.cc index 13af2940..eb09d257 100644 --- a/src/async.cc +++ b/src/async.cc @@ -1,4 +1,5 @@ #include +#include #include "nan.h" #include "keytar.h" @@ -145,3 +146,67 @@ void FindPasswordWorker::HandleOKCallback() { callback->Call(2, argv); } + + + +FindCredentialsWorker::FindCredentialsWorker( + const std::string& service, + Nan::Callback* callback +) : AsyncWorker(callback), + service(service) {} + +FindCredentialsWorker::~FindCredentialsWorker() {} + +void FindCredentialsWorker::Execute() { + std::string error; + KEYTAR_OP_RESULT result = keytar::FindCredentials(service, + &credentials, + &error); + if (result == keytar::FAIL_ERROR) { + SetErrorMessage(error.c_str()); + } else if (result == keytar::FAIL_NONFATAL) { + success = false; + } else { + success = true; + } +} + +void FindCredentialsWorker::HandleOKCallback() { + Nan::HandleScope scope; + + if (success) { + v8::Local val = Nan::New(credentials.size()); + unsigned int idx = 0; + std::vector::iterator it; + for (it = credentials.begin(); it != credentials.end(); it++) { + keytar::Credentials cred = *it; + v8::Local obj = Nan::New(); + + v8::Local account = Nan::New( + cred.first.data(), + cred.first.length()).ToLocalChecked(); + + v8::Local password = Nan::New( + cred.second.data(), + cred.second.length()).ToLocalChecked(); + + obj->Set(Nan::New("account").ToLocalChecked(), account); + obj->Set(Nan::New("password").ToLocalChecked(), password); + + Nan::Set(val, idx, obj); + ++idx; + } + + v8::Local argv[] = { + Nan::Null(), + val + }; + callback->Call(2, argv); + } else { + v8::Local argv[] = { + Nan::Null(), + Nan::New(0) + }; + callback->Call(2, argv); + } +} diff --git a/src/async.h b/src/async.h index 8ab9573f..73a89662 100644 --- a/src/async.h +++ b/src/async.h @@ -4,6 +4,8 @@ #include #include "nan.h" +#include "credentials.h" + class SetPasswordWorker : public Nan::AsyncWorker { public: SetPasswordWorker(const std::string& service, const std::string& account, const std::string& password, @@ -65,4 +67,19 @@ class FindPasswordWorker : public Nan::AsyncWorker { bool success; }; +class FindCredentialsWorker : public Nan::AsyncWorker { + public: + FindCredentialsWorker(const std::string& service, Nan::Callback* callback); + + ~FindCredentialsWorker(); + + void Execute(); + void HandleOKCallback(); + + private: + const std::string service; + std::vector credentials; + bool success; +}; + #endif // SRC_ASYNC_H_ diff --git a/src/credentials.h b/src/credentials.h new file mode 100644 index 00000000..99e071b1 --- /dev/null +++ b/src/credentials.h @@ -0,0 +1,13 @@ +#ifndef SRC_CREDENTIALS_H_ +#define SRC_CREDENTIALS_H_ + +#include +#include + +namespace keytar { + +typedef std::pair Credentials; + +} + +#endif // SRC_CREDENTIALS_H_ diff --git a/src/keytar.h b/src/keytar.h index 685e3c6a..1a6d3ada 100644 --- a/src/keytar.h +++ b/src/keytar.h @@ -2,6 +2,9 @@ #define SRC_KEYTAR_H_ #include +#include + +#include "credentials.h" namespace keytar { @@ -29,6 +32,10 @@ KEYTAR_OP_RESULT FindPassword(const std::string& service, std::string* password, std::string* error); +KEYTAR_OP_RESULT FindCredentials(const std::string& service, + std::vector*, + std::string* error); + } // namespace keytar #endif // SRC_KEYTAR_H_ diff --git a/src/keytar_mac.cc b/src/keytar_mac.cc index 4741ebb4..574ce348 100644 --- a/src/keytar_mac.cc +++ b/src/keytar_mac.cc @@ -1,9 +1,43 @@ +#include #include "keytar.h" +#include "credentials.h" -#include namespace keytar { +/** + * Converts a CFString to a std::string + * + * This either uses CFStringGetCStringPtr or (if that fails) + * CFStringGetCString, trying to be as efficient as possible. + */ +const std::string CFStringToStdString(CFStringRef cfstring) { + const char* cstr = CFStringGetCStringPtr(cfstring, kCFStringEncodingUTF8); + + if (cstr != NULL) { + return std::string(cstr); + } + + CFIndex length = CFStringGetLength(cfstring); + // Worst case: 2 bytes per character + NUL + CFIndex cstrPtrLen = length * 2 + 1; + char* cstrPtr = static_cast(malloc(cstrPtrLen)); + + Boolean result = CFStringGetCString(cfstring, + cstrPtr, + cstrPtrLen, + kCFStringEncodingUTF8); + + std::string stdstring; + if (result) { + stdstring = std::string(cstrPtr); + } + + free(cstrPtr); + + return stdstring; +} + const std::string errorStatusToString(OSStatus status) { std::string errorStr; CFStringRef errorMessageString = SecCopyErrorMessageString(status, NULL); @@ -149,4 +183,97 @@ KEYTAR_OP_RESULT FindPassword(const std::string& service, return SUCCESS; } +Credentials getCredentialsForItem(CFDictionaryRef item) { + CFStringRef service = (CFStringRef) CFDictionaryGetValue(item, + kSecAttrService); + CFStringRef account = (CFStringRef) CFDictionaryGetValue(item, + kSecAttrAccount); + + CFMutableDictionaryRef query = CFDictionaryCreateMutable( + NULL, + 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + + CFDictionaryAddValue(query, kSecAttrService, service); + CFDictionaryAddValue(query, kSecClass, kSecClassGenericPassword); + CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitOne); + CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue); + CFDictionaryAddValue(query, kSecReturnData, kCFBooleanTrue); + CFDictionaryAddValue(query, kSecAttrAccount, account); + + CFTypeRef result; + OSStatus status = SecItemCopyMatching((CFDictionaryRef) query, &result); + + if (status == errSecSuccess) { + CFDataRef passwordData = (CFDataRef) CFDictionaryGetValue( + (CFDictionaryRef) result, + CFSTR("v_Data")); + CFStringRef password = CFStringCreateFromExternalRepresentation( + NULL, + passwordData, + kCFStringEncodingUTF8); + + Credentials cred = Credentials( + CFStringToStdString(account), + CFStringToStdString(password)); + CFRelease(password); + + return cred; + } + + return Credentials(); +} + +KEYTAR_OP_RESULT FindCredentials(const std::string& service, + std::vector* credentials, + std::string* error) { + CFStringRef serviceStr = CFStringCreateWithCString( + NULL, + service.c_str(), + kCFStringEncodingUTF8); + + CFMutableDictionaryRef query = CFDictionaryCreateMutable( + NULL, + 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + CFDictionaryAddValue(query, kSecClass, kSecClassGenericPassword); + CFDictionaryAddValue(query, kSecAttrService, serviceStr); + CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitAll); + CFDictionaryAddValue(query, kSecReturnRef, kCFBooleanTrue); + CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue); + + CFTypeRef result; + OSStatus status = SecItemCopyMatching((CFDictionaryRef) query, &result); + + if (status == errSecSuccess) { + CFArrayRef resultArray = (CFArrayRef) result; + int resultCount = CFArrayGetCount(resultArray); + + for (int idx = 0; idx < resultCount; idx++) { + CFDictionaryRef item = (CFDictionaryRef) CFArrayGetValueAtIndex( + resultArray, + idx); + + Credentials cred = getCredentialsForItem(item); + credentials->push_back(cred); + } + } else if (status == errSecItemNotFound) { + return FAIL_NONFATAL; + } else { + *error = errorStatusToString(status); + return FAIL_ERROR; + } + + + if (result != NULL) { + CFRelease(result); + } + + CFRelease(query); + + return SUCCESS; +} + } // namespace keytar diff --git a/src/keytar_posix.cc b/src/keytar_posix.cc index 7e94cd02..3a5ba7ca 100644 --- a/src/keytar_posix.cc +++ b/src/keytar_posix.cc @@ -1,7 +1,11 @@ #include "keytar.h" +// This is needed to make the builds on Ubuntu 14.04 / libsecret v0.16 work. +// The API we use has already stabilized. +#define SECRET_API_SUBJECT_TO_CHANGE #include #include +#include namespace keytar { @@ -121,4 +125,60 @@ KEYTAR_OP_RESULT FindPassword(const std::string& service, return SUCCESS; } +KEYTAR_OP_RESULT FindCredentials(const std::string& service, + std::vector* credentials, + std::string* errStr) { + GError* error = NULL; + + GHashTable* attributes = g_hash_table_new(NULL, NULL); + g_hash_table_replace(attributes, + (gpointer) "service", + (gpointer) service.c_str()); + + GList* items = secret_service_search_sync( + NULL, + &schema, // The schema. + attributes, + static_cast(SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK | + SECRET_SEARCH_LOAD_SECRETS), + NULL, // Cancellable. (unneeded) + &error); // Reference to the error. + + g_hash_table_destroy(attributes); + + if (error != NULL) { + *errStr = std::string(error->message); + g_error_free(error); + return FAIL_ERROR; + } + + GList* current = items; + for (current = items; current != NULL; current = current->next) { + SecretItem* item = reinterpret_cast(current->data); + + GHashTable* itemAttrs = secret_item_get_attributes(item); + char* account = strdup( + reinterpret_cast(g_hash_table_lookup(itemAttrs, "account"))); + + SecretValue* secret = secret_item_get_secret(item); + char* password = strdup(secret_value_get_text(secret)); + + if (account == NULL || password == NULL) { + if (account) + free(account); + + if (password) + free(password); + + continue; + } + + credentials->push_back(Credentials(account, password)); + free(account); + free(password); + } + + return SUCCESS; +} + } // namespace keytar diff --git a/src/keytar_win.cc b/src/keytar_win.cc index b152d093..18fac691 100644 --- a/src/keytar_win.cc +++ b/src/keytar_win.cc @@ -5,6 +5,8 @@ #include #include +#include "credentials.h" + namespace keytar { LPWSTR utf8ToWideChar(std::string utf8) { @@ -85,9 +87,15 @@ KEYTAR_OP_RESULT SetPassword(const std::string& service, return FAIL_ERROR; } + LPWSTR user_name = utf8ToWideChar(account); + if (target_name == NULL) { + return FAIL_ERROR; + } + CREDENTIAL cred = { 0 }; cred.Type = CRED_TYPE_GENERIC; cred.TargetName = target_name; + cred.UserName = user_name; cred.CredentialBlobSize = password.size(); cred.CredentialBlob = (LPBYTE)(password.data()); cred.Persist = CRED_PERSIST_LOCAL_MACHINE; @@ -181,4 +189,42 @@ KEYTAR_OP_RESULT FindPassword(const std::string& service, return SUCCESS; } +KEYTAR_OP_RESULT FindCredentials(const std::string& service, + std::vector* credentials, + std::string* errStr) { + LPWSTR filter = utf8ToWideChar(service + "*"); + + DWORD count; + CREDENTIAL **creds; + + bool result = ::CredEnumerate(filter, 0, &count, &creds); + if (!result) { + DWORD code = ::GetLastError(); + if (code == ERROR_NOT_FOUND) { + return FAIL_NONFATAL; + } else { + *errStr = getErrorMessage(code); + return FAIL_ERROR; + } + } + + for (unsigned int i = 0; i < count; ++i) { + CREDENTIAL* cred = creds[i]; + + if (cred->UserName == NULL || cred->CredentialBlobSize == NULL) { + continue; + } + + std::string login = wideCharToAnsi(cred->UserName); + std::string password(reinterpret_cast(cred->CredentialBlob)); + + credentials->push_back(Credentials(login, password)); + } + + CredFree(creds); + + return SUCCESS; +} + + } // namespace keytar diff --git a/src/main.cc b/src/main.cc index 73150de4..012aafd0 100644 --- a/src/main.cc +++ b/src/main.cc @@ -35,11 +35,19 @@ NAN_METHOD(FindPassword) { Nan::AsyncQueueWorker(worker); } +NAN_METHOD(FindCredentials) { + FindCredentialsWorker* worker = new FindCredentialsWorker( + *v8::String::Utf8Value(info[0]), + new Nan::Callback(info[1].As())); + Nan::AsyncQueueWorker(worker); +} + void Init(v8::Handle exports) { Nan::SetMethod(exports, "getPassword", GetPassword); Nan::SetMethod(exports, "setPassword", SetPassword); Nan::SetMethod(exports, "deletePassword", DeletePassword); Nan::SetMethod(exports, "findPassword", FindPassword); + Nan::SetMethod(exports, "findCredentials", FindCredentials); } } // namespace