diff --git a/db.go b/db.go index 5b67654c02d1320d1a3db1f292ab09a53e9fa66d..de2c9d40e7e21d8eed3e659dd16cf3db85ffee76 100644 --- a/db.go +++ b/db.go @@ -1,6 +1,7 @@ package main import ( + "crypto/rand" "embed" "fmt" "io/fs" @@ -15,7 +16,7 @@ var migrationFS embed.FS func initDB(db *sqlx.DB) error { // Create table tracking migration state - const query = `CREATE TABLE IF NOT EXISTS migrations (name TEXT UNIQUE);` + query := `CREATE TABLE IF NOT EXISTS migrations (name TEXT UNIQUE);` _, err := db.Exec(query) if err != nil { @@ -88,5 +89,18 @@ func initDB(db *sqlx.DB) error { log.Println("it has", len(data), "bytes") } + // Initialize persistent state like password salt + buf := make([]byte, 16) + _, err = rand.Read(buf) + if err != nil { + return err + } + + query = `INSERT OR IGNORE INTO state VALUES ('pwsalt', ?)` + _, err = db.Exec(query, buf) + if err != nil { + return err + } + return nil } diff --git a/main.go b/main.go index 9e4f47f38570b38c59de6c1fe830740cb78a07df..ea6c73b0d82723f47861c3a7f42e846118e61c40 100644 --- a/main.go +++ b/main.go @@ -2,13 +2,12 @@ package main import ( "context" - "database/sql" + "crypto/sha256" "embed" - "errors" + "fmt" "log" "net/http" - "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" _ "modernc.org/sqlite" // Imported for side effects: registers DB driver @@ -35,31 +34,59 @@ func httpError(w http.ResponseWriter, msg string, err error, status int) { http.Error(w, msg, status) } +func hashPassword(ctx context.Context, db *sqlx.DB, pass string) (string, error) { + // Look up password salt from DB and hash password with it + query := `SELECT val FROM state WHERE key = 'pwsalt'` + + var salt []byte + err := db.GetContext(ctx, &salt, query) + if err != nil { + return "", err + } + + h := sha256.New() + + _, err = h.Write(salt) + if err != nil { + return "", err + } + + fmt.Fprint(h, pass) + + return fmt.Sprintf("%02x", h.Sum(nil)), nil +} + type authProvider struct { db *sqlx.DB } func (a authProvider) Valid(ctx context.Context, user, pass string) (bool, error) { - query, args, err := squirrel.Select("password"). - From("users"). - Where(squirrel.Eq{"name": user}). - ToSql() + query := `SELECT count(*) FROM users;` + + var count int + err := a.db.GetContext(ctx, &count, query) if err != nil { return false, err } - var dbPass string - err = a.db.GetContext(ctx, &dbPass, query, args...) - if errors.Is(err, sql.ErrNoRows) { - // User not found isn't an error, it's just an invalid auth. - return false, nil + // If no entry in user DB, just allow everything, so that initial user creation works. + if count == 0 { + log.Println("user database empty, allowing access by", user, "regardless of password") + return true, nil } + hashedPw, err := hashPassword(ctx, a.db, pass) + + log.Printf("hashed pw for %s: %s", user, hashedPw) + + query = `SELECT count(*) FROM users WHERE name = ? AND password = ?` + + err = a.db.GetContext(ctx, &count, query, user, hashedPw) if err != nil { return false, err } - if dbPass == pass { + if count == 1 { return true, nil } diff --git a/migrations/0004-state.sql b/migrations/0004-state.sql new file mode 100644 index 0000000000000000000000000000000000000000..a80f7b0219b48315e33ebf3f2e1b54272ee0a0bf --- /dev/null +++ b/migrations/0004-state.sql @@ -0,0 +1,5 @@ +CREATE TABLE state ( + key TEXT, + val TEXT, + UNIQUE(key) +); \ No newline at end of file