Просмотр исходного кода

fix(nodetoken): stop refusing every node-token key file on Windows

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.
MHSanaei 6 часов назад
Родитель
Сommit
3fc3992a46

+ 1 - 1
docs/content/docs/en/reference/env-vars.mdx

@@ -42,7 +42,7 @@ default. Encryption at rest is opt-in and fails closed: with any mode other than
 | Variable                  | Default                         | Description                                                                                                                                                            |
 | Variable                  | Default                         | Description                                                                                                                                                            |
 | ------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | ------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | `NODE_TOKEN_ENCRYPTION`   | `off`                           | `off`, `migration` (reads accept plaintext or ciphertext, writes encrypt), or `required` (same writes, startup fails without a key). Note the missing `XUI_` prefix.    |
 | `NODE_TOKEN_ENCRYPTION`   | `off`                           | `off`, `migration` (reads accept plaintext or ciphertext, writes encrypt), or `required` (same writes, startup fails without a key). Note the missing `XUI_` prefix.    |
-| `XUI_NODE_TOKEN_KEY_FILE` | `/etc/x-ui/node_token_key.json` | JSON keyring, mode `0600` or stricter. Loaded first.                                                                                                                   |
+| `XUI_NODE_TOKEN_KEY_FILE` | `/etc/x-ui/node_token_key.json` | JSON keyring, mode `0600` or stricter (not checked on Windows, where NTFS permissions protect it). Loaded first.                                                       |
 | `XUI_NODE_TOKEN_KEY`      | —                               | A single base64 32-byte key, read only when the key file fails to load. Its key id is fixed to `env`, so it cannot rotate.                                              |
 | `XUI_NODE_TOKEN_KEY`      | —                               | A single base64 32-byte key, read only when the key file fails to load. Its key id is fixed to `env`, so it cannot rotate.                                              |
 
 
 The key file names the active key plus every older key still needed to decrypt:
 The key file names the active key plus every older key still needed to decrypt:

+ 1 - 1
docs/content/docs/fa/reference/env-vars.mdx

@@ -42,7 +42,7 @@ icon: Variable
 | Variable                  | Default                         | Description                                                                                                                                                            |
 | Variable                  | Default                         | Description                                                                                                                                                            |
 | ------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | ------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | `NODE_TOKEN_ENCRYPTION`   | `off`                           | ‏`off`، `migration` (خواندن هم متن ساده و هم متن رمزشده را می‌پذیرد، نوشتن همیشه رمز می‌کند) یا `required` (نوشتن یکسان، اما بدون کلید اجرا شکست می‌خورد). به نبودِ پیشوند `XUI_` توجه کنید. |
 | `NODE_TOKEN_ENCRYPTION`   | `off`                           | ‏`off`، `migration` (خواندن هم متن ساده و هم متن رمزشده را می‌پذیرد، نوشتن همیشه رمز می‌کند) یا `required` (نوشتن یکسان، اما بدون کلید اجرا شکست می‌خورد). به نبودِ پیشوند `XUI_` توجه کنید. |
-| `XUI_NODE_TOKEN_KEY_FILE` | `/etc/x-ui/node_token_key.json` | حلقه‌کلید JSON با دسترسی `0600` یا محدودتر. نخست همین بارگذاری می‌شود.                                                                                                 |
+| `XUI_NODE_TOKEN_KEY_FILE` | `/etc/x-ui/node_token_key.json` | حلقه‌کلید JSON با دسترسی `0600` یا محدودتر (در ویندوز بررسی نمی‌شود و مجوزهای NTFS از آن محافظت می‌کنند). نخست همین بارگذاری می‌شود.                                     |
 | `XUI_NODE_TOKEN_KEY`      | —                               | یک کلید ۳۲ بایتی base64 که فقط هنگام شکست بارگذاری فایل کلید خوانده می‌شود. شناسه‌ی کلید آن ثابت و برابر `env` است، پس امکان چرخش ندارد.                                |
 | `XUI_NODE_TOKEN_KEY`      | —                               | یک کلید ۳۲ بایتی base64 که فقط هنگام شکست بارگذاری فایل کلید خوانده می‌شود. شناسه‌ی کلید آن ثابت و برابر `env` است، پس امکان چرخش ندارد.                                |
 
 
 فایل کلید، کلید فعال به‌همراه هر کلید قدیمی‌ای را که هنوز برای رمزگشایی لازم است نام می‌برد:
 فایل کلید، کلید فعال به‌همراه هر کلید قدیمی‌ای را که هنوز برای رمزگشایی لازم است نام می‌برد:

+ 1 - 1
docs/content/docs/ru/reference/env-vars.mdx

@@ -43,7 +43,7 @@ API-токены узлов — и сохранённый токен PIA — п
 | Variable                  | Default                         | Description                                                                                                                                                            |
 | Variable                  | Default                         | Description                                                                                                                                                            |
 | ------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | ------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | `NODE_TOKEN_ENCRYPTION`   | `off`                           | `off`, `migration` (чтение принимает открытый текст или шифротекст, запись всегда шифрует) или `required` (запись та же, но без ключа запуск не удастся). Префикса `XUI_` здесь нет. |
 | `NODE_TOKEN_ENCRYPTION`   | `off`                           | `off`, `migration` (чтение принимает открытый текст или шифротекст, запись всегда шифрует) или `required` (запись та же, но без ключа запуск не удастся). Префикса `XUI_` здесь нет. |
-| `XUI_NODE_TOKEN_KEY_FILE` | `/etc/x-ui/node_token_key.json` | JSON-связка ключей с правами `0600` или строже. Загружается первой.                                                                                                    |
+| `XUI_NODE_TOKEN_KEY_FILE` | `/etc/x-ui/node_token_key.json` | JSON-связка ключей с правами `0600` или строже (в Windows не проверяется: файл защищают права NTFS). Загружается первой.                                               |
 | `XUI_NODE_TOKEN_KEY`      | —                               | Один 32-байтный ключ в base64, читается только при неудачной загрузке файла ключей. Его идентификатор фиксирован (`env`), поэтому ротация невозможна.                   |
 | `XUI_NODE_TOKEN_KEY`      | —                               | Один 32-байтный ключ в base64, читается только при неудачной загрузке файла ключей. Его идентификатор фиксирован (`env`), поэтому ротация невозможна.                   |
 
 
 Файл ключей задаёт активный ключ и все прежние ключи, ещё нужные для расшифровки:
 Файл ключей задаёт активный ключ и все прежние ключи, ещё нужные для расшифровки:

+ 1 - 1
docs/content/docs/zh/reference/env-vars.mdx

@@ -40,7 +40,7 @@ icon: Variable
 | Variable                  | Default                         | Description                                                                                                                                                            |
 | Variable                  | Default                         | Description                                                                                                                                                            |
 | ------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | ------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | `NODE_TOKEN_ENCRYPTION`   | `off`                           | `off`、`migration`(读取时接受明文或密文,写入一律加密)或 `required`(写入相同,但缺少密钥时启动失败)。注意此处没有 `XUI_` 前缀。                                     |
 | `NODE_TOKEN_ENCRYPTION`   | `off`                           | `off`、`migration`(读取时接受明文或密文,写入一律加密)或 `required`(写入相同,但缺少密钥时启动失败)。注意此处没有 `XUI_` 前缀。                                     |
-| `XUI_NODE_TOKEN_KEY_FILE` | `/etc/x-ui/node_token_key.json` | JSON 密钥环,权限须为 `0600` 或更严格。优先加载。                                                                                                                      |
+| `XUI_NODE_TOKEN_KEY_FILE` | `/etc/x-ui/node_token_key.json` | JSON 密钥环,权限须为 `0600` 或更严格(Windows 上不检查,由 NTFS 权限保护)。优先加载。                                                                                |
 | `XUI_NODE_TOKEN_KEY`      | —                               | 单个 base64 编码的 32 字节密钥,仅在密钥文件加载失败时读取。其密钥 ID 固定为 `env`,因此无法轮换。                                                                      |
 | `XUI_NODE_TOKEN_KEY`      | —                               | 单个 base64 编码的 32 字节密钥,仅在密钥文件加载失败时读取。其密钥 ID 固定为 `env`,因此无法轮换。                                                                      |
 
 
 密钥文件同时记录活动密钥和所有仍需用于解密的旧密钥:
 密钥文件同时记录活动密钥和所有仍需用于解密的旧密钥:

+ 4 - 2
internal/crypto/nodetoken/keysource.go

@@ -6,6 +6,7 @@ import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
+	"runtime"
 	"strings"
 	"strings"
 )
 )
 
 
@@ -76,7 +77,8 @@ func decodeKey(b64 string) ([keyLen]byte, error) {
 	return out, nil
 	return out, nil
 }
 }
 
 
-// FileKeySource accepts only key files that are mode 0600 or stricter.
+// 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 {
 type FileKeySource struct {
 	Path string
 	Path string
 }
 }
@@ -86,7 +88,7 @@ func (f FileKeySource) Load() (*Keyring, error) {
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("nodetoken: stat key file %s: %w", f.Path, err)
 		return nil, fmt.Errorf("nodetoken: stat key file %s: %w", f.Path, err)
 	}
 	}
-	if perm := info.Mode().Perm(); perm&0o077 != 0 {
+	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)
 		return nil, fmt.Errorf("nodetoken: key file %s has insecure mode %#o (want 0600)", f.Path, perm)
 	}
 	}
 	data, err := os.ReadFile(f.Path)
 	data, err := os.ReadFile(f.Path)

+ 28 - 9
internal/crypto/nodetoken/nodetoken_test.go

@@ -3,8 +3,10 @@ package nodetoken
 import (
 import (
 	"encoding/base64"
 	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
+	"fmt"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
+	"runtime"
 	"strings"
 	"strings"
 	"testing"
 	"testing"
 )
 )
@@ -212,21 +214,26 @@ func TestParseMode(t *testing.T) {
 	}
 	}
 }
 }
 
 
-func TestFileKeySourceRejectsLoosePerms(t *testing.T) {
-	dir := t.TempDir()
-	p := filepath.Join(dir, "k.json")
+// writeKeyFile writes a one-key keyring and chmods it, since WriteFile's mode
+// passes through the umask.
+func writeKeyFile(t *testing.T, mode os.FileMode) string {
+	t.Helper()
+	p := filepath.Join(t.TempDir(), "k.json")
 	key := make([]byte, keyLen)
 	key := make([]byte, keyLen)
 	body, _ := json.Marshal(keyFile{Active: "k1", Keys: map[string]string{"k1": base64.StdEncoding.EncodeToString(key)}})
 	body, _ := json.Marshal(keyFile{Active: "k1", Keys: map[string]string{"k1": base64.StdEncoding.EncodeToString(key)}})
-	if err := os.WriteFile(p, body, 0o644); err != nil {
+	if err := os.WriteFile(p, body, mode); err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
-	if _, err := (FileKeySource{Path: p}).Load(); err == nil {
-		t.Fatal("0644 key file must be rejected")
-	}
-	if err := os.Chmod(p, 0o600); err != nil {
+	if err := os.Chmod(p, mode); err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
-	kr, err := (FileKeySource{Path: p}).Load()
+	return p
+}
+
+// Windows reports every writable file as 0666, so a mode check there refused
+// every key file, an owner-only one included.
+func TestFileKeySourceLoadsOwnerOnlyKeyFile(t *testing.T) {
+	kr, err := (FileKeySource{Path: writeKeyFile(t, 0o600)}).Load()
 	if err != nil {
 	if err != nil {
 		t.Fatalf("0600 key file should load: %v", err)
 		t.Fatalf("0600 key file should load: %v", err)
 	}
 	}
@@ -235,6 +242,18 @@ func TestFileKeySourceRejectsLoosePerms(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestFileKeySourceRejectsLoosePerms(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("POSIX permission bits are not meaningful on Windows")
+	}
+	p := writeKeyFile(t, 0o644)
+	_, err := (FileKeySource{Path: p}).Load()
+	want := fmt.Sprintf("nodetoken: key file %s has insecure mode 0644 (want 0600)", p)
+	if err == nil || err.Error() != want {
+		t.Fatalf("Load() error = %v, want %q", err, want)
+	}
+}
+
 func TestEnvKeySource(t *testing.T) {
 func TestEnvKeySource(t *testing.T) {
 	key := make([]byte, keyLen)
 	key := make([]byte, keyLen)
 	for i := range key {
 	for i := range key {