User Tools

Site Tools


Password-file like account and authentication module

… for mini-services. Accounts always read/write from/to file “by design” =).

accounts.go
/*
 * Author, Copyright: Oleg Borodin <onborodin@gmail.com>
 */
 
package accounts
 
import (
    "math/rand"
    "fmt"
    "os"
    "bufio"
    "regexp"
    "errors"
    "github.com/GehirnInc/crypt"
    _ "github.com/GehirnInc/crypt/sha256_crypt"
)
 
type AccountDB struct {
    FileName string
}
 
type Account struct{
    Name string
    Digest string
}
 
 
var passwordMinLen int = 4
var nameMinLen int = 4
 
const letters = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
 
func randString(n int) string {
    arr := make([]byte, n)
    for i := range arr {
        arr[i] = letters[rand.Intn(len(letters))]
    }
    return string(arr)
}
 
func createHash(key string) (string, error) {
    crypt := crypt.SHA256.New()
    return crypt.Generate([]byte(key), []byte("$5$" + randString(12)))
}
 
func validateName(name string) error {
    if len(name) < nameMinLen {
        return errors.New(fmt.Sprintf("name less %d chars", nameMinLen))
    }
    return nil
}
 
func validatePassword(password string) error {
    if len(password) < passwordMinLen {
        return errors.New(fmt.Sprintf("password less %d chars", passwordMinLen))
    }
    return nil
}
 
func validateFileName(name string) error {
    if len(name) < 1 {
        return errors.New(fmt.Sprintf("filename not set"))
    }
    return nil
}
 
func (accountDB AccountDB) Set(name string, password string) error {
 
    if err := validateFileName(accountDB.FileName); err != nil {
        return err
    }
    if err := validateName(name); err != nil {
        return err
    }
    if err := validatePassword(password); err != nil {
        return err
    }
 
    file, err := os.OpenFile(accountDB.FileName, os.O_RDONLY|os.O_CREATE, 0640)
    if err != nil {
        return err
    }
    scanner := bufio.NewScanner(file)
 
    digest, _ := createHash(password)
    accounts := map[string]string{name: string(digest)}
    re := regexp.MustCompile(`(.+):(.+)`)
 
    for scanner.Scan() {
        str := scanner.Text()
        if re.MatchString(str) {
            match := re.FindStringSubmatch(str)
            name := match[1]
            digest := match[2]
            if _, exists :=  accounts[name]; !exists {
                accounts[name] = digest
            }
        }
    }
    file.Close()
 
    _ = os.Rename(accountDB.FileName, accountDB.FileName + ".bak")
 
    file, err = os.OpenFile(accountDB.FileName, os.O_WRONLY|os.O_CREATE, 0640)
    if err != nil {
        return err
    }
 
    for key, value := range accounts {
        file.Write([]byte(fmt.Sprintf("%s:%s\n", key, value)))
    }
    file.Close()
    return nil
}
 
func (accountDB AccountDB) Print() error {
    file, err := os.OpenFile(accountDB.FileName, os.O_RDONLY, 0640)
    defer file.Close()
    if err != nil {
        return err
    }
 
    scanner := bufio.NewScanner(file)
    re := regexp.MustCompile(`^(.+):(.+)`)
 
    for scanner.Scan() {
        str := scanner.Text()
        if re.MatchString(str) {
            match := re.FindStringSubmatch(str)
            fmt.Println(match[1], match[2])
        }
    }
    return nil
}
 
func (accountDB AccountDB) List() ([]Account, error) {
    file, err := os.OpenFile(accountDB.FileName, os.O_RDONLY, 0640)
    defer file.Close()
    if err != nil {
        return nil, err
    }
 
    scanner := bufio.NewScanner(file)
    re := regexp.MustCompile(`^(.+):(.+)`)
 
    var accountList []Account
    for scanner.Scan() {
        str := scanner.Text()
        if re.MatchString(str) {
            match := re.FindStringSubmatch(str)
 
            account := Account{
                Name: match[1],
                Digest: match[2],
            }
            accountList = append(accountList, account)
        }
    }
    return accountList, nil
}
 
func (accountDB AccountDB) Auth(name string, password string) error {
 
    if err := validateFileName(accountDB.FileName); err != nil {
        return err
    }
 
    file, err := os.OpenFile(accountDB.FileName, os.O_RDONLY, 0640)
    defer file.Close()
    if err != nil {
        return err
    }
    scanner := bufio.NewScanner(file)
 
    re := regexp.MustCompile(`^(.+):(.+)`)
 
    for scanner.Scan() {
        str := scanner.Text()
        if re.MatchString(str) {
            crypt := crypt.SHA256.New()
            match := re.FindStringSubmatch(str)
            if match[1] == name {
                return crypt.Verify(match[2], []byte(password))
            }
        }
    }
    return errors.New(fmt.Sprintf("account %s not found", name))
}
 
func (accountDB AccountDB) Delete(name string) error {
 
    if err := validateFileName(accountDB.FileName); err != nil {
        return err
    }
 
    file, err := os.OpenFile(accountDB.FileName, os.O_RDONLY, 0640)
    if err != nil {
        return err
    }
    scanner := bufio.NewScanner(file)
 
    re := regexp.MustCompile(`(.+):(.+)`)
 
    accounts := map[string]string{}
 
    for scanner.Scan() {
        str := scanner.Text()
        if re.MatchString(str) {
            match := re.FindStringSubmatch(str)
            xname := match[1]
            xdigest := match[2]
            if xname != name {
                if _, exists :=  accounts[xname]; !exists {
                    accounts[xname] = xdigest
                }
            }
        }
    }
    file.Close()
 
    //err = os.Remove(accountDB.FileName)
    err = os.Rename(accountDB.FileName, accountDB.FileName + ".bak")
    if err != nil {
        return err
    }
 
    file, err = os.OpenFile(accountDB.FileName, os.O_WRONLY|os.O_CREATE, 0640)
    if err != nil {
        return err
    }
    for xname, xdigest := range accounts {
        //fmt.Println("write:", xname, xdigest)
        file.Write([]byte(fmt.Sprintf("%s:%s\n", xname, xdigest)))
    }
    file.Close()
    return nil
}
 
func New(filename string) *AccountDB {
    return &AccountDB{ FileName: filename }
}