mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 17:34:39 +08:00
feat: add TRANSPORT_ENCRYPTION toggle for easier deployment
- Add TRANSPORT_ENCRYPTION env config (default: false) - Allow HTTP/IP access when transport encryption is disabled - Add /api/crypto/config endpoint to expose encryption status - Update WebCryptoEnvironmentCheck with 'disabled' status - Update ExchangeConfigModal and AITradersPage to allow form submission when disabled - Add i18n translations for disabled status (EN/CN) - Update README with two deployment modes documentation
This commit is contained in:
11
.env.example
11
.env.example
@@ -37,6 +37,17 @@ DATA_ENCRYPTION_KEY=your-base64-encoded-32-byte-key
|
|||||||
# Note: Replace newlines with \n for single-line format
|
# Note: Replace newlines with \n for single-line format
|
||||||
RSA_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----\nYOUR_KEY_HERE\n-----END RSA PRIVATE KEY-----
|
RSA_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----\nYOUR_KEY_HERE\n-----END RSA PRIVATE KEY-----
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# Security Options
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# Transport encryption for API keys (default: false)
|
||||||
|
# When enabled, browser uses Web Crypto API to encrypt API keys before sending
|
||||||
|
# Requires HTTPS or localhost to work
|
||||||
|
# Set to true for enhanced security (HTTPS required)
|
||||||
|
# Set to false for easier deployment (HTTP/IP access allowed)
|
||||||
|
TRANSPORT_ENCRYPTION=false
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# Optional: External Services
|
# Optional: External Services
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|||||||
55
README.md
55
README.md
@@ -167,6 +167,61 @@ Access Web Interface: **http://localhost:3000**
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Server Deployment
|
||||||
|
|
||||||
|
### Quick Deploy (HTTP via IP)
|
||||||
|
|
||||||
|
By default, transport encryption is **disabled**, allowing you to access NOFX via IP address without HTTPS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deploy to your server
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Access via `http://YOUR_SERVER_IP:3000` - works immediately.
|
||||||
|
|
||||||
|
### Enhanced Security (HTTPS)
|
||||||
|
|
||||||
|
For enhanced security, enable transport encryption in `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TRANSPORT_ENCRYPTION=true
|
||||||
|
```
|
||||||
|
|
||||||
|
When enabled, browser uses Web Crypto API to encrypt API keys before transmission. This requires:
|
||||||
|
- `https://` - Any domain with SSL
|
||||||
|
- `http://localhost` - Local development
|
||||||
|
|
||||||
|
### Quick HTTPS Setup with Cloudflare
|
||||||
|
|
||||||
|
1. **Add your domain to Cloudflare** (free plan works)
|
||||||
|
- Go to [dash.cloudflare.com](https://dash.cloudflare.com)
|
||||||
|
- Add your domain and update nameservers
|
||||||
|
|
||||||
|
2. **Create DNS record**
|
||||||
|
- Type: `A`
|
||||||
|
- Name: `nofx` (or your subdomain)
|
||||||
|
- Content: Your server IP
|
||||||
|
- Proxy status: **Proxied** (orange cloud)
|
||||||
|
|
||||||
|
3. **Configure SSL/TLS**
|
||||||
|
- Go to SSL/TLS settings
|
||||||
|
- Set encryption mode to **Flexible**
|
||||||
|
|
||||||
|
```
|
||||||
|
User ──[HTTPS]──→ Cloudflare ──[HTTP]──→ Your Server:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Enable transport encryption**
|
||||||
|
```bash
|
||||||
|
# Edit .env and set
|
||||||
|
TRANSPORT_ENCRYPTION=true
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Done!** Access via `https://nofx.yourdomain.com`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Initial Setup (Web Interface)
|
## Initial Setup (Web Interface)
|
||||||
|
|
||||||
After starting the system, configure through the web interface:
|
After starting the system, configure through the web interface:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"nofx/config"
|
||||||
"nofx/crypto"
|
"nofx/crypto"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -20,15 +21,35 @@ func NewCryptoHandler(cryptoService *crypto.CryptoService) *CryptoHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Crypto Config Endpoint ====================
|
||||||
|
|
||||||
|
// HandleGetCryptoConfig Get crypto configuration
|
||||||
|
func (h *CryptoHandler) HandleGetCryptoConfig(c *gin.Context) {
|
||||||
|
cfg := config.Get()
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"transport_encryption": cfg.TransportEncryption,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Public Key Endpoint ====================
|
// ==================== Public Key Endpoint ====================
|
||||||
|
|
||||||
// HandleGetPublicKey Get server public key
|
// HandleGetPublicKey Get server public key
|
||||||
func (h *CryptoHandler) HandleGetPublicKey(c *gin.Context) {
|
func (h *CryptoHandler) HandleGetPublicKey(c *gin.Context) {
|
||||||
publicKey := h.cryptoService.GetPublicKeyPEM()
|
cfg := config.Get()
|
||||||
|
if !cfg.TransportEncryption {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"public_key": "",
|
||||||
|
"algorithm": "",
|
||||||
|
"transport_encryption": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, map[string]string{
|
publicKey := h.cryptoService.GetPublicKeyPEM()
|
||||||
"public_key": publicKey,
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"algorithm": "RSA-OAEP-2048",
|
"public_key": publicKey,
|
||||||
|
"algorithm": "RSA-OAEP-2048",
|
||||||
|
"transport_encryption": true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ func (s *Server) setupRoutes() {
|
|||||||
api.GET("/config", s.handleGetSystemConfig)
|
api.GET("/config", s.handleGetSystemConfig)
|
||||||
|
|
||||||
// Crypto related endpoints (no authentication required)
|
// Crypto related endpoints (no authentication required)
|
||||||
|
api.GET("/crypto/config", s.cryptoHandler.HandleGetCryptoConfig)
|
||||||
api.GET("/crypto/public-key", s.cryptoHandler.HandleGetPublicKey)
|
api.GET("/crypto/public-key", s.cryptoHandler.HandleGetPublicKey)
|
||||||
api.POST("/crypto/decrypt", s.cryptoHandler.HandleDecryptSensitiveData)
|
api.POST("/crypto/decrypt", s.cryptoHandler.HandleDecryptSensitiveData)
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ type Config struct {
|
|||||||
APIServerPort int
|
APIServerPort int
|
||||||
JWTSecret string
|
JWTSecret string
|
||||||
RegistrationEnabled bool
|
RegistrationEnabled bool
|
||||||
|
|
||||||
|
// Security configuration
|
||||||
|
// TransportEncryption enables browser-side encryption for API keys
|
||||||
|
// Requires HTTPS or localhost. Set to false for HTTP access via IP.
|
||||||
|
TransportEncryption bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes global configuration (from .env)
|
// Init initializes global configuration (from .env)
|
||||||
@@ -43,6 +48,12 @@ func Init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transport encryption: default false for easier deployment
|
||||||
|
// Set TRANSPORT_ENCRYPTION=true to enable (requires HTTPS or localhost)
|
||||||
|
if v := os.Getenv("TRANSPORT_ENCRYPTION"); v != "" {
|
||||||
|
cfg.TransportEncryption = strings.ToLower(v) == "true"
|
||||||
|
}
|
||||||
|
|
||||||
global = cfg
|
global = cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,61 @@ npm run dev
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 服务器部署
|
||||||
|
|
||||||
|
### 快速部署 (HTTP/IP 访问)
|
||||||
|
|
||||||
|
默认情况下,传输加密已**禁用**,可直接通过 IP 地址访问 NOFX:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 部署到你的服务器
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
通过 `http://你的服务器IP:3000` 访问 - 立即可用。
|
||||||
|
|
||||||
|
### 增强安全 (HTTPS)
|
||||||
|
|
||||||
|
如需增强安全性,在 `.env` 中启用传输加密:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TRANSPORT_ENCRYPTION=true
|
||||||
|
```
|
||||||
|
|
||||||
|
启用后,浏览器会使用 Web Crypto API 在传输前加密 API 密钥。此功能需要:
|
||||||
|
- `https://` - 任何有 SSL 证书的域名
|
||||||
|
- `http://localhost` - 本地开发
|
||||||
|
|
||||||
|
### Cloudflare 快速配置 HTTPS
|
||||||
|
|
||||||
|
1. **添加域名到 Cloudflare** (免费计划即可)
|
||||||
|
- 访问 [dash.cloudflare.com](https://dash.cloudflare.com)
|
||||||
|
- 添加域名并更新 DNS 服务器
|
||||||
|
|
||||||
|
2. **创建 DNS 记录**
|
||||||
|
- 类型: `A`
|
||||||
|
- 名称: `nofx` (或你的子域名)
|
||||||
|
- 内容: 你的服务器 IP
|
||||||
|
- 代理状态: **已代理** (橙色云朵)
|
||||||
|
|
||||||
|
3. **配置 SSL/TLS**
|
||||||
|
- 进入 SSL/TLS 设置
|
||||||
|
- 加密模式选择 **灵活**
|
||||||
|
|
||||||
|
```
|
||||||
|
用户 ──[HTTPS]──→ Cloudflare ──[HTTP]──→ 你的服务器:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **启用传输加密**
|
||||||
|
```bash
|
||||||
|
# 编辑 .env 并设置
|
||||||
|
TRANSPORT_ENCRYPTION=true
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **完成!** 通过 `https://nofx.你的域名.com` 访问
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 初始配置 (Web 界面)
|
## 初始配置 (Web 界面)
|
||||||
|
|
||||||
启动系统后,通过 Web 界面进行配置:
|
启动系统后,通过 Web 界面进行配置:
|
||||||
|
|||||||
@@ -1844,7 +1844,10 @@ function ExchangeConfigModal({
|
|||||||
color: '#EAECEF',
|
color: '#EAECEF',
|
||||||
}}
|
}}
|
||||||
aria-label={t('selectExchange', language)}
|
aria-label={t('selectExchange', language)}
|
||||||
disabled={webCryptoStatus !== 'secure'}
|
disabled={
|
||||||
|
webCryptoStatus !== 'secure' &&
|
||||||
|
webCryptoStatus !== 'disabled'
|
||||||
|
}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useState, type ReactNode } from 'react'
|
import { useCallback, useEffect, useState, type ReactNode } from 'react'
|
||||||
import { Loader2, ShieldAlert, ShieldCheck } from 'lucide-react'
|
import { Loader2, ShieldAlert, ShieldCheck, ShieldMinus } from 'lucide-react'
|
||||||
import { diagnoseWebCryptoEnvironment } from '../lib/crypto'
|
import { CryptoService, diagnoseWebCryptoEnvironment } from '../lib/crypto'
|
||||||
import { t, type Language } from '../i18n/translations'
|
import { t, type Language } from '../i18n/translations'
|
||||||
|
|
||||||
export type WebCryptoCheckStatus =
|
export type WebCryptoCheckStatus =
|
||||||
@@ -9,6 +9,7 @@ export type WebCryptoCheckStatus =
|
|||||||
| 'secure'
|
| 'secure'
|
||||||
| 'insecure'
|
| 'insecure'
|
||||||
| 'unsupported'
|
| 'unsupported'
|
||||||
|
| 'disabled' // Transport encryption disabled
|
||||||
|
|
||||||
interface WebCryptoEnvironmentCheckProps {
|
interface WebCryptoEnvironmentCheckProps {
|
||||||
language: Language
|
language: Language
|
||||||
@@ -28,11 +29,19 @@ export function WebCryptoEnvironmentCheck({
|
|||||||
onStatusChange?.(status)
|
onStatusChange?.(status)
|
||||||
}, [onStatusChange, status])
|
}, [onStatusChange, status])
|
||||||
|
|
||||||
const runCheck = useCallback(() => {
|
const runCheck = useCallback(async () => {
|
||||||
setStatus('checking')
|
setStatus('checking')
|
||||||
setSummary(null)
|
setSummary(null)
|
||||||
|
|
||||||
setTimeout(() => {
|
try {
|
||||||
|
// First check if transport encryption is enabled on the server
|
||||||
|
const config = await CryptoService.fetchCryptoConfig()
|
||||||
|
|
||||||
|
if (!config.transport_encryption) {
|
||||||
|
setStatus('disabled')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const result = diagnoseWebCryptoEnvironment()
|
const result = diagnoseWebCryptoEnvironment()
|
||||||
setSummary(
|
setSummary(
|
||||||
t('environmentCheck.summary', language, {
|
t('environmentCheck.summary', language, {
|
||||||
@@ -52,8 +61,11 @@ export function WebCryptoEnvironmentCheck({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setStatus('secure')
|
setStatus('secure')
|
||||||
}, 0)
|
} catch {
|
||||||
}, [language, t])
|
// If we can't fetch config, assume encryption is disabled
|
||||||
|
setStatus('disabled')
|
||||||
|
}
|
||||||
|
}, [language])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runCheck()
|
runCheck()
|
||||||
@@ -109,6 +121,17 @@ export function WebCryptoEnvironmentCheck({
|
|||||||
<div>{t('environmentCheck.unsupportedDesc', language)}</div>
|
<div>{t('environmentCheck.unsupportedDesc', language)}</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
disabled: () => (
|
||||||
|
<div className="flex items-start gap-2 text-gray-400 text-xs">
|
||||||
|
<ShieldMinus className="w-4 h-4 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold">
|
||||||
|
{t('environmentCheck.disabledTitle', language)}
|
||||||
|
</div>
|
||||||
|
<div>{t('environmentCheck.disabledDesc', language)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
checking: () => (
|
checking: () => (
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 text-xs"
|
className="flex items-center gap-2 text-xs"
|
||||||
|
|||||||
@@ -382,7 +382,10 @@ export function ExchangeConfigModal({
|
|||||||
color: '#EAECEF',
|
color: '#EAECEF',
|
||||||
}}
|
}}
|
||||||
aria-label={t('selectExchange', language)}
|
aria-label={t('selectExchange', language)}
|
||||||
disabled={webCryptoStatus !== 'secure'}
|
disabled={
|
||||||
|
webCryptoStatus !== 'secure' &&
|
||||||
|
webCryptoStatus !== 'disabled'
|
||||||
|
}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
|
|||||||
@@ -963,6 +963,9 @@ export const translations = {
|
|||||||
unsupportedDesc:
|
unsupportedDesc:
|
||||||
'Open NOFX over HTTPS (or http://localhost during development) and avoid insecure iframes/reverse proxies so the browser can enable Web Crypto.',
|
'Open NOFX over HTTPS (or http://localhost during development) and avoid insecure iframes/reverse proxies so the browser can enable Web Crypto.',
|
||||||
summary: 'Current origin: {origin} • Protocol: {protocol}',
|
summary: 'Current origin: {origin} • Protocol: {protocol}',
|
||||||
|
disabledTitle: 'Transport encryption disabled',
|
||||||
|
disabledDesc:
|
||||||
|
'Server-side transport encryption is disabled. API keys will be transmitted in plaintext. Enable TRANSPORT_ENCRYPTION=true for enhanced security.',
|
||||||
},
|
},
|
||||||
|
|
||||||
environmentSteps: {
|
environmentSteps: {
|
||||||
@@ -1905,6 +1908,9 @@ export const translations = {
|
|||||||
unsupportedDesc:
|
unsupportedDesc:
|
||||||
'请通过 HTTPS 或本机 localhost 访问 NOFX,并避免嵌入不安全 iframe/反向代理,以符合浏览器的 Web Crypto 规则。',
|
'请通过 HTTPS 或本机 localhost 访问 NOFX,并避免嵌入不安全 iframe/反向代理,以符合浏览器的 Web Crypto 规则。',
|
||||||
summary: '当前来源:{origin} · 协议:{protocol}',
|
summary: '当前来源:{origin} · 协议:{protocol}',
|
||||||
|
disabledTitle: '传输加密已禁用',
|
||||||
|
disabledDesc:
|
||||||
|
'服务端传输加密已关闭,API 密钥将以明文传输。如需增强安全性,请设置 TRANSPORT_ENCRYPTION=true。',
|
||||||
},
|
},
|
||||||
|
|
||||||
environmentSteps: {
|
environmentSteps: {
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ export interface EncryptedPayload {
|
|||||||
ts?: number // 可选:unix 秒,用于重放保护
|
ts?: number // 可选:unix 秒,用于重放保护
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CryptoConfig {
|
||||||
|
transport_encryption: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface WebCryptoEnvironmentInfo {
|
export interface WebCryptoEnvironmentInfo {
|
||||||
isBrowser: boolean
|
isBrowser: boolean
|
||||||
isSecureContext: boolean
|
isSecureContext: boolean
|
||||||
@@ -20,6 +24,11 @@ export interface WebCryptoEnvironmentInfo {
|
|||||||
export class CryptoService {
|
export class CryptoService {
|
||||||
private static publicKey: CryptoKey | null = null
|
private static publicKey: CryptoKey | null = null
|
||||||
private static publicKeyPEM: string | null = null
|
private static publicKeyPEM: string | null = null
|
||||||
|
private static _transportEncryption: boolean | null = null
|
||||||
|
|
||||||
|
static get transportEncryption(): boolean {
|
||||||
|
return this._transportEncryption === true
|
||||||
|
}
|
||||||
|
|
||||||
static async initialize(publicKeyPEM: string) {
|
static async initialize(publicKeyPEM: string) {
|
||||||
if (this.publicKey && this.publicKeyPEM === publicKeyPEM) {
|
if (this.publicKey && this.publicKeyPEM === publicKeyPEM) {
|
||||||
@@ -29,6 +38,16 @@ export class CryptoService {
|
|||||||
this.publicKey = await this.importPublicKey(publicKeyPEM)
|
this.publicKey = await this.importPublicKey(publicKeyPEM)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async fetchCryptoConfig(): Promise<CryptoConfig> {
|
||||||
|
const response = await fetch('/api/crypto/config')
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch crypto config: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
this._transportEncryption = data.transport_encryption
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
private static async importPublicKey(pem: string): Promise<CryptoKey> {
|
private static async importPublicKey(pem: string): Promise<CryptoKey> {
|
||||||
const pemHeader = '-----BEGIN PUBLIC KEY-----'
|
const pemHeader = '-----BEGIN PUBLIC KEY-----'
|
||||||
const pemFooter = '-----END PUBLIC KEY-----'
|
const pemFooter = '-----END PUBLIC KEY-----'
|
||||||
@@ -153,7 +172,11 @@ export class CryptoService {
|
|||||||
throw new Error(`Failed to fetch public key: ${response.statusText}`)
|
throw new Error(`Failed to fetch public key: ${response.statusText}`)
|
||||||
}
|
}
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
return data.public_key
|
// Update transport encryption flag from server response
|
||||||
|
if (typeof data.transport_encryption === 'boolean') {
|
||||||
|
this._transportEncryption = data.transport_encryption
|
||||||
|
}
|
||||||
|
return data.public_key || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
static async decryptSensitiveData(
|
static async decryptSensitiveData(
|
||||||
|
|||||||
Reference in New Issue
Block a user