67 lines
1.9 KiB
Go
67 lines
1.9 KiB
Go
|
|
package security
|
||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/rand"
|
||
|
|
"encoding/hex"
|
||
|
|
"fmt"
|
||
|
|
"strings"
|
||
|
|
)
|
||
|
|
|
||
|
|
// GenerateToken returns a cryptographically random access token. The prefix
|
||
|
|
// makes accidentally-leaked tokens grep-able in logs and lets frontends tell
|
||
|
|
// app tokens apart from other credential kinds.
|
||
|
|
func GenerateToken(prefix string) (string, error) {
|
||
|
|
b := make([]byte, 24)
|
||
|
|
if _, err := rand.Read(b); err != nil {
|
||
|
|
return "", fmt.Errorf("read random: %w", err)
|
||
|
|
}
|
||
|
|
p := strings.TrimSpace(prefix)
|
||
|
|
if p == "" {
|
||
|
|
p = "ca"
|
||
|
|
}
|
||
|
|
return fmt.Sprintf("%s_live_%s", p, hex.EncodeToString(b)), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GenerateAPIKeyPreview returns the last 4 characters of an API key, for
|
||
|
|
// display in the admin UI ("...ab12") so operators can identify a key
|
||
|
|
// without exposing it.
|
||
|
|
func GenerateAPIKeyPreview(key string) string {
|
||
|
|
key = strings.TrimSpace(key)
|
||
|
|
if len(key) <= 4 {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
return key[len(key)-4:]
|
||
|
|
}
|
||
|
|
|
||
|
|
// RandomSlug returns a short random slug for cases where a name has no
|
||
|
|
// ASCII slug characters (pure CJK names, emoji, ...). Used as a fallback so
|
||
|
|
// every app/skill is still addressable on disk.
|
||
|
|
func RandomSlug() string {
|
||
|
|
b := make([]byte, 5)
|
||
|
|
if _, err := rand.Read(b); err != nil {
|
||
|
|
// rand.Read failing is catastrophic; fall back to a fixed value so we
|
||
|
|
// never return an empty slug.
|
||
|
|
return "app"
|
||
|
|
}
|
||
|
|
return "app-" + hex.EncodeToString(b)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Slugify converts an arbitrary string into a filesystem- and URL-safe slug.
|
||
|
|
// Used for app and skill identifiers that become directory / file names.
|
||
|
|
// Non-ASCII characters (CJK, etc.) are dropped; callers that need a guaranteed
|
||
|
|
// usable slug should fall back to a generated token when the result is empty.
|
||
|
|
func Slugify(s string) string {
|
||
|
|
s = strings.ToLower(strings.TrimSpace(s))
|
||
|
|
var b strings.Builder
|
||
|
|
b.Grow(len(s))
|
||
|
|
for _, r := range s {
|
||
|
|
switch {
|
||
|
|
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
|
||
|
|
b.WriteRune(r)
|
||
|
|
case r == ' ' || r == '-' || r == '_':
|
||
|
|
b.WriteRune('-')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return strings.Trim(b.String(), "-")
|
||
|
|
}
|