pin

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 19, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

Package pin provides secure PIN management, including hashing, validation, and lockout mechanisms.

Package pin provides PIN management services including creation, verification, change, reset, and security question handling for USSD-based user authentication.

Index

Constants

View Source
const (
	// MaxPINAttempts is the number of consecutive wrong PIN entries allowed
	// before the account is locked.
	MaxPINAttempts = 3

	// LockoutDuration is how long an account stays locked after exceeding
	// MaxPINAttempts.
	LockoutDuration = 15 * time.Minute

	// BcryptCost is the bcrypt work factor used for hashing PINs and
	// security question answers.
	BcryptCost = 10
)

Service constants.

View Source
const PINLength = 4

PINLength is the required number of digits in a valid PIN.

Variables

View Source
var (
	// ErrAccountLocked is returned when a PIN operation is attempted on a
	// locked account.
	ErrAccountLocked = errors.New("account is temporarily locked")

	// ErrPINNotSet is returned when a PIN operation requires an existing PIN
	// but the user has not set one.
	ErrPINNotSet = errors.New("PIN has not been set")

	// ErrPINIncorrect is returned when the supplied PIN does not match the
	// stored hash.
	ErrPINIncorrect = errors.New("incorrect PIN")

	// ErrPINSameAsOld is returned when a new PIN is identical to the current
	// one during a change operation.
	ErrPINSameAsOld = errors.New("new PIN must be different from current PIN")

	// ErrSecurityAnswerMismatch is returned when one or more security
	// question answers do not match.
	ErrSecurityAnswerMismatch = errors.New("security answers do not match")

	// ErrInsufficientQuestions is returned when fewer than the required
	// number of security questions are provided.
	ErrInsufficientQuestions = errors.New("at least 2 security questions are required")
)

Service errors.

View Source
var (
	// ErrPINLength is returned when the PIN is not exactly PINLength digits.
	ErrPINLength = fmt.Errorf("PIN must be exactly %d digits", PINLength)

	// ErrPINNotDigits is returned when the PIN contains non-digit characters.
	ErrPINNotDigits = errors.New("PIN must contain only digits")

	// ErrPINAllSame is returned when every digit in the PIN is identical
	// (e.g. 0000, 1111).
	ErrPINAllSame = errors.New("PIN must not be all the same digit")

	// ErrPINSequential is returned when the PIN is a sequential run of
	// ascending or descending digits (e.g. 1234, 9876).
	ErrPINSequential = errors.New("PIN must not be a sequential number")
)

PIN validation errors.

View Source
var (
	// ErrSecurityQuestionNotFound is returned when no security questions
	// exist for the requested user.
	ErrSecurityQuestionNotFound = errors.New("security questions not found for user")
)

Repository errors.

Functions

func NormalizeAnswer

func NormalizeAnswer(answer string) string

NormalizeAnswer prepares a security question answer for hashing by trimming whitespace and lowercasing. This reduces false negatives from trivial formatting differences (e.g. "Nakuru" vs "nakuru").

func ValidatePIN

func ValidatePIN(pin string) error

ValidatePIN checks that pin satisfies all strength requirements:

  • Exactly PINLength digits
  • Not all the same digit (e.g. 1111)
  • Not a sequential ascending or descending run (e.g. 1234, 4321)

Returns nil if the PIN is acceptable.

Types

type QuestionAnswer

type QuestionAnswer struct {
	QuestionID int
	Answer     string
}

QuestionAnswer pairs a predefined question ID with the user's plaintext answer. The answer is normalized and hashed before storage.

type SecurityQuestionRepository

type SecurityQuestionRepository struct {
	// contains filtered or unexported fields
}

SecurityQuestionRepository provides data access for the security_questions table. It is safe for concurrent use because each method scopes its own GORM session via WithContext.

func NewSecurityQuestionRepository

func NewSecurityQuestionRepository(db *gorm.DB) *SecurityQuestionRepository

NewSecurityQuestionRepository creates a new SecurityQuestionRepository backed by the provided GORM connection. The caller must ensure db is non-nil.

func (*SecurityQuestionRepository) DeleteByUserID

func (r *SecurityQuestionRepository) DeleteByUserID(ctx context.Context, userID string) error

DeleteByUserID removes all security questions for the given user. This is typically called when an account is being deactivated.

func (*SecurityQuestionRepository) GetByUserID

GetByUserID returns all security questions for the given user, ordered by question_id. Returns ErrSecurityQuestionNotFound if no rows exist.

func (*SecurityQuestionRepository) GetQuestionIDsByUserID

func (r *SecurityQuestionRepository) GetQuestionIDsByUserID(ctx context.Context, userID string) ([]int, error)

GetQuestionIDsByUserID returns the question IDs that the user has configured, in ascending order. This is used to prompt the user with their own questions during recovery without exposing answer hashes.

func (*SecurityQuestionRepository) UpsertForUser

func (r *SecurityQuestionRepository) UpsertForUser(ctx context.Context, questions []models.SecurityQuestion) error

UpsertForUser inserts or updates security questions for a user. Each question is upserted on the (user_id, question_id) unique constraint, replacing the answer_hash if the row already exists. The operation runs inside a transaction so that all questions succeed or none do.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service provides PIN management operations including creation, verification, change, reset, and security question handling. It sends SMS notifications as side effects of certain operations via an contracts.AccountNotifier.

All methods that access the database accept a context.Context for cancellation and timeout propagation.

func NewService

func NewService(
	userRepo repository.UserRepository,
	sqRepo *SecurityQuestionRepository,
	notifier contracts.AccountNotifier,
	lockout time.Duration,
) *Service

NewService creates a new PIN management service. If notifier is nil, a notifications.NoOpAccountNotifier is used. If lockout is 0, the default LockoutDuration is applied.

func (*Service) ChangePIN

func (s *Service) ChangePIN(ctx context.Context, userID, oldPin, newPin string) error

ChangePIN verifies the old PIN then sets a new one. The new PIN must differ from the old and pass strength validation. Sends an SMS on success or failure.

func (*Service) GetRemainingAttempts

func (s *Service) GetRemainingAttempts(ctx context.Context, userID string) (int, error)

GetRemainingAttempts returns how many PIN attempts the user has left before lockout. Returns 0 if the account is locked.

func (*Service) GetUserQuestionIDs

func (s *Service) GetUserQuestionIDs(ctx context.Context, userID string) ([]int, error)

GetUserQuestionIDs returns the predefined question IDs that the user has configured for recovery. Returns an empty slice if none are set.

func (*Service) HasPIN

func (s *Service) HasPIN(ctx context.Context, userID string) (bool, error)

HasPIN reports whether the user has a PIN set.

func (*Service) IsLocked

func (s *Service) IsLocked(ctx context.Context, userID string) (bool, time.Time, error)

IsLocked reports whether the user's account is currently locked due to failed PIN attempts. If locked, it returns the time the lockout expires.

func (*Service) ResetPIN

func (s *Service) ResetPIN(ctx context.Context, userID, newPin string) error

ResetPIN sets a new PIN without requiring the old one. This is called after the caller has verified the user's identity via security questions. It clears any lockout state. Sends an SMS on success or failure.

func (*Service) SetPIN

func (s *Service) SetPIN(ctx context.Context, userID, pin string) error

SetPIN creates a PIN for a user who does not yet have one. The PIN is validated for strength, hashed with bcrypt, and stored. Returns a validation error if the PIN is weak.

func (*Service) SetSecurityQuestions

func (s *Service) SetSecurityQuestions(ctx context.Context, userID string, questions []QuestionAnswer) error

SetSecurityQuestions stores hashed answers for the given questions. At least 2 questions are required. Answers are normalized (trimmed, lowercased) before hashing with bcrypt. Existing questions for the user are upserted.

func (*Service) VerifyPIN

func (s *Service) VerifyPIN(ctx context.Context, userID, pin string) (bool, error)

VerifyPIN checks the supplied PIN against the stored hash. On failure it increments the attempt counter and sends an SMS notification. If the maximum attempts are exceeded the account is locked and a lockout notification is sent.

Returns (true, nil) on success, (false, nil) on wrong PIN (with side effects), or (false, error) on system errors or if the account is locked.

func (*Service) VerifySecurityAnswers

func (s *Service) VerifySecurityAnswers(ctx context.Context, userID string, answers []QuestionAnswer) (bool, error)

VerifySecurityAnswers checks the supplied answers against the stored hashes for the given user. All answers must match for the result to be true. Answers are normalized before comparison.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL