Skip to content

Commit

Permalink
Reset a user password using CLI (thomiceli#226)
Browse files Browse the repository at this point in the history
  • Loading branch information
thomiceli committed Apr 2, 2024
1 parent fc9a75c commit 1c1e3a8
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 76 deletions.
7 changes: 7 additions & 0 deletions docs/administration/reset-password.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Reset a user password

To reset a user password, run the following command using the Opengist binary:

```bash
./opengist admin reset-password <username> <new-password>
```
50 changes: 50 additions & 0 deletions internal/cli/admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package cli

import (
"fmt"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/utils"
"github.com/urfave/cli/v2"
)

var CmdAdmin = cli.Command{
Name: "admin",
Usage: "Admin commands",
Subcommands: []*cli.Command{
&CmdAdminResetPassword,
},
}

var CmdAdminResetPassword = cli.Command{
Name: "reset-password",
Usage: "Reset the password for a given user",
ArgsUsage: "[username] [password]",
Action: func(ctx *cli.Context) error {
initialize(ctx)
if ctx.NArg() < 2 {
return fmt.Errorf("username and password are required")
}
username := ctx.Args().Get(0)
plainPassword := ctx.Args().Get(1)

user, err := db.GetUserByUsername(username)
if err != nil {
fmt.Printf("Cannot get user %s: %s\n", username, err)
return err
}
password, err := utils.Argon2id.Hash(plainPassword)
if err != nil {
fmt.Printf("Cannot hash password for user %s: %s\n", username, err)
return err
}
user.Password = password

if err = user.Update(); err != nil {
fmt.Printf("Cannot update password for user %s: %s\n", username, err)
return err
}

fmt.Printf("Password for user %s has been reset.\n", username)
return nil
},
}
2 changes: 1 addition & 1 deletion internal/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func App() error {
app.Usage = "A self-hosted pastebin powered by Git."
app.HelpName = "opengist"

app.Commands = []*cli.Command{&CmdVersion, &CmdStart, &CmdHook}
app.Commands = []*cli.Command{&CmdVersion, &CmdStart, &CmdHook, &CmdAdmin}
app.DefaultCommand = CmdStart.Name
app.Flags = []cli.Flag{
&ConfigFlag,
Expand Down
76 changes: 76 additions & 0 deletions internal/utils/argon2id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package utils

import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"golang.org/x/crypto/argon2"
"strings"
)

type Argon2ID struct {
format string
version int
time uint32
memory uint32
keyLen uint32
saltLen uint32
threads uint8
}

var Argon2id = Argon2ID{
format: "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
version: argon2.Version,
time: 1,
memory: 64 * 1024,
keyLen: 32,
saltLen: 16,
threads: 4,
}

func (a Argon2ID) Hash(plain string) (string, error) {
salt := make([]byte, a.saltLen)
if _, err := rand.Read(salt); err != nil {
return "", err
}

hash := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, a.keyLen)

return fmt.Sprintf(a.format, a.version, a.memory, a.time, a.threads,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(hash),
), nil
}

func (a Argon2ID) Verify(plain, hash string) (bool, error) {
if hash == "" {
return false, nil
}

hashParts := strings.Split(hash, "$")

if len(hashParts) != 6 {
return false, errors.New("invalid hash")
}

_, err := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d", &a.memory, &a.time, &a.threads)
if err != nil {
return false, err
}

salt, err := base64.RawStdEncoding.DecodeString(hashParts[4])
if err != nil {
return false, err
}

decodedHash, err := base64.RawStdEncoding.DecodeString(hashParts[5])
if err != nil {
return false, err
}

hashToCompare := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, uint32(len(decodedHash)))

return subtle.ConstantTimeCompare(decodedHash, hashToCompare) == 1, nil
}
4 changes: 2 additions & 2 deletions internal/web/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func processRegister(ctx echo.Context) error {

user := dto.ToUser()

password, err := argon2id.hash(user.Password)
password, err := utils.Argon2id.Hash(user.Password)
if err != nil {
return errorRes(500, "Cannot hash password", err)
}
Expand Down Expand Up @@ -129,7 +129,7 @@ func processLogin(ctx echo.Context) error {
return redirect(ctx, "/login")
}

if ok, err := argon2id.verify(password, user.Password); !ok {
if ok, err := utils.Argon2id.Verify(password, user.Password); !ok {
if err != nil {
return errorRes(500, "Cannot check for password", err)
}
Expand Down
5 changes: 3 additions & 2 deletions internal/web/git_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"github.com/thomiceli/opengist/internal/utils"
"net/http"
"os"
"os/exec"
Expand Down Expand Up @@ -98,7 +99,7 @@ func gitHttp(ctx echo.Context) error {
return plainText(ctx, 404, "Check your credentials or make sure you have access to the Gist")
}

if ok, err := argon2id.verify(authPassword, gist.User.Password); !ok || gist.User.Username != authUsername {
if ok, err := utils.Argon2id.Verify(authPassword, gist.User.Password); !ok || gist.User.Username != authUsername {
if err != nil {
return errorRes(500, "Cannot verify password", err)
}
Expand All @@ -115,7 +116,7 @@ func gitHttp(ctx echo.Context) error {
return errorRes(401, "Invalid credentials", nil)
}

if ok, err := argon2id.verify(authPassword, user.Password); !ok {
if ok, err := utils.Argon2id.Verify(authPassword, user.Password); !ok {
if err != nil {
return errorRes(500, "Cannot check for password", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/web/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func passwordProcess(ctx echo.Context) error {
return html(ctx, "settings.html")
}

password, err := argon2id.hash(dto.Password)
password, err := utils.Argon2id.Hash(dto.Password)
if err != nil {
return errorRes(500, "Cannot hash password", err)
}
Expand Down
70 changes: 0 additions & 70 deletions internal/web/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,12 @@ package web

import (
"context"
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/i18n"
"golang.org/x/crypto/argon2"
"html/template"
"net/http"
"strconv"
Expand Down Expand Up @@ -219,68 +214,3 @@ func addMetadataToSearchQuery(input, key, value string) string {

return strings.TrimSpace(resultBuilder.String())
}

type Argon2ID struct {
format string
version int
time uint32
memory uint32
keyLen uint32
saltLen uint32
threads uint8
}

var argon2id = Argon2ID{
format: "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
version: argon2.Version,
time: 1,
memory: 64 * 1024,
keyLen: 32,
saltLen: 16,
threads: 4,
}

func (a Argon2ID) hash(plain string) (string, error) {
salt := make([]byte, a.saltLen)
if _, err := rand.Read(salt); err != nil {
return "", err
}

hash := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, a.keyLen)

return fmt.Sprintf(a.format, a.version, a.memory, a.time, a.threads,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(hash),
), nil
}

func (a Argon2ID) verify(plain, hash string) (bool, error) {
if hash == "" {
return false, nil
}

hashParts := strings.Split(hash, "$")

if len(hashParts) != 6 {
return false, errors.New("invalid hash")
}

_, err := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d", &a.memory, &a.time, &a.threads)
if err != nil {
return false, err
}

salt, err := base64.RawStdEncoding.DecodeString(hashParts[4])
if err != nil {
return false, err
}

decodedHash, err := base64.RawStdEncoding.DecodeString(hashParts[5])
if err != nil {
return false, err
}

hashToCompare := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, uint32(len(decodedHash)))

return subtle.ConstantTimeCompare(decodedHash, hashToCompare) == 1, nil
}

0 comments on commit 1c1e3a8

Please sign in to comment.