users.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: Apache-2.0
  4
  5package users
  6
  7import (
  8	"crypto/rand"
  9	"database/sql"
 10	"encoding/base64"
 11	"time"
 12
 13	"git.sr.ht/~amolith/willow/db"
 14	"golang.org/x/crypto/argon2"
 15)
 16
 17const (
 18	argon2Time    = 2
 19	saltLength    = 16
 20	argon2KeyLen  = 64
 21	argon2Memory  = 64 * 1024
 22	argon2Threads = 4
 23)
 24
 25// argonHash accepts two strings for the user's password and a random salt,
 26// hashes the password using the salt, and returns the hash as a base64-encoded
 27// string.
 28func argonHash(password, salt string) (string, error) {
 29	decodedSalt, err := base64.StdEncoding.DecodeString(salt)
 30	if err != nil {
 31		return "", err
 32	}
 33
 34	return base64.StdEncoding.EncodeToString(argon2.IDKey([]byte(password), decodedSalt, argon2Time, argon2Memory, argon2Threads, argon2KeyLen)), nil
 35}
 36
 37// generateSalt generates a random salt and returns it as a base64-encoded
 38// string.
 39func generateSalt() (string, error) {
 40	salt := make([]byte, saltLength)
 41
 42	_, err := rand.Read(salt)
 43	if err != nil {
 44		return "", err
 45	}
 46
 47	return base64.StdEncoding.EncodeToString(salt), nil
 48}
 49
 50// Register accepts a username and password, hashes the password and stores the
 51// hash and salt in the database.
 52func Register(dbConn *sql.DB, username, password string) error {
 53	salt, err := generateSalt()
 54	if err != nil {
 55		return err
 56	}
 57
 58	hash, err := argonHash(password, salt)
 59	if err != nil {
 60		return err
 61	}
 62
 63	return db.CreateUser(dbConn, username, hash, salt)
 64}
 65
 66// Delete removes a user from the database.
 67func Delete(dbConn *sql.DB, username string) error { return db.DeleteUser(dbConn, username) }
 68
 69// UserAuthorised accepts a username string, a token string, and returns true if the
 70// user is authorised, false if not, and an error if one is encountered.
 71func UserAuthorised(dbConn *sql.DB, username, token string) (bool, error) {
 72	dbHash, dbSalt, err := db.GetUser(dbConn, username)
 73	if err != nil {
 74		return false, err
 75	}
 76
 77	providedHash, err := argonHash(token, dbSalt)
 78	if err != nil {
 79		return false, err
 80	}
 81
 82	return dbHash == providedHash, nil
 83}
 84
 85// SessionAuthorised accepts a session string and returns true if the session is
 86// valid and false if not.
 87func SessionAuthorised(dbConn *sql.DB, session string) (bool, error) {
 88	dbResult, expiry, err := db.GetSession(dbConn, session)
 89	if dbResult == "" || expiry.Before(time.Now()) || err != nil {
 90		return false, err
 91	}
 92
 93	return true, nil
 94}
 95
 96// InvalidateSession invalidates a session by setting the expiration date to now.
 97func InvalidateSession(dbConn *sql.DB, session string) error {
 98	return db.InvalidateSession(dbConn, session, time.Now())
 99}
100
101// CreateSession accepts a username, generates a token, stores it in the
102// database, and returns it.
103func CreateSession(dbConn *sql.DB, username string) (string, time.Time, error) {
104	token, err := generateSalt()
105	if err != nil {
106		return "", time.Time{}, err
107	}
108
109	expiry := time.Now().Add(7 * 24 * time.Hour)
110
111	err = db.CreateSession(dbConn, username, token, expiry)
112	if err != nil {
113		return "", time.Time{}, err
114	}
115
116	return token, expiry, nil
117}
118
119// GetUsers returns a list of all users in the database as a slice of strings.
120func GetUsers(dbConn *sql.DB) ([]string, error) { return db.GetUsers(dbConn) }