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.

280 lines
7.8 KiB

4 years ago
const path = require('path');
5 years ago
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const { CipherSeed } = require('aezeed');
5 years ago
const iocane = require("iocane");
4 years ago
const compose = require("docker-compose");
5 years ago
const diskLogic = require('logic/disk.js');
4 years ago
const lndApiService = require('services/lndApi.js');
5 years ago
const bashService = require('services/bash.js');
const NodeError = require('models/errors.js').NodeError;
const JWTHelper = require('utils/jwt.js');
const constants = require('utils/const.js');
const UUID = require('utils/UUID.js');
const saltRounds = 10;
const SYSTEM_USER = UUID.fetchBootUUID() || 'admin';
let devicePassword = '';
let changePasswordStatus;
resetChangePasswordStatus();
function resetChangePasswordStatus() {
changePasswordStatus = { percent: 0 };
}
async function sleepSeconds(seconds) {
return new Promise(resolve => {
setTimeout(resolve, seconds * constants.TIME.ONE_SECOND_IN_MILLIS);
});
}
// Caches the password.
function cachePassword(password) {
devicePassword = password;
}
// Gets the cached the password.
function getCachedPassword() {
return devicePassword;
}
// Change the device and lnd password.
async function changePassword(currentPassword, newPassword, jwt) {
4 years ago
5 years ago
resetChangePasswordStatus();
changePasswordStatus.percent = 1; // eslint-disable-line no-magic-numbers
4 years ago
// restart lnd
try {
await compose.restartOne('lnd', { cwd: constants.DOCKER_COMPOSE_DIRECTORY });
} catch (error) {
4 years ago
throw new Error('Unable to change password as lnd wouldn\'t restart');
}
4 years ago
5 years ago
changePasswordStatus.percent = 40; // eslint-disable-line no-magic-numbers
let complete = false;
let attempt = 0;
const MAX_ATTEMPTS = 20;
do {
try {
attempt++;
// call lnapi to change password
changePasswordStatus.percent = 60 + attempt; // eslint-disable-line no-magic-numbers
4 years ago
await lndApiService.changePassword(currentPassword, newPassword, jwt);
5 years ago
4 years ago
// update user file
const user = await diskLogic.readUserFile();
5 years ago
const credentials = hashCredentials(SYSTEM_USER, newPassword);
4 years ago
// re-encrypt seed with new password
const decryptedSeed = await iocane.createSession().decrypt(user.seed, currentPassword);
const encryptedSeed = await iocane.createSession().encrypt(decryptedSeed, newPassword);
4 years ago
// update user file
4 years ago
await diskLogic.writeUserFile({ name: user.name, password: credentials.password, seed: encryptedSeed });
5 years ago
// update ssh password
4 years ago
// await hashAccountPassword(newPassword);
5 years ago
complete = true;
// cache the password for later use
cachePassword(newPassword);
changePasswordStatus.percent = 100;
} catch (error) {
// wait for lnd to boot up
if (error.response.status === constants.STATUS_CODES.BAD_GATEWAY) {
await sleepSeconds(1);
// user supplied incorrect credentials
} else if (error.response.status === constants.STATUS_CODES.FORBIDDEN) {
changePasswordStatus.unauthorized = true;
5 years ago
// unknown error occurred
} else {
changePasswordStatus.error = true;
changePasswordStatus.percent = 100;
throw error;
}
}
} while (!complete && attempt < MAX_ATTEMPTS && !changePasswordStatus.unauthorized && !changePasswordStatus.error);
if (!complete && attempt === MAX_ATTEMPTS) {
changePasswordStatus.error = true;
changePasswordStatus.percent = 100;
4 years ago
throw new Error('Unable to change password');
5 years ago
}
}
function getChangePasswordStatus() {
return changePasswordStatus;
}
// Returns an object with the hashed credentials inside.
function hashCredentials(username, password) {
const hash = bcrypt.hashSync(password, saltRounds);
return { password: hash, username, plainTextPassword: password };
}
// Returns true if the user is registered otherwise false.
async function isRegistered() {
try {
await diskLogic.readUserFile();
return { registered: true };
} catch (error) {
return { registered: false };
}
}
// Derives the root umbrel seed and persists it to disk to be used for
// determinstically deriving further entropy for any other Umbrel service.
async function deriveUmbrelSeed(user) {
if (await diskLogic.umbrelSeedFileExists()) {
return;
}
const mnemonic = (await seed(user)).seed.join(' ');
const {entropy} = CipherSeed.fromMnemonic(mnemonic);
const umbrelSeed = crypto
.createHmac('sha256', entropy)
.update('umbrel-seed')
.digest('hex');
return diskLogic.writeUmbrelSeedFile(umbrelSeed);
}
5 years ago
// Log the user into the device. Caches the password if login is successful. Then returns jwt.
async function login(user) {
try {
const jwt = await JWTHelper.generateJWT(user.username);
// Cache plain text password
// cachePassword(user.plainTextPassword);
cachePassword(user.password);
4 years ago
//unlock lnd wallet
// await lndApiService.unlock(user.plainTextPassword, jwt);
deriveUmbrelSeed(user)
5 years ago
return { jwt: jwt };
} catch (error) {
throw new NodeError('Unable to generate JWT');
}
}
4 years ago
async function getInfo() {
try {
const user = await diskLogic.readUserFile();
//remove sensitive info
delete user.password;
delete user.seed;
return user;
} catch (error) {
throw new NodeError('Unable to get account info');
}
};
5 years ago
async function seed(user) {
//Decrypt mnemonic seed
try {
const { seed } = await diskLogic.readUserFile();
const decryptedSeed = await iocane.createSession().decrypt(seed, user.plainTextPassword);
return { seed: decryptedSeed.split(",") };
} catch (error) {
throw new NodeError('Unable to decrypt mnemonic seed');
}
}
// Registers the the user to the device. Returns an error if a user already exists.
async function register(user, seed) {
if ((await isRegistered()).registered) {
throw new NodeError('User already exists', 400); // eslint-disable-line no-magic-numbers
}
//Encrypt mnemonic seed for storage
let encryptedSeed;
try {
encryptedSeed = await iocane.createSession().encrypt(seed.join(), user.plainTextPassword);
} catch (error) {
throw new NodeError('Unable to encrypt mnemonic seed');
}
//save user
try {
await diskLogic.writeUserFile({ name: user.name, password: user.password, seed: encryptedSeed });
} catch (error) {
throw new NodeError('Unable to register user');
}
//derive Umbrel seed
try {
await deriveUmbrelSeed(user);
} catch (error) {
throw new NodeError('Unable to create Umbrel seed');
}
5 years ago
//generate JWt
let jwt;
try {
jwt = await JWTHelper.generateJWT(user.username);
} catch (error) {
4 years ago
await diskLogic.deleteUserFile();
5 years ago
throw new NodeError('Unable to generate JWT');
}
//initialize lnd wallet
try {
4 years ago
await lndApiService.initializeWallet(user.plainTextPassword, seed, jwt);
5 years ago
} catch (error) {
await diskLogic.deleteUserFile();
throw new NodeError(error.response.data);
}
//return token
return { jwt: jwt };
}
// Generate and return a new jwt token.
async function refresh(user) {
try {
const jwt = await JWTHelper.generateJWT(user.username);
return { jwt: jwt };
} catch (error) {
throw new NodeError('Unable to generate JWT');
}
}
module.exports = {
changePassword,
getCachedPassword,
getChangePasswordStatus,
hashCredentials,
isRegistered,
4 years ago
getInfo,
5 years ago
seed,
login,
register,
refresh,
};