package authz import ( "crypto/rand" "encoding/csv" "encoding/hex" "fmt" "log/slog" "os" "sync" "golang.org/x/crypto/bcrypt" ) // TokenSize is the number of random bytes used for token generation const TokenSize = 32 // AccessID represents a unique authentication identifier type AccessID string // FriendlyName represents a human-readable identifier type FriendlyName string // TokenMap maps AccessIDs to password hashes type TokenMap map[AccessID]string // SafeTokenMap provides thread-safe access to TokenMap type SafeTokenMap struct { mu sync.RWMutex tokens TokenMap } // NewSafeTokenMap creates a new thread-safe token map func NewSafeTokenMap() *SafeTokenMap { return &SafeTokenMap{ tokens: make(TokenMap), } } // Get retrieves a hash for the given AccessID func (s *SafeTokenMap) Get(id AccessID) (string, bool) { s.mu.RLock() defer s.mu.RUnlock() hash, exists := s.tokens[id] return hash, exists } // Set stores a hash for the given AccessID func (s *SafeTokenMap) Set(id AccessID, hash string) { s.mu.Lock() defer s.mu.Unlock() s.tokens[id] = hash } // LoadFromFile loads tokens from a CSV file func (s *SafeTokenMap) LoadFromFile(path string) error { tokens, _, err := LoadTokensFromFile(path) if err != nil { return err } s.mu.Lock() defer s.mu.Unlock() s.tokens = tokens return nil } // IdentityMap manages mappings between AccessIDs and FriendlyNames type IdentityMap struct { mu sync.RWMutex IDToName map[AccessID]FriendlyName NameToID map[FriendlyName]AccessID } // NewTokenMap creates a new token map func NewTokenMap() TokenMap { return make(TokenMap) } // NewIdentityMap creates a new identity mapping func NewIdentityMap() *IdentityMap { return &IdentityMap{ IDToName: make(map[AccessID]FriendlyName), NameToID: make(map[FriendlyName]AccessID), } } // LoadTokensFromFile loads tokens and identities from a csv file func LoadTokensFromFile(path string) (TokenMap, *IdentityMap, error) { tm := make(TokenMap) im := NewIdentityMap() contents, err := os.Open(path) if err != nil { slog.Error("File reading error", slog.Any("error", err)) return nil, nil, err } defer contents.Close() r := csv.NewReader(contents) tokens, err := r.ReadAll() if err != nil { return nil, nil, fmt.Errorf("file reading error: %w", err) } for _, row := range tokens { if len(row) != 3 { return nil, nil, fmt.Errorf("invalid row format, expected: access_id,friendly_name,hash") } accessID, friendlyName, hash := AccessID(row[0]), FriendlyName(row[1]), row[2] tm[accessID] = hash im.Register(accessID, friendlyName) } return tm, im, nil } // Register adds a mapping between an AccessID and FriendlyName func (im *IdentityMap) Register(id AccessID, name FriendlyName) { im.mu.Lock() defer im.mu.Unlock() im.IDToName[id] = name im.NameToID[name] = id } // GetID retrieves the AccessID for a given FriendlyName func (im *IdentityMap) GetID(name FriendlyName) (AccessID, bool) { im.mu.RLock() defer im.mu.RUnlock() id, exists := im.NameToID[name] return id, exists } // GetName retrieves the FriendlyName for a given AccessID func (im *IdentityMap) GetName(id AccessID) (FriendlyName, bool) { im.mu.RLock() defer im.mu.RUnlock() name, exists := im.IDToName[id] return name, exists } // GenerateAccessID creates a new random access identifier func GenerateAccessID() (AccessID, error) { idBytes := make([]byte, 16) // 16 bytes = 128 bits if _, err := rand.Read(idBytes); err != nil { return "", fmt.Errorf("failed to generate access ID: %w", err) } return AccessID(hex.EncodeToString(idBytes)), nil } // GenerateNewToken generates a new secure random token and its bcrypt hash // The token is 32 bytes (256 bits) of cryptographically secure random data // encoded as a 64-character hex string. The hash is a bcrypt hash of the // random bytes using default cost parameters. func GenerateNewToken() (string, string, error) { tokenBytes := make([]byte, TokenSize) if _, err := rand.Read(tokenBytes); err != nil { return "", "", fmt.Errorf("failed to generate random token: %w", err) } hashBytes, err := bcrypt.GenerateFromPassword(tokenBytes, bcrypt.DefaultCost) if err != nil { return "", "", err } token := hex.EncodeToString(tokenBytes) hash := string(hashBytes) return token, hash, nil }