feat(iot): bcrypt-хэш паролей устройств и бриджа + EMQX на bcrypt (P0-3 ч.1)

- handler CreateIoTDevice: пароль устройства хранится bcrypt-хэшем (hashPassword),
  открытый пароль отдаётся один раз в 201.
- store.EnsureBridgeDevice: пароль бриджа тоже bcrypt-хэшем.
- emqx/emqx.conf: password_hash_algorithm name=bcrypt, salt_rounds=10 (подтверждено
  по emqx/emqx: HOCON-ключ salt_rounds; Go bcrypt.DefaultCost=10).
- Добавлена зависимость golang.org/x/crypto v0.42.0.
This commit is contained in:
Repinoid
2026-10-10 19:21:10 +03:00
parent 59bd743c87
commit 3b7c8a0a63
5 changed files with 38 additions and 5 deletions
+23 -1
View File
@@ -18,6 +18,8 @@ import (
"strconv"
"time"
"golang.org/x/crypto/bcrypt"
"gitea.services.ngcloud.ru/Nail/IoT/internal/service/store"
)
@@ -110,6 +112,17 @@ func generateMQTTPassword() (string, error) {
return "", lastErr
}
// hashPassword — bcrypt-хэш пароля устройства. EMQX (postgres-auth) проверяет
// bcrypt (см. emqx/emqx.conf password_hash_algorithm). В БД хранится ТОЛЬКО хэш;
// открытый пароль отдаётся один раз при создании устройства.
func hashPassword(plain string) (string, error) {
h, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(h), nil
}
// identRe — допустимый charset идентификаторов (namespace/name/device_id):
// только [a-zA-Z0-9] и дефисы, не с дефиса. Исключает _, +, #, /, пробелы, юникод.
// Причины запретов (ревью 2026-10-10):
@@ -177,6 +190,15 @@ func (h *Handler) CreateIoTDevice(w http.ResponseWriter, r *http.Request) {
return
}
// В БД храним bcrypt-хэш (EMQX проверяет bcrypt — emqx/emqx.conf). Открытый
// пароль отдаём ОДИН раз в ответе 201 (иначе его негде взять).
hash, err := hashPassword(password)
if err != nil {
h.Log.Error("hash device password", "err", err)
writeJSON(w, http.StatusInternalServerError, errResp("failed to generate credentials"))
return
}
phase := "Active"
if !enabled {
phase = "Disabled"
@@ -187,7 +209,7 @@ func (h *Handler) CreateIoTDevice(w http.ResponseWriter, r *http.Request) {
Name: req.Name,
DeviceID: req.DeviceID,
Enabled: enabled,
MQTTPassword: password,
MQTTPassword: hash,
Metadata: req.Metadata,
Phase: phase,
}
+10 -2
View File
@@ -3,6 +3,8 @@ package store
import (
"context"
"fmt"
"golang.org/x/crypto/bcrypt"
)
// bridgeNamespace — служебный namespace строки бриджа в iot_devices.
@@ -17,14 +19,20 @@ const bridgeNamespace = "__bridge"
// Нужно для PG-auth EMQX (хвост 3): EMQX зависит только от PostgreSQL,
// монолит при старте гарантирует наличие строки бриджа.
func (s *DeviceStore) EnsureBridgeDevice(ctx context.Context, username, password string) error {
_, err := s.db.ExecContext(ctx, `
// Пароль бриджа храним bcrypt-хэшем (EMQX проверяет bcrypt), как и пароли
// устройств.
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("store: hash bridge password: %w", err)
}
_, err = s.db.ExecContext(ctx, `
INSERT INTO iot_devices (namespace, name, device_id, enabled, mqtt_password, metadata, phase)
VALUES ($1, $2, $3, true, $4, '{}'::jsonb, 'Active')
ON CONFLICT (namespace, name) DO UPDATE
SET device_id = EXCLUDED.device_id,
enabled = true,
mqtt_password = EXCLUDED.mqtt_password`,
bridgeNamespace, "iot-bridge", username, password)
bridgeNamespace, "iot-bridge", username, string(hash))
if err != nil {
return fmt.Errorf("store: ensure bridge device: %w", err)
}