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://, 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) }