mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-01 19:52:09 +03:00
3fc3992a46
FileKeySource rejected any key file whose mode had group or other bits, but Windows has no such bits: Stat reports every writable file as 0666. On the Windows builds release.yml ships, the key file therefore never loaded, not even one written 0600, and only XUI_NODE_TOKEN_KEY could supply a key. The mode check now applies off Windows only, the stance the DB permission tests already take; there the file's NTFS ACL guards it, and env-vars.mdx says so in all four locales. The load test is split so the half that must hold everywhere, an owner-only file loading, also runs on Windows, and the rejection half asserts the exact error instead of any error.
118 lines
3.3 KiB
Go
118 lines
3.3 KiB
Go
package nodetoken
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
// KeySource loads a startup keyring from a protected file or environment.
|
|
// Keys are never accepted on the command line.
|
|
type KeySource interface {
|
|
Load() (*Keyring, error)
|
|
}
|
|
|
|
// keyFile identifies the active key and all base64-encoded rotation keys.
|
|
type keyFile struct {
|
|
Active string `json:"active"`
|
|
Keys map[string]string `json:"keys"`
|
|
}
|
|
|
|
func parseKeyring(active string, b64keys map[string]string) (*Keyring, error) {
|
|
if err := validateKeyID(active); err != nil {
|
|
return nil, fmt.Errorf("nodetoken: active key id: %w", err)
|
|
}
|
|
if active == "" {
|
|
return nil, errors.New("nodetoken: key source has no active key id")
|
|
}
|
|
if len(b64keys) == 0 {
|
|
return nil, errors.New("nodetoken: key source has no keys")
|
|
}
|
|
kr := &Keyring{ActiveID: active, Keys: make(map[string][keyLen]byte, len(b64keys))}
|
|
for id, b64 := range b64keys {
|
|
if err := validateKeyID(id); err != nil {
|
|
return nil, fmt.Errorf("nodetoken: key id %q: %w", id, err)
|
|
}
|
|
raw, err := decodeKey(b64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("nodetoken: key %q: %w", id, err)
|
|
}
|
|
kr.Keys[id] = raw
|
|
}
|
|
if _, ok := kr.Keys[active]; !ok {
|
|
return nil, fmt.Errorf("nodetoken: active key %q absent from keys", active)
|
|
}
|
|
return kr, nil
|
|
}
|
|
|
|
func validateKeyID(id string) error {
|
|
if id == "" {
|
|
return errors.New("must not be empty")
|
|
}
|
|
if strings.Contains(id, ":") {
|
|
return errors.New("must not contain ':'")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func decodeKey(b64 string) ([keyLen]byte, error) {
|
|
var out [keyLen]byte
|
|
raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(b64))
|
|
if err != nil {
|
|
// tolerate url-safe / unpadded encodings too
|
|
if raw2, err2 := base64.RawStdEncoding.DecodeString(strings.TrimSpace(b64)); err2 == nil {
|
|
raw = raw2
|
|
} else {
|
|
return out, fmt.Errorf("base64 decode: %w", err)
|
|
}
|
|
}
|
|
if len(raw) != keyLen {
|
|
return out, fmt.Errorf("key must be %d bytes, got %d", keyLen, len(raw))
|
|
}
|
|
copy(out[:], raw)
|
|
return out, nil
|
|
}
|
|
|
|
// FileKeySource accepts only key files that are mode 0600 or stricter. Windows has
|
|
// no such bits (Stat reports 0666), so there the file's NTFS ACL is what guards it.
|
|
type FileKeySource struct {
|
|
Path string
|
|
}
|
|
|
|
func (f FileKeySource) Load() (*Keyring, error) {
|
|
info, err := os.Stat(f.Path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("nodetoken: stat key file %s: %w", f.Path, err)
|
|
}
|
|
if perm := info.Mode().Perm(); runtime.GOOS != "windows" && perm&0o077 != 0 {
|
|
return nil, fmt.Errorf("nodetoken: key file %s has insecure mode %#o (want 0600)", f.Path, perm)
|
|
}
|
|
data, err := os.ReadFile(f.Path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("nodetoken: read key file %s: %w", f.Path, err)
|
|
}
|
|
var kf keyFile
|
|
if err := json.Unmarshal(data, &kf); err != nil {
|
|
return nil, fmt.Errorf("nodetoken: parse key file %s: %w", f.Path, err)
|
|
}
|
|
return parseKeyring(kf.Active, kf.Keys)
|
|
}
|
|
|
|
// EnvKeySource reads a single base64 32-byte key from an environment variable.
|
|
// The key id is fixed ("env"); for multi-key rotation prefer a key file.
|
|
type EnvKeySource struct {
|
|
Var string
|
|
}
|
|
|
|
func (e EnvKeySource) Load() (*Keyring, error) {
|
|
v := strings.TrimSpace(os.Getenv(e.Var))
|
|
if v == "" {
|
|
return nil, fmt.Errorf("nodetoken: env %s is empty", e.Var)
|
|
}
|
|
return parseKeyring("env", map[string]string{"env": v})
|
|
}
|