Posted Nov 26, 2024 at 5:43 PM
Edited Dec 17, 2024 at 8:37 PM
A couple of months ago I had the idea to create an offline password manager client that still allowed you to sync passwords between devices provided they were on the same local network as a server component. I wanted this because I want the benefit of not having my passwords available on the open internet for anyone competent enough to hack into the LastPass servers (which is apparently a good number of people), but the convenience of being able to create a new account on one device and still have the password saved on all my other devices.
I’ve been working on this idea on and off since then, and I’ve named the project QPass. Right now I think the standalone client component is almost finished. I thought it would be fun to document my progress and how everything has come together so far.
Go doesn’t yet have the best GUI support. There are many frameworks to use, but they’re all a bit half-baked in some way or another (for now, they are all still in active development). I tried several frameworks from go-gtk, to qt, to Fyne and none of them felt particularly ergonomic or intuitive to use to me. That’s when I found Gio UI. It’s still a bit half-baked (more on that later), but it has cross-platform support and the fact that is uses the immediate mode paradigm made it much more intuitive for me to use.
Ironically, the first thing I figured out after choosing a GUI framework had absolutely nothing to do with the GUI. It was to do with storing the user and password data.
First of all, I had to choose some kind of database. I settled on SQLite as the obvious choice. It’s light, fast, and is easy for me to work with since I’m already used to using SQL all the time in my career as a web developer.
Then, I had to choose an encryption algorithm for all the password data. I settled on XChaCha20-Poly1305 because people far more qualified than me have deemed it most appropriate for this kind of task.
Luckily for me, Go already has a package for XChaCha20-Poly1305. Unluckily for me, it’s a bit more manual than just calling some Encrypt
function with the master password as the key and being off to the races. I had to roll my own wrapper around it in order to easily use it in the rest of the application. Here’s what the code for that for that look like:
import (
"crypto/rand"
"encoding/base64"
"golang.org/x/crypto/argon2"
chacha "golang.org/x/crypto/chacha20poly1305"
)
// key is the master password hash, keep reading to see how that's generated
func Encrypt(s string, key []byte) (string, error) {
aead, err := chacha.NewX(key)
if err != nil {
return "", err
}
sbytes := []byte(s)
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(sbytes)+aead.Overhead())
if _, err := rand.Read(nonce); err != nil {
return "", err
}
encrypted := aead.Seal(nonce, nonce, sbytes, nil)
encStr := base64.RawStdEncoding.EncodeToString(encrypted)
return encStr, nil
}
func Decrypt(s string, key []byte) (string, error) {
b, err := base64.RawStdEncoding.DecodeString(s)
if err != nil {
return "", err
}
aead, err := chacha.NewX(key)
if err != nil {
return "", err
}
nonce, enc := b[:aead.NonceSize()], b[aead.NonceSize():]
text, err := aead.Open(nil, nonce, enc, nil)
if err != nil {
return "", err
}
return string(text), nil
}
// Converts the user's master password and a salt into an encryption key to be used with
// the above functions
func GetKey(password, salt string) []byte {
key := argon2.IDKey([]byte(password), []byte(salt), 3, 64 * 1024, 2, 32)
return key
}
func GenSalt(n uint32) (string, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.RawStdEncoding.EncodeToString(b), nil
}
Figuring out how to do that was not super easy. There’s no real resources online that I could find on specifically using this package with a user-generated password as the key in a secure way. I eventually figured out that for this purpose it’s best to run the user’s master password through a key derivation function and then use that as the encryption key for XChaCha20. After a little more research, I landed on Argon2 as the most appropriate for this task, located the appropriate Go package for it, and implemented the solution you see above. From what I can tell I’m among the first to document the solution for this exact problem in Go on the internet, so if you like me are a non-cybersecurity expert trying to do something similar: you’re welcome. I just saved you a good couple hours of work.
For those of you who aren’t familiar with how login information is usually stored, allow me to explain:
Normally, if you were making some service that allowed users to log in – like a website, for example – you would hash the password using some secure one-way function, store the hash, and throw away the plaintext password. Then, when the user tries to login, you would hash the password they typed in and compare it against what you have in the database. If it matches the stored hash, then the user can be logged in. This is perfectly reasonable and secure for the vast majority of applications.
In my case, however, the hash of the master password is being used to encrypt all the other password data, and it’s all being stored in the same SQLite database, which is just a single file on the user’s computer. If I store the master password the way I would store any other password, then all it would take for ALL the passwords to be compromised is someone gaining access to that one file because they would then have the encrypted password data and the key to decrypting it. Obviously that’s not gonna fly.
So if I can’t store the master password, how can I handle user authentication? Well, what I ended up doing was encrypting the user’s username the same way I encrypt all their passwords, then I store the encrypted username and the salt used to hash the master password in the database. When a user tries to log in, I iterate through all the users in the database. For each of them, I take the password that was typed into the login form, hash it with the stored salt, then try to use that hash to decrypt the username. If it works and the decrypted username matches the username they typed in, then we know this is the correct user and so we authenticate them. If we make it through the whole users table without hitting a match, then we display the ol’ “Username or Password is incorrect” and we genuinely don’t know which one is wrong.
This solution works well for the number of users that I expect to be on any given instance of the client application (which I imagine will rarely be more than one), though admittedly I don’t think it’s very scalable. I would not recommend using this approach if you expect to have millions of users, but for my uses it’s perfectly serviceable.
I don’t want to go over all of the minutia of building out the UI, but I did want to expand on my “half-baked” remark from Part 1.
Right now, this is what the main screen of QPass looks like:
You may notice that the passwords are arranged in a table-like fashion. You may also notice that the columns of said table are slightly offset from the headers. That’s due to the single biggest annoyance I’ve had with Gio. The lack of a good table layout. There’s options in development, sure, but both the experimental table nor the underlying grid had the same problem: I simply couldn’t click the buttons on the right side of the row when using them. It just didn’t work. I have no idea why. I spent hours trying to figure out why. As a workaround, I had to use a list layout consisting of flexed rows to keep the columns spaced equally. This causes the content of the table to not line up perfectly with the headers and is the source of much annoyance for me personally.
All said though, if that’s the biggest issue I have with Gio then I can probably live with it. The rest of the process of building out the GUI has been a joy, despite the verbose code.
That’s all I have for now. Next up I’m going to be designing a protocol for the client and server to use to sync password data and implementing that. I also plan to add a few more features to the client such as better clipboard support, the ability to edit/delete passwords, and the ability to change your master password. Expect another write-up when that’s all done.
If you want to check out the code, make contributions, or try QPass for yourself, you can find the repository on GitHub here. Thanks for reading! I hope to have another update out soon.
Bonus Bail