I released my credential manager last month and only now did I come around to write the blog post. It's a complete rewrite with an authentication system, and it is web-based because if it isn't web-based, nobody cares. I decided to use a symmetric encryption algorithm (AES) again because it's perfect for this use case... coincidentally (kidding) my old credential manager that was built in C# used the same thing. But instead of web-based it was built as a desktop application.
Predecessor
As you may know, the predecessor was a desktop application, + it was written in C# as a terminal application. There was nothing inherently wrong with it per se. It did its job. You make the AES key + the store, you would upload the key and store to modify the store. The only problem with it was that it was a desktop application, and it wasn't convenient because you would have to run the actual .exe in order to get it running. It was using .NET 7, so it was cross-platform at least that had it going for it.
Successor
Here's the source code. Anyways, the successor doesn't use any C#, it uses Next.js, TypeScript, MongoDB and Prisma ORM. I chose these meticulously...
Database
While I was building this out, I knew I didn't want to store the credentials inside a file but in a non-SQL database encrypted with AES. That's what I did, and the database of choice I used was MongoDB. I felt like it was more appropriate to use a non-SQL database for this because I really didn't need my data "structured." If anything, I needed it FLEXIBLE, and that's what non-SQL databases provide. Flexible, unstructured data 😋.
model Vault {
id String @id @default(uuid()) @map("_id")
name String
maxCredentials Int
credentials Credential[]
secret String
iv String
}
model Credential {
id String @id @default(uuid()) @map("_id")
type String
name String
iv String
data String
createdAt DateTime @default(now())
updatedAt DateTime
vaultId String
vault Vault @relation(fields: [vaultId], references: [id])
}
This is my Prisma schema, I ended up using an ORM to simplify everything; you might wonder why the Vault has a secret and IV key. It's not the actual key/secret to the actual Vault. I just used it in order to check whether or not the key that the user provided on their end would successfully be able to successfully decrypt the secret, so if they failed to provide a valid key, the secret would fail, and it would return an error 500 response saying that the key that was provided is no good.
The only thing inside the Credential model that is encrypted is the data: string field. There's no reason to encrypt the type name, date, name, or type. If anything it will cause us problems, maybe in the future, I will categorize them, and if I encrypt them, I'll have to decrypt them and then categorize them, which is much slower than just not decrypting them.
Authentication
If you had read the project description on GitHub, you would have realized it was "One user, multiple vaults." While I was building out the authentication system, I hard-coded the credentials in a .env because I didn't want people to be able to create multiple accounts. Instead, I want them to share the account and make their vaults accordingly. In order to manage the session, all I did was use the iron-session library, and because I wasn't going to display any of their personal information, all I had to do was store the data in the cookie, encrypt it, and check whether or not it was valid, and that's that. Even if the account is compromised, the attacker still needs the AES key to enter the vault.export async function isAuthenticated() {
const session = await getIronSession(cookies(), SESSION_OPTIONS)
const COOKIE_AGE_OFFSET = COOKIE_MAX_AGE(session?.remember) * 1000
if (!session || Object.keys(session).length === 0 || Date.now() > (session.timeStamp + COOKIE_AGE_OFFSET)) {
return false
} else {
return true
}
}
Iron-session made it convenient for me. It did all the heavy lifting. All I did was grab the user's cookies from the headers and check whether or not it was a valid session and if the timestamp had expired. I also embedded it into the middleware so that every time the user visits an authenticated page, it checks whether or not the given cookie is valid; I also don't think the expiration works... right now.
Encryption
The encryption I used was the same as in my old project, which was AES. There was no need to change it because it is an industry-standard. I believe I used 256?
const generatedKey = randomBytes(32);
const cipher = forge.cipher.createCipher('AES-CBC', forge.util.createBuffer(generatedKey));
cipher.start({ iv });
cipher.update(forge.util.createBuffer("secret"));
const success = cipher.finish();
if (!success)
return err_route(VAULT_ENCRYPTION_FAILED.status,
VAULT_ENCRYPTION_FAILED.msg,
VAULT_ENCRYPTION_FAILED.code);
const encrypted = cipher.output;
const headers = new Headers();
headers.set("Content-Disposition", 'attachment; filename="aes-key.aes"');
headers.set("Content-Type", 'application/octet-stream');
await prisma.vault.create({
data: {
name: name,
maxCredentials: maxCredentials,
secret: Buffer.from(encrypted.getBytes(), 'binary').toString("base64"),
iv: Buffer.from(iv, 'binary').toString("base64")
}
});
prisma.$disconnect();
return new NextResponse(Buffer.from(generatedKey).toString('base64'), { status: 200, headers });
This is the code I used to generate their key. I stored the IV but not the actual secret. As I mentioned before, a secret, after being decrypted, will spell "secret". All secret is a message with the name "secret" encrypted with the key we generated and helps us validate whether or not the given key is valid.
Conclusion
This breakdown was relatively brief. I'm also writing this at 2:32 am, and I have work at 12. I hope you guys enjoyed it! Sorry, if it's not organized, I'm still experimenting with how I should format it.