mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 20:39:35 +00:00
fix(api-token): hash tokens at rest and show plaintext only once
Store API tokens as SHA-256 hashes instead of plaintext and return the token value only in the create response. List no longer exposes the token, and the UI drops the Show/Copy buttons in favor of a one-time reveal modal at creation. Match hashes the presented bearer token before the constant-time compare, and a migration hashes any pre-existing plaintext rows in place so existing tokens keep authenticating. Docs and translations updated.
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/mhsanaei/3x-ui/v3/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v3/util/crypto"
|
||||
"github.com/mhsanaei/3x-ui/v3/util/random"
|
||||
)
|
||||
|
||||
@@ -18,16 +19,18 @@ const apiTokenLength = 48
|
||||
type ApiTokenView struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
// toView builds the metadata view returned by List. It never carries the
|
||||
// token value: only a SHA-256 hash is stored, and the plaintext is shown
|
||||
// exactly once at creation time.
|
||||
func toView(t *model.ApiToken) *ApiTokenView {
|
||||
return &ApiTokenView{
|
||||
Id: t.Id,
|
||||
Name: t.Name,
|
||||
Token: t.Token,
|
||||
Enabled: t.Enabled,
|
||||
CreatedAt: t.CreatedAt,
|
||||
}
|
||||
@@ -62,15 +65,18 @@ func (s *ApiTokenService) Create(name string) (*ApiTokenView, error) {
|
||||
if count > 0 {
|
||||
return nil, common.NewError("a token with that name already exists")
|
||||
}
|
||||
plaintext := random.Seq(apiTokenLength)
|
||||
row := &model.ApiToken{
|
||||
Name: name,
|
||||
Token: random.Seq(apiTokenLength),
|
||||
Token: crypto.HashTokenSHA256(plaintext),
|
||||
Enabled: true,
|
||||
}
|
||||
if err := db.Create(row).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toView(row), nil
|
||||
view := toView(row)
|
||||
view.Token = plaintext
|
||||
return view, nil
|
||||
}
|
||||
|
||||
func (s *ApiTokenService) Delete(id int) error {
|
||||
@@ -97,8 +103,9 @@ func (s *ApiTokenService) SetEnabled(id int, enabled bool) error {
|
||||
}
|
||||
|
||||
// Match returns true when the presented bearer token matches any enabled
|
||||
// row in api_tokens. Uses constant-time compare per row so a remote
|
||||
// attacker can't time-attack tokens byte-by-byte.
|
||||
// row in api_tokens. Tokens are stored as SHA-256 hashes, so the presented
|
||||
// value is hashed before a constant-time compare per row keeps a remote
|
||||
// attacker from timing the comparison byte-by-byte.
|
||||
func (s *ApiTokenService) Match(presented string) bool {
|
||||
if presented == "" {
|
||||
return false
|
||||
@@ -108,10 +115,10 @@ func (s *ApiTokenService) Match(presented string) bool {
|
||||
if err := db.Model(model.ApiToken{}).Where("enabled = ?", true).Find(&rows).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
presentedBytes := []byte(presented)
|
||||
presentedHash := []byte(crypto.HashTokenSHA256(presented))
|
||||
matched := false
|
||||
for _, r := range rows {
|
||||
if subtle.ConstantTimeCompare([]byte(r.Token), presentedBytes) == 1 {
|
||||
if subtle.ConstantTimeCompare([]byte(r.Token), presentedHash) == 1 {
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1119,7 +1119,9 @@
|
||||
"apiTokenNamePlaceholder": "مثل central-panel-a",
|
||||
"apiTokenNameRequired": "الاسم مطلوب",
|
||||
"apiTokenEmpty": "لا توجد رموز بعد — أنشئ واحدًا لمصادقة الروبوتات أو اللوحات البعيدة.",
|
||||
"apiTokenDeleteWarning": "أي عميل يستخدم هذا الرمز سيفقد المصادقة فورًا."
|
||||
"apiTokenDeleteWarning": "أي عميل يستخدم هذا الرمز سيفقد المصادقة فورًا.",
|
||||
"apiTokenCreatedTitle": "تم إنشاء الرمز",
|
||||
"apiTokenCreatedNotice": "انسخ هذا الرمز الآن. لأسباب أمنية لا يتم تخزينه بصيغة قابلة للقراءة ولن يتم عرضه مرة أخرى."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "تم تغيير المعلمات.",
|
||||
|
||||
@@ -1119,7 +1119,9 @@
|
||||
"apiTokenNamePlaceholder": "e.g. central-panel-a",
|
||||
"apiTokenNameRequired": "Name is required",
|
||||
"apiTokenEmpty": "No tokens yet — create one to authenticate bots or remote panels.",
|
||||
"apiTokenDeleteWarning": "Any caller using this token will stop authenticating immediately."
|
||||
"apiTokenDeleteWarning": "Any caller using this token will stop authenticating immediately.",
|
||||
"apiTokenCreatedTitle": "Token created",
|
||||
"apiTokenCreatedNotice": "Copy this token now. For security it is not stored in readable form and will not be shown again."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "The parameters have been changed.",
|
||||
|
||||
@@ -1119,7 +1119,9 @@
|
||||
"apiTokenNamePlaceholder": "por ejemplo central-panel-a",
|
||||
"apiTokenNameRequired": "El nombre es obligatorio",
|
||||
"apiTokenEmpty": "Aún no hay tokens — crea uno para autenticar bots o paneles remotos.",
|
||||
"apiTokenDeleteWarning": "Cualquier cliente que use este token dejará de autenticarse inmediatamente."
|
||||
"apiTokenDeleteWarning": "Cualquier cliente que use este token dejará de autenticarse inmediatamente.",
|
||||
"apiTokenCreatedTitle": "Token creado",
|
||||
"apiTokenCreatedNotice": "Copia este token ahora. Por seguridad, no se almacena de forma legible y no se volverá a mostrar."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "Los parámetros han sido modificados.",
|
||||
|
||||
@@ -1119,7 +1119,9 @@
|
||||
"apiTokenNamePlaceholder": "مثلاً central-panel-a",
|
||||
"apiTokenNameRequired": "نام الزامی است",
|
||||
"apiTokenEmpty": "هنوز توکنی وجود ندارد — برای احراز هویت رباتها یا پنلهای راه دور یکی بسازید.",
|
||||
"apiTokenDeleteWarning": "هر کلاینتی که از این توکن استفاده میکند بلافاصله احراز هویتش قطع میشود."
|
||||
"apiTokenDeleteWarning": "هر کلاینتی که از این توکن استفاده میکند بلافاصله احراز هویتش قطع میشود.",
|
||||
"apiTokenCreatedTitle": "توکن ساخته شد",
|
||||
"apiTokenCreatedNotice": "اکنون این توکن را کپی کنید. بهدلیل امنیتی بهصورت قابلخواندن ذخیره نمیشود و دوباره نمایش داده نخواهد شد."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "پارامترها تغییر کردهاند.",
|
||||
|
||||
@@ -1119,7 +1119,9 @@
|
||||
"apiTokenNamePlaceholder": "misalnya central-panel-a",
|
||||
"apiTokenNameRequired": "Nama wajib diisi",
|
||||
"apiTokenEmpty": "Belum ada token — buat satu untuk mengautentikasi bot atau panel jarak jauh.",
|
||||
"apiTokenDeleteWarning": "Setiap pemanggil yang menggunakan token ini akan berhenti terautentikasi segera."
|
||||
"apiTokenDeleteWarning": "Setiap pemanggil yang menggunakan token ini akan berhenti terautentikasi segera.",
|
||||
"apiTokenCreatedTitle": "Token dibuat",
|
||||
"apiTokenCreatedNotice": "Salin token ini sekarang. Demi keamanan, token tidak disimpan dalam bentuk yang dapat dibaca dan tidak akan ditampilkan lagi."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "Parameter telah diubah.",
|
||||
|
||||
@@ -1119,7 +1119,9 @@
|
||||
"apiTokenNamePlaceholder": "例: central-panel-a",
|
||||
"apiTokenNameRequired": "名前は必須です",
|
||||
"apiTokenEmpty": "トークンがまだありません — ボットやリモートパネルを認証するために作成してください。",
|
||||
"apiTokenDeleteWarning": "このトークンを使用しているクライアントは直ちに認証できなくなります。"
|
||||
"apiTokenDeleteWarning": "このトークンを使用しているクライアントは直ちに認証できなくなります。",
|
||||
"apiTokenCreatedTitle": "トークンを作成しました",
|
||||
"apiTokenCreatedNotice": "このトークンを今すぐコピーしてください。セキュリティ上、読み取り可能な形式では保存されず、再表示されません。"
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "パラメーターが変更されました。",
|
||||
|
||||
@@ -1119,7 +1119,9 @@
|
||||
"apiTokenNamePlaceholder": "ex.: central-panel-a",
|
||||
"apiTokenNameRequired": "O nome é obrigatório",
|
||||
"apiTokenEmpty": "Nenhum token ainda — crie um para autenticar bots ou painéis remotos.",
|
||||
"apiTokenDeleteWarning": "Qualquer cliente usando este token deixará de se autenticar imediatamente."
|
||||
"apiTokenDeleteWarning": "Qualquer cliente usando este token deixará de se autenticar imediatamente.",
|
||||
"apiTokenCreatedTitle": "Token criado",
|
||||
"apiTokenCreatedNotice": "Copie este token agora. Por segurança, ele não é armazenado de forma legível e não será exibido novamente."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "Os parâmetros foram alterados.",
|
||||
|
||||
@@ -1119,7 +1119,9 @@
|
||||
"apiTokenNamePlaceholder": "например, central-panel-a",
|
||||
"apiTokenNameRequired": "Имя обязательно",
|
||||
"apiTokenEmpty": "Токенов пока нет — создайте один для аутентификации ботов или удалённых панелей.",
|
||||
"apiTokenDeleteWarning": "Любой клиент, использующий этот токен, немедленно потеряет аутентификацию."
|
||||
"apiTokenDeleteWarning": "Любой клиент, использующий этот токен, немедленно потеряет аутентификацию.",
|
||||
"apiTokenCreatedTitle": "Токен создан",
|
||||
"apiTokenCreatedNotice": "Скопируйте этот токен сейчас. В целях безопасности он не хранится в читаемом виде и больше не будет показан."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "Настройки изменены",
|
||||
|
||||
@@ -1119,7 +1119,9 @@
|
||||
"apiTokenNamePlaceholder": "örn. central-panel-a",
|
||||
"apiTokenNameRequired": "Ad zorunludur",
|
||||
"apiTokenEmpty": "Henüz token yok — bot veya uzak panelleri doğrulamak için bir tane oluşturun.",
|
||||
"apiTokenDeleteWarning": "Bu tokenı kullanan tüm istemciler anında kimlik doğrulamasını kaybeder."
|
||||
"apiTokenDeleteWarning": "Bu tokenı kullanan tüm istemciler anında kimlik doğrulamasını kaybeder.",
|
||||
"apiTokenCreatedTitle": "Belirteç oluşturuldu",
|
||||
"apiTokenCreatedNotice": "Bu belirteci şimdi kopyalayın. Güvenlik nedeniyle okunabilir biçimde saklanmaz ve tekrar gösterilmez."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "Parametreler değiştirildi.",
|
||||
|
||||
@@ -1119,7 +1119,9 @@
|
||||
"apiTokenNamePlaceholder": "наприклад, central-panel-a",
|
||||
"apiTokenNameRequired": "Назва обов'язкова",
|
||||
"apiTokenEmpty": "Поки немає токенів — створіть один для автентифікації ботів або віддалених панелей.",
|
||||
"apiTokenDeleteWarning": "Будь-який клієнт, що використовує цей токен, негайно втратить автентифікацію."
|
||||
"apiTokenDeleteWarning": "Будь-який клієнт, що використовує цей токен, негайно втратить автентифікацію.",
|
||||
"apiTokenCreatedTitle": "Токен створено",
|
||||
"apiTokenCreatedNotice": "Скопіюйте цей токен зараз. З міркувань безпеки він не зберігається у читабельному вигляді й більше не відображатиметься."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "Параметри було змінено.",
|
||||
|
||||
@@ -1119,7 +1119,9 @@
|
||||
"apiTokenNamePlaceholder": "ví dụ: central-panel-a",
|
||||
"apiTokenNameRequired": "Tên là bắt buộc",
|
||||
"apiTokenEmpty": "Chưa có token nào — tạo một token để xác thực bot hoặc panel từ xa.",
|
||||
"apiTokenDeleteWarning": "Mọi client đang dùng token này sẽ ngừng xác thực ngay lập tức."
|
||||
"apiTokenDeleteWarning": "Mọi client đang dùng token này sẽ ngừng xác thực ngay lập tức.",
|
||||
"apiTokenCreatedTitle": "Đã tạo token",
|
||||
"apiTokenCreatedNotice": "Hãy sao chép token này ngay bây giờ. Vì lý do bảo mật, token không được lưu ở dạng đọc được và sẽ không hiển thị lại."
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "Các tham số đã được thay đổi.",
|
||||
|
||||
@@ -1119,7 +1119,9 @@
|
||||
"apiTokenNamePlaceholder": "例如 central-panel-a",
|
||||
"apiTokenNameRequired": "名称必填",
|
||||
"apiTokenEmpty": "暂无令牌 — 创建一个用于认证机器人或远程面板。",
|
||||
"apiTokenDeleteWarning": "使用此令牌的任何调用方将立即无法认证。"
|
||||
"apiTokenDeleteWarning": "使用此令牌的任何调用方将立即无法认证。",
|
||||
"apiTokenCreatedTitle": "令牌已创建",
|
||||
"apiTokenCreatedNotice": "请立即复制此令牌。出于安全考虑,它不会以可读形式存储,也不会再次显示。"
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "参数已更改。",
|
||||
|
||||
@@ -1119,7 +1119,9 @@
|
||||
"apiTokenNamePlaceholder": "例如 central-panel-a",
|
||||
"apiTokenNameRequired": "名稱必填",
|
||||
"apiTokenEmpty": "尚無令牌 — 建立一個以認證機器人或遠端面板。",
|
||||
"apiTokenDeleteWarning": "使用此令牌的任何呼叫方將立即無法認證。"
|
||||
"apiTokenDeleteWarning": "使用此令牌的任何呼叫方將立即無法認證。",
|
||||
"apiTokenCreatedTitle": "權杖已建立",
|
||||
"apiTokenCreatedNotice": "請立即複製此權杖。基於安全考量,它不會以可讀形式儲存,也不會再次顯示。"
|
||||
},
|
||||
"toasts": {
|
||||
"modifySettings": "參數已更改。",
|
||||
|
||||
Reference in New Issue
Block a user