package sys import ( "context" "hotgo/internal/consts" "hotgo/internal/dao" "hotgo/internal/library/contexts" "hotgo/internal/library/immessagecrypto" "hotgo/internal/library/imtenant" "hotgo/internal/model/entity" "hotgo/internal/model/input/sysin" "hotgo/internal/service" "strings" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" ) const ( imMessageEncryptionKeyTable = "im_message_encryption_keys" imTenantMessageEncryptionColumn = "message_encryption_enabled" ) type sSysImMessageEncryption struct{} func NewSysImMessageEncryption() *sSysImMessageEncryption { return &sSysImMessageEncryption{} } func init() { service.RegisterSysImMessageEncryption(NewSysImMessageEncryption()) } func (s *sSysImMessageEncryption) List(ctx context.Context, in *sysin.ImMessageEncryptionListInp) (list []*sysin.ImMessageEncryptionListModel, total int, err error) { if err = in.Filter(ctx); err != nil { return } if err = s.requireSuperAdmin(ctx); err != nil { return nil, 0, err } db := imtenant.RegistryDB() mod := db.Model(dao.ImTenant.Table()+" t").Safe().Ctx(ctx). LeftJoin(dao.ImTenantConfig.Table()+" c", "c.tenant_id=t.id"). Fields( "t.id tenant_id", "t.code tenant_code", "t.name tenant_name", "COALESCE(c."+imTenantMessageEncryptionColumn+",0) message_encryption_enabled", "COALESCE((SELECT MAX(k.key_version) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id AND k.status='active'),0) active_version", "COALESCE((SELECT MAX(k.key_version) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id AND k.status='pending'),0) pending_version", "(SELECT COUNT(1) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id) key_count", "(SELECT MAX(COALESCE(k.activated_at,k.created_at)) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id) last_rotated_at", ) if in.TenantId > 0 { mod = mod.Where("t.id", in.TenantId) } if keyword := strings.TrimSpace(in.Keyword); keyword != "" { mod = mod.Where("t.code LIKE ? OR t.name LIKE ?", "%"+keyword+"%", "%"+keyword+"%") } if in.Enabled != nil { mod = mod.Where("COALESCE(c."+imTenantMessageEncryptionColumn+",0)=?", *in.Enabled) } if err = mod.Page(in.Page, in.PerPage).OrderAsc("t.id").ScanAndCount(&list, &total, false); err != nil { return nil, 0, gerror.Wrap(err, "获取商户消息加密配置失败") } return } func (s *sSysImMessageEncryption) Initialize(ctx context.Context, in *sysin.ImMessageEncryptionActionInp) error { if err := in.Filter(ctx); err != nil { return err } if err := s.requireSuperAdmin(ctx); err != nil { return err } tenant, err := s.tenantByID(ctx, in.TenantId) if err != nil { return err } dek, err := immessagecrypto.GenerateDEK() if err != nil { return err } ciphertext, nonce, err := immessagecrypto.WrapDEK(ctx, tenant.Id, 1, dek) if err != nil { return err } err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { count, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Where("tenant_id", tenant.Id).Count() if queryErr != nil { return gerror.Wrap(queryErr, "检查商户消息密钥失败") } if count > 0 { return gerror.New("该商户已经初始化消息密钥") } _, insertErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Data(g.Map{ "tenant_id": tenant.Id, "key_version": 1, "algorithm": consts.ImMessageKeyAlgorithm, "encrypted_dek": ciphertext, "dek_nonce": nonce, "status": consts.ImMessageKeyStatusActive, "created_at": gtime.Now(), "activated_at": gtime.Now(), "created_by": contexts.GetUserId(ctx), "remark": strings.TrimSpace(in.Remark), }).Insert() if insertErr != nil { return gerror.Wrap(insertErr, "初始化商户消息密钥失败") } return nil }) if err != nil { return err } _ = writeImAdminAudit(ctx, tenant.Id, "message_key_initialize", imMessageEncryptionKeyTable, 0, "success", g.Map{"version": 1}) return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "message.keys.updated", "message_encryption", 1) } func (s *sSysImMessageEncryption) Rotate(ctx context.Context, in *sysin.ImMessageEncryptionActionInp) error { if err := in.Filter(ctx); err != nil { return err } if err := s.requireSuperAdmin(ctx); err != nil { return err } tenant, err := s.tenantByID(ctx, in.TenantId) if err != nil { return err } var version uint err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { pending, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx). Where("tenant_id", tenant.Id).Where("status", consts.ImMessageKeyStatusPending).Count() if queryErr != nil { return gerror.Wrap(queryErr, "检查待激活消息密钥失败") } if pending > 0 { return gerror.New("该商户已有待激活密钥,请等待IM Server确认") } value, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx). Fields("COALESCE(MAX(key_version),0)").Where("tenant_id", tenant.Id).Value() if queryErr != nil { return gerror.Wrap(queryErr, "获取消息密钥版本失败") } if value.Uint() == 0 { return gerror.New("请先初始化商户消息密钥") } version = value.Uint() + 1 dek, generateErr := immessagecrypto.GenerateDEK() if generateErr != nil { return generateErr } ciphertext, nonce, wrapErr := immessagecrypto.WrapDEK(ctx, tenant.Id, version, dek) if wrapErr != nil { return wrapErr } _, insertErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Data(g.Map{ "tenant_id": tenant.Id, "key_version": version, "algorithm": consts.ImMessageKeyAlgorithm, "encrypted_dek": ciphertext, "dek_nonce": nonce, "status": consts.ImMessageKeyStatusPending, "created_at": gtime.Now(), "created_by": contexts.GetUserId(ctx), "remark": strings.TrimSpace(in.Remark), }).Insert() if insertErr != nil { return gerror.Wrap(insertErr, "创建待激活消息密钥失败") } return nil }) if err != nil { return err } _ = writeImAdminAudit(ctx, tenant.Id, "message_key_rotate", imMessageEncryptionKeyTable, 0, "success", g.Map{"version": version, "status": consts.ImMessageKeyStatusPending}) return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "message.keys.updated", "message_encryption", version) } func (s *sSysImMessageEncryption) Switch(ctx context.Context, in *sysin.ImMessageEncryptionSwitchInp) error { if err := in.Filter(ctx); err != nil { return err } if err := s.requireSuperAdmin(ctx); err != nil { return err } tenant, err := s.tenantByID(ctx, in.TenantId) if err != nil { return err } if in.Enabled == 1 { count, queryErr := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx). Where("tenant_id", tenant.Id).Where("status", consts.ImMessageKeyStatusActive).Count() if queryErr != nil { return gerror.Wrap(queryErr, "检查商户消息密钥失败") } if count == 0 { return gerror.New("启用消息加密前必须先初始化有效密钥") } } result, err := imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx). Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id). Data(g.Map{imTenantMessageEncryptionColumn: in.Enabled, dao.ImTenantConfig.Columns().UpdatedBy: contexts.GetUserId(ctx)}).Update() if err != nil { return gerror.Wrap(err, "更新商户消息加密开关失败") } if affected, _ := result.RowsAffected(); affected == 0 { return gerror.New("商户配置不存在") } _ = writeImAdminAudit(ctx, tenant.Id, "message_encryption_switch", "im_tenant_config", tenant.Id, "success", g.Map{"enabled": in.Enabled}) return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "tenant.features.updated", "message_encryption", 0) } func (s *sSysImMessageEncryption) GetKey(ctx context.Context, in *sysin.ImMessageKeyGetInp) (*sysin.ImMessageKeyModel, error) { tenant, err := s.tenantByCode(ctx, in.TenantCode) if err != nil { return nil, err } enabledValue, err := imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx). Fields(imTenantMessageEncryptionColumn).Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).Value() if err != nil { return nil, gerror.Wrap(err, "获取商户消息加密开关失败") } mod := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Where("tenant_id", tenant.Id) if in.Version > 0 { mod = mod.Where("key_version", in.Version).WhereIn("status", []string{consts.ImMessageKeyStatusPending, consts.ImMessageKeyStatusActive, consts.ImMessageKeyStatusRetired}) } else { mod = mod.Where("status", consts.ImMessageKeyStatusActive).OrderDesc("key_version") } row, err := mod.One() if err != nil { return nil, gerror.Wrap(err, "获取商户消息密钥失败") } if row.IsEmpty() { return nil, gerror.New("商户消息密钥不存在") } version := row["key_version"].Uint() dek, err := immessagecrypto.UnwrapDEK(ctx, tenant.Id, version, row["encrypted_dek"].Bytes(), row["dek_nonce"].Bytes()) if err != nil { return nil, err } return &sysin.ImMessageKeyModel{ TenantId: tenant.Id, TenantCode: tenant.Code, Enabled: enabledValue.Int() == 1, Version: version, Algorithm: row["algorithm"].String(), Status: row["status"].String(), Key: immessagecrypto.EncodeDEK(dek), }, nil } func (s *sSysImMessageEncryption) Ack(ctx context.Context, in *sysin.ImMessageKeyAckInp) error { tenant, err := s.tenantByCode(ctx, in.TenantCode) if err != nil { return err } if in.Version == 0 { return gerror.New("消息密钥版本不能为空") } err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { row, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx). Where("tenant_id", tenant.Id).Where("key_version", in.Version).One() if queryErr != nil { return gerror.Wrap(queryErr, "获取待激活消息密钥失败") } if row.IsEmpty() || row["status"].String() != consts.ImMessageKeyStatusPending { return gerror.New("待激活消息密钥不存在") } if _, updateErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx). Where("tenant_id", tenant.Id).Where("status", consts.ImMessageKeyStatusActive). Data(g.Map{"status": consts.ImMessageKeyStatusRetired, "retired_at": gtime.Now()}).Update(); updateErr != nil { return gerror.Wrap(updateErr, "退役旧消息密钥失败") } if _, updateErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx). Where("tenant_id", tenant.Id).Where("key_version", in.Version). Data(g.Map{"status": consts.ImMessageKeyStatusActive, "activated_at": gtime.Now()}).Update(); updateErr != nil { return gerror.Wrap(updateErr, "激活新消息密钥失败") } return nil }) if err != nil { return err } return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "message.keys.activated", "message_encryption", in.Version) } func (s *sSysImMessageEncryption) requireSuperAdmin(ctx context.Context) error { if !service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx)) { return gerror.New("只有最高权限平台管理员可以管理消息密钥") } return nil } func (s *sSysImMessageEncryption) tenantByID(ctx context.Context, tenantID int64) (*entity.ImTenant, error) { var tenant *entity.ImTenant if err := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantID).Scan(&tenant); err != nil { return nil, gerror.Wrap(err, "获取商户失败") } if tenant == nil || tenant.Id == 0 { return nil, gerror.New("商户不存在") } return tenant, nil } func (s *sSysImMessageEncryption) tenantByCode(ctx context.Context, code string) (*entity.ImTenant, error) { var tenant *entity.ImTenant if err := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx). Where(dao.ImTenant.Columns().Code, strings.TrimSpace(code)).Scan(&tenant); err != nil { return nil, gerror.Wrap(err, "获取商户失败") } if tenant == nil || tenant.Id == 0 { return nil, gerror.New("商户不存在") } return tenant, nil }