2
0
mirror of https://github.com/QuantumNous/new-api.git synced 2026-08-31 02:41:34 +00:00

feat(auth): make password encryption opt-in #6743

This commit is contained in:
CaIon 2026-08-29 20:53:42 +08:00
parent 8454082f93
commit 918427d8ab
No known key found for this signature in database
GPG Key ID: DFDCE3CC3CCED6EB
9 changed files with 46 additions and 9 deletions

View File

@ -79,6 +79,8 @@
# 会话密钥
# SESSION_SECRET=random_string
# 登录密码请求体 RSA-OAEP 加密;默认关闭,且不能替代 HTTPS
# PASSWORD_LOGIN_ENCRYPTION_ENABLED=true
# false/未配置:本地 HTTP 模式,关闭 refresh/logout OriginGuard且不得设置 TRUSTED_URL兼容本地开发代理。
# true启用 Secure Refresh Cookie 和严格 OriginGuard必须同时列出全部可信 HTTPS Origin。
# SESSION_COOKIE_TRUSTED_URL 多项用英文逗号分隔;不支持通配符、路径或域名后缀匹配。

View File

@ -60,6 +60,7 @@ var ItemsPerPage = 10
var MaxRecentItems = 1000
var PasswordLoginEnabled = true
var PasswordLoginEncryptionEnabled = false
var PasswordRegisterEnabled = true
var EmailVerificationEnabled = false
var GitHubOAuthEnabled = false

View File

@ -87,6 +87,7 @@ func InitEnv() {
DebugEnabled = os.Getenv("DEBUG") == "true"
MemoryCacheEnabled = os.Getenv("MEMORY_CACHE_ENABLED") == "true"
IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
PasswordLoginEncryptionEnabled = GetEnvOrDefaultBool("PASSWORD_LOGIN_ENCRYPTION_ENABLED", false)
initNodeNameIdentity()
TLSInsecureSkipVerify = GetEnvOrDefaultBool("TLS_INSECURE_SKIP_VERIFY", false)
if TLSInsecureSkipVerify {

View File

@ -94,6 +94,8 @@ func GetStatus(c *gin.Context) {
"password_register_enabled": common.PasswordRegisterEnabled,
"default_use_auto_group": setting.DefaultUseAutoGroup,
"password_login_encryption_enabled": common.PasswordLoginEncryptionEnabled,
"usd_exchange_rate": operation_setting.USDExchangeRate,
"price": operation_setting.Price,
"stripe_unit_price": setting.StripeUnitPrice,

View File

@ -40,12 +40,17 @@ var (
)
func GetPasswordEncryptionKey(c *gin.Context) {
if !common.PasswordLoginEncryptionEnabled {
common.ApiSuccess(c, gin.H{"enabled": false})
return
}
keyID, publicKey := common.PasswordEncryptionPublicKey()
if keyID == "" || publicKey == "" {
common.ApiErrorI18n(c, i18n.MsgDatabaseError)
return
}
common.ApiSuccess(c, gin.H{
"enabled": true,
"kid": keyID,
"public_key": publicKey,
})
@ -64,7 +69,11 @@ func Login(c *gin.Context) {
}
username := loginRequest.Username
password := loginRequest.Password
if loginRequest.PasswordEncrypted != "" {
if common.PasswordLoginEncryptionEnabled {
if loginRequest.PasswordEncrypted == "" || loginRequest.EncryptionKeyID == "" {
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
password, err = common.DecryptPassword(loginRequest.PasswordEncrypted, loginRequest.EncryptionKeyID)
if err != nil {
common.ApiErrorI18n(c, i18n.MsgUserUsernameOrPasswordError)

View File

@ -318,9 +318,11 @@ func InitResources() error {
common.FatalLog("failed to initialize authorization: " + err.Error())
return err
}
if err = model.InitPasswordEncryption(); err != nil {
common.FatalLog("failed to initialize password encryption: " + err.Error())
return err
if common.PasswordLoginEncryptionEnabled {
if err = model.InitPasswordEncryption(); err != nil {
common.FatalLog("failed to initialize password encryption: " + err.Error())
return err
}
}
model.CheckSetup()

View File

@ -48,22 +48,34 @@ import type {
export async function login(payload: LoginPayload): Promise<LoginResponse> {
const turnstile = payload.turnstile ?? ''
try {
const encryptedPassword = await encryptPassword(payload.password)
let passwordFields:
| { password: string }
| { password_encrypted: string; encryption_key_id: string }
if (payload.passwordEncryptionEnabled) {
const encryptedPassword = await encryptPassword(payload.password)
passwordFields = {
password_encrypted: encryptedPassword.password_encrypted,
encryption_key_id: encryptedPassword.encryption_key_id,
}
} else {
passwordFields = { password: payload.password }
}
const res = await api.post<LoginResponse>(
`/api/user/login?turnstile=${turnstile}`,
{
username: payload.username,
password_encrypted: encryptedPassword.password_encrypted,
encryption_key_id: encryptedPassword.encryption_key_id,
...passwordFields,
},
{ skipAuthRefresh: true }
)
if (!res.data?.success) {
if (payload.passwordEncryptionEnabled && !res.data?.success) {
clearPasswordEncryptionCache()
}
return res.data
} catch (error: unknown) {
clearPasswordEncryptionCache()
if (payload.passwordEncryptionEnabled) {
clearPasswordEncryptionCache()
}
throw error
}
}

View File

@ -84,6 +84,10 @@ export function UserAuthForm({
(status?.password_login_enabled ??
status?.data?.password_login_enabled ??
true) !== false
const passwordLoginEncryptionEnabled =
(status?.password_login_encryption_enabled ??
status?.data?.password_login_encryption_enabled ??
false) === true
const {
isTurnstileEnabled,
turnstileSiteKey,
@ -171,6 +175,7 @@ export function UserAuthForm({
username: data.username,
password: data.password,
turnstile: submittedTurnstileToken,
passwordEncryptionEnabled: passwordLoginEncryptionEnabled,
})
if (res.success) {

View File

@ -26,6 +26,7 @@ export interface LoginPayload {
username: string
password: string
turnstile?: string
passwordEncryptionEnabled?: boolean
}
export interface TwoFAPayload {
@ -133,6 +134,7 @@ export interface SystemStatus {
oauth_register_enabled?: boolean
register_enabled?: boolean
password_login_enabled?: boolean
password_login_encryption_enabled?: boolean
password_register_enabled?: boolean
custom_oauth_providers?: CustomOAuthProviderInfo[]
[key: string]: unknown
@ -178,6 +180,7 @@ export interface SystemStatus {
oauth_register_enabled?: boolean
register_enabled?: boolean
password_login_enabled?: boolean
password_login_encryption_enabled?: boolean
password_register_enabled?: boolean
custom_oauth_providers?: CustomOAuthProviderInfo[]
[key: string]: unknown