golib/auth/csvauth/credential.go

173 lines
4.1 KiB
Go

package csvauth
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"os"
"slices"
"strconv"
"strings"
)
type BasicAuthVerifier interface {
Verify(string, string) error
}
const DefaultPurpose = "login"
type Purpose = string
type Name = string
// Credential represents a row in the CSV file
type Credential struct {
Purpose Purpose
Name Name
plain string
Params []string
Salt []byte
Derived []byte
Roles []string
Extra string
}
func (c Credential) Secret() string {
return c.plain
}
func FromRecord(record []string) (Credential, error) {
var roleList, extra string
purpose, name, paramList, salt64, derived := record[0], record[1], record[2], record[3], record[4]
if len(record) >= 6 {
roleList = record[5]
}
if len(record) >= 7 {
extra = record[6]
}
return FromFields(purpose, name, paramList, salt64, derived, roleList, extra)
}
func FromFields(purpose, name, paramList, saltBase64, derived, roleList, extra string) (Credential, error) {
var credential Credential
credential.Name = name
if len(purpose) == 0 {
purpose = DefaultPurpose
}
credential.Purpose = purpose
var roles []string
if len(roleList) > 0 {
roleList = strings.ReplaceAll(roleList, ",", " ")
roles = strings.Split(roleList, " ")
}
credential.Roles = roles
credential.Extra = extra
paramList = strings.ReplaceAll(paramList, ",", " ")
credential.Params = strings.Split(paramList, " ")
if len(credential.Params) == 0 {
fmt.Fprintf(os.Stderr, "no algorithm parameters for %q\n", name)
}
switch credential.Params[0] {
case "aes-128-gcm":
if len(credential.Params) > 1 {
return credential, fmt.Errorf("invalid plain parameters %#v", credential.Params)
}
salt, err := base64.RawURLEncoding.DecodeString(saltBase64)
if err != nil {
return credential, err
}
credential.Salt = salt
bytes, err := base64.RawURLEncoding.DecodeString(derived)
if err != nil {
return credential, err
}
credential.Derived = bytes
case "plain":
if len(credential.Params) > 1 {
return credential, fmt.Errorf("invalid plain parameters %#v", credential.Params)
}
credential.plain = derived
h := sha256.Sum256([]byte(derived))
credential.Derived = h[:]
case "pbkdf2":
var err error
credential.Salt, err = base64.RawURLEncoding.DecodeString(saltBase64)
if err != nil {
fmt.Fprintf(os.Stderr, "could not decode salt %q for %q\n", saltBase64, name)
}
credential.Derived, err = base64.RawURLEncoding.DecodeString(derived)
if err != nil {
fmt.Fprintf(os.Stderr, "could not decode derived data %q for %q\n", derived, name)
}
iters, err := strconv.Atoi(credential.Params[1])
if err != nil {
return credential, err
}
if iters <= 0 {
return credential, fmt.Errorf("invalid iterations %s", credential.Params[1])
}
size, err := strconv.Atoi(credential.Params[2])
if err != nil {
return credential, err
}
if size < 8 || size > 32 {
return credential, fmt.Errorf("invalid size %s", credential.Params[2])
}
if !slices.Contains([]string{"SHA-256", "SHA-1"}, credential.Params[3]) {
return credential, fmt.Errorf("invalid hash %s", credential.Params[3])
}
case "bcrypt":
if len(credential.Params) > 1 {
return credential, fmt.Errorf("invalid bcrypt parameters %#v", credential.Params)
}
credential.Derived = []byte(derived)
default:
return credential, fmt.Errorf("invalid algorithm %s", credential.Params[0])
}
return credential, nil
}
func (c Credential) ToRecord() []string {
var paramList, salt, derived string
paramList = strings.Join(c.Params, " ")
switch c.Params[0] {
case "aes-128-gcm":
salt = base64.RawURLEncoding.EncodeToString(c.Salt)
derived = base64.RawURLEncoding.EncodeToString(c.Derived)
case "plain":
salt = ""
derived = c.plain
case "pbkdf2":
salt = base64.RawURLEncoding.EncodeToString(c.Salt)
derived = base64.RawURLEncoding.EncodeToString(c.Derived)
case "bcrypt":
derived = string(c.Derived)
default:
panic(fmt.Errorf("unknown algorithm %q", c.Params[0]))
}
purpose := c.Purpose
if len(purpose) == 0 {
purpose = DefaultPurpose
}
record := []string{purpose, c.Name, paramList, salt, derived, strings.Join(c.Roles, " "), c.Extra}
return record
}