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