diff --git a/README b/README index cf93c07..96d28bd 100644 --- a/README +++ b/README @@ -1,5 +1,8 @@ ``` go get github.com/mattn/go-sqlite3 +go get golang.org/x/crypto/bcrypt +go mod download golang.org/x/term +go mod tidy go run keygen.go nano server.go diff --git a/client.go b/client.go index 075689a..f17c1d0 100644 --- a/client.go +++ b/client.go @@ -7,12 +7,16 @@ import ( "fmt" "net" "time" + "golang.org/x/term" + "syscall" ) type Request struct { + Username string + Password string Operation string Site string - Password string + VaultData string Timestamp int64 Nonce string } @@ -41,16 +45,28 @@ func main() { } defer conn.Close() - var op, site, pwd string + var username, op, site, vaultData string + fmt.Print("Username: ") + fmt.Scanln(&username) + + fmt.Print("Password: ") + bytePassword, err := term.ReadPassword(int(syscall.Stdin)) + fmt.Println() + if err != nil { + panic(err) + } + password := string(bytePassword) - fmt.Print("Operation (store/get): ") + fmt.Print("Operation (register/store/get): ") fmt.Scanln(&op) - fmt.Print("Site: ") - fmt.Scanln(&site) + if op == "store" || op == "get" { + fmt.Print("Site: ") + fmt.Scanln(&site) + } if op == "store" { - fmt.Print("Password: ") - fmt.Scanln(&pwd) + fmt.Print("Password to store: ") + fmt.Scanln(&vaultData) } nonce, err := generateNonce() @@ -59,9 +75,11 @@ func main() { } req := Request{ + Username: username, + Password: password, Operation: op, Site: site, - Password: pwd, + VaultData: vaultData, Timestamp: time.Now().Unix(), Nonce: nonce, } diff --git a/go.mod b/go.mod index 1efa97c..0c2de43 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,10 @@ module cliVault go 1.24.3 -require github.com/mattn/go-sqlite3 v1.14.28 // indirect +require ( + github.com/mattn/go-sqlite3 v1.14.28 + golang.org/x/crypto v0.39.0 + golang.org/x/term v0.32.0 +) + +require golang.org/x/sys v0.33.0 // indirect diff --git a/go.sum b/go.sum index 42e5bac..e7717c7 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,8 @@ github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= diff --git a/server.go b/server.go index 4b28bd2..b74f475 100644 --- a/server.go +++ b/server.go @@ -1,6 +1,7 @@ package main import ( + "golang.org/x/crypto/bcrypt" "crypto/aes" "crypto/cipher" "database/sql" @@ -13,6 +14,7 @@ import ( _ "github.com/mattn/go-sqlite3" ) +const AllowRegistration = true // Disable after users are added! var key = []byte("2f5680a7fb57ce83e8e83dbb1114ee31") // 32 bytes var nonceStore = struct { @@ -23,9 +25,11 @@ var nonceStore = struct { const nonceExpiry = 30 * time.Second type Request struct { + Username string + Password string Operation string Site string - Password string + VaultData string Timestamp int64 // Unix time (seconds) Nonce string // Random client-supplied nonce } @@ -77,6 +81,15 @@ func isValidNonce(nonce string, timestamp int64) bool { return true } +func hashPassword(pw string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(pw), 12) + return string(bytes), err +} + +func checkPassword(hash, pw string) error { + return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)) +} + func encrypt(text string) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { @@ -130,14 +143,41 @@ func handleConnection(conn net.Conn, db *sql.DB) { return } + if req.Operation == "register" { + if AllowRegistration == false { + enc.Encode(Response{"Registration Disabled!"}) + return + } + hashed, err := hashPassword(req.Password) + if err != nil { + enc.Encode(Response{"Failed to hash password"}) + return + } + _, err = db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", req.Username, hashed) + if err != nil { + enc.Encode(Response{"Registration failed (user exists?)"}) + } else { + enc.Encode(Response{"Registration successful"}) + } + return + } + // Authenticate all other operations + var storedHash string + err := db.QueryRow("SELECT password FROM users WHERE username = ?", req.Username).Scan(&storedHash) + if err != nil || checkPassword(storedHash, req.Password) != nil { + enc.Encode(Response{"Authentication failed"}) + return + } + switch req.Operation { case "store": - encrypted, err := encrypt(req.Password) + encrypted, err := encrypt(req.VaultData) if err != nil { enc.Encode(Response{"Encryption failed"}) return } - _, err = db.Exec("INSERT OR REPLACE INTO accounts (site, password) VALUES (?, ?)", req.Site, encrypted) + _, err = db.Exec("INSERT OR REPLACE INTO accounts (user, site, password) VALUES (?, ?, ?)", + req.Username, req.Site, encrypted) if err != nil { enc.Encode(Response{"Database error"}) return @@ -146,7 +186,7 @@ func handleConnection(conn net.Conn, db *sql.DB) { case "get": var encrypted []byte - err := db.QueryRow("SELECT password FROM accounts WHERE site = ?", req.Site).Scan(&encrypted) + err := db.QueryRow("SELECT password FROM accounts WHERE user = ? AND site = ?", req.Username, req.Site).Scan(&encrypted) if err == sql.ErrNoRows { enc.Encode(Response{"Site not found"}) return @@ -174,10 +214,20 @@ func main() { } defer db.Close() - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS accounts ( - site TEXT PRIMARY KEY, - password BLOB - )`) + _, err = db.Exec(` +CREATE TABLE IF NOT EXISTS users ( + username TEXT PRIMARY KEY, + password TEXT +); + +CREATE TABLE IF NOT EXISTS accounts ( + user TEXT, + site TEXT, + password BLOB, + PRIMARY KEY(user, site), + FOREIGN KEY(user) REFERENCES users(username) +) +`) if err != nil { panic(err) }