You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
275 lines
7.1 KiB
275 lines
7.1 KiB
/*!
|
|
* node-keychain
|
|
* Copyright(c) 2015 Nicholas Penree <nick@penree.com>
|
|
* MIT Licensed
|
|
*/
|
|
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var spawn = require('child_process').spawn;
|
|
var noop = function () {};
|
|
|
|
// Polyfill Buffer.from for Node < 4 that didn't have a #from method
|
|
if (!Buffer.from) {
|
|
Buffer.from = function (data, encoding, len) {
|
|
return new Buffer(data, encoding, len);
|
|
};
|
|
}
|
|
// Between Node >=4 to < 4.5 Buffer.from was inherited from Uint8Array
|
|
// And behaved differently, it was backported in 4.5.
|
|
if (Buffer.from === Uint8Array.from) {
|
|
throw new Error('Node >= 4.0.0 to < 4.5.0 are unsupported')
|
|
}
|
|
|
|
/**
|
|
* Basic Keychain Access on Mac computers running Node.js
|
|
*
|
|
* @class KeychainAccess
|
|
* @api public
|
|
*/
|
|
|
|
function KeychainAccess() {
|
|
this.executablePath = '/usr/bin/security';
|
|
}
|
|
|
|
/**
|
|
* Retreive a password from the keychain.
|
|
*
|
|
* @param {Object} opts Object containing `account` and `service`
|
|
* @param {Function} fn Callback
|
|
* @api public
|
|
*/
|
|
|
|
KeychainAccess.prototype.getPassword = function(opts, fn) {
|
|
opts = opts || {};
|
|
opts.type = (opts.type || 'generic').toLowerCase();
|
|
fn = fn || noop;
|
|
var err;
|
|
|
|
if (process.platform !== 'darwin') {
|
|
err = new KeychainAccess.errors.UnsupportedPlatformError(null, process.platform);
|
|
fn(err, null);
|
|
return;
|
|
}
|
|
|
|
if (!opts.account) {
|
|
err = new KeychainAccess.errors.NoAccountProvidedError();
|
|
fn(err, null);
|
|
return;
|
|
}
|
|
|
|
if (!opts.service) {
|
|
err = new KeychainAccess.errors.NoServiceProvidedError();
|
|
fn(err, null);
|
|
return;
|
|
}
|
|
|
|
var security = spawn(this.executablePath, [ 'find-'+opts.type+'-password', '-a', opts.account, '-s', opts.service, '-g' ]);
|
|
var keychain = '';
|
|
var password = '';
|
|
|
|
security.on('error', function(err) {
|
|
err = new KeychainAccess.errors.ServiceFailureError(null, err.message);
|
|
fn(err, null);
|
|
return;
|
|
});
|
|
|
|
security.stdout.on('data', function(d) {
|
|
keychain += d.toString();
|
|
});
|
|
|
|
// For better or worse, the last line (containing the actual password) is actually written to stderr instead of stdout.
|
|
// Reference: http://blog.macromates.com/2006/keychain-access-from-shell/
|
|
security.stderr.on('data', function(d) {
|
|
password += d.toString();
|
|
});
|
|
|
|
security.on('close', function(code, signal) {
|
|
if (code !== 0) {
|
|
err = new KeychainAccess.errors.PasswordNotFoundError();
|
|
fn(err, null);
|
|
return;
|
|
}
|
|
|
|
if (/password/.test(password)) {
|
|
// When keychain escapes a char into octal it also includes a hex
|
|
// encoded version.
|
|
//
|
|
// e.g. password 'passWith\' becomes:
|
|
// password: 0x70617373576974685C "passWith\134"
|
|
//
|
|
// And if the password does not contain ASCII it leaves out the quoted
|
|
// version altogether:
|
|
//
|
|
// e.g. password '∆˚ˆ©ƒ®∂çµ˚¬˙ƒ®†¥' becomes:
|
|
// password: 0xE28886CB9ACB86C2A9C692C2AEE28882C3A7C2B5CB9AC2ACCB99C692C2AEE280A0C2A5
|
|
if (/0x([0-9a-fA-F]+)/.test(password)) {
|
|
var hexPassword = password.match(/0x([0-9a-fA-F]+)/, '')[1];
|
|
fn(null, Buffer.from(hexPassword, 'hex').toString());
|
|
}
|
|
// Otherwise the password will be in quotes:
|
|
// password: "passWithoutSlash"
|
|
else {
|
|
fn(null, password.match(/"(.*)\"/, '')[1]);
|
|
}
|
|
}
|
|
else {
|
|
err = new KeychainAccess.errors.PasswordNotFoundError();
|
|
fn(err, null);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Set/update a password in the keychain.
|
|
*
|
|
* @param {Object} opts Object containing `account`, `service`, and `password`
|
|
* @param {Function} fn Callback
|
|
* @api public
|
|
*/
|
|
|
|
KeychainAccess.prototype.setPassword = function(opts, fn) {
|
|
opts = opts || {};
|
|
opts.type = (opts.type || 'generic').toLowerCase();
|
|
fn = fn || noop;
|
|
var err;
|
|
|
|
if (process.platform !== 'darwin') {
|
|
err = new KeychainAccess.errors.UnsupportedPlatformError(null, process.platform);
|
|
fn(err, null);
|
|
return;
|
|
}
|
|
|
|
if (!opts.account) {
|
|
err = new KeychainAccess.errors.NoAccountProvidedError();
|
|
fn(err, null);
|
|
return;
|
|
}
|
|
|
|
if (!opts.service) {
|
|
err = new KeychainAccess.errors.NoServiceProvidedError();
|
|
fn(err, null);
|
|
return;
|
|
}
|
|
|
|
if (!opts.password) {
|
|
err = new KeychainAccess.errors.NoPasswordProvidedError();
|
|
fn(err, null);
|
|
return;
|
|
}
|
|
|
|
var security = spawn(this.executablePath, [ 'add-'+opts.type+'-password', '-a', opts.account, '-s', opts.service, '-w', opts.password ]);
|
|
var self = this;
|
|
|
|
security.on('error', function(err) {
|
|
err = new KeychainAccess.errors.ServiceFailureError(null, err.message);
|
|
fn(err, null);
|
|
return;
|
|
});
|
|
|
|
security.on('close', function(code, signal) {
|
|
if (code !== 0) {
|
|
if (code == 45) {
|
|
self.deletePassword(opts, function(err) {
|
|
if (err) {
|
|
fn(err);
|
|
return;
|
|
}
|
|
|
|
self.setPassword(opts, fn);
|
|
return;
|
|
});
|
|
} else {
|
|
var msg = 'Security returned a non-successful error code: ' + code;
|
|
err = new KeychainAccess.errors.ServiceFailureError(msg);
|
|
err.exitCode = code;
|
|
fn(err);
|
|
return;
|
|
}
|
|
} else {
|
|
fn(null, opts.password);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Delete a password from the keychain.
|
|
*
|
|
* @param {Object} opts Object containing `account`, `service`, and `password`
|
|
* @param {Function} fn Callback
|
|
* @api public
|
|
*/
|
|
|
|
KeychainAccess.prototype.deletePassword = function(opts, fn) {
|
|
opts = opts || {};
|
|
opts.type = (opts.type || 'generic').toLowerCase();
|
|
fn = fn || noop;
|
|
var err;
|
|
|
|
if (process.platform !== 'darwin') {
|
|
err = new KeychainAccess.errors.UnsupportedPlatformError(null, process.platform);
|
|
fn(err, null);
|
|
return;
|
|
}
|
|
|
|
if (!opts.account) {
|
|
err = new KeychainAccess.errors.NoAccountProvidedError();
|
|
fn(err, null);
|
|
return;
|
|
}
|
|
|
|
if (!opts.service) {
|
|
err = new KeychainAccess.errors.NoServiceProvidedError();
|
|
fn(err, null);
|
|
return;
|
|
}
|
|
|
|
var security = spawn(this.executablePath, [ 'delete-'+opts.type+'-password', '-a', opts.account, '-s', opts.service ]);
|
|
|
|
security.on('error', function(err) {
|
|
err = new KeychainAccess.errors.ServiceFailureError(null, err.message);
|
|
fn(err, null);
|
|
return;
|
|
});
|
|
|
|
security.on('close', function(code, signal) {
|
|
if (code !== 0) {
|
|
err = new KeychainAccess.errors.PasswordNotFoundError();
|
|
fn(err);
|
|
return;
|
|
}
|
|
fn(null);
|
|
});
|
|
};
|
|
|
|
function errorClass(code, defaultMsg) {
|
|
var errorType = code + 'Error';
|
|
var ErrorClass = function (msg, append) {
|
|
this.type = errorType;
|
|
this.code = code;
|
|
this.message = (msg || defaultMsg) + (append || '');
|
|
this.stack = (new Error()).stack;
|
|
};
|
|
|
|
ErrorClass.prototype = Object.create(Error.prototype);
|
|
ErrorClass.prototype.constructor = ErrorClass;
|
|
KeychainAccess.errors[errorType] = ErrorClass
|
|
}
|
|
|
|
KeychainAccess.errors = {};
|
|
errorClass('UnsupportedPlatform', 'Expected darwin platform, got: ');
|
|
errorClass('NoAccountProvided', 'An account is required');
|
|
errorClass('NoServiceProvided', 'A service is required');
|
|
errorClass('NoPasswordProvided', 'A password is required');
|
|
errorClass('ServiceFailure', 'Keychain failed to start child process: ');
|
|
errorClass('PasswordNotFound', 'Could not find password');
|
|
|
|
|
|
/**
|
|
* Expose new Keychain Access
|
|
*/
|
|
|
|
module.exports = new KeychainAccess();
|