1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package pass_table2021import (22 "context"23 "fmt"24 "strings"2526 "github.com/foxcpp/maddy/framework/config"27 modconfig "github.com/foxcpp/maddy/framework/config/module"28 "github.com/foxcpp/maddy/framework/module"29 "golang.org/x/crypto/bcrypt"30 "golang.org/x/text/secure/precis"31)3233type Auth struct {34 modName string35 instName string36 inlineArgs []string3738 table module.Table39}4041func New(modName, instName string, _, inlineArgs []string) (module.Module, error) {42 return &Auth{43 modName: modName,44 instName: instName,45 inlineArgs: inlineArgs,46 }, nil47}4849func (a *Auth) Init(cfg *config.Map) error {50 if len(a.inlineArgs) != 0 {51 return modconfig.ModuleFromNode("table", a.inlineArgs, cfg.Block, cfg.Globals, &a.table)52 }5354 cfg.Custom("table", false, true, nil, modconfig.TableDirective, &a.table)55 _, err := cfg.Process()56 return err57}5859func (a *Auth) Name() string {60 return a.modName61}6263func (a *Auth) InstanceName() string {64 return a.instName65}6667func (a *Auth) Lookup(ctx context.Context, username string) (string, bool, error) {68 key, err := precis.UsernameCaseMapped.CompareKey(username)69 if err != nil {70 return "", false, err71 }7273 return a.table.Lookup(ctx, key)74}7576func (a *Auth) AuthPlain(username, password string) error {77 key, err := precis.UsernameCaseMapped.CompareKey(username)78 if err != nil {79 return err80 }8182 hash, ok, err := a.table.Lookup(context.TODO(), key)83 if !ok {84 return module.ErrUnknownCredentials85 }86 if err != nil {87 return err88 }8990 parts := strings.SplitN(hash, ":", 2)91 if len(parts) != 2 {92 return fmt.Errorf("%s: auth plain %s: no hash tag", a.modName, key)93 }94 hashVerify := HashVerify[parts[0]]95 if hashVerify == nil {96 return fmt.Errorf("%s: auth plain %s: unknown hash: %s", a.modName, key, parts[0])97 }98 return hashVerify(password, parts[1])99}100101func (a *Auth) ListUsers() ([]string, error) {102 tbl, ok := a.table.(module.MutableTable)103 if !ok {104 return nil, fmt.Errorf("%s: table is not mutable, no management functionality available", a.modName)105 }106107 l, err := tbl.Keys()108 if err != nil {109 return nil, fmt.Errorf("%s: list users: %w", a.modName, err)110 }111 return l, nil112}113114func (a *Auth) CreateUser(username, password string) error {115 return a.CreateUserHash(username, password, HashBcrypt, HashOpts{116 BcryptCost: bcrypt.DefaultCost,117 })118}119120func (a *Auth) CreateUserHash(username, password string, hashAlgo string, opts HashOpts) error {121 tbl, ok := a.table.(module.MutableTable)122 if !ok {123 return fmt.Errorf("%s: table is not mutable, no management functionality available", a.modName)124 }125126 if _, ok := HashCompute[hashAlgo]; !ok {127 return fmt.Errorf("%s: unknown hash function: %v", a.modName, hashAlgo)128 }129130 key, err := precis.UsernameCaseMapped.CompareKey(username)131 if err != nil {132 return fmt.Errorf("%s: create user %s (raw): %w", a.modName, username, err)133 }134135 _, ok, err = tbl.Lookup(context.TODO(), key)136 if err != nil {137 return fmt.Errorf("%s: create user %s: %w", a.modName, key, err)138 }139 if ok {140 return fmt.Errorf("%s: credentials for %s already exist", a.modName, key)141 }142143 hash, err := HashCompute[hashAlgo](opts, password)144 if err != nil {145 return fmt.Errorf("%s: create user %s: hash generation: %w", a.modName, key, err)146 }147148 if err := tbl.SetKey(key, hashAlgo+":"+hash); err != nil {149 return fmt.Errorf("%s: create user %s: %w", a.modName, key, err)150 }151 return nil152}153154func (a *Auth) SetUserPassword(username, password string) error {155 tbl, ok := a.table.(module.MutableTable)156 if !ok {157 return fmt.Errorf("%s: table is not mutable, no management functionality available", a.modName)158 }159160 key, err := precis.UsernameCaseMapped.CompareKey(username)161 if err != nil {162 return fmt.Errorf("%s: set password %s (raw): %w", a.modName, username, err)163 }164165 // TODO: Allow to customize hash function.166 hash, err := HashCompute[HashBcrypt](HashOpts{167 BcryptCost: bcrypt.DefaultCost,168 }, password)169 if err != nil {170 return fmt.Errorf("%s: set password %s: hash generation: %w", a.modName, key, err)171 }172173 if err := tbl.SetKey(key, "bcrypt:"+hash); err != nil {174 return fmt.Errorf("%s: set password %s: %w", a.modName, key, err)175 }176 return nil177}178179func (a *Auth) DeleteUser(username string) error {180 tbl, ok := a.table.(module.MutableTable)181 if !ok {182 return fmt.Errorf("%s: table is not mutable, no management functionality available", a.modName)183 }184185 key, err := precis.UsernameCaseMapped.CompareKey(username)186 if err != nil {187 return fmt.Errorf("%s: del user %s (raw): %w", a.modName, username, err)188 }189190 if err := tbl.RemoveKey(key); err != nil {191 return fmt.Errorf("%s: del user %s: %w", a.modName, key, err)192 }193 return nil194}195196func init() {197 module.Register("auth.pass_table", New)198}