Blob


1 package mailmux
3 import (
4 "database/sql"
5 "errors"
6 "fmt"
7 "net/mail"
8 "os"
9 "path"
11 _ "github.com/mattn/go-sqlite3"
12 "golang.org/x/crypto/bcrypt"
13 )
15 // UserDB is an implementation of UserStore backed by a SQLite3 database.
16 // Users are fully authenticated after confirming ownership of their
17 // email address by supplying a matching ticket to a generated ticket file.
18 type UserDB struct {
19 *sql.DB
20 TicketDir string
21 }
23 // CreateUserDB creates the named user database file and ticket directory.
24 // If the database file already exists, it is truncated. If successfullly created,
25 // the database's tables are initialised so the database is ready to use.
26 func CreateUserDB(name, ticketDir string) (*UserDB, error) {
27 _, err := os.Create(name)
28 if err != nil {
29 return nil, err
30 }
31 if err := os.MkdirAll(ticketDir, 0666); err != nil {
32 return nil, fmt.Errorf("create user db: %w", err)
33 }
34 db, err := OpenUserDB(name, ticketDir)
35 if err != nil {
36 return nil, fmt.Errorf("create user db: %w", err)
37 }
38 return db, db.initialise()
39 }
41 // OpenUserDB opens the named user database file and ticket directory.
42 func OpenUserDB(name, dir string) (*UserDB, error) {
43 db, err := sql.Open("sqlite3", name)
44 if err != nil {
45 return nil, err
46 }
47 return &UserDB{db, dir}, db.Ping()
48 }
50 func (db *UserDB) initialise() error {
51 stmt := `CREATE TABLE IF NOT EXISTS users (
52 username TEXT PRIMARY KEY,
53 password BLOB NOT NULL
54 );`
55 _, err := db.Exec(stmt)
56 return err
57 }
59 func (db *UserDB) Lookup(name string) (User, error) {
60 var u User
61 row := db.QueryRow("SELECT username, password FROM users WHERE username = ?", name)
62 if err := row.Scan(&u.name, &u.password); err != nil {
63 if errors.Is(err, sql.ErrNoRows) {
64 return User{}, fmt.Errorf("lookup %s: %w", name, ErrUnknownUser)
65 }
66 return User{}, fmt.Errorf("lookup %s: %w", name, err)
67 }
68 return u, nil
69 }
71 func (db *UserDB) add(username string, pw Password) error {
72 addr, err := mail.ParseAddress(username)
73 if err != nil {
74 return fmt.Errorf("add %s: invalid username: %w", username, err)
75 }
76 if addr.Name != "" {
77 return fmt.Errorf("add %s: proper name present", username)
78 }
80 hashed, err := bcrypt.GenerateFromPassword(pw, bcrypt.DefaultCost)
81 if err != nil {
82 return fmt.Errorf("add %s: %w", username, err)
83 }
84 _, err = db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", username, hashed)
85 if err != nil {
86 return fmt.Errorf("add %s: %w", username, err)
87 }
88 if err := createTicket(db.TicketDir, username, pw); err != nil {
89 return fmt.Errorf("add %s: create ticket: %w", username, err)
90 }
91 return nil
92 }
94 func (db *UserDB) Change(name string, new Password) error {
95 _, err := db.Lookup(name)
96 if errors.Is(err, ErrUnknownUser) {
97 return db.add(name, new)
98 }
99 if err != nil {
100 return fmt.Errorf("change %s: %w", name, err)
103 hashed, err := bcrypt.GenerateFromPassword(new, bcrypt.DefaultCost)
104 if err != nil {
105 return fmt.Errorf("change %s: %w", name, err)
107 _, err = db.Exec("UPDATE users SET password = ? WHERE username = ?", hashed, name)
108 if err != nil {
109 return fmt.Errorf("change %s: %w", name, err)
111 return nil
114 func (db *UserDB) Delete(name string) error {
115 _, err := db.Lookup(name)
116 if err != nil {
117 return fmt.Errorf("delete %s: %w", name, err)
119 _, err = db.Exec("DELETE FROM users WHERE username = ?", name)
120 if err != nil {
121 return fmt.Errorf("delete %s: %w", name, err)
123 return nil
126 func (db *UserDB) Authenticate(name string, pw Password) error {
127 u, err := db.Lookup(name)
128 if err != nil {
129 return fmt.Errorf("authenticate %s: %w", name, err)
131 if err := bcrypt.CompareHashAndPassword(u.password, pw); err != nil {
132 return fmt.Errorf("authenticate %s: %w", name, err)
134 return nil
137 // TODO tickets aren't implemented yet
138 func createTicket(dir, username string, pw Password) error {
139 f, err := os.Create(path.Join(dir, username))
140 if err != nil {
141 return err
143 defer f.Close()
145 // TODO hash username, password, random bytes
146 // _, err := f.Write(b)
147 if _, err := f.Write(testTicket); err != nil {
148 return err
150 return nil