mirror of
https://github.com/therootcompany/golib.git
synced 2026-03-02 23:57:59 +00:00
feat(auth/csvauth): add token support,make secrets non-printing
This commit is contained in:
parent
dd48b2420b
commit
85d42550bf
@ -33,6 +33,7 @@ func showHelp() {
|
||||
fmt.Fprintf(os.Stderr, `csvauth - create, update, and verify users, passwords, and tokens
|
||||
|
||||
EXAMPLES
|
||||
csvauth store --token 'my-new-token'
|
||||
csvauth store --ask-password 'my-new-user'
|
||||
csvauth verify 'my-new-user'
|
||||
|
||||
@ -255,12 +256,13 @@ func handleInit(keyenv, keypath, csvpath string) error {
|
||||
|
||||
func handleSet(args []string, aesKey []byte, csvFile csvauth.NamedReadCloser) {
|
||||
storeFlags := flag.NewFlagSet("csvauth-store", flag.ContinueOnError)
|
||||
purpose := storeFlags.String("purpose", "login", "'login' for users, or a service account name, such as 'basecamp_api_key'")
|
||||
purpose := storeFlags.String("purpose", "login", "'login' for users, 'token' for tokens, or a service account name, such as 'basecamp_api_key'")
|
||||
roleList := storeFlags.String("roles", "", "a comma- or space-separated list of roles (defined by you), such as 'triage audit'")
|
||||
extra := storeFlags.String("extra", "", "free form data to retrieve with the user (hint: JSON might be nice)")
|
||||
algorithm := storeFlags.String("algorithm", "", "Hash algorithm: aes, plain, pbkdf2[,iters[,size[,hash]]], or bcrypt[,cost]")
|
||||
askPassword := storeFlags.Bool("ask-password", false, "Read password from stdin")
|
||||
passwordFile := storeFlags.String("password-file", "", "Read password from file")
|
||||
askPassword := storeFlags.Bool("ask-password", false, "Read password or token from stdin")
|
||||
useToken := storeFlags.Bool("token", false, "generate token")
|
||||
passwordFile := storeFlags.String("password-file", "", "Read password or token from file")
|
||||
// storeFlags.StringVar(&tsvPath, "tsv", tsvPath, "Credentials file to use")
|
||||
if err := storeFlags.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
@ -276,20 +278,38 @@ func handleSet(args []string, aesKey []byte, csvFile csvauth.NamedReadCloser) {
|
||||
|
||||
name := storeFlags.Arg(0)
|
||||
switch name {
|
||||
case "id", "name", "purpose":
|
||||
case "", "id", "name", "purpose":
|
||||
if *useToken {
|
||||
fmt.Fprintf(os.Stderr, "invalid token name %q\n", name)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "invalid username %q\n", name)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *useToken {
|
||||
if *purpose != csvauth.PurposeDefault && *purpose != csvauth.PurposeToken {
|
||||
fmt.Fprintf(os.Stderr, "token purpose must be 'token', not %q\n", *purpose)
|
||||
os.Exit(1)
|
||||
}
|
||||
*purpose = csvauth.PurposeToken
|
||||
}
|
||||
|
||||
if len(*algorithm) == 0 {
|
||||
if *purpose == "login" {
|
||||
switch *purpose {
|
||||
case csvauth.PurposeDefault:
|
||||
*algorithm = "pbkdf2"
|
||||
} else {
|
||||
case csvauth.PurposeToken:
|
||||
fallthrough
|
||||
// *algorithm = "plain"
|
||||
default:
|
||||
*algorithm = "aes-128-gcm"
|
||||
}
|
||||
}
|
||||
if *purpose != "login" {
|
||||
switch *purpose {
|
||||
case csvauth.PurposeDefault, csvauth.PurposeToken:
|
||||
// no change
|
||||
default:
|
||||
*askPassword = true
|
||||
}
|
||||
|
||||
@ -339,7 +359,7 @@ func handleSet(args []string, aesKey []byte, csvFile csvauth.NamedReadCloser) {
|
||||
_ = csvFile.Close()
|
||||
|
||||
var exists bool
|
||||
if len(*purpose) > 0 && *purpose != "login" {
|
||||
if len(*purpose) > 0 && *purpose != csvauth.PurposeDefault && *purpose != csvauth.PurposeToken {
|
||||
if _, err := auth.LoadServiceAccount(*purpose); err != nil {
|
||||
if !errors.Is(err, csvauth.ErrNotFound) {
|
||||
fmt.Fprintf(os.Stderr, "could not load %s: %v\n", *purpose, err)
|
||||
@ -382,9 +402,10 @@ func handleSet(args []string, aesKey []byte, csvFile csvauth.NamedReadCloser) {
|
||||
|
||||
func handleCheck(args []string, aesKey []byte, csvFile csvauth.NamedReadCloser) {
|
||||
checkFlags := flag.NewFlagSet("csvauth-check", flag.ContinueOnError)
|
||||
purpose := checkFlags.String("purpose", "login", "'login' for users, or a service account name, such as 'basecamp_api_key'")
|
||||
_ = checkFlags.Bool("ask-password", true, "Read password from stdin")
|
||||
passwordFile := checkFlags.String("password-file", "", "Read password from file")
|
||||
purpose := checkFlags.String("purpose", "login", "'login' for users, 'token' for tokens, or a service account name, such as 'basecamp_api_key'")
|
||||
_ = checkFlags.Bool("ask-password", true, "Read password or token from stdin")
|
||||
useToken := checkFlags.Bool("token", false, "generate token")
|
||||
passwordFile := checkFlags.String("password-file", "", "Read password or token from file")
|
||||
// storeFlags.StringVar(&tsvPath, "tsv", tsvPath, "Credentials file to use")
|
||||
if err := checkFlags.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
@ -400,10 +421,24 @@ func handleCheck(args []string, aesKey []byte, csvFile csvauth.NamedReadCloser)
|
||||
|
||||
name := checkFlags.Arg(0)
|
||||
switch name {
|
||||
case "id", "name", "purpose":
|
||||
case "", "id", "name", "purpose":
|
||||
if !*useToken {
|
||||
fmt.Fprintf(os.Stderr, "invalid username %q\n", name)
|
||||
os.Exit(1)
|
||||
}
|
||||
if name != "" {
|
||||
fmt.Fprintf(os.Stderr, "invalid token name %q\n", name)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if *useToken {
|
||||
if *purpose != csvauth.PurposeDefault && *purpose != csvauth.PurposeToken {
|
||||
fmt.Fprintf(os.Stderr, "token purpose must be 'token', not %q\n", *purpose)
|
||||
os.Exit(1)
|
||||
}
|
||||
*purpose = csvauth.PurposeToken
|
||||
}
|
||||
|
||||
var pass string
|
||||
if len(*passwordFile) > 0 {
|
||||
@ -434,7 +469,7 @@ func handleCheck(args []string, aesKey []byte, csvFile csvauth.NamedReadCloser)
|
||||
|
||||
var v csvauth.BasicAuthVerifier
|
||||
var err error
|
||||
if *purpose != "login" {
|
||||
if *purpose != csvauth.PurposeDefault && *purpose != csvauth.PurposeToken {
|
||||
v, err = auth.LoadServiceAccount(*purpose)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "couldn't load %s: %v", *purpose, err)
|
||||
@ -444,7 +479,13 @@ func handleCheck(args []string, aesKey []byte, csvFile csvauth.NamedReadCloser)
|
||||
v = auth
|
||||
}
|
||||
|
||||
if err := v.Verify(name, pass); err != nil {
|
||||
if *purpose == csvauth.PurposeToken {
|
||||
if err := auth.VerifyToken(pass); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "token not verified: %v\n", err)
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
} else if err := v.Verify(name, pass); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "user '%s' not found or incorrect secret\n", name)
|
||||
os.Exit(1)
|
||||
return
|
||||
|
||||
@ -14,7 +14,13 @@ type BasicAuthVerifier interface {
|
||||
Verify(string, string) error
|
||||
}
|
||||
|
||||
const DefaultPurpose = "login"
|
||||
const (
|
||||
// deprecated, misspelling of PurposeDefault
|
||||
DefaultPurpose = "login"
|
||||
PurposeDefault = "login"
|
||||
PurposeToken = "token"
|
||||
hashIDSep = "~"
|
||||
)
|
||||
|
||||
type Purpose = string
|
||||
type Name = string
|
||||
@ -43,6 +49,7 @@ type Credential struct {
|
||||
Derived []byte
|
||||
Roles []string
|
||||
Extra string
|
||||
hashID string
|
||||
}
|
||||
|
||||
func (c Credential) Secret() string {
|
||||
@ -64,13 +71,20 @@ func FromRecord(record []string) (Credential, error) {
|
||||
|
||||
func FromFields(purpose, name, paramList, saltBase64, derived, roleList, extra string) (Credential, error) {
|
||||
var credential Credential
|
||||
credential.Name = name
|
||||
|
||||
if len(purpose) == 0 {
|
||||
purpose = DefaultPurpose
|
||||
purpose = PurposeDefault
|
||||
}
|
||||
credential.Purpose = purpose
|
||||
|
||||
if credential.Purpose == PurposeToken {
|
||||
name, hashID, _ := splitLast(name, hashIDSep)
|
||||
credential.Name = name
|
||||
// this can only be verified if plain or aes
|
||||
credential.hashID = hashID
|
||||
} else {
|
||||
credential.Name = name
|
||||
}
|
||||
|
||||
var roles []string
|
||||
if len(roleList) > 0 {
|
||||
roleList = strings.ReplaceAll(roleList, ",", " ")
|
||||
@ -156,6 +170,19 @@ func FromFields(purpose, name, paramList, saltBase64, derived, roleList, extra s
|
||||
return credential, nil
|
||||
}
|
||||
|
||||
func splitLast(s, sep string) (before, after string, found bool) {
|
||||
if sep == "" {
|
||||
return s, "", false
|
||||
}
|
||||
|
||||
idx := strings.LastIndex(s, sep)
|
||||
if idx == -1 {
|
||||
return s, "", false
|
||||
}
|
||||
|
||||
return s[:idx], s[idx+len(sep):], true
|
||||
}
|
||||
|
||||
func (c Credential) ToRecord() []string {
|
||||
var paramList, salt, derived string
|
||||
|
||||
@ -178,9 +205,13 @@ func (c Credential) ToRecord() []string {
|
||||
|
||||
purpose := c.Purpose
|
||||
if len(purpose) == 0 {
|
||||
purpose = DefaultPurpose
|
||||
purpose = PurposeDefault
|
||||
}
|
||||
|
||||
record := []string{purpose, c.Name, paramList, salt, derived, strings.Join(c.Roles, " "), c.Extra}
|
||||
name := c.Name
|
||||
if c.hashID != "" {
|
||||
name += hashIDSep + c.hashID
|
||||
}
|
||||
record := []string{purpose, name, paramList, salt, derived, strings.Join(c.Roles, " "), c.Extra}
|
||||
return record
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ service1 acme aes-128-gcm 2z92DVgMF9Hn-GBy i37kF34cwa64j3tmnrvlJ5ZSekWD-w token
|
||||
service2 acme plain token2 token2
|
||||
service3 user3 pbkdf2 1000 16 SHA-256 DYdA9iz1EN81bESTXcSgUg IzkeBCxRVmqybOBeAntfdA token3
|
||||
service4 user4 bcrypt $2a$12$HueMNxFGYIYtNNTySFW/Lu4vAMqpdcchBnJrW.VdYgP9xPQdITipu token4
|
||||
token api~vkdAIZ2O aes-128-gcm kRHkOnbVdKfxwWNo wC7edBEuz5htfvaagBfRUOHc60g api1
|
||||
token api~b5ZF2sRQ aes-128-gcm GowoB3l3eDiE4bPI rtXBX9QbdrochVlr1SG8UWGllao api2
|
||||
login user1 pbkdf2 1000 16 SHA-256 R-NgfDcY1A6L5a4jO89TNw -Pe9o-NwYvF6M4tlCwhm_g pass1
|
||||
login user2 bcrypt $2a$12$pad8UgUphO43PioF1JlSHOblRPdaX.ikTqjA8D1EfrcBiNGI9WQ/y pass2
|
||||
login user3 aes-128-gcm YC0xno0-W9pWR6rK D9CZFCtGGJecLpCv2Fk1I-wcXmN3 pass3
|
||||
|
||||
|
@ -4,10 +4,12 @@ import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/pbkdf2"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -27,6 +29,7 @@ import (
|
||||
var ErrNotFound = errors.New("not found")
|
||||
var ErrUnauthorized = errors.New("unauthorized")
|
||||
var ErrUnknownAlgorithm = errors.New("unknown algorithm")
|
||||
var ErrLockedCredential = errors.New("credential is locked")
|
||||
|
||||
const (
|
||||
defaultIters = 1000 // original 2000 recommendation
|
||||
@ -64,6 +67,7 @@ func NewNamedReadCloser(r io.ReadCloser, name string) NamedReadCloser {
|
||||
type Auth struct {
|
||||
aes128key [16]byte
|
||||
credentials map[Name]Credential
|
||||
tokens map[string]Credential
|
||||
serviceAccounts map[Purpose]Credential
|
||||
mux sync.Mutex
|
||||
}
|
||||
@ -76,6 +80,7 @@ func New(aes128key []byte) *Auth {
|
||||
return &Auth{
|
||||
aes128key: aes128Arr,
|
||||
credentials: map[Name]Credential{},
|
||||
tokens: map[string]Credential{},
|
||||
serviceAccounts: map[Purpose]Credential{},
|
||||
}
|
||||
}
|
||||
@ -115,12 +120,24 @@ func (a *Auth) LoadCSV(f NamedReadCloser, comma rune) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(credential.Purpose) == 0 || credential.Purpose == DefaultPurpose {
|
||||
if _, ok := a.credentials[credential.Name]; ok {
|
||||
switch credential.Purpose {
|
||||
case "", PurposeDefault, PurposeToken:
|
||||
name := credential.Name
|
||||
if credential.Purpose == PurposeToken {
|
||||
name += hashIDSep + credential.hashID
|
||||
}
|
||||
|
||||
if _, ok := a.credentials[name]; ok {
|
||||
fmt.Fprintf(os.Stderr, "overwriting cache of previous value for %s: %s\n", credential.Purpose, credential.Name)
|
||||
}
|
||||
a.credentials[credential.Name] = credential
|
||||
} else {
|
||||
a.credentials[name] = credential
|
||||
if credential.Purpose == PurposeToken {
|
||||
if _, ok := a.tokens[credential.hashID]; ok {
|
||||
fmt.Fprintf(os.Stderr, "overwriting cache of previous value for %s: %s\n", credential.Purpose, credential.Name)
|
||||
}
|
||||
a.tokens[credential.hashID] = credential
|
||||
}
|
||||
default:
|
||||
if _, ok := a.serviceAccounts[credential.Purpose]; ok {
|
||||
fmt.Fprintf(os.Stderr, "overwriting cache of previous value for %s: %s\n", credential.Purpose, credential.Name)
|
||||
}
|
||||
@ -144,6 +161,10 @@ func (a *Auth) NewCredential(purpose, name, secret string, params []string, role
|
||||
Extra: extra,
|
||||
}
|
||||
|
||||
if purpose == PurposeToken {
|
||||
c.hashID = a.tokenCacheID(secret)
|
||||
}
|
||||
|
||||
switch c.Params[0] {
|
||||
case "plain":
|
||||
if len(params) != 1 {
|
||||
@ -304,8 +325,17 @@ func (a *Auth) LoadCredential(name Name) (Credential, error) {
|
||||
|
||||
func (a *Auth) CacheCredential(c Credential) error {
|
||||
a.mux.Lock()
|
||||
a.credentials[c.Name] = c
|
||||
a.mux.Unlock()
|
||||
defer a.mux.Unlock()
|
||||
|
||||
name := c.Name
|
||||
if c.Purpose == PurposeToken {
|
||||
name += hashIDSep + c.hashID
|
||||
}
|
||||
a.credentials[name] = c
|
||||
|
||||
if c.Purpose == PurposeToken {
|
||||
a.tokens[c.hashID] = c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -383,12 +413,48 @@ func (a *Auth) Verify(name, secret string) error {
|
||||
return c.Verify(name, secret)
|
||||
}
|
||||
|
||||
func (a *Auth) VerifyToken(secret string) error {
|
||||
hashID := a.tokenCacheID(secret)
|
||||
|
||||
a.mux.Lock()
|
||||
c, ok := a.tokens[hashID]
|
||||
a.mux.Unlock()
|
||||
|
||||
if !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
if c.plain == "" {
|
||||
var err error
|
||||
if c.plain, err = a.maybeDecryptCredential(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return c.Verify(hashID, secret)
|
||||
}
|
||||
|
||||
func (a *Auth) tokenCacheID(secret string) string {
|
||||
key := a.aes128key[:]
|
||||
mac := hmac.New(sha256.New, key)
|
||||
message := []byte(secret)
|
||||
mac.Write(message)
|
||||
nameBytes := mac.Sum(nil)[:6]
|
||||
|
||||
name := base64.RawURLEncoding.EncodeToString(nameBytes)
|
||||
return name
|
||||
}
|
||||
|
||||
// Verify checks Basic Auth credentials
|
||||
func (c Credential) Verify(name, secret string) error {
|
||||
// (name is ignored, as it is assumed to have been used for lookup)
|
||||
func (c Credential) Verify(_, secret string) error {
|
||||
known := c.Derived
|
||||
var derived []byte
|
||||
switch c.Params[0] {
|
||||
case "aes-128-gcm":
|
||||
// we hash because encrypted comparisons are NOT timing safe
|
||||
if c.plain == "" {
|
||||
return ErrLockedCredential
|
||||
}
|
||||
knownHash := sha256.Sum256([]byte(c.plain))
|
||||
known = knownHash[:]
|
||||
|
||||
@ -421,6 +487,7 @@ func (c Credential) Verify(name, secret string) error {
|
||||
return ErrUnknownAlgorithm
|
||||
}
|
||||
|
||||
// all values MUST be hashed before comparing, for timing safety
|
||||
if bytes.Equal(known, derived) {
|
||||
return nil
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user