Skip to content

Commit

Permalink
Merge pull request #236 from dappnode/v0.2.6
Browse files Browse the repository at this point in the history
v0.2.6 Release
  • Loading branch information
eduadiez authored Jul 16, 2019
2 parents e6cabee + a45a9a7 commit 6bfd0cc
Show file tree
Hide file tree
Showing 10 changed files with 249 additions and 3 deletions.
2 changes: 2 additions & 0 deletions build/src/src/calls/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ module.exports = {
notificationsGet: require("./notificationsGet"),
notificationsRemove: require("./notificationsRemove"),
notificationsTest: require("./notificationsTest"),
passwordChange: require("./passwordChange"),
passwordIsSecure: require("./passwordIsSecure"),
removePackage: require("./removePackage"),
requestChainData: require("./requestChainData"),
resolveRequest: require("./resolveRequest"),
Expand Down
27 changes: 27 additions & 0 deletions build/src/src/calls/passwordChange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { changePassword } = require("modules/passwordManager");
// External calls
const passwordIsSecure = require("./passwordIsSecure");

/**
* Changes the user `dappnode`'s password in the host machine
* Only allows it if the current password has the salt `insecur3`
*
* @param {string} newPassword super-secure-password
*/
const passwordChange = async ({ newPassword }) => {
if (!newPassword) throw Error("Argument newPassword must be defined");

await changePassword(newPassword);

// Update the DB "is-password-secure" check
await passwordIsSecure();

return {
message: `Changed password`,
logMessage: true,
userAction: true,
privateKwargs: true
};
};

module.exports = passwordChange;
27 changes: 27 additions & 0 deletions build/src/src/calls/passwordIsSecure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { isPasswordSecure } = require("modules/passwordManager");

let isSecureCache = false;

/**
* Checks if the user `dappnode`'s password in the host machine
* is NOT the insecure default set at installation time.
* It does so by checking if the current salt is `insecur3`
*
* - This check will be run every time this node app is started
* - If the password is SECURE it will NOT be run anymore
* and this call will return true always
* - If the password is INSECURE this check will be run every
* time the admin requests it (on page load)
*
* @returns {bool} true = is secure / false = is not
*/
const passwordIsSecure = async () => {
if (!isSecureCache) isSecureCache = await isPasswordSecure();

return {
message: `Checked if password is secure`,
result: isSecureCache
};
};

module.exports = passwordIsSecure;
10 changes: 10 additions & 0 deletions build/src/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ require("./utils/getVersionData");
// Start HTTP API
require("./httpApi");

// Initial calls to check this DAppNode's status
const passwordIsSecure = require("./calls/passwordIsSecure");
passwordIsSecure()
.then(({ result }) => {
logs.info("Host user password is " + (result ? "secure" : "INSECURE"));
})
.catch(e => {
logs.error(`Error checking if host user password is secure: ${e.message}`);
});

/*
* Connection configuration
* ************************
Expand Down
12 changes: 12 additions & 0 deletions build/src/src/logUserAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ const onlyUserAction = format(info => {
return _info;
});

/**
* Private fields
*/
const privateFields = format(info => {
const _info = Object.assign({}, info);
if (_info.privateKwargs && _info.kwargs && typeof _info.kwargs === "object")
for (const key of Object.keys(_info.kwargs)) _info.kwargs[key] = "********";
delete _info.privateKwargs;
return _info;
});

/**
* Limit the length of objects.
* RPC calls like copyTo may content really big dataUrls as kwargs,
Expand All @@ -85,6 +96,7 @@ const logger = createLogger({
],
format: format.combine(
onlyUserAction(),
privateFields(),
limitLength(),
format.timestamp(),
format.json()
Expand Down
86 changes: 86 additions & 0 deletions build/src/src/modules/passwordManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const shell = require("utils/shell");

const insecureSalt = "insecur3";

const baseCommand = `docker run --rm -v /etc:/etc --privileged --entrypoint=""`;

// If the DAPPMANAGER image changes, this node app MUST be reseted
let cacheDappmanagerImage;
async function getDappmanagerImage() {
if (cacheDappmanagerImage) return cacheDappmanagerImage;
const res = await shell(
`docker ps --filter "name=dappmanager.dnp.dappnode.eth" --format "{{.Image}}"`
);
if (!res) throw Error("No image found for dappmanager.dnp.dappnode.eth");
const dappmanagerImage = res.trim();
cacheDappmanagerImage = dappmanagerImage;
return dappmanagerImage;
}

/**
* Checks if the user `dappnode`'s password in the host machine
* is NOT the insecure default set at installation time.
* It does so by checking if the current salt is `insecur3`
*
* @returns {bool} true = is secure / false = is not
*/
async function isPasswordSecure() {
const image = await getDappmanagerImage();
try {
const res = await shell(
`${baseCommand} ${image} sh -c "grep dappnode:.*${insecureSalt} /etc/shadow"`
);
return !res;
} catch (e) {
/**
* From the man grep page:
* The exit status is 0 if selected lines are found and 1 otherwise
* The exit status is 2 if an error occurred
*/
if (e.code == 1) return true;
else throw e;
}
}

/**
* Changes the user `dappnode`'s password in the host machine
* Only allows it if the current password is considered insecure
*
* @param {string} newPassword = "super-secure-password"
*/
async function changePassword(newPassword) {
if (!newPassword) throw Error("newPassword must be defined");
if (typeof newPassword !== "string")
throw Error("newPassword must be a string");

/**
* Make sure the password is OK:
* - Is longer than 8 characters, for security
* - Does not contain the `'` character, which would break the command
* - Does not contain non-ascii characters which may cause trouble in the command
*/
if (newPassword.length < 8)
throw Error("password length must be at least 8 characters");
if (!/^((?!['])[\x20-\x7F])*$/.test(newPassword))
throw Error(
`Password must contain only ASCII characters and not the ' character`
);

if (await isPasswordSecure())
throw Error(
"The password can only be changed if it's the insecure default"
);

const image = await getDappmanagerImage();
const res = await shell(
`${baseCommand} -e PASS='${newPassword}' ${image} sh -c 'echo dappnode:$PASS | chpasswd'`
);

// Check the return for success? #### TODO
return res;
}

module.exports = {
isPasswordSecure,
changePassword
};
2 changes: 1 addition & 1 deletion build/src/src/registerHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const logs = require("./logs")(module);

const wrapErrors = (handler, event) =>
// function(args, kwargs, details)
async function(_, kwargs) {
async function(_, kwargs = {}) {
logs.debug(`In-call to ${event}`);
// 0. args: an array with call arguments
// 1. kwargs: an object with call arguments
Expand Down
82 changes: 82 additions & 0 deletions build/src/test/modules/passwordManager.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const proxyquire = require("proxyquire");
const expect = require("chai").expect;

describe("Module > passwordManager", () => {
const checkImageCmd = `docker ps --filter "name=dappmanager.dnp.dappnode.eth" --format "{{.Image}}"`;
const image = "dappmanager.dnp.dappnode.eth:0.2.0";
const grepCommand = `docker run --rm -v /etc:/etc --privileged --entrypoint="" dappmanager.dnp.dappnode.eth:0.2.0 sh -c "grep dappnode:.*insecur3 /etc/shadow"`;
const passwordHash = `dappnode:$6$insecur3$rnEv9Amdjn3ctXxPYOlzj/cwvLT43GjWzkPECIHNqd8Vvza5bMG8QqMwEIBKYqnj609D.4ngi4qlmt29dLE.71:18004:0:99999:7:::`;

it("Should check if the password is secure", async () => {
const { isPasswordSecure } = proxyquire("modules/passwordManager", {
"utils/shell": async cmd => {
if (cmd === checkImageCmd) return image;
if (cmd == grepCommand) return passwordHash;
throw Error(`Unknown command ${cmd}`);
}
});
const isSecure = await isPasswordSecure();
expect(isSecure).to.equal(false);
});

it("Should change the password", async () => {
let lastCmd;
const { changePassword } = proxyquire("modules/passwordManager", {
"utils/shell": async cmd => {
lastCmd = cmd;
if (cmd === checkImageCmd) return image;
if (cmd == grepCommand) return passwordHash;
if (cmd.includes("chpasswd")) return "";
throw Error(`Unknown command ${cmd}`);
}
});

const newPassword = "secret-password";
await changePassword(newPassword);
expect(lastCmd).to.equal(
`docker run --rm -v /etc:/etc --privileged --entrypoint="" -e PASS='${newPassword}' dappmanager.dnp.dappnode.eth:0.2.0 sh -c 'echo dappnode:$PASS | chpasswd'`
);
});

it("Should block changing the password when it's secure", async () => {
const { changePassword } = proxyquire("modules/passwordManager", {
"utils/shell": async cmd => {
if (cmd === checkImageCmd) return image;
if (cmd == grepCommand) return "";
throw Error(`Unknown command ${cmd}`);
}
});

let errorMessage = "---did not throw---";
try {
await changePassword("password");
} catch (e) {
errorMessage = e.message;
}

expect(errorMessage).to.equal(
`The password can only be changed if it's the insecure default`
);
});

it("Should block changing the password if the input contains problematic characters", async () => {
const { changePassword } = proxyquire("modules/passwordManager", {
"utils/shell": async cmd => {
if (cmd === checkImageCmd) return image;
if (cmd == grepCommand) return passwordHash;
throw Error(`Unknown command ${cmd}`);
}
});

let errorMessage = "---did not throw---";
try {
await changePassword("password'ops");
} catch (e) {
errorMessage = e.message;
}

expect(errorMessage).to.equal(
`Password must contain only ASCII characters and not the ' character`
);
});
});
2 changes: 1 addition & 1 deletion dappnode_package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dappmanager.dnp.dappnode.eth",
"version": "0.2.5",
"version": "0.2.6",
"description": "Dappnode package responsible for providing the DappNode Package Manager",
"avatar": "/ipfs/QmdT2GX9ybaoE25HBk7is8CDnfn2KaFTpZVkLdfkAQs1PN",
"type": "dncore",
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ services:
build:
context: .
dockerfile: ./build/Dockerfile
image: "dappmanager.dnp.dappnode.eth:0.2.5"
image: "dappmanager.dnp.dappnode.eth:0.2.6"
container_name: DAppNodeCore-dappmanager.dnp.dappnode.eth
restart: always
volumes:
Expand Down

0 comments on commit 6bfd0cc

Please sign in to comment.