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) }