// 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=. --go_opt=paths=source_relative userdb.proto import ( "crypto/rand" "crypto/subtle" "errors" "fmt" "sync" "golang.org/x/crypto/scrypt" "blitiri.com.ar/go/chasquid/internal/normalize" "blitiri.com.ar/go/chasquid/internal/protoio" ) // DB represents a single user database. type DB struct { fname string db *ProtoDB // Lock protecting db. mu sync.RWMutex } // New returns a new user database, on the given file name. 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) } // Authenticate returns true if the password is valid for the user, false // otherwise. 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) } // PasswordMatches returns true if the given password is a match. 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 } } // AddUser to the database. If the user is already present, override it. // Note we enforce that the name has been normalized previously. func (db *DB) AddUser(name, plainPassword string) error { if norm, err := normalize.User(name); err != nil || name != norm { return errors.New("invalid username") } 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<