Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Adds a findCredentials method which yields an array of account/password objects of all matching saved credentials #56 #85

Merged
merged 1 commit into from
Dec 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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' }`.
1 change: 1 addition & 0 deletions binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
'src/async.cc',
'src/main.cc',
'src/keytar.h',
'src/credentials.h',
],
'conditions': [
['OS=="mac"', {
Expand Down
6 changes: 6 additions & 0 deletions lib/keytar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
28 changes: 28 additions & 0 deletions spec/keytar-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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() {
Expand Down Expand Up @@ -68,4 +72,28 @@ describe("keytar", function() {
assert.equal(await keytar.findPassword(service), null)
})
})

describe('findCredentials(service)', function() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a test here that checks the behavior when no credentials are found.

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)
})
});
})
65 changes: 65 additions & 0 deletions src/async.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <string>
#include <vector>

#include "nan.h"
#include "keytar.h"
Expand Down Expand Up @@ -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<v8::Array> val = Nan::New<v8::Array>(credentials.size());
unsigned int idx = 0;
std::vector<keytar::Credentials>::iterator it;
for (it = credentials.begin(); it != credentials.end(); it++) {
keytar::Credentials cred = *it;
v8::Local<v8::Object> obj = Nan::New<v8::Object>();

v8::Local<v8::String> account = Nan::New<v8::String>(
cred.first.data(),
cred.first.length()).ToLocalChecked();

v8::Local<v8::String> password = Nan::New<v8::String>(
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<v8::Value> argv[] = {
Nan::Null(),
val
};
callback->Call(2, argv);
} else {
v8::Local<v8::Value> argv[] = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd prefer to see an empty array returned here, so users don't have to do null checking on the result of the operation.

Nan::Null(),
Nan::New<v8::Array>(0)
};
callback->Call(2, argv);
}
}
17 changes: 17 additions & 0 deletions src/async.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include <string>
#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,
Expand Down Expand Up @@ -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<keytar::Credentials> credentials;
bool success;
};

#endif // SRC_ASYNC_H_
13 changes: 13 additions & 0 deletions src/credentials.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#ifndef SRC_CREDENTIALS_H_
#define SRC_CREDENTIALS_H_

#include <string>
#include <utility>

namespace keytar {

typedef std::pair<std::string, std::string> Credentials;

}

#endif // SRC_CREDENTIALS_H_
7 changes: 7 additions & 0 deletions src/keytar.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
#define SRC_KEYTAR_H_

#include <string>
#include <vector>

#include "credentials.h"

namespace keytar {

Expand Down Expand Up @@ -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<Credentials>*,
std::string* error);

} // namespace keytar

#endif // SRC_KEYTAR_H_
129 changes: 128 additions & 1 deletion src/keytar_mac.cc
Original file line number Diff line number Diff line change
@@ -1,9 +1,43 @@
#include <Security/Security.h>
#include "keytar.h"
#include "credentials.h"

#include <Security/Security.h>

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<char*>(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);
Expand Down Expand Up @@ -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>* 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
Loading