Introducing a Secure Password Manager🔐
A secure Password Manager which stores passwords in the encrypted form inside the database to avoid the leak of sensitive information.
Disclaimer: Please don't enter in your original passwords yet and also its an open showcase app, so whatever you enter can be seen to other people with the same link. Use it to experience the different functionalities and dynamic features of the app.
Live Link: https://main.d3qwkjcxzk7m67.amplifyapp.com/
Source Code: https://github.com/GeoBrodas/aws-password-manager/tree/main
Features:👀
- Dynamic entry of data
- Fluid animations
- Fast loading
- Cross-platform support and responsiveness.
- Password encryption using
aes-256-gcm
algorithm.
Tech Stack and Resources⚛️
- React Library
- MongoDB Atlas
- NodeJs
- Express
- Cryptr NPM module for NodeJs.
- Axios for Api requests.
- Material UI
- Heroku for Back-end And Front-end on AWS-Amplify.
Inspiration💡
I usually store my passwords in the browser itself, but one fine day it turned out that my google browser just popped out a warning saying there was a data breach in their database and that there was an urgent need to change certain passwords because they were exposed!
And then I just got tired of changing all the passwords and wondered....what if could make my own password manager which will be encrypted in the database with fewer chances of being exposed and of course, only I will be having access to the app, and that's when I started building a secure Password manager using React and Cryptr for encryption on the server-side. So without further ado let's get started with the development journey!
Stages of the App development Journey🚶🏽♂️
So the first thing I divided my build procedure into 5 stages so that I could focus on each stage every day and in 5 days I would be ready with the app:-
Colour Inspiration🎨
This was really cool. I took the color palette used in the Rescue Armour from the "Iron Man: Armoured Adventures" animated series.
1. Front-End side using react for dynamic rendering. 👁️
So for the first day, I focussed on the front-end side, basically, I wanted to create cards, and delete them on button click all to be rendered dynamically. For this, we need firstly a form with two inputs that could take in the account name and password entered by the user. And then the card component which would display the entered credentials of the user. And subsequently, each card should contain a delete button. To add the card as usual I gave onChange attributes to both the inputs and used the useState Hook to store the credentials entered. To submit the information, I then created a prop that would take in one parameter and that is the credit
object declared for the useState.
const [credit, setCredit] = useState({
accName: "",
pass: "",
});
function handleChange(event) {
const { name, value } = event.target;
setCredit((prevNote) => {
return {
...prevNote,
[name]: value,
};
});
}
function submitCred(event) {
props.onAdd(credit);
setCredit({
accName: "",
pass: "",
});
event.preventDefault();
}
I then passed these props to my main App.jsx file.
const [allCreds, setCred] = useState([]);
function addCred(newCred) {
setCred((prevCreds) => {
return [...prevCreds, newCred];
});
}
<InputTextArea onAdd={addCred} />
This would store the data in the allCreds array as objects which then can be used to render all the information into card components using the Map function.
<div className="flexbox">
{allCreds.map((cred, index) => {
return (
<Card
key={index}
id={index}
name={cred.accName}
pass={cred.pass}
onDelete={deleteCred}
/>
);
})}
</div>
This Card component would take in another prop called onDelete which will return the index of the card which was returned when the onClick event was triggered.
id
is set by the index parameter via the Map function.
function removeCard() {
props.onDelete(props.id);
}
In the main App.jsx file the delete function contains a filter function that will return all the card components excluding the card component whose index was passed to the delete function.
function deleteCred(mid, id) {
setCred((prevCreds) => {
return prevCreds.filter((cred, index) => {
return index !== id;
});
});
}
With this, we achieve all the full front-end objectives in our React-application!
2. Setting up MongoDB database and Read, Create, and delete them from the Front-End side. 🗂️
Firstly you need to have a MongoDB Atlas for hosting your database on the cloud. MongoDB has a free tier plan of 512 MB, which can be used to test early-stage apps. Then I connected my application with the MongoDB database. First thing I installed express, cors, and mongoose on the back-end, and on the front-end Axios to make API requests to the back-end. Cors will help to make a connection between our back-end and front-end.
The schema model for every request we make to the MongoDb database will be as follows:-
const CredSchema = new mongoose.Schema({
accName: {
type: String,
required: true,
},
pass: {
type: String,
required: true,
},
});
So after all the setup let's head towards the first task: - To submit our credentials from the front-end to the back-end. We can do this by making an Axios Post request when the user clicks the submit button.
Axios.post("https://localhost:3001/insert", {
accName: newCred.accName,
pass: newCred.pass,
});
On the server-side, we have to make a post route to receive the API request from Axios and then use Mongoose to create the entry into the database.
app.post("/insert", async (req, res) => {
const name = req.body.accName;
const password = req.body.pass;
const newCred = new CredModel({
accName: name,
pass: password,
});
try {
newCred.save();
res.send("Inserted Data");
} catch (err) {
console.log(err);
}
});
One job is done, 2 to go! Now we have to render all the information from the database to the front-end went the page loads. For this, we can use the useEffect hook to make an Axios Get request when the page first loads. The response that the request returns can be used to then set the state of allCreds
state.
useEffect(() => {
Axios.get("https://localhost:3001/read").then(
(response) => {
setCred(response.data);
}
);
}, []);
And finally, the tricky part to delete the card when the user clicks the delete button.
Now when the Get requests return all the data from the database, it returns a unique ID with the property name _id
. Let's name the mid
as in for MongoDB id. We can get hold of this mid
from the map function we created to render all the credentials from the database.
Note: I don't want all the code to clutter and hence I'm removing some props to make you'll understand😊
{allCreds.map((cred, index) => {
return (
<Card
key={index}
mid={cred._id}
/>
);
})}
This prop can be passed to the delete function in our card component as a second parameter.
function removeCard() {
props.onDelete(props.mid, props.id);
}
In our App.jsx file we pass this mid
prop.
In our Axios delete request, the URL here is enclosed within back-ticks instead of "". This is a really useful feature of Javascript. Note how we are passing the mid
prop to the back-end by enclosing it within a ${mid}
.
function deleteCred(mid, id) {
setCred((prevCreds) => {
return prevCreds.filter((cred, index) => {
return index !== id;
});
});
Axios.delete(`https://localhost:3001/delete/${mid}`);
//use back-tickssss--importantttt!!!!
}
On our server-side, we will then make a delete route and use the mongoose findByIdAndRemove
method to look through the database for the entry matching with the mid
and remove it instantly.
app.delete("/delete/:id", async (req, res) => {
const id = req.params.id;
await CredModel.findByIdAndRemove(id).exec();
res.send("deleted item: " + id);
});
3. Encryption at server-side and decryption to show password.🔐
For encryption to store our passwords in encrypted form, we can use a simple npm package called cryptr
. Now we can set this up by making a secret key, after which we can encrypt and decrypt strings by simply calling the encrypt
and decrypt
function provided by cryptr.
const Cryptr = require("cryptr");
const cryptr = new Cryptr("yoursecretkey");
We want the passwords to be encrypted as soon as we receive the post request from Axios on the client-side.
const name = req.body.accName;
const password = cryptr.encrypt(req.body.pass);
const newCred = new CredModel({
accName: name,
pass: password,
});
The passwords will be now encrypted in our database using the aes-256-gcm
algorithm.
Now trust me, this was the toughest part in the development, that is to display the original password to the user when the user clicks the 👁 button.
Now what I did, is to make the user trigger the onClick event and pass it a function that takes two parameters and then passes that to the main App.jsx file.
function showP() {
props.seePassword(props.pass, props.id);
}
In my main App.jsx file, I passed this prop as a function to the card element which is in the Map function.
{allCreds.map((cred, index) => {
return (
<Card
key={index}
id={index}
seePassword={getPassword}
pass={cred.pass}
/>
);
})}
In our getPassword function, we are passing the encrypted password which can be tapped with the Map function and using Axios make a post request to the server-side to send all the decrypted passwords back to the front-end side.
//App.jsx file
function getPassword(password, id) {
Axios.post("https://localhost:3001/showpassword", {
password: password,
}).then((response) => {
setCred(
allCreds.map((cred, index) => {
return index === id
? {
accName: response.data,
pass: cred.pass,
}
: cred;
})
);
});
}
//index.js file at server-side
app.post("/showpassword", (req, res) => {
res.send(cryptr.decrypt(req.body.password));
});
The response which we get from the server-side containing all the passwords can be run through a map function. The map function only returns the decrypted password back which gets matched with the id
of the card component which the user clicked on. Using the ternary operator, we can use the setCred function from the useState to set the state of allCreds array by making the name of the credential equal to the response.
4. Making our code more leak-safe use environment variables.🛡️
This is best done using an npm package called dotenv
.
Remember the secret we stored our key. Well if you're committing your code to GitHub frequently this key will easily get exposed, if anyone refers to your code changes by referring to the commits. So make sure you store your environment variables first, add to the .gitignore file, and then commit to your remote repository.
//index.js file ---Server side
//require the dotenv module at the earliest in your file.
require("dotenv").config();
const cryptr = new Cryptr(process.env.SECRET);
//.env file ----Server side
SECRET=yoursecretkey
Make sure to follow the format dotenv module specifies, that is, the constant has to be entirely capitalized with no quotes surrounding the key.
5. Deploying 🚀
I deployed my server file to Heroku. It had been a while since I had used Heroku, came across a lot of errors, but somehow managed to deploy it by seeing some videos and referring to the documentation.
I know I had to deploy the production build of React and not the development build. I had never deployed a React project ever but I directly went to the AWS Amplify console and it auto-generated the build settings for me, which was surprising for me because hosting platforms like Netlify don't and the developer has to mention the build settings. The process hardly took me 4 mins, and the app was up and running!🚀
Thank you for reading till here!
Hope you liked this blog, if yes hit a ❤
Do let me know if you want the source code for the back-end part. I didn't share it for security reasons, as there are environment variables in it and the password to the MongoDB cluster.
Hit me up or retweet this blog if you liked it.
For me building this app was a big leap because I didn't have the confidence to build a full-stack app like this.
Top comments (0)