-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #236 from dappnode/v0.2.6
v0.2.6 Release
- Loading branch information
Showing
10 changed files
with
249 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters