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