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