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:
@@ -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.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
fmt.Printf("error loading database: %v\n", err)
|
if *adduser != "" && os.IsNotExist(err) {
|
||||||
os.Exit(1)
|
fmt.Printf("creating database\n")
|
||||||
}
|
} else {
|
||||||
|
fmt.Printf("error loading database: %v\n", err)
|
||||||
for _, w := range ws {
|
os.Exit(1)
|
||||||
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 == "" {
|
||||||
|
|||||||
@@ -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")
|
|
||||||
}
|
}
|
||||||
|
|||||||
224
internal/userdb/userdb.pb.go
Normal file
224
internal/userdb/userdb.pb.go
Normal 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,
|
||||||
|
}
|
||||||
28
internal/userdb/userdb.proto
Normal file
28
internal/userdb/userdb.proto
Normal 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;
|
||||||
|
}
|
||||||
@@ -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
4
test/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
# Ignore the user databases - we create them each time.
|
||||||
|
t-*/config/**/users
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#chasquid-userdb-v1
|
|
||||||
user SCRYPT@n:14,r:8,p:1,l:32,r00XqNmRkV505R2X6KT8+Q== aAiBBIVNNzmDXwxLLdJezFuxGtc2/wcHsy3FiOMAH4c=
|
|
||||||
@@ -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 &
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#chasquid-userdb-v1
|
|
||||||
user SCRYPT@n:14,r:8,p:1,l:32,r00XqNmRkV505R2X6KT8+Q== aAiBBIVNNzmDXwxLLdJezFuxGtc2/wcHsy3FiOMAH4c=
|
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user