Files
story/internal/deviceauth/directory_secret.go
T

85 lines
2.6 KiB
Go
Raw Normal View History

2026-08-26 06:41:07 +08:00
package deviceauth
import (
"bytes"
"context"
"errors"
"io"
"os"
"path/filepath"
"regexp"
"strings"
)
const deviceSecretReferencePrefix = "device-secret://"
var deviceSecretNamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`)
// DirectorySecretResolver resolves a per-device credential from a controlled
// secret mount. It is suitable for Docker/Kubernetes Secrets and cloud KMS CSI
// drivers: the database stores only device-secret://<name>, never secret bytes.
// Files are read for every verification so an atomic secret-mount rotation is
// observed without restarting the API process.
type DirectorySecretResolver struct {
root string
}
func NewDirectorySecretResolver(root string) (*DirectorySecretResolver, error) {
if strings.TrimSpace(root) == "" || !filepath.IsAbs(root) {
return nil, errors.New("device secret directory must be an absolute path")
}
absolute, err := filepath.Abs(filepath.Clean(root))
if err != nil {
return nil, errors.New("resolve device secret directory")
}
resolved, err := filepath.EvalSymlinks(absolute)
if err != nil {
return nil, errors.New("resolve device secret directory")
}
info, err := os.Stat(resolved)
if err != nil || !info.IsDir() {
return nil, errors.New("device secret directory is not readable")
}
return &DirectorySecretResolver{root: resolved}, nil
}
func (r *DirectorySecretResolver) Resolve(ctx context.Context, reference string) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if !strings.HasPrefix(reference, deviceSecretReferencePrefix) {
return nil, ErrCredentialInvalid
}
name := strings.TrimPrefix(reference, deviceSecretReferencePrefix)
if !deviceSecretNamePattern.MatchString(name) || name == "." || name == ".." {
return nil, ErrCredentialInvalid
}
candidate := filepath.Join(r.root, name)
resolved, err := filepath.EvalSymlinks(candidate)
if err != nil || !pathWithin(r.root, resolved) {
return nil, ErrCredentialInvalid
}
file, err := os.Open(resolved)
if err != nil {
return nil, ErrCredentialInvalid
}
defer file.Close()
secret, err := io.ReadAll(io.LimitReader(file, 4097))
if err != nil || len(secret) > 4096 {
return nil, ErrCredentialInvalid
}
secret = bytes.TrimRight(secret, "\r\n")
if len(secret) < 32 {
return nil, ErrCredentialInvalid
}
if err := ctx.Err(); err != nil {
return nil, err
}
return append([]byte(nil), secret...), nil
}
func pathWithin(root, candidate string) bool {
relative, err := filepath.Rel(root, candidate)
return err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) && !filepath.IsAbs(relative)
}