1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2025-12-18 14:47:03 +00:00
Files
go-chasquid-smtp/internal/userdb/userdb.go
Alberto Bertogli 2caaec3d8b userdb: Use a constant-time byte comparison in PasswordMatches
PasswordMatches calculates the proposed derived key, and then compares
it with the actual derived key. That comparison is done using
bytes.Equal, which is not in constant time.

In theory, users with knowledge of the salt could use timing to extract
information about the actual derived key.

In practice, the salt is not being exposed to users, and the caller of
PasswordMatches will add a delay to password checks, so it should not be
easy to exploit via chasquid.

But just to be safe and more future-proof, this patch changes the
comparison to be in constant time.
2017-04-26 10:26:54 +01:00

217 lines
5.4 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 (
"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"
)
type DB struct {
fname string
db *ProtoDB
// Lock protecting db.
mu sync.RWMutex
}
var (
ErrInvalidUsername = errors.New("invalid username")
)
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)
}
// 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
}
}
// Add a user 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 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
}
// RemoveUser from the database. Returns True if the user was there, False
// otherwise.
func (db *DB) RemoveUser(name string) bool {
db.mu.Lock()
_, present := db.db.Users[name]
delete(db.db.Users, name)
db.mu.Unlock()
return present
}
// HasUser returns true if the user is present, False otherwise.
func (db *DB) HasUser(name string) bool {
db.mu.Lock()
_, present := db.db.Users[name]
db.mu.Unlock()
return present
}
///////////////////////////////////////////////////////////
// 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))
}
// This comparison should be high enough up the stack that it doesn't
// matter, but do it in constant time just in case.
return subtle.ConstantTimeCompare(dk, []byte(s.Encrypted)) == 1
}