User Tools

Site Tools


accountdb.go

accountdb.go
/*
 * Copyright 2019 Oleg Borodin  <borodin@unix7.org>
 */
 
package accountdb
 
import (
    "strings"
    "fmt"
    "io/ioutil"
    "path/filepath"
    "math/rand"
    "errors"
    "os"
    "github.com/GehirnInc/crypt"
    _ "github.com/GehirnInc/crypt/sha256_crypt"
)
 
 
type Account struct {
    Username string
    Digest string
}
 
type AccountDB struct {
    FileName string
}
 
const (
    DigestMinLen = 56
    UsernameMinLen = 4
    PasswordMinLen = 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(password string) (string, error) {
    crypt := crypt.SHA256.New()
    return crypt.Generate([]byte(password), []byte("$5$" + randString(12)))
}
 
func checkHash(digest string, password string) error {
    crypt := crypt.SHA256.New()
    return crypt.Verify(digest, []byte(password))
}
 
func (accountDB AccountDB) readFile() ([]Account, error) {
    var data []byte
    var err error
    if data, err = ioutil.ReadFile(accountDB.FileName); err != nil {
        return nil, err
    }
    records := strings.Split(string(data), "\n")
 
    var accountArray []Account
    for _, recordString := range records {
 
        if len(recordString) == 0 {
            continue
        }
 
        recordArray := strings.SplitN(recordString, ":", 2)
        username, digest := recordArray[0], recordArray[1]
 
        if len(username) < UsernameMinLen {
            continue
        }
        if len(digest) < DigestMinLen {
            continue
        }
 
        accountArray = append(accountArray, Account{
            Username: username,
            Digest: digest,
        })
    }
    return accountArray, nil
}
 
func (accountDB AccountDB) writeFile(accountArray []Account) error {
    var data string
    for _, account := range accountArray {
        record := account.Username + ":" + account.Digest + "\n"
        data = data + record
    }
    fileName, _ := filepath.Abs(accountDB.FileName)
    os.Rename(fileName, fileName + "~")
    return ioutil.WriteFile(fileName, []byte(data), 0640)
}
 
func (accountDB AccountDB) uniqAccounts(accountArray []Account) ([]Account, error) {
 
    accountMap := make(map[string]string)
    for _, account := range accountArray {
        if _, has :=  accountMap[account.Username]; !has {
            accountMap[account.Username] = account.Digest
        }
    }
 
    var uniqArray []Account
    for username, digest := range accountMap {
        record := Account{
            Username: username,
            Digest: digest,
        }
        uniqArray = append(uniqArray, record)
    }
 
    return uniqArray, nil
}
 
func (accountDB AccountDB) Auth(username string, password string) error {
    accountArray, err := accountDB.readFile()
    if err != nil {
        return err
    }
 
    accountArray, _= accountDB.uniqAccounts(accountArray)
 
    for _, account := range accountArray {
        if username == account.Username {
            return checkHash(account.Digest, password)
        }
    }
    return errors.New("username not found")
}
 
 
func (accountDB AccountDB) Set(username string, password string) error {
 
    if len(password) < PasswordMinLen {
        return errors.New(fmt.Sprintf("password less %d chars", PasswordMinLen))
    }
 
    hasAccount := false
    var accountArray []Account
    var err error
 
    accountArray, err = accountDB.readFile()
 
    if err == nil && accountArray != nil {
        accountArray, _= accountDB.uniqAccounts(accountArray)
        for i, account := range accountArray {
            if username == account.Username {
                accountArray[i].Digest, _ = createHash(password)
                hasAccount = true
            }
        }
    }
 
    if !hasAccount {
        digest, _ := createHash(password)
        accountArray = append(accountArray, Account{
            Username: username,
            Digest: digest,
        })
    }
 
    return accountDB.writeFile(accountArray)
}
 
func (accountDB AccountDB) Delete(username string) error {
 
    var accountArray []Account
    var reducedArray []Account
    var err error
 
    accountArray, err = accountDB.readFile()
    hasAccount := false
    if err == nil && accountArray != nil {
        accountArray, _= accountDB.uniqAccounts(accountArray)
        for _, account := range accountArray {
            if username == account.Username {
                hasAccount = true
                continue
            }
            reducedArray = append(reducedArray, account)
        }
    }
    if hasAccount {
        return accountDB.writeFile(reducedArray)
    }
    return nil
}
 
func New(fileName string) *AccountDB {
    fileName, _ = filepath.Abs(fileName)
    return &AccountDB{
        FileName: fileName,
    }
}