1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2025-12-17 14:37:02 +00:00
Files
go-chasquid-smtp/internal/userdb/userdb.go
Alberto Bertogli 394067bbd3 userdb: Use protocol buffers instead of our custom format
Protocol buffers are a more portable, practical and safe format for the user
database.
2016-10-10 00:51:03 +01:00

220 lines
5.5 KiB
Go

// Package userdb implements a simple user database.
//
//
// Format
//
// The user database is a file containing a list of users and their passwords,
// encrypted with some scheme.
// We use a text-encoded protobuf, the structure can be found in userdb.proto.
//
// We write text instead of binary to make it easier for administrators to
// troubleshoot, and since performance is not an issue for our expected usage.
//
// Users must be UTF-8 and NOT contain whitespace; the library will enforce
// this.
//
//
// Schemes
//
// The default scheme is SCRYPT, with hard-coded parameters. The API does not
// allow the user to change this, at least for now.
// A PLAIN scheme is also supported for debugging purposes.
//
//
// Writing
//
// The functions that write a database file will not preserve ordering,
// invalid lines, empty lines, or any formatting.
//
// It is also not safe for concurrent use from different processes.
//
package userdb
//go:generate protoc --go_out=. userdb.proto
import (
"bytes"
"crypto/rand"
"errors"
"fmt"
"strings"
"sync"
"unicode/utf8"
"golang.org/x/crypto/scrypt"
"blitiri.com.ar/go/chasquid/internal/protoio"
)
type DB struct {
fname string
db *ProtoDB
// Lock protecting db.
mu sync.RWMutex
}
var (
ErrInvalidUsername = errors.New("username contains invalid characters")
)
func New(fname string) *DB {
return &DB{
fname: fname,
db: &ProtoDB{Users: map[string]*Password{}},
}
}
// Load the database from the given file.
// Return the database, and a fatal error if the database could not be
// loaded.
func Load(fname string) (*DB, error) {
db := New(fname)
err := protoio.ReadTextMessage(fname, db.db)
// Reading may result in an empty protobuf or dictionary; make sure we
// return an empty but usable structure.
// This simplifies many of our uses, as we can assume the map is not nil.
if db.db == nil || db.db.Users == nil {
db.db = &ProtoDB{Users: map[string]*Password{}}
}
return db, err
}
// Reload the database, refreshing its contents from the current file on disk.
// If there are errors reading from the file, they are returned and the
// database is not changed.
func (db *DB) Reload() error {
newdb, err := Load(db.fname)
if err != nil {
return err
}
db.mu.Lock()
db.db = newdb.db
db.mu.Unlock()
return nil
}
// Write the database to disk. It will do a complete rewrite each time, and is
// not safe to call it from different processes in parallel.
func (db *DB) Write() error {
db.mu.RLock()
defer db.mu.RUnlock()
return protoio.WriteTextMessage(db.fname, db.db, 0660)
}
// Does this user exist in the database?
func (db *DB) Exists(user string) bool {
db.mu.RLock()
_, ok := db.db.Users[user]
db.mu.RUnlock()
return ok
}
// Is this password valid for the user?
func (db *DB) Authenticate(name, plainPassword string) bool {
db.mu.RLock()
passwd, ok := db.db.Users[name]
db.mu.RUnlock()
if !ok {
return false
}
return passwd.PasswordMatches(plainPassword)
}
func (p *Password) PasswordMatches(plain string) bool {
switch s := p.Scheme.(type) {
case nil:
return false
case *Password_Scrypt:
return s.Scrypt.PasswordMatches(plain)
case *Password_Plain:
return s.Plain.PasswordMatches(plain)
default:
return false
}
}
// Check if the given user name is valid.
// User names have to be UTF-8, and must not have some particular characters,
// including whitespace.
func ValidUsername(name string) bool {
return utf8.ValidString(name) &&
!strings.ContainsAny(name, illegalUsernameChars)
}
// Illegal characters. Only whitespace for now, to prevent/minimize the
// chances of parsing issues.
// TODO: do we want to stop other characters, specifically about email? Or
// keep this generic and handle the mail-specific filtering in chasquid?
const illegalUsernameChars = "\t\n\v\f\r \xa0\x85"
// Add a user to the database. If the user is already present, override it.
func (db *DB) AddUser(name, plainPassword string) error {
if !ValidUsername(name) {
return ErrInvalidUsername
}
s := &Scrypt{
// Use hard-coded standard parameters for now.
// Follow the recommendations from the scrypt paper.
LogN: 14, R: 8, P: 1, KeyLen: 32,
// 16 bytes of salt (will be filled later).
Salt: make([]byte, 16),
}
n, err := rand.Read(s.Salt)
if n != 16 || err != nil {
return fmt.Errorf("failed to get salt - %d - %v", n, err)
}
s.Encrypted, err = scrypt.Key([]byte(plainPassword), s.Salt,
1<<s.LogN, int(s.R), int(s.P), int(s.KeyLen))
if err != nil {
return fmt.Errorf("scrypt failed: %v", err)
}
db.mu.Lock()
db.db.Users[name] = &Password{
Scheme: &Password_Scrypt{s},
}
db.mu.Unlock()
return nil
}
///////////////////////////////////////////////////////////
// Encryption schemes
//
// Plain text scheme. Useful mostly for testing and debugging.
// TODO: Do we really need this? Removing it would make accidents less likely
// to happen. Consider doing so when we add another scheme, so we a least have
// two and multi-scheme support does not bit-rot.
func (p *Plain) PasswordMatches(plain string) bool {
return plain == string(p.Password)
}
// scrypt scheme, which we use by default.
func (s *Scrypt) PasswordMatches(plain string) bool {
dk, err := scrypt.Key([]byte(plain), s.Salt,
1<<s.LogN, int(s.R), int(s.P), int(s.KeyLen))
if err != nil {
// The encryption failed, this is due to the parameters being invalid.
// We validated them before, so something went really wrong.
// TODO: do we want to return false instead?
panic(fmt.Sprintf("scrypt failed: %v", err))
}
return bytes.Equal(dk, []byte(s.Encrypted))
}