Files
cloudron-box/api/user.js
T
2014-04-06 21:34:55 -07:00

225 lines
7.4 KiB
JavaScript

'use strict';
var db = require('./database.js'),
DatabaseError = db.DatabaseError,
crypto = require('crypto'),
aes = require('../common/aes-helper.js'),
util = require('util'),
debug = require('debug')('server:user'),
assert = require('assert'),
ursa = require('ursa'),
safe = require('safetydance');
exports = module.exports = {
UserError: UserError,
list: listUsers,
create: createUser,
verify: verifyUser,
remove: removeUser,
get: getUser,
changePassword: changePassword,
update: updateUser
};
var CRYPTO_SALT_SIZE = 64; // 512-bit salt
var CRYPTO_ITERATIONS = 10000; // iterations
var CRYPTO_KEY_LENGTH = 512; // bits
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function UserError(err, reason) {
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.message = safe.JSON.stringify(err);
this.code = err ? err.code : null;
this.reason = reason || UserError.INTERNAL_ERROR;
}
util.inherits(UserError, Error);
UserError.DATABASE_ERROR = 1;
UserError.INTERNAL_ERROR = 2;
UserError.ALREADY_EXISTS = 3;
UserError.NOT_FOUND = 4;
UserError.WRONG_USER_OR_PASSWORD = 5;
UserError.ARGUMENTS = 6;
function ensureArgs(args, expected) {
assert(args.length === expected.length);
for (var i = 0; i < args.length; ++i) {
if (expected[i]) {
assert(typeof args[i] === expected[i]);
}
}
}
function listUsers(callback) {
ensureArgs(arguments, ['function']);
db.USERS_TABLE.getAll(false, function (error, result) {
if (error) {
debug('Unable to get all users.', error);
return callback(new UserError('Unable to list users', UserError.DATABASE_ERROR));
}
return callback(null, result);
});
}
function createUser(username, password, email, options, callback) {
ensureArgs(arguments, ['string', 'string', 'string', 'object', 'function']);
if (username.length === 0) {
return callback(new UserError('username empty', UserError.ARGUMENTS));
}
if (password.length === 0) {
return callback(new UserError('password empty', UserError.ARGUMENTS));
}
if (email.length === 0) {
return callback(new UserError('email empty', UserError.ARGUMENTS));
}
crypto.randomBytes(CRYPTO_SALT_SIZE, function (error, salt) {
if (error) {
return callback(new UserError('Failed to generate random bytes', UserError.INTERNAL_ERROR));
}
crypto.pbkdf2(password, salt, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
if (error) {
return callback(new UserError('Failed to hash password', UserError.INTERNAL_ERROR));
}
// now generate the pub/priv keypairs for volume header
var keyPair = ursa.generatePrivateKey();
var now = (new Date()).toUTCString();
var admin = !(db.USERS_TABLE.count()); // currently the first user is the admin
var user = {
username: username,
email: email,
password: new Buffer(derivedKey, 'binary').toString('hex'),
privatePemCipher: aes.encrypt(keyPair.toPrivatePem(), password, salt),
publicPem: keyPair.toPublicPem(),
admin: admin,
salt: salt.toString('hex'),
created_at: now,
updated_at: now
};
db.USERS_TABLE.put(user, function (error) {
if (error) {
if (error.reason === DatabaseError.ALREADY_EXISTS) {
return callback(new UserError('Already exists', UserError.ALREADY_EXISTS));
}
return callback(error);
}
callback(null, user);
});
});
});
}
function verifyUser(username, password, callback) {
ensureArgs(arguments, ['string', 'string', 'function']);
if (username.length === 0) {
return callback(new UserError('username empty', UserError.ARGUMENTS));
}
if (password.length === 0) {
return callback(new UserError('password empty', UserError.ARGUMENTS));
}
db.USERS_TABLE.get(username, function (error, user) {
if (error) {
if (error.reason === DatabaseError.NOT_FOUND) {
return callback(new UserError('Username not found', UserError.NOT_FOUND));
}
return callback(error);
}
var saltBinary = new Buffer(user.salt, 'hex');
crypto.pbkdf2(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
if (error) {
return callback(new UserError('Failed to hash password', UserError.INTERNAL_ERROR));
}
var derivedKeyHex = new Buffer(derivedKey, 'binary').toString('hex');
if (derivedKeyHex !== user.password) {
return callback(new UserError('Username and password do not match', UserError.WRONG_USER_OR_PASSWORD));
}
callback(null, user);
});
});
}
function removeUser(username, callback) {
ensureArgs(arguments, ['string', 'function']);
// TODO we might want to cleanup volumes assigned to this user as well - Johannes
db.USERS_TABLE.remove(username, function (error, user) {
if (error) return callback(error);
callback(null, user);
});
}
function getUser(username, callback) {
ensureArgs(arguments, ['string', 'function']);
db.USERS_TABLE.get(username, function (error, result) {
if (error) return callback(error);
return callback(null, result);
});
}
function updateUser(username, options, callback) {
ensureArgs(arguments, ['string', 'object', 'function']);
callback(new UserError('not implemented', UserError.INTERNAL_ERROR));
}
function changePassword(username, oldPassword, newPassword, callback) {
ensureArgs(arguments, ['string', 'string', 'string', 'function']);
if (newPassword.length === 0) {
debug('Empty passwords are not allowed.');
return callback(new UserError('No empty passwords allowed', UserError.INTERNAL_ERROR));
}
verifyUser(username, oldPassword, function (error, user) {
if (error) return callback(error);
var saltBuffer = new Buffer(user.salt, 'hex');
crypto.pbkdf2(newPassword, saltBuffer, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
if (error) {
return callback(new UserError('Failed to hash password', UserError.INTERNAL_ERROR));
}
var privateKeyPem = aes.decrypt(user.privatePemCipher, oldPassword, saltBuffer);
var keyPair = ursa.createPrivateKey(privateKeyPem, oldPassword, 'utf8');
user.updated_at = (new Date()).toUTCString();
user.password = new Buffer(derivedKey, 'binary').toString('hex');
user.privatePemCipher = aes.encrypt(keyPair.toPrivatePem(), newPassword, saltBuffer);
db.USERS_TABLE.update(user, function (error) {
if (error) {
if (error.reason === DatabaseError.NOT_FOUND) {
return callback(new UserError('User does not exist', UserError.NOT_FOUND));
}
return callback(error);
}
callback(null, user);
});
});
});
}