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:
+2
-2
@@ -40,8 +40,8 @@ authentication = [
|
||||
verify = verify_none
|
||||
}
|
||||
password_hash_algorithm {
|
||||
name = plain
|
||||
salt_position = disable
|
||||
name = bcrypt
|
||||
salt_rounds = 10
|
||||
}
|
||||
query = "SELECT mqtt_password AS password_hash FROM iot_devices WHERE enabled = true AND (namespace || '_' || device_id = ${username} OR (namespace = '__bridge' AND ${username} = device_id)) LIMIT 1"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/lib/pq v1.11.2
|
||||
golang.org/x/crypto v0.42.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -20,6 +20,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user