Blame


1 5e70998a 2022-04-12 o package mailmux
2 5e70998a 2022-04-12 o
3 5e70998a 2022-04-12 o import (
4 5e70998a 2022-04-12 o "database/sql"
5 5e70998a 2022-04-12 o "errors"
6 5e70998a 2022-04-12 o "fmt"
7 7583dd64 2022-04-13 o "net/mail"
8 8d82f4f7 2022-04-12 o "os"
9 8d82f4f7 2022-04-12 o "path"
10 5e70998a 2022-04-12 o
11 5e70998a 2022-04-12 o _ "github.com/mattn/go-sqlite3"
12 7583dd64 2022-04-13 o "golang.org/x/crypto/bcrypt"
13 5e70998a 2022-04-12 o )
14 5e70998a 2022-04-12 o
15 acca18c6 2022-04-23 o // maxUsernameSize defines a reasonable maximum length for an email address.
16 acca18c6 2022-04-23 o const maxUsernameLength = 255
17 acca18c6 2022-04-23 o
18 5e70998a 2022-04-12 o // UserDB is an implementation of UserStore backed by a SQLite3 database.
19 5e70998a 2022-04-12 o // Users are fully authenticated after confirming ownership of their
20 5e70998a 2022-04-12 o // email address by supplying a matching ticket to a generated ticket file.
21 5e70998a 2022-04-12 o type UserDB struct {
22 5e70998a 2022-04-12 o *sql.DB
23 5e70998a 2022-04-12 o TicketDir string
24 5e70998a 2022-04-12 o }
25 5e70998a 2022-04-12 o
26 7583dd64 2022-04-13 o // CreateUserDB creates the named user database file and ticket directory.
27 7583dd64 2022-04-13 o // If the database file already exists, it is truncated. If successfullly created,
28 7583dd64 2022-04-13 o // the database's tables are initialised so the database is ready to use.
29 7583dd64 2022-04-13 o func CreateUserDB(name, ticketDir string) (*UserDB, error) {
30 7583dd64 2022-04-13 o _, err := os.Create(name)
31 7583dd64 2022-04-13 o if err != nil {
32 7583dd64 2022-04-13 o return nil, err
33 7583dd64 2022-04-13 o }
34 7583dd64 2022-04-13 o if err := os.MkdirAll(ticketDir, 0666); err != nil {
35 7583dd64 2022-04-13 o return nil, fmt.Errorf("create user db: %w", err)
36 7583dd64 2022-04-13 o }
37 7583dd64 2022-04-13 o db, err := OpenUserDB(name, ticketDir)
38 7583dd64 2022-04-13 o if err != nil {
39 7583dd64 2022-04-13 o return nil, fmt.Errorf("create user db: %w", err)
40 7583dd64 2022-04-13 o }
41 be20893f 2022-04-25 o return db, initialiseUserDB(db.DB)
42 7583dd64 2022-04-13 o }
43 7583dd64 2022-04-13 o
44 5e70998a 2022-04-12 o // OpenUserDB opens the named user database file and ticket directory.
45 a039d5d3 2022-05-12 o // If the database does not exist, it is created and its tables are
46 be20893f 2022-04-25 o // initialised ready for use.
47 5e70998a 2022-04-12 o func OpenUserDB(name, dir string) (*UserDB, error) {
48 5e70998a 2022-04-12 o db, err := sql.Open("sqlite3", name)
49 5e70998a 2022-04-12 o if err != nil {
50 5e70998a 2022-04-12 o return nil, err
51 5e70998a 2022-04-12 o }
52 be20893f 2022-04-25 o if err := initialiseUserDB(db); err != nil {
53 925168e0 2022-04-14 o return nil, err
54 925168e0 2022-04-14 o }
55 5e70998a 2022-04-12 o return &UserDB{db, dir}, db.Ping()
56 5e70998a 2022-04-12 o }
57 5e70998a 2022-04-12 o
58 be20893f 2022-04-25 o func initialiseUserDB(db *sql.DB) error {
59 7583dd64 2022-04-13 o stmt := `CREATE TABLE IF NOT EXISTS users (
60 7583dd64 2022-04-13 o username TEXT PRIMARY KEY,
61 f7343271 2022-04-25 o password BLOB NOT NULL,
62 e2ab3f7c 2022-04-25 o modtime INTEGER NOT NULL DEFAULT (unixepoch())
63 7583dd64 2022-04-13 o );`
64 7583dd64 2022-04-13 o _, err := db.Exec(stmt)
65 7583dd64 2022-04-13 o return err
66 7583dd64 2022-04-13 o }
67 7583dd64 2022-04-13 o
68 5e70998a 2022-04-12 o func (db *UserDB) Lookup(name string) (User, error) {
69 5e70998a 2022-04-12 o var u User
70 5e70998a 2022-04-12 o row := db.QueryRow("SELECT username, password FROM users WHERE username = ?", name)
71 5f060a33 2022-07-04 o var passwd string
72 5f060a33 2022-07-04 o if err := row.Scan(&u.name, &passwd); err != nil {
73 5e70998a 2022-04-12 o if errors.Is(err, sql.ErrNoRows) {
74 71bdede3 2022-04-21 o return User{}, ErrUnknownUser
75 5e70998a 2022-04-12 o }
76 71bdede3 2022-04-21 o return User{}, err
77 5e70998a 2022-04-12 o }
78 5f060a33 2022-07-04 o u.password = Password(passwd)
79 5e70998a 2022-04-12 o return u, nil
80 5e70998a 2022-04-12 o }
81 5e70998a 2022-04-12 o
82 5e70998a 2022-04-12 o func (db *UserDB) add(username string, pw Password) error {
83 acca18c6 2022-04-23 o if len(username) > maxUsernameLength {
84 a039d5d3 2022-05-12 o return fmt.Errorf("add %s: bad username: too long", username)
85 acca18c6 2022-04-23 o }
86 7583dd64 2022-04-13 o addr, err := mail.ParseAddress(username)
87 5e70998a 2022-04-12 o if err != nil {
88 a039d5d3 2022-05-12 o return fmt.Errorf("add %s: bad username: %w", username, err)
89 7583dd64 2022-04-13 o }
90 7583dd64 2022-04-13 o if addr.Name != "" {
91 a039d5d3 2022-05-12 o return fmt.Errorf("add %s: bad username: proper name present", username)
92 7583dd64 2022-04-13 o }
93 7583dd64 2022-04-13 o
94 7583dd64 2022-04-13 o hashed, err := bcrypt.GenerateFromPassword(pw, bcrypt.DefaultCost)
95 7583dd64 2022-04-13 o if err != nil {
96 5e70998a 2022-04-12 o return fmt.Errorf("add %s: %w", username, err)
97 5e70998a 2022-04-12 o }
98 e2ab3f7c 2022-04-25 o _, err = db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", username, hashed)
99 7583dd64 2022-04-13 o if err != nil {
100 7583dd64 2022-04-13 o return fmt.Errorf("add %s: %w", username, err)
101 7583dd64 2022-04-13 o }
102 5e70998a 2022-04-12 o if err := createTicket(db.TicketDir, username, pw); err != nil {
103 5e70998a 2022-04-12 o return fmt.Errorf("add %s: create ticket: %w", username, err)
104 5e70998a 2022-04-12 o }
105 5e70998a 2022-04-12 o return nil
106 5e70998a 2022-04-12 o }
107 5e70998a 2022-04-12 o
108 5e70998a 2022-04-12 o func (db *UserDB) Change(name string, new Password) error {
109 5e70998a 2022-04-12 o _, err := db.Lookup(name)
110 5e70998a 2022-04-12 o if errors.Is(err, ErrUnknownUser) {
111 5e70998a 2022-04-12 o return db.add(name, new)
112 5e70998a 2022-04-12 o }
113 5e70998a 2022-04-12 o if err != nil {
114 71bdede3 2022-04-21 o return fmt.Errorf("lookup %s: %w", name, err)
115 5e70998a 2022-04-12 o }
116 5e70998a 2022-04-12 o
117 7583dd64 2022-04-13 o hashed, err := bcrypt.GenerateFromPassword(new, bcrypt.DefaultCost)
118 5e70998a 2022-04-12 o if err != nil {
119 71bdede3 2022-04-21 o return fmt.Errorf("generate hash: %w", err)
120 5e70998a 2022-04-12 o }
121 e2ab3f7c 2022-04-25 o q := "UPDATE users SET password = ?, modtime = unixepoch() WHERE username = ?"
122 e2ab3f7c 2022-04-25 o _, err = db.Exec(q, hashed, name)
123 71bdede3 2022-04-21 o return err
124 5e70998a 2022-04-12 o }
125 5e70998a 2022-04-12 o
126 5e70998a 2022-04-12 o func (db *UserDB) Delete(name string) error {
127 5e70998a 2022-04-12 o _, err := db.Lookup(name)
128 5e70998a 2022-04-12 o if err != nil {
129 5e70998a 2022-04-12 o return fmt.Errorf("delete %s: %w", name, err)
130 5e70998a 2022-04-12 o }
131 5e70998a 2022-04-12 o _, err = db.Exec("DELETE FROM users WHERE username = ?", name)
132 5e70998a 2022-04-12 o if err != nil {
133 5e70998a 2022-04-12 o return fmt.Errorf("delete %s: %w", name, err)
134 5e70998a 2022-04-12 o }
135 5e70998a 2022-04-12 o return nil
136 5e70998a 2022-04-12 o }
137 8d82f4f7 2022-04-12 o
138 7583dd64 2022-04-13 o func (db *UserDB) Authenticate(name string, pw Password) error {
139 7583dd64 2022-04-13 o u, err := db.Lookup(name)
140 7583dd64 2022-04-13 o if err != nil {
141 7583dd64 2022-04-13 o return fmt.Errorf("authenticate %s: %w", name, err)
142 7583dd64 2022-04-13 o }
143 7583dd64 2022-04-13 o if err := bcrypt.CompareHashAndPassword(u.password, pw); err != nil {
144 7583dd64 2022-04-13 o return fmt.Errorf("authenticate %s: %w", name, err)
145 7583dd64 2022-04-13 o }
146 7583dd64 2022-04-13 o return nil
147 7583dd64 2022-04-13 o }
148 7583dd64 2022-04-13 o
149 8d82f4f7 2022-04-12 o // TODO tickets aren't implemented yet
150 8d82f4f7 2022-04-12 o func createTicket(dir, username string, pw Password) error {
151 8d82f4f7 2022-04-12 o f, err := os.Create(path.Join(dir, username))
152 8d82f4f7 2022-04-12 o if err != nil {
153 8d82f4f7 2022-04-12 o return err
154 8d82f4f7 2022-04-12 o }
155 8d82f4f7 2022-04-12 o defer f.Close()
156 8d82f4f7 2022-04-12 o
157 8d82f4f7 2022-04-12 o // TODO hash username, password, random bytes
158 8d82f4f7 2022-04-12 o // _, err := f.Write(b)
159 8d82f4f7 2022-04-12 o if _, err := f.Write(testTicket); err != nil {
160 8d82f4f7 2022-04-12 o return err
161 8d82f4f7 2022-04-12 o }
162 8d82f4f7 2022-04-12 o return nil
163 8d82f4f7 2022-04-12 o }