From c28f7a4038d8d9c06820d341c2c4eca22031511f Mon Sep 17 00:00:00 2001 From: Morall0 Date: Tue, 7 Oct 2025 19:45:56 -0600 Subject: [PATCH 1/6] Adding password hashing and storing the hash --- security/hash.go | 39 +++++++++++++++++++++++++++++++++++++++ store/sqlite.go | 19 ++++++++++++++++--- store/store.go | 2 +- 3 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 security/hash.go diff --git a/security/hash.go b/security/hash.go new file mode 100644 index 0000000..46eb31a --- /dev/null +++ b/security/hash.go @@ -0,0 +1,39 @@ +package security + +import ( + "crypto/rand" + "golang.org/x/crypto/argon2" +) + +type Params struct { + Memory uint32 + Iterations uint32 + Parallelism uint8 + SaltLength uint32 + KeyLength uint32 +} + +func HashPassword(password string, p *Params) (hash []byte, err error) { + // Generate a cryptographically secure random salt. + salt, err := generateSalt(p.SaltLength) + if err != nil { + return nil, err + } + + // Pass the plaintext password, salt and parameters to the argon2.IDKey + // function. This will generate a hash of the password using the Argon2id + // variant. + hash = argon2.IDKey([]byte(password), salt, p.Iterations, p.Memory, p.Parallelism, p.KeyLength) + + return hash, nil +} + +func generateSalt( n uint32) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return nil, err + } + + return b, nil +} diff --git a/store/sqlite.go b/store/sqlite.go index 0414336..c141195 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -1,8 +1,11 @@ package store import ( - "context" - "database/sql" + "context" + "database/sql" + "log" + + "lidsol.org/papeador/security" ) type SQLiteStore struct{ @@ -23,7 +26,17 @@ func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { return err } - res, err := s.DB.ExecContext(ctx, "INSERT INTO user (username,passhash,email) VALUES (?, ?, ?)", u.Username, u.Passhash, u.Email) + // Initializing params for Argon2 + p := &security.Params{ + Memory: 64 * 1024, + Iterations: 3, + Parallelism: 2, + SaltLength: 16, + KeyLength: 32, + } + + passhash, err := security.HashPassword(u.Password, p) + res, err := s.DB.ExecContext(ctx, "INSERT INTO user (username,passhash,email) VALUES (?, ?, ?)", u.Username, passhash, u.Email) if err != nil { return err } diff --git a/store/store.go b/store/store.go index 85808ac..c571665 100644 --- a/store/store.go +++ b/store/store.go @@ -13,7 +13,7 @@ var ( type User struct { UserID int64 `json:"user_id"` Username string `json:"username"` - Passhash string `json:"passhash"` + Password string `json:"password"` Email string `json:"email"` } From 94bfc548296bc7c7503ccc26bf164080aac0afd2 Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Mon, 13 Oct 2025 19:43:54 -0600 Subject: [PATCH 2/6] store/sqlite.go: Indent with tabs --- manifest.scm | 2 +- store/sqlite.go | 112 ++++++++++++++++++++++++------------------------ 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/manifest.scm b/manifest.scm index c7b1882..d4bb5dc 100644 --- a/manifest.scm +++ b/manifest.scm @@ -1 +1 @@ -(specifications->manifest (list "sqlite" "go")) +(specifications->manifest (list "sqlite" "go" "podman" "pkg-config" "btrfs-progs-static" "gpgme" "go-github-com-containerd-btrfs-v2" "bat")) diff --git a/store/sqlite.go b/store/sqlite.go index c141195..778b943 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -9,79 +9,79 @@ import ( ) type SQLiteStore struct{ - DB *sql.DB + DB *sql.DB } func NewSQLiteStore(db *sql.DB) Store { - return &SQLiteStore{DB: db} + return &SQLiteStore{DB: db} } func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { - // Verify unique username/email - var username string - err := s.DB.QueryRowContext(ctx, "SELECT username FROM user WHERE username=? OR email=?", u.Username, u.Email).Scan(&username) - if err == nil { - return ErrAlreadyExists - } else if err != sql.ErrNoRows { - return err - } + // Verify unique username/email + var username string + err := s.DB.QueryRowContext(ctx, "SELECT username FROM user WHERE username=? OR email=?", u.Username, u.Email).Scan(&username) + if err == nil { + return ErrAlreadyExists + } else if err != sql.ErrNoRows { + return err + } // Initializing params for Argon2 p := &security.Params{ - Memory: 64 * 1024, - Iterations: 3, - Parallelism: 2, - SaltLength: 16, - KeyLength: 32, - } + Memory: 64 * 1024, + Iterations: 3, + Parallelism: 2, + SaltLength: 16, + KeyLength: 32, + } passhash, err := security.HashPassword(u.Password, p) - res, err := s.DB.ExecContext(ctx, "INSERT INTO user (username,passhash,email) VALUES (?, ?, ?)", u.Username, passhash, u.Email) - if err != nil { - return err - } - if id, ierr := res.LastInsertId(); ierr == nil { - u.UserID = id - } - return nil + res, err := s.DB.ExecContext(ctx, "INSERT INTO user (username,passhash,email) VALUES (?, ?, ?)", u.Username, passhash, u.Email) + if err != nil { + return err + } + if id, ierr := res.LastInsertId(); ierr == nil { + u.UserID = id + } + return nil } func (s *SQLiteStore) CreateContest(ctx context.Context, c *Contest) error { - var name string - err := s.DB.QueryRowContext(ctx, "SELECT contest_name FROM contest WHERE contest_name=?", c.ContestName).Scan(&name) - if err == nil { - return ErrAlreadyExists - } else if err != sql.ErrNoRows { - return err - } + var name string + err := s.DB.QueryRowContext(ctx, "SELECT contest_name FROM contest WHERE contest_name=?", c.ContestName).Scan(&name) + if err == nil { + return ErrAlreadyExists + } else if err != sql.ErrNoRows { + return err + } - res, err := s.DB.ExecContext(ctx, "INSERT INTO contest (contest_name) VALUES (?)", c.ContestName) - if err != nil { - return err - } - if id, ierr := res.LastInsertId(); ierr == nil { - c.ContestID = id - } - return nil + res, err := s.DB.ExecContext(ctx, "INSERT INTO contest (contest_name) VALUES (?)", c.ContestName) + if err != nil { + return err + } + if id, ierr := res.LastInsertId(); ierr == nil { + c.ContestID = id + } + return nil } func (s *SQLiteStore) CreateProblem(ctx context.Context, p *Problem) error { - if p.ContestID != nil { - var cid int64 - err := s.DB.QueryRowContext(ctx, "SELECT contest_id FROM contest WHERE contest_id=?", *p.ContestID).Scan(&cid) - if err == sql.ErrNoRows { - return ErrNotFound - } else if err != nil { - return err - } - } + if p.ContestID != nil { + var cid int64 + err := s.DB.QueryRowContext(ctx, "SELECT contest_id FROM contest WHERE contest_id=?", *p.ContestID).Scan(&cid) + if err == sql.ErrNoRows { + return ErrNotFound + } else if err != nil { + return err + } + } - res, err := s.DB.ExecContext(ctx, "INSERT INTO problem (contest_id, creator_id, problem_name, description) VALUES (?, ?, ?, ?)", p.ContestID, p.CreatorID, p.ProblemName, p.Description) - if err != nil { - return err - } - if id, ierr := res.LastInsertId(); ierr == nil { - p.ProblemID = id - } - return nil + res, err := s.DB.ExecContext(ctx, "INSERT INTO problem (contest_id, creator_id, problem_name, description) VALUES (?, ?, ?, ?)", p.ContestID, p.CreatorID, p.ProblemName, p.Description) + if err != nil { + return err + } + if id, ierr := res.LastInsertId(); ierr == nil { + p.ProblemID = id + } + return nil } From fef5a2ad3f669c894de22746a5762fd5c572cbcd Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Tue, 14 Oct 2025 19:04:38 -0600 Subject: [PATCH 3/6] Add JWT (WIP) --- api/contest.go | 24 ++++++++++++++++++++++-- go.mod | 10 +++++++--- go.sum | 8 ++++++-- store/sqlite.go | 10 +++++++++- store/store.go | 37 +++++++++++++++++++------------------ 5 files changed, 63 insertions(+), 26 deletions(-) diff --git a/api/contest.go b/api/contest.go index c7f1a3c..3eb8229 100644 --- a/api/contest.go +++ b/api/contest.go @@ -5,17 +5,37 @@ import ( "log" "net/http" + "lidsol.org/papeador/security" "lidsol.org/papeador/store" ) +type ContestRequestContent struct { + store.Contest + Username string `json:"username"` + JWT string `json:"jwt"` +} + func (api *ApiContext) createContest(w http.ResponseWriter, r *http.Request) { - var in store.Contest + var in ContestRequestContent if err := json.NewDecoder(r.Body).Decode(&in); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - if err := api.Store.CreateContest(r.Context(), &in); err != nil { + ok, err := security.ValidateJWT(in.JWT, in.Username) + if err != nil { + log.Println("WHATAADF") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if !ok { + http.Error(w, "Token inválida", http.StatusUnauthorized) + return + } + + contData := store.Contest{ContestID: in.ContestID, ContestName: in.ContestName} + + if err := api.Store.CreateContest(r.Context(), &contData); err != nil { if err == store.ErrAlreadyExists { http.Error(w, "El nombre del contest ya esta registrado", http.StatusConflict) log.Println("El nombre del contest ya esta registrado") diff --git a/go.mod b/go.mod index 0c1c08a..1881bd8 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,23 @@ module lidsol.org/papeador -go 1.23.0 +go 1.24.0 toolchain go1.24.3 -require modernc.org/sqlite v1.38.2 +require ( + golang.org/x/crypto v0.43.0 + modernc.org/sqlite v1.38.2 +) require ( github.com/dustin/go-humanize v1.0.1 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/sys v0.34.0 // indirect + golang.org/x/sys v0.37.0 // indirect modernc.org/libc v1.66.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index aac187a..2a8dac3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -10,6 +12,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= @@ -17,8 +21,8 @@ golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= diff --git a/store/sqlite.go b/store/sqlite.go index 778b943..36fd429 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -3,7 +3,7 @@ package store import ( "context" "database/sql" - "log" + // "log" "lidsol.org/papeador/security" ) @@ -37,11 +37,19 @@ func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { passhash, err := security.HashPassword(u.Password, p) res, err := s.DB.ExecContext(ctx, "INSERT INTO user (username,passhash,email) VALUES (?, ?, ?)", u.Username, passhash, u.Email) + + if err != nil { + return err + } + + token, err := security.GenerateJWT(username) if err != nil { return err } + if id, ierr := res.LastInsertId(); ierr == nil { u.UserID = id + u.JWT = token } return nil } diff --git a/store/store.go b/store/store.go index c571665..2b85223 100644 --- a/store/store.go +++ b/store/store.go @@ -1,38 +1,39 @@ package store import ( - "context" - "errors" + "context" + "errors" ) var ( - ErrAlreadyExists = errors.New("already exists") - ErrNotFound = errors.New("not found") + ErrAlreadyExists = errors.New("already exists") + ErrNotFound = errors.New("not found") ) type User struct { - UserID int64 `json:"user_id"` - Username string `json:"username"` - Password string `json:"password"` - Email string `json:"email"` + UserID int64 `json:"user_id"` + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email"` + JWT string `json:"jwt"` } type Contest struct { - ContestID int64 `json:"contest_id"` - ContestName string `json:"contest_name"` + ContestID int64 `json:"contest_id"` + ContestName string `json:"contest_name"` } type Problem struct { - ProblemID int64 `json:"problem_id"` - ContestID *int64 `json:"contest_id"` - CreatorID int64 `json:"creator_id"` - ProblemName string `json:"problem_name"` - Description string `json:"description"` + ProblemID int64 `json:"problem_id"` + ContestID *int64 `json:"contest_id"` + CreatorID int64 `json:"creator_id"` + ProblemName string `json:"problem_name"` + Description string `json:"description"` } // Store defines persistence operations used by the HTTP layer. type Store interface { - CreateUser(ctx context.Context, u *User) error - CreateContest(ctx context.Context, c *Contest) error - CreateProblem(ctx context.Context, p *Problem) error + CreateUser(ctx context.Context, u *User) error + CreateContest(ctx context.Context, c *Contest) error + CreateProblem(ctx context.Context, p *Problem) error } From 37bbe9a6761956436b6274916b2c17438bbdc605 Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Mon, 3 Nov 2025 20:11:45 -0600 Subject: [PATCH 4/6] Using jwt library --- api/contest.go | 3 +++ api/user.go | 4 +++- go.mod | 3 ++- go.sum | 2 ++ store/sqlite.go | 8 ++++---- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/api/contest.go b/api/contest.go index 3eb8229..c93c045 100644 --- a/api/contest.go +++ b/api/contest.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "fmt" "log" "net/http" @@ -22,6 +23,8 @@ func (api *ApiContext) createContest(w http.ResponseWriter, r *http.Request) { return } + fmt.Printf("E: %v\n", in) + fmt.Printf("E: %v\n", in.Username) ok, err := security.ValidateJWT(in.JWT, in.Username) if err != nil { log.Println("WHATAADF") diff --git a/api/user.go b/api/user.go index 22a0bd3..a3803ea 100644 --- a/api/user.go +++ b/api/user.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "fmt" "log" "net/http" @@ -16,6 +17,7 @@ func (api *ApiContext) createUser(w http.ResponseWriter, r *http.Request) { return } + fmt.Printf("E: %v\n", in) if err := api.Store.CreateUser(r.Context(), &in); err != nil { if err == store.ErrAlreadyExists { http.Error(w, "El usuario ya está registrado", http.StatusConflict) @@ -29,4 +31,4 @@ func (api *ApiContext) createUser(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(in) -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index 1881bd8..c6a5ebd 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,14 @@ go 1.24.0 toolchain go1.24.3 require ( + github.com/golang-jwt/jwt/v5 v5.3.0 golang.org/x/crypto v0.43.0 modernc.org/sqlite v1.38.2 ) require ( + github.com/cristalhq/jwt/v5 v5.4.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect diff --git a/go.sum b/go.sum index 2a8dac3..ffecb9c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/cristalhq/jwt/v5 v5.4.0 h1:Wxi1TocFHaijyV608j7v7B9mPc4ZNjvWT3LKBO0d4QI= +github.com/cristalhq/jwt/v5 v5.4.0/go.mod h1:+b/BzaCWEpFDmXxspJ5h4SdJ1N/45KMjKOetWzmHvDA= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= diff --git a/store/sqlite.go b/store/sqlite.go index 36fd429..8481d58 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -17,9 +17,9 @@ func NewSQLiteStore(db *sql.DB) Store { } func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { - // Verify unique username/email - var username string - err := s.DB.QueryRowContext(ctx, "SELECT username FROM user WHERE username=? OR email=?", u.Username, u.Email).Scan(&username) + // Verify unique duplicateUsername/email + var duplicateUsername string + err := s.DB.QueryRowContext(ctx, "SELECT username FROM user WHERE username=? OR email=?", u.Username, u.Email).Scan(&duplicateUsername) if err == nil { return ErrAlreadyExists } else if err != sql.ErrNoRows { @@ -42,7 +42,7 @@ func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { return err } - token, err := security.GenerateJWT(username) + token, err := security.GenerateJWT(u.Username) if err != nil { return err } From 464b7fef63c15b1dd682048a09bab51711e46c6f Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Sat, 22 Nov 2025 23:20:21 -0600 Subject: [PATCH 5/6] JWT module --- security/jwt.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 security/jwt.go diff --git a/security/jwt.go b/security/jwt.go new file mode 100644 index 0000000..d8ad556 --- /dev/null +++ b/security/jwt.go @@ -0,0 +1,66 @@ +package security + +import ( + "encoding/json" + "os" + "strings" + "time" + + "github.com/cristalhq/jwt/v5" +) + +var secretKey []byte = []byte(os.Getenv("JWT_KEY")) + +func GenerateJWT(username string) (string, error) { + + signer, err := jwt.NewSignerHS(jwt.HS256, secretKey) + if err != nil { + return "", err + } + + claims := &jwt.RegisteredClaims{ + Audience: []string{"admin"}, + ID: "asdf", + Subject: strings.Clone(username), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * 100)), + } + + builder := jwt.NewBuilder(signer) + + token, err := builder.Build(claims) + if err != nil { + return "", err + } + + return token.String(), nil +} + +func ValidateJWT(tokenStr, username string) (bool, error) { + verifier, err := jwt.NewVerifierHS(jwt.HS256, secretKey) + if err != nil { + return false, err + } + + tokenBytes := []byte(tokenStr) + newToken, err := jwt.Parse(tokenBytes, verifier) + if err != nil { + return false, err + } + + err = verifier.Verify(newToken) + if err != nil { + return false, err + } + + var newClaims jwt.RegisteredClaims + errClaims := json.Unmarshal(newToken.Claims(), &newClaims) + if errClaims != nil { + return false, err + } + + if newClaims.IsSubject(username) { + return true, nil + } + + return false, nil +} From e83f924f948ebbb07cb661f122608229c132e0ec Mon Sep 17 00:00:00 2001 From: Morall0 Date: Sun, 23 Nov 2025 17:43:29 -0600 Subject: [PATCH 6/6] security functions to validate username, password and email --- api/user.go | 13 ++++++++++ security/ensure.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++ store/sqlite.go | 27 +++++++++++++++++++-- 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 security/ensure.go diff --git a/api/user.go b/api/user.go index a3803ea..6ec4fbe 100644 --- a/api/user.go +++ b/api/user.go @@ -6,6 +6,7 @@ import ( "log" "net/http" + "lidsol.org/papeador/security" "lidsol.org/papeador/store" ) @@ -23,6 +24,18 @@ func (api *ApiContext) createUser(w http.ResponseWriter, r *http.Request) { http.Error(w, "El usuario ya está registrado", http.StatusConflict) log.Println("El usuario ya está registrado") return + } else if err == security.ErrInvalidUsername { + http.Error(w, "El nombre de usuario solo puede contener caracteres, número y guiones", http.StatusUnprocessableEntity) + log.Println("La nombre de usuario que se intentó registrar es inválido") + return + } else if err == security.ErrInvalidPassword { + http.Error(w, "La contraseña debe contener al menos de 12 a 64 caracteres, una mayúscula, una minúscula, un número y un caracter especial, sin espacios", http.StatusUnprocessableEntity) + log.Println("La contraseña que se intentó registrar es insegura") + return + } else if err == security.ErrInvalidEmail { + http.Error(w, "El correo que se intenta registrar es inválido", http.StatusUnprocessableEntity) + log.Println("El correo que se intentó registrar es inválido") + return } http.Error(w, err.Error(), http.StatusInternalServerError) log.Println(err) diff --git a/security/ensure.go b/security/ensure.go new file mode 100644 index 0000000..ee434f1 --- /dev/null +++ b/security/ensure.go @@ -0,0 +1,59 @@ +package security + +import ( + "net/mail" + "regexp" + "strings" + "errors" +) + +var ( + lower = regexp.MustCompile(`[a-z]`) + upper = regexp.MustCompile(`[A-Z]`) + digit = regexp.MustCompile(`\d`) + special = regexp.MustCompile(`[!@#\$%\^&\*\(\)\-\_\=\+\[\]\{\};:'",.<>\/\?\\\|` + "`~]") + space = regexp.MustCompile(`\s`) + uname = regexp.MustCompile(`^[A-Za-z0-9_-]{3,20}$`) + + ErrInvalidUsername = errors.New("invalid username") + ErrInvalidPassword = errors.New("invalid password") + ErrInvalidEmail = errors.New("invalid email") +) + +func IsValidUsername(username string) error { + + if !uname.MatchString(username) { + return ErrInvalidUsername + } + + return nil +} + +func IsValidPassword(password string) error { + valid_length := len(password) >= 12 && len(password) <= 64 + + if !valid_length { + return ErrInvalidPassword + } + + if !(lower.MatchString(password) && + upper.MatchString(password) && + digit.MatchString(password) && + special.MatchString(password) && + !space.MatchString(password)) { + return ErrInvalidPassword + } + + return nil +} + +func ValidateEmail(email string) (string, error) { + email = strings.TrimSpace(email) + _, err := mail.ParseAddress(email) + + if err != nil { + return "", ErrInvalidEmail + } + + return strings.ToLower(email), nil +} diff --git a/store/sqlite.go b/store/sqlite.go index 8481d58..225ee58 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -17,7 +17,24 @@ func NewSQLiteStore(db *sql.DB) Store { } func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { - // Verify unique duplicateUsername/email + // Check for valid username + if err:=security.IsValidUsername(u.Username); err != nil { + return err + } + + // Check for a valid email + if email, err := security.ValidateEmail(u.Email); err == nil { + u.Email = email // to_lower and trimmed + } else { + return err + } + + // Check for a secure password + if err:=security.IsValidPassword(u.Password); err != nil { + return err + } + + // Verify duplicated Username/email var duplicateUsername string err := s.DB.QueryRowContext(ctx, "SELECT username FROM user WHERE username=? OR email=?", u.Username, u.Email).Scan(&duplicateUsername) if err == nil { @@ -35,7 +52,13 @@ func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { KeyLength: 32, } - passhash, err := security.HashPassword(u.Password, p) + // Password hashing + passhash, err := security.HashPassword(u.Password, p); + if err != nil { + return err + } + + // Inserting user res, err := s.DB.ExecContext(ctx, "INSERT INTO user (username,passhash,email) VALUES (?, ?, ?)", u.Username, passhash, u.Email) if err != nil {