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

This commit is contained in:
CaIon
2026-08-29 20:54:07 +08:00
parent 8454082f93
commit 918427d8ab
9 changed files with 46 additions and 9 deletions
+2
View File
@@ -79,6 +79,8 @@
# 会话密钥 # 会话密钥
# SESSION_SECRET=random_string # SESSION_SECRET=random_string
# 登录密码请求体 RSA-OAEP 加密;默认关闭,且不能替代 HTTPS
# PASSWORD_LOGIN_ENCRYPTION_ENABLED=true
# false/未配置:本地 HTTP 模式,关闭 refresh/logout OriginGuard,且不得设置 TRUSTED_URL;兼容本地开发代理。 # false/未配置:本地 HTTP 模式,关闭 refresh/logout OriginGuard,且不得设置 TRUSTED_URL;兼容本地开发代理。
# true:启用 Secure Refresh Cookie 和严格 OriginGuard,必须同时列出全部可信 HTTPS Origin。 # true:启用 Secure Refresh Cookie 和严格 OriginGuard,必须同时列出全部可信 HTTPS Origin。
# SESSION_COOKIE_TRUSTED_URL 多项用英文逗号分隔;不支持通配符、路径或域名后缀匹配。 # SESSION_COOKIE_TRUSTED_URL 多项用英文逗号分隔;不支持通配符、路径或域名后缀匹配。
+1
View File
@@ -60,6 +60,7 @@ var ItemsPerPage = 10
var MaxRecentItems = 1000 var MaxRecentItems = 1000
var PasswordLoginEnabled = true var PasswordLoginEnabled = true
var PasswordLoginEncryptionEnabled = false
var PasswordRegisterEnabled = true var PasswordRegisterEnabled = true
var EmailVerificationEnabled = false var EmailVerificationEnabled = false
var GitHubOAuthEnabled = false var GitHubOAuthEnabled = false
+1
View File
@@ -87,6 +87,7 @@ func InitEnv() {
DebugEnabled = os.Getenv("DEBUG") == "true" DebugEnabled = os.Getenv("DEBUG") == "true"
MemoryCacheEnabled = os.Getenv("MEMORY_CACHE_ENABLED") == "true" MemoryCacheEnabled = os.Getenv("MEMORY_CACHE_ENABLED") == "true"
IsMasterNode = os.Getenv("NODE_TYPE") != "slave" IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
PasswordLoginEncryptionEnabled = GetEnvOrDefaultBool("PASSWORD_LOGIN_ENCRYPTION_ENABLED", false)
initNodeNameIdentity() initNodeNameIdentity()
TLSInsecureSkipVerify = GetEnvOrDefaultBool("TLS_INSECURE_SKIP_VERIFY", false) TLSInsecureSkipVerify = GetEnvOrDefaultBool("TLS_INSECURE_SKIP_VERIFY", false)
if TLSInsecureSkipVerify { if TLSInsecureSkipVerify {
+2
View File
@@ -94,6 +94,8 @@ func GetStatus(c *gin.Context) {
"password_register_enabled": common.PasswordRegisterEnabled, "password_register_enabled": common.PasswordRegisterEnabled,
"default_use_auto_group": setting.DefaultUseAutoGroup, "default_use_auto_group": setting.DefaultUseAutoGroup,
"password_login_encryption_enabled": common.PasswordLoginEncryptionEnabled,
"usd_exchange_rate": operation_setting.USDExchangeRate, "usd_exchange_rate": operation_setting.USDExchangeRate,
"price": operation_setting.Price, "price": operation_setting.Price,
"stripe_unit_price": setting.StripeUnitPrice, "stripe_unit_price": setting.StripeUnitPrice,
+10 -1
View File
@@ -40,12 +40,17 @@ var (
) )
func GetPasswordEncryptionKey(c *gin.Context) { func GetPasswordEncryptionKey(c *gin.Context) {
if !common.PasswordLoginEncryptionEnabled {
common.ApiSuccess(c, gin.H{"enabled": false})
return
}
keyID, publicKey := common.PasswordEncryptionPublicKey() keyID, publicKey := common.PasswordEncryptionPublicKey()
if keyID == "" || publicKey == "" { if keyID == "" || publicKey == "" {
common.ApiErrorI18n(c, i18n.MsgDatabaseError) common.ApiErrorI18n(c, i18n.MsgDatabaseError)
return return
} }
common.ApiSuccess(c, gin.H{ common.ApiSuccess(c, gin.H{
"enabled": true,
"kid": keyID, "kid": keyID,
"public_key": publicKey, "public_key": publicKey,
}) })
@@ -64,7 +69,11 @@ func Login(c *gin.Context) {
} }
username := loginRequest.Username username := loginRequest.Username
password := loginRequest.Password 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) password, err = common.DecryptPassword(loginRequest.PasswordEncrypted, loginRequest.EncryptionKeyID)
if err != nil { if err != nil {
common.ApiErrorI18n(c, i18n.MsgUserUsernameOrPasswordError) common.ApiErrorI18n(c, i18n.MsgUserUsernameOrPasswordError)
+5 -3
View File
@@ -318,9 +318,11 @@ func InitResources() error {
common.FatalLog("failed to initialize authorization: " + err.Error()) common.FatalLog("failed to initialize authorization: " + err.Error())
return err return err
} }
if err = model.InitPasswordEncryption(); err != nil { if common.PasswordLoginEncryptionEnabled {
common.FatalLog("failed to initialize password encryption: " + err.Error()) if err = model.InitPasswordEncryption(); err != nil {
return err common.FatalLog("failed to initialize password encryption: " + err.Error())
return err
}
} }
model.CheckSetup() model.CheckSetup()
+17 -5
View File
@@ -48,22 +48,34 @@ import type {
export async function login(payload: LoginPayload): Promise<LoginResponse> { export async function login(payload: LoginPayload): Promise<LoginResponse> {
const turnstile = payload.turnstile ?? '' const turnstile = payload.turnstile ?? ''
try { 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>( const res = await api.post<LoginResponse>(
`/api/user/login?turnstile=${turnstile}`, `/api/user/login?turnstile=${turnstile}`,
{ {
username: payload.username, username: payload.username,
password_encrypted: encryptedPassword.password_encrypted, ...passwordFields,
encryption_key_id: encryptedPassword.encryption_key_id,
}, },
{ skipAuthRefresh: true } { skipAuthRefresh: true }
) )
if (!res.data?.success) { if (payload.passwordEncryptionEnabled && !res.data?.success) {
clearPasswordEncryptionCache() clearPasswordEncryptionCache()
} }
return res.data return res.data
} catch (error: unknown) { } catch (error: unknown) {
clearPasswordEncryptionCache() if (payload.passwordEncryptionEnabled) {
clearPasswordEncryptionCache()
}
throw error throw error
} }
} }
@@ -84,6 +84,10 @@ export function UserAuthForm({
(status?.password_login_enabled ?? (status?.password_login_enabled ??
status?.data?.password_login_enabled ?? status?.data?.password_login_enabled ??
true) !== false true) !== false
const passwordLoginEncryptionEnabled =
(status?.password_login_encryption_enabled ??
status?.data?.password_login_encryption_enabled ??
false) === true
const { const {
isTurnstileEnabled, isTurnstileEnabled,
turnstileSiteKey, turnstileSiteKey,
@@ -171,6 +175,7 @@ export function UserAuthForm({
username: data.username, username: data.username,
password: data.password, password: data.password,
turnstile: submittedTurnstileToken, turnstile: submittedTurnstileToken,
passwordEncryptionEnabled: passwordLoginEncryptionEnabled,
}) })
if (res.success) { if (res.success) {
+3
View File
@@ -26,6 +26,7 @@ export interface LoginPayload {
username: string username: string
password: string password: string
turnstile?: string turnstile?: string
passwordEncryptionEnabled?: boolean
} }
export interface TwoFAPayload { export interface TwoFAPayload {
@@ -133,6 +134,7 @@ export interface SystemStatus {
oauth_register_enabled?: boolean oauth_register_enabled?: boolean
register_enabled?: boolean register_enabled?: boolean
password_login_enabled?: boolean password_login_enabled?: boolean
password_login_encryption_enabled?: boolean
password_register_enabled?: boolean password_register_enabled?: boolean
custom_oauth_providers?: CustomOAuthProviderInfo[] custom_oauth_providers?: CustomOAuthProviderInfo[]
[key: string]: unknown [key: string]: unknown
@@ -178,6 +180,7 @@ export interface SystemStatus {
oauth_register_enabled?: boolean oauth_register_enabled?: boolean
register_enabled?: boolean register_enabled?: boolean
password_login_enabled?: boolean password_login_enabled?: boolean
password_login_encryption_enabled?: boolean
password_register_enabled?: boolean password_register_enabled?: boolean
custom_oauth_providers?: CustomOAuthProviderInfo[] custom_oauth_providers?: CustomOAuthProviderInfo[]
[key: string]: unknown [key: string]: unknown