1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2025-12-17 14:37:02 +00:00

userdb: Use protocol buffers instead of our custom format

Protocol buffers are a more portable, practical and safe format for the user
database.
This commit is contained in:
Alberto Bertogli
2016-09-18 01:12:41 +01:00
parent 5c6fb934fe
commit 394067bbd3
12 changed files with 384 additions and 316 deletions

View File

@@ -136,13 +136,10 @@ func loadDomain(s *Server, name, dir string) {
if _, err := os.Stat(dir + "/users"); err == nil { if _, err := os.Stat(dir + "/users"); err == nil {
glog.Infof(" adding users") glog.Infof(" adding users")
udb, warnings, err := userdb.Load(dir + "/users") udb, err := userdb.Load(dir + "/users")
if err != nil { if err != nil {
glog.Errorf(" error: %v", err) glog.Errorf(" error: %v", err)
} else { } else {
for _, w := range warnings {
glog.Warningf(" %v", w)
}
s.AddUserDB(name, udb) s.AddUserDB(name, udb)
// TODO: periodically reload the database. // TODO: periodically reload the database.
} }

View File

@@ -29,23 +29,19 @@ func main() {
os.Exit(1) os.Exit(1)
} }
db, ws, err := userdb.Load(*dbFname) db, err := userdb.Load(*dbFname)
if err != nil { if err != nil {
if *adduser != "" && os.IsNotExist(err) {
fmt.Printf("creating database\n")
} else {
fmt.Printf("error loading database: %v\n", err) fmt.Printf("error loading database: %v\n", err)
os.Exit(1) os.Exit(1)
} }
for _, w := range ws {
fmt.Printf("warning: %v\n", w)
} }
if *adduser == "" { if *adduser == "" {
fmt.Printf("database loaded\n") fmt.Printf("database loaded\n")
if len(ws) == 0 { return
os.Exit(0)
} else {
os.Exit(1)
}
} }
if *password == "" { if *password == "" {

View File

@@ -3,25 +3,15 @@
// //
// Format // Format
// //
// The user database is a single text file, with one line per user. // The user database is a file containing a list of users and their passwords,
// All contents must be UTF-8. // encrypted with some scheme.
// We use a text-encoded protobuf, the structure can be found in userdb.proto.
// //
// For extensibility, the first line MUST be: // 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.
// //
// #chasquid-userdb-v1
//
// Then, each line is structured as follows:
//
// user SP scheme SP password
//
// Where user is the username in question (usually without the domain,
// although this package is agnostic to it); scheme is the encryption scheme
// used for the password; and finally the password, encrypted with the
// referenced scheme and base64-encoded.
//
// Lines with parsing errors, including unknown schemes, will be ignored.
// Users must be UTF-8 and NOT contain whitespace; the library will enforce // Users must be UTF-8 and NOT contain whitespace; the library will enforce
// this as well. // this.
// //
// //
// Schemes // Schemes
@@ -40,178 +30,87 @@
// //
package userdb package userdb
//go:generate protoc --go_out=. userdb.proto
import ( import (
"bufio"
"bytes" "bytes"
"crypto/rand" "crypto/rand"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"os"
"strings" "strings"
"sync" "sync"
"unicode/utf8" "unicode/utf8"
"golang.org/x/crypto/scrypt" "golang.org/x/crypto/scrypt"
"blitiri.com.ar/go/chasquid/internal/safeio" "blitiri.com.ar/go/chasquid/internal/protoio"
) )
type user struct {
name string
scheme scheme
password string
}
type DB struct { type DB struct {
fname string fname string
finfo os.FileInfo db *ProtoDB
// Map of username -> user structure // Lock protecting db.
users map[string]user
// Lock protecting the users map.
mu sync.RWMutex mu sync.RWMutex
} }
var ( var (
ErrMissingHeader = errors.New("missing '#chasquid-userdb-v1' header")
ErrInvalidUsername = errors.New("username contains invalid characters") ErrInvalidUsername = errors.New("username contains invalid characters")
) )
func New(fname string) *DB { func New(fname string) *DB {
return &DB{ return &DB{
fname: fname, fname: fname,
users: map[string]user{}, db: &ProtoDB{Users: map[string]*Password{}},
} }
} }
// Load the database from the given file. // Load the database from the given file.
// Return the database, a list of warnings (if any), and a fatal error if the // Return the database, and a fatal error if the database could not be
// database could not be loaded. // loaded.
func Load(fname string) (*DB, []error, error) { func Load(fname string) (*DB, error) {
f, err := os.Open(fname) db := New(fname)
if err != nil { err := protoio.ReadTextMessage(fname, db.db)
return nil, nil, err
// 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{}}
} }
db := &DB{ return db, err
fname: fname,
users: map[string]user{},
}
db.finfo, err = f.Stat()
if err != nil {
return nil, nil, err
}
// Special case: an empty file is a valid, empty database.
// This simplifies clients.
if db.finfo.Size() == 0 {
return db, nil, nil
}
scanner := bufio.NewScanner(f)
scanner.Scan()
if scanner.Text() != "#chasquid-userdb-v1" {
return nil, nil, ErrMissingHeader
}
var warnings []error
// Now the users, one per line. Skip invalid ones.
for i := 2; scanner.Scan(); i++ {
var name, schemeStr, b64passwd string
n, err := fmt.Sscanf(scanner.Text(), "%s %s %s",
&name, &schemeStr, &b64passwd)
if err != nil || n != 3 {
warnings = append(warnings, fmt.Errorf(
"line %d: error parsing - %d elements - %v", i, n, err))
break
}
if !ValidUsername(name) {
warnings = append(warnings, fmt.Errorf(
"line %d: invalid username", i))
continue
}
password, err := base64.StdEncoding.DecodeString(b64passwd)
if err != nil {
warnings = append(warnings, fmt.Errorf(
"line %d: error decoding password: %v", i, err))
continue
}
sc, err := schemeFromString(schemeStr)
if err != nil {
warnings = append(warnings, fmt.Errorf(
"line %d: error in scheme: %v", i, err))
continue
}
u := user{
name: name,
scheme: sc,
password: string(password),
}
db.users[name] = u
}
if err := scanner.Err(); err != nil {
return nil, warnings, err
}
return db, warnings, nil
} }
// Reload the database, refreshing its contents from the current file on disk. // 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 // If there are errors reading from the file, they are returned and the
// database is not changed. Warnings are returned regardless. // database is not changed.
func (db *DB) Reload() ([]error, error) { func (db *DB) Reload() error {
newdb, warnings, err := Load(db.fname) newdb, err := Load(db.fname)
if err != nil { if err != nil {
return warnings, err return err
} }
db.mu.Lock() db.mu.Lock()
db.users = newdb.users db.db = newdb.db
db.finfo = newdb.finfo
db.mu.Unlock() db.mu.Unlock()
return warnings, nil return nil
} }
// Write the database to disk. It will do a complete rewrite each time, and is // 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. // not safe to call it from different processes in parallel.
func (db *DB) Write() error { func (db *DB) Write() error {
buf := new(bytes.Buffer)
buf.WriteString("#chasquid-userdb-v1\n")
db.mu.RLock() db.mu.RLock()
defer db.mu.RUnlock() defer db.mu.RUnlock()
// TODO: Sort the usernames, just to be friendlier. return protoio.WriteTextMessage(db.fname, db.db, 0660)
for _, user := range db.users {
if strings.ContainsAny(user.name, illegalUsernameChars) {
return ErrInvalidUsername
}
fmt.Fprintf(buf, "%s %s %s\n",
user.name, user.scheme.String(),
base64.StdEncoding.EncodeToString([]byte(user.password)))
}
mode := os.FileMode(0660)
if db.finfo != nil {
mode = db.finfo.Mode()
}
return safeio.WriteFile(db.fname, buf.Bytes(), mode)
} }
// Does this user exist in the database? // Does this user exist in the database?
func (db *DB) Exists(user string) bool { func (db *DB) Exists(user string) bool {
db.mu.RLock() db.mu.RLock()
_, ok := db.users[user] _, ok := db.db.Users[user]
db.mu.RUnlock() db.mu.RUnlock()
return ok return ok
@@ -220,14 +119,27 @@ func (db *DB) Exists(user string) bool {
// Is this password valid for the user? // Is this password valid for the user?
func (db *DB) Authenticate(name, plainPassword string) bool { func (db *DB) Authenticate(name, plainPassword string) bool {
db.mu.RLock() db.mu.RLock()
u, ok := db.users[name] passwd, ok := db.db.Users[name]
db.mu.RUnlock() db.mu.RUnlock()
if !ok { if !ok {
return false return false
} }
return u.scheme.PasswordMatches(plainPassword, u.password) 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. // Check if the given user name is valid.
@@ -250,31 +162,29 @@ func (db *DB) AddUser(name, plainPassword string) error {
return ErrInvalidUsername return ErrInvalidUsername
} }
s := scryptScheme{ s := &Scrypt{
// Use hard-coded standard parameters for now. // Use hard-coded standard parameters for now.
// Follow the recommendations from the scrypt paper. // Follow the recommendations from the scrypt paper.
logN: 14, r: 8, p: 1, keyLen: 32, LogN: 14, R: 8, P: 1, KeyLen: 32,
// 16 bytes of salt (will be filled later). // 16 bytes of salt (will be filled later).
salt: make([]byte, 16), Salt: make([]byte, 16),
} }
n, err := rand.Read(s.salt) n, err := rand.Read(s.Salt)
if n != 16 || err != nil { if n != 16 || err != nil {
return fmt.Errorf("failed to get salt - %d - %v", n, err) return fmt.Errorf("failed to get salt - %d - %v", n, err)
} }
encrypted, err := scrypt.Key([]byte(plainPassword), s.salt, s.Encrypted, err = scrypt.Key([]byte(plainPassword), s.Salt,
1<<s.logN, s.r, s.p, s.keyLen) 1<<s.LogN, int(s.R), int(s.P), int(s.KeyLen))
if err != nil { if err != nil {
return fmt.Errorf("scrypt failed: %v", err) return fmt.Errorf("scrypt failed: %v", err)
} }
db.mu.Lock() db.mu.Lock()
db.users[name] = user{ db.db.Users[name] = &Password{
name: name, Scheme: &Password_Scrypt{s},
scheme: s,
password: string(encrypted),
} }
db.mu.Unlock() db.mu.Unlock()
@@ -285,46 +195,18 @@ func (db *DB) AddUser(name, plainPassword string) error {
// Encryption schemes // Encryption schemes
// //
type scheme interface {
String() string
PasswordMatches(plain, encrypted string) bool
}
// Plain text scheme. Useful mostly for testing and debugging. // Plain text scheme. Useful mostly for testing and debugging.
// TODO: Do we really need this? Removing it would make accidents less likely // 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 // to happen. Consider doing so when we add another scheme, so we a least have
// two and multi-scheme support does not bit-rot. // two and multi-scheme support does not bit-rot.
type plainScheme struct{} func (p *Plain) PasswordMatches(plain string) bool {
return plain == string(p.Password)
func (s plainScheme) String() string {
return "PLAIN"
}
func (s plainScheme) PasswordMatches(plain, encrypted string) bool {
return plain == encrypted
} }
// scrypt scheme, which we use by default. // scrypt scheme, which we use by default.
type scryptScheme struct { func (s *Scrypt) PasswordMatches(plain string) bool {
logN uint // 1<<logN requires this to be uint dk, err := scrypt.Key([]byte(plain), s.Salt,
r, p int 1<<s.LogN, int(s.R), int(s.P), int(s.KeyLen))
keyLen int
salt []byte
}
func (s scryptScheme) String() string {
// We're encoding the salt in base64, which uses "/+=", and the URL
// variant uses "-_=". We use standard encoding, but shouldn't use any of
// those as separators, just to be safe.
// It's important that the salt be last, as we can only scan
// space-delimited strings.
return fmt.Sprintf("SCRYPT@n:%d,r:%d,p:%d,l:%d,%s",
s.logN, s.r, s.p, s.keyLen,
base64.StdEncoding.EncodeToString(s.salt))
}
func (s scryptScheme) PasswordMatches(plain, encrypted string) bool {
dk, err := scrypt.Key([]byte(plain), s.salt, 1<<s.logN, s.r, s.p, s.keyLen)
if err != nil { if err != nil {
// The encryption failed, this is due to the parameters being invalid. // The encryption failed, this is due to the parameters being invalid.
@@ -333,32 +215,5 @@ func (s scryptScheme) PasswordMatches(plain, encrypted string) bool {
panic(fmt.Sprintf("scrypt failed: %v", err)) panic(fmt.Sprintf("scrypt failed: %v", err))
} }
return bytes.Equal(dk, []byte(encrypted)) return bytes.Equal(dk, []byte(s.Encrypted))
}
func schemeFromString(s string) (scheme, error) {
if s == "PLAIN" {
return plainScheme{}, nil
} else if strings.HasPrefix(s, "SCRYPT@") {
sc := scryptScheme{}
var b64salt string
n, err := fmt.Sscanf(s, "SCRYPT@n:%d,r:%d,p:%d,l:%d,%s",
&sc.logN, &sc.r, &sc.p, &sc.keyLen, &b64salt)
if n != 5 || err != nil {
return nil, fmt.Errorf("error scanning scrypt: %d %v", n, err)
}
sc.salt, err = base64.StdEncoding.DecodeString(b64salt)
if err != nil {
return nil, fmt.Errorf("error decoding salt: %v", err)
}
// Perform some sanity checks on the parameters, just in case.
if (sc.logN >= 32) || (sc.r*sc.p >= 1<<30) || (sc.keyLen < 24) {
return nil, fmt.Errorf("invalid scrypt parameters")
}
return sc, nil
}
return nil, fmt.Errorf("unknown scheme")
} }

View File

@@ -0,0 +1,224 @@
// Code generated by protoc-gen-go.
// source: userdb.proto
// DO NOT EDIT!
/*
Package userdb is a generated protocol buffer package.
It is generated from these files:
userdb.proto
It has these top-level messages:
ProtoDB
Password
Scrypt
Plain
*/
package userdb
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type ProtoDB struct {
Users map[string]*Password `protobuf:"bytes,1,rep,name=users" json:"users,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
}
func (m *ProtoDB) Reset() { *m = ProtoDB{} }
func (m *ProtoDB) String() string { return proto.CompactTextString(m) }
func (*ProtoDB) ProtoMessage() {}
func (*ProtoDB) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *ProtoDB) GetUsers() map[string]*Password {
if m != nil {
return m.Users
}
return nil
}
type Password struct {
// Types that are valid to be assigned to Scheme:
// *Password_Scrypt
// *Password_Plain
Scheme isPassword_Scheme `protobuf_oneof:"scheme"`
}
func (m *Password) Reset() { *m = Password{} }
func (m *Password) String() string { return proto.CompactTextString(m) }
func (*Password) ProtoMessage() {}
func (*Password) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
type isPassword_Scheme interface {
isPassword_Scheme()
}
type Password_Scrypt struct {
Scrypt *Scrypt `protobuf:"bytes,2,opt,name=scrypt,oneof"`
}
type Password_Plain struct {
Plain *Plain `protobuf:"bytes,3,opt,name=plain,oneof"`
}
func (*Password_Scrypt) isPassword_Scheme() {}
func (*Password_Plain) isPassword_Scheme() {}
func (m *Password) GetScheme() isPassword_Scheme {
if m != nil {
return m.Scheme
}
return nil
}
func (m *Password) GetScrypt() *Scrypt {
if x, ok := m.GetScheme().(*Password_Scrypt); ok {
return x.Scrypt
}
return nil
}
func (m *Password) GetPlain() *Plain {
if x, ok := m.GetScheme().(*Password_Plain); ok {
return x.Plain
}
return nil
}
// XXX_OneofFuncs is for the internal use of the proto package.
func (*Password) XXX_OneofFuncs() (func(msg proto.Message, b *proto.Buffer) error, func(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error), func(msg proto.Message) (n int), []interface{}) {
return _Password_OneofMarshaler, _Password_OneofUnmarshaler, _Password_OneofSizer, []interface{}{
(*Password_Scrypt)(nil),
(*Password_Plain)(nil),
}
}
func _Password_OneofMarshaler(msg proto.Message, b *proto.Buffer) error {
m := msg.(*Password)
// scheme
switch x := m.Scheme.(type) {
case *Password_Scrypt:
b.EncodeVarint(2<<3 | proto.WireBytes)
if err := b.EncodeMessage(x.Scrypt); err != nil {
return err
}
case *Password_Plain:
b.EncodeVarint(3<<3 | proto.WireBytes)
if err := b.EncodeMessage(x.Plain); err != nil {
return err
}
case nil:
default:
return fmt.Errorf("Password.Scheme has unexpected type %T", x)
}
return nil
}
func _Password_OneofUnmarshaler(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error) {
m := msg.(*Password)
switch tag {
case 2: // scheme.scrypt
if wire != proto.WireBytes {
return true, proto.ErrInternalBadWireType
}
msg := new(Scrypt)
err := b.DecodeMessage(msg)
m.Scheme = &Password_Scrypt{msg}
return true, err
case 3: // scheme.plain
if wire != proto.WireBytes {
return true, proto.ErrInternalBadWireType
}
msg := new(Plain)
err := b.DecodeMessage(msg)
m.Scheme = &Password_Plain{msg}
return true, err
default:
return false, nil
}
}
func _Password_OneofSizer(msg proto.Message) (n int) {
m := msg.(*Password)
// scheme
switch x := m.Scheme.(type) {
case *Password_Scrypt:
s := proto.Size(x.Scrypt)
n += proto.SizeVarint(2<<3 | proto.WireBytes)
n += proto.SizeVarint(uint64(s))
n += s
case *Password_Plain:
s := proto.Size(x.Plain)
n += proto.SizeVarint(3<<3 | proto.WireBytes)
n += proto.SizeVarint(uint64(s))
n += s
case nil:
default:
panic(fmt.Sprintf("proto: unexpected type %T in oneof", x))
}
return n
}
type Scrypt struct {
LogN uint64 `protobuf:"varint,1,opt,name=logN" json:"logN,omitempty"`
R int32 `protobuf:"varint,2,opt,name=r" json:"r,omitempty"`
P int32 `protobuf:"varint,3,opt,name=p" json:"p,omitempty"`
KeyLen int32 `protobuf:"varint,4,opt,name=keyLen" json:"keyLen,omitempty"`
Salt []byte `protobuf:"bytes,5,opt,name=salt,proto3" json:"salt,omitempty"`
Encrypted []byte `protobuf:"bytes,6,opt,name=encrypted,proto3" json:"encrypted,omitempty"`
}
func (m *Scrypt) Reset() { *m = Scrypt{} }
func (m *Scrypt) String() string { return proto.CompactTextString(m) }
func (*Scrypt) ProtoMessage() {}
func (*Scrypt) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
type Plain struct {
Password []byte `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"`
}
func (m *Plain) Reset() { *m = Plain{} }
func (m *Plain) String() string { return proto.CompactTextString(m) }
func (*Plain) ProtoMessage() {}
func (*Plain) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} }
func init() {
proto.RegisterType((*ProtoDB)(nil), "userdb.ProtoDB")
proto.RegisterType((*Password)(nil), "userdb.Password")
proto.RegisterType((*Scrypt)(nil), "userdb.Scrypt")
proto.RegisterType((*Plain)(nil), "userdb.Plain")
}
func init() { proto.RegisterFile("userdb.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 289 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x44, 0x91, 0x41, 0x4b, 0x03, 0x31,
0x10, 0x85, 0x4d, 0xdb, 0xc4, 0x76, 0xba, 0x4a, 0x99, 0x83, 0x84, 0xe2, 0x41, 0x56, 0x94, 0x9e,
0x16, 0xa9, 0x17, 0xf1, 0x58, 0x14, 0x44, 0x44, 0x24, 0xe2, 0x0f, 0xd8, 0xba, 0x41, 0xc5, 0x75,
0x37, 0x24, 0x5b, 0x65, 0xaf, 0x5e, 0xfc, 0xdb, 0x26, 0xb3, 0xe9, 0xf6, 0x36, 0xef, 0x7b, 0x33,
0x6f, 0x26, 0x04, 0x92, 0x8d, 0xd3, 0xb6, 0x58, 0x67, 0xc6, 0xd6, 0x4d, 0x8d, 0xa2, 0x53, 0xe9,
0x1f, 0x83, 0xfd, 0xa7, 0x40, 0x6e, 0x56, 0x78, 0x01, 0x3c, 0x50, 0x27, 0xd9, 0xc9, 0x70, 0x31,
0x5d, 0xce, 0xb3, 0x38, 0x11, 0xfd, 0xec, 0x25, 0x98, 0xb7, 0x55, 0x63, 0x5b, 0xd5, 0x35, 0xce,
0xef, 0x01, 0x76, 0x10, 0x67, 0x30, 0xfc, 0xd4, 0xad, 0x9f, 0x66, 0x8b, 0x89, 0x0a, 0x25, 0x9e,
0x03, 0xff, 0xce, 0xcb, 0x8d, 0x96, 0x03, 0xcf, 0xa6, 0xcb, 0x59, 0x9f, 0x98, 0x3b, 0xf7, 0x53,
0xdb, 0x42, 0x75, 0xf6, 0xf5, 0xe0, 0x8a, 0xa5, 0x1a, 0xc6, 0x5b, 0x8c, 0x0b, 0x10, 0xee, 0xd5,
0xb6, 0xa6, 0x89, 0x83, 0x87, 0xdb, 0xc1, 0x67, 0xa2, 0x77, 0x7b, 0x2a, 0xfa, 0x78, 0x06, 0xdc,
0x94, 0xf9, 0x47, 0x25, 0x87, 0xd4, 0x78, 0xd0, 0x6f, 0x08, 0xd0, 0xf7, 0x75, 0xee, 0x6a, 0x1c,
0x02, 0xdf, 0xf5, 0x97, 0x4e, 0x7f, 0x19, 0x88, 0x2e, 0x05, 0x11, 0x46, 0x65, 0xfd, 0xf6, 0x48,
0x07, 0x8f, 0x14, 0xd5, 0x98, 0x00, 0xb3, 0xb4, 0x94, 0x2b, 0x66, 0x83, 0x32, 0x94, 0xec, 0x95,
0xc1, 0x23, 0x10, 0xfe, 0x51, 0x0f, 0xba, 0x92, 0x23, 0x42, 0x51, 0x85, 0x1c, 0x97, 0x97, 0x8d,
0xe4, 0x9e, 0x26, 0x8a, 0x6a, 0x3c, 0x86, 0x89, 0xae, 0x68, 0x8d, 0x2e, 0xa4, 0x20, 0x63, 0x07,
0xd2, 0x53, 0xe0, 0x74, 0x20, 0xce, 0x61, 0x6c, 0xe2, 0xa3, 0xe9, 0x8c, 0x44, 0xf5, 0x7a, 0x2d,
0xe8, 0xa7, 0x2e, 0xff, 0x03, 0x00, 0x00, 0xff, 0xff, 0xe4, 0x93, 0xae, 0x19, 0xb9, 0x01, 0x00,
0x00,
}

View File

@@ -0,0 +1,28 @@
syntax = "proto3";
package userdb;
message ProtoDB {
map<string, Password> users = 1;
}
message Password {
oneof scheme {
Scrypt scrypt = 2;
Plain plain = 3;
}
}
message Scrypt {
uint64 logN = 1;
int32 r = 2;
int32 p = 3;
int32 keyLen = 4;
bytes salt = 5;
bytes encrypted = 6;
}
message Plain {
bytes password = 1;
}

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"reflect"
"testing" "testing"
) )
@@ -32,17 +33,17 @@ func mustCreateDB(t *testing.T, content string) string {
} }
func dbEquals(a, b *DB) bool { func dbEquals(a, b *DB) bool {
if a.users == nil || b.users == nil { if a.db == nil || b.db == nil {
return a.users == nil && b.users == nil return a.db == nil && b.db == nil
} }
if len(a.users) != len(b.users) { if len(a.db.Users) != len(b.db.Users) {
return false return false
} }
for k, av := range a.users { for k, av := range a.db.Users {
bv, ok := b.users[k] bv, ok := b.db.Users[k]
if !ok || av.name != bv.name || av.password != bv.password { if !ok || !reflect.DeepEqual(av, bv) {
return false return false
} }
} }
@@ -51,66 +52,30 @@ func dbEquals(a, b *DB) bool {
} }
var emptyDB = &DB{ var emptyDB = &DB{
users: map[string]user{}, db: &ProtoDB{Users: map[string]*Password{}},
} }
const (
scryptNoSalt = ("#chasquid-userdb-v1\n" +
"user1 SCRYPT@n:14,r:8,p:1,l:32, " +
"WyZPRd08NPAkWgBuqB5kwK4fEuB6FHu/X1pA1SxnXhc=")
scryptInvalidSalt = ("#chasquid-userdb-v1\n" +
"user1 SCRYPT@n:99,r:8,p:1,l:16,not-valid$base64!nono== " +
"WyZPRd08NPAkWgBuqB5kwK4fEuB6FHu/X1pA1SxnXhc=")
scryptMissingR = ("#chasquid-userdb-v1\n" +
"user1 SCRYPT@n:14,r:,p:1,l:32,gY3a3PIzehu7xu6KM9PeOQ== " +
"WyZPRd08NPAkWgBuqB5kwK4fEuB6FHu/X1pA1SxnXhc=")
scryptBadN = ("#chasquid-userdb-v1\n" +
"user1 SCRYPT@n:99,r:8,p:1,l:32,gY3a3PIzehu7xu6KM9PeOQ== " +
"WyZPRd08NPAkWgBuqB5kwK4fEuB6FHu/X1pA1SxnXhc=")
scryptShortKeyLen = ("#chasquid-userdb-v1\n" +
"user1 SCRYPT@n:99,r:8,p:1,l:16,gY3a3PIzehu7xu6KM9PeOQ== " +
"WyZPRd08NPAkWgBuqB5kwK4fEuB6FHu/X1pA1SxnXhc=")
)
// Test various cases of loading an empty/broken database. // Test various cases of loading an empty/broken database.
func TestLoad(t *testing.T) { func TestEmptyLoad(t *testing.T) {
cases := []struct { cases := []struct {
desc string desc string
content string content string
fatal bool fatal bool
fatalErr error fatalErr error
warns bool
}{ }{
{"empty file", "", false, nil, false}, {"empty file", "", false, nil},
{"header \\n", "#chasquid-userdb-v1\n", false, nil, false}, {"invalid ", "users: < invalid >", true, nil},
{"header \\r\\n", "#chasquid-userdb-v1\r\n", false, nil, false},
{"header EOF", "#chasquid-userdb-v1", false, nil, false},
{"missing header", "this is not the header",
true, ErrMissingHeader, false},
{"invalid user", "#chasquid-userdb-v1\nnam\xa0e PLAIN pass\n",
false, nil, true},
{"too few fields", "#chasquid-userdb-v1\nfield1 field2\n",
false, nil, true},
{"too many fields", "#chasquid-userdb-v1\nf1 f2 f3 f4\n",
false, nil, true},
{"unknown scheme", "#chasquid-userdb-v1\nuser SCHEME pass\n",
false, nil, true},
{"scrypt no salt", scryptNoSalt, false, nil, true},
{"scrypt invalid salt", scryptInvalidSalt, false, nil, true},
{"scrypt missing R", scryptMissingR, false, nil, true},
{"scrypt bad N", scryptBadN, false, nil, true},
{"scrypt short key len", scryptShortKeyLen, false, nil, true},
} }
for _, c := range cases { for _, c := range cases {
testOneLoad(t, c.desc, c.content, c.fatal, c.fatalErr, c.warns) testOneLoad(t, c.desc, c.content, c.fatal, c.fatalErr)
} }
} }
func testOneLoad(t *testing.T, desc, content string, fatal bool, fatalErr error, warns bool) { func testOneLoad(t *testing.T, desc, content string, fatal bool, fatalErr error) {
fname := mustCreateDB(t, content) fname := mustCreateDB(t, content)
defer removeIfSuccessful(t, fname) defer removeIfSuccessful(t, fname)
db, warnings, err := Load(fname) db, err := Load(fname)
if fatal { if fatal {
if err == nil { if err == nil {
t.Errorf("case %q: expected error loading, got nil", desc) t.Errorf("case %q: expected error loading, got nil", desc)
@@ -122,24 +87,13 @@ func testOneLoad(t *testing.T, desc, content string, fatal bool, fatalErr error,
t.Fatalf("case %q: error loading database: %v", desc, err) t.Fatalf("case %q: error loading database: %v", desc, err)
} }
if warns && warnings == nil {
t.Errorf("case %q: expected warnings, got nil", desc)
} else if !warns {
for _, w := range warnings {
t.Errorf("case %q: warning loading database: %v", desc, w)
}
}
if db != nil && !dbEquals(db, emptyDB) { if db != nil && !dbEquals(db, emptyDB) {
t.Errorf("case %q: DB not empty: %#v", desc, db) t.Errorf("case %q: DB not empty: %#v", desc, db.db.Users)
} }
} }
func mustLoad(t *testing.T, fname string) *DB { func mustLoad(t *testing.T, fname string) *DB {
db, warnings, err := Load(fname) db, err := Load(fname)
for _, w := range warnings {
t.Errorf("warning loading database: %v", w)
}
if err != nil { if err != nil {
t.Fatalf("error loading database: %v", err) t.Fatalf("error loading database: %v", err)
} }
@@ -178,8 +132,8 @@ func TestWrite(t *testing.T) {
if !db.Exists(name) { if !db.Exists(name) {
t.Errorf("user %q not in database", name) t.Errorf("user %q not in database", name)
} }
if _, ok := db.users[name].scheme.(scryptScheme); !ok { if db.db.Users[name].GetScheme() == nil {
t.Errorf("user %q not using scrypt: %#v", name, db.users[name]) t.Errorf("user %q not using scrypt: %#v", name, db.db.Users[name])
} }
} }
@@ -210,13 +164,10 @@ func TestNew(t *testing.T) {
db1.AddUser("user", "passwd") db1.AddUser("user", "passwd")
db1.Write() db1.Write()
db2, ws, err := Load(fname) db2, err := Load(fname)
if err != nil { if err != nil {
t.Fatalf("error loading: %v", err) t.Fatalf("error loading: %v", err)
} }
if len(ws) != 0 {
t.Errorf("warnings loading: %v", ws)
}
if !dbEquals(db1, db2) { if !dbEquals(db1, db2) {
t.Errorf("databases differ. db1:%v != db2:%v", db1, db2) t.Errorf("databases differ. db1:%v != db2:%v", db1, db2)
@@ -236,12 +187,11 @@ func TestInvalidUsername(t *testing.T) {
t.Errorf("AddUser(%q) worked, expected it to fail", name) t.Errorf("AddUser(%q) worked, expected it to fail", name)
} }
} }
}
// Add an invalid user from behind, and check that Write fails. func plainPassword(p string) *Password {
db.users["in valid"] = user{"in valid", plainScheme{}, "password"} return &Password{
err := db.Write() Scheme: &Password_Plain{&Plain{[]byte(p)}},
if err == nil {
t.Errorf("Write worked, expected it to fail")
} }
} }
@@ -252,7 +202,7 @@ func TestPlainScheme(t *testing.T) {
defer removeIfSuccessful(t, fname) defer removeIfSuccessful(t, fname)
db := mustLoad(t, fname) db := mustLoad(t, fname)
db.users["user"] = user{"user", plainScheme{}, "pass word"} db.db.Users["user"] = plainPassword("pass word")
err := db.Write() err := db.Write()
if err != nil { if err != nil {
t.Errorf("Write failed: %v", err) t.Errorf("Write failed: %v", err)
@@ -268,35 +218,43 @@ func TestPlainScheme(t *testing.T) {
} }
func TestReload(t *testing.T) { func TestReload(t *testing.T) {
content := "#chasquid-userdb-v1\nu1 PLAIN pass\n" content := "users:< key: 'u1' value:< plain:< password: 'pass' >>>"
fname := mustCreateDB(t, content) fname := mustCreateDB(t, content)
defer removeIfSuccessful(t, fname) defer removeIfSuccessful(t, fname)
db := mustLoad(t, fname) db := mustLoad(t, fname)
// Add some things to the file, including a broken line. // Add a valid line to the file.
content += "u2 UNKNOWN pass\n" content += "users:< key: 'u2' value:< plain:< password: 'pass' >>>"
content += "u3 PLAIN pass\n" ioutil.WriteFile(fname, []byte(content), 0660)
ioutil.WriteFile(fname, []byte(content), db.finfo.Mode())
warnings, err := db.Reload() err := db.Reload()
if err != nil { if err != nil {
t.Errorf("Reload failed: %v", err) t.Errorf("Reload failed: %v", err)
} }
if len(warnings) != 1 { if len(db.db.Users) != 2 {
t.Errorf("expected 1 warning, got %v", warnings) t.Errorf("expected 2 users, got %d", len(db.db.Users))
}
if len(db.users) != 2 {
t.Errorf("expected 2 users, got %d", len(db.users))
} }
// Cause an error loading, check the database is not changed. // And now a broken one.
db.fname = "/does/not/exist" content += "users:< invalid >"
warnings, err = db.Reload() ioutil.WriteFile(fname, []byte(content), 0660)
err = db.Reload()
if err == nil { if err == nil {
t.Errorf("expected error, got nil") t.Errorf("expected error, got nil")
} }
if len(db.users) != 2 { if len(db.db.Users) != 2 {
t.Errorf("expected 2 users, got %d", len(db.users)) t.Errorf("expected 2 users, got %d", len(db.db.Users))
}
// Cause an even bigger error loading, check the database is not changed.
db.fname = "/does/not/exist"
err = db.Reload()
if err == nil {
t.Errorf("expected error, got nil")
}
if len(db.db.Users) != 2 {
t.Errorf("expected 2 users, got %d", len(db.db.Users))
} }
} }

4
test/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Ignore the user databases - we create them each time.
t-*/config/**/users

View File

@@ -1,2 +0,0 @@
#chasquid-userdb-v1
user SCRYPT@n:14,r:8,p:1,l:32,r00XqNmRkV505R2X6KT8+Q== aAiBBIVNNzmDXwxLLdJezFuxGtc2/wcHsy3FiOMAH4c=

View File

@@ -6,6 +6,7 @@ set -e
init init
generate_certs_for testserver generate_certs_for testserver
add_user testserver user secretpassword
mkdir -p .logs mkdir -p .logs
chasquid -v=2 --log_dir=.logs --config_dir=config & chasquid -v=2 --log_dir=.logs --config_dir=config &

View File

@@ -1,2 +0,0 @@
#chasquid-userdb-v1
user SCRYPT@n:14,r:8,p:1,l:32,r00XqNmRkV505R2X6KT8+Q== aAiBBIVNNzmDXwxLLdJezFuxGtc2/wcHsy3FiOMAH4c=

View File

@@ -33,6 +33,7 @@ mkdir -p .exim4
EXIMDIR="$PWD/.exim4" envsubst < config/exim4.in > .exim4/config EXIMDIR="$PWD/.exim4" envsubst < config/exim4.in > .exim4/config
generate_certs_for srv-chasquid generate_certs_for srv-chasquid
add_user srv-chasquid user secretpassword
# Launch chasquid at port 1025 (in config). # Launch chasquid at port 1025 (in config).
# Use outgoing port 2025 which is where exim will be at. # Use outgoing port 2025 which is where exim will be at.

View File

@@ -32,6 +32,14 @@ function chasquid() {
go run ${TBASE}/../../chasquid.go "$@" go run ${TBASE}/../../chasquid.go "$@"
} }
function add_user() {
go run ${TBASE}/../../cmd/chasquid-userdb/chasquid-userdb.go \
--database "config/domains/${1}/users" \
--add_user "${2}" \
--password "${3}" \
>> .add_user_logs
}
function run_msmtp() { function run_msmtp() {
# msmtp will check that the rc file is only user readable. # msmtp will check that the rc file is only user readable.
chmod 600 msmtprc chmod 600 msmtprc