Add "ntfy user hash"

This commit is contained in:
binwiederhier
2025-07-26 12:14:21 +02:00
parent 4457e9e26f
commit f99801a2e6
5 changed files with 68 additions and 11 deletions

View File

@@ -981,12 +981,15 @@ func (a *Manager) addUserTx(tx *sql.Tx, username, password string, role Role, ha
if !AllowedUsername(username) || !AllowedRole(role) {
return ErrInvalidArgument
}
var hash []byte
var hash string
var err error = nil
if hashed {
hash = []byte(password)
hash = password
if err := AllowedPasswordHash(hash); err != nil {
return err
}
} else {
hash, err = bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost)
hash, err = a.HashPassword(password)
if err != nil {
return err
}
@@ -1328,12 +1331,15 @@ func (a *Manager) ChangePassword(username, password string, hashed bool) error {
}
func (a *Manager) changePasswordTx(tx *sql.Tx, username, password string, hashed bool) error {
var hash []byte
var hash string
var err error
if hashed {
hash = []byte(password)
hash = password
if err := AllowedPasswordHash(hash); err != nil {
return err
}
} else {
hash, err = bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost)
hash, err = a.HashPassword(password)
if err != nil {
return err
}
@@ -1640,6 +1646,15 @@ func (a *Manager) readTier(rows *sql.Rows) (*Tier, error) {
}, nil
}
// HashPassword hashes the given password using bcrypt with the configured cost
func (a *Manager) HashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost)
if err != nil {
return "", err
}
return string(hash), nil
}
// Close closes the underlying database
func (a *Manager) Close() error {
return a.db.Close()
@@ -1681,7 +1696,7 @@ func (a *Manager) maybeProvisionUsersAndAccess() error {
if err := a.addUserTx(tx, user.Name, user.Hash, user.Role, true, true); err != nil && !errors.Is(err, ErrUserExists) {
return fmt.Errorf("failed to add provisioned user %s: %v", user.Name, err)
}
} else if existingUser.Hash != user.Hash || existingUser.Role != user.Role {
} else if existingUser.Provisioned && (existingUser.Hash != user.Hash || existingUser.Role != user.Role) {
log.Tag(tag).Info("Updating provisioned user %s", user.Name)
if err := a.changePasswordTx(tx, user.Name, user.Hash, true); err != nil {
return fmt.Errorf("failed to change password for provisioned user %s: %v", user.Name, err)

View File

@@ -340,7 +340,7 @@ func TestManager_UserManagement(t *testing.T) {
func TestManager_ChangePassword(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false))
require.Nil(t, a.AddUser("jane", "$2b$10$OyqU72muEy7VMd1SAU2Iru5IbeSMgrtCGHu/fWLmxL1MwlijQXWbG", RoleUser, true))
require.Nil(t, a.AddUser("jane", "$2a$10$OyqU72muEy7VMd1SAU2Iru5IbeSMgrtCGHu/fWLmxL1MwlijQXWbG", RoleUser, true))
_, err := a.Authenticate("phil", "phil")
require.Nil(t, err)
@@ -354,7 +354,7 @@ func TestManager_ChangePassword(t *testing.T) {
_, err = a.Authenticate("phil", "newpass")
require.Nil(t, err)
require.Nil(t, a.ChangePassword("jane", "$2b$10$CNaCW.q1R431urlbQ5Drh.zl48TiiOeJSmZgfcswkZiPbJGQ1ApSS", true))
require.Nil(t, a.ChangePassword("jane", "$2a$10$CNaCW.q1R431urlbQ5Drh.zl48TiiOeJSmZgfcswkZiPbJGQ1ApSS", true))
_, err = a.Authenticate("jane", "jane")
require.Equal(t, ErrUnauthenticated, err)
_, err = a.Authenticate("jane", "newpass")

View File

@@ -274,6 +274,14 @@ func AllowedTier(tier string) bool {
return allowedTierRegex.MatchString(tier)
}
// AllowedPasswordHash checks if the given password hash is a valid bcrypt hash
func AllowedPasswordHash(hash string) error {
if !strings.HasPrefix(hash, "$2a$") && !strings.HasPrefix(hash, "$2b$") && !strings.HasPrefix(hash, "$2y$") {
return ErrPasswordHashInvalid
}
return nil
}
// Error constants used by the package
var (
ErrUnauthenticated = errors.New("unauthenticated")
@@ -281,6 +289,7 @@ var (
ErrInvalidArgument = errors.New("invalid argument")
ErrUserNotFound = errors.New("user not found")
ErrUserExists = errors.New("user already exists")
ErrPasswordHashInvalid = errors.New("password hash but be a bcrypt hash, use 'ntfy user hash' to generate")
ErrTierNotFound = errors.New("tier not found")
ErrTokenNotFound = errors.New("token not found")
ErrPhoneNumberNotFound = errors.New("phone number not found")