parent
5e813ef079
commit
b4c925e910
@ -0,0 +1,79 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/openimsdk/open-im-server/v3/internal/rpc/encryption"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var configPath = flag.String("config", "./config", "path to config directory")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Load encryption service config independently
|
||||||
|
config, err := loadEncryptionConfig(*configPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load encryption config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the encryption service
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := encryption.Start(ctx, config); err != nil {
|
||||||
|
log.Fatalf("Failed to start encryption service: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadEncryptionConfig loads configuration from multiple files following OpenIM pattern
|
||||||
|
func loadEncryptionConfig(configDir string) (*encryption.Config, error) {
|
||||||
|
config := &encryption.Config{}
|
||||||
|
|
||||||
|
// Load main encryption config (only contains rpc, prometheus, and encryption-specific settings)
|
||||||
|
encryptionConfigFile := filepath.Join(configDir, "openim-rpc-encryption.yml")
|
||||||
|
if err := loadYAMLFile(encryptionConfigFile, config); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load encryption config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load shared MongoDB configuration
|
||||||
|
mongoConfigFile := filepath.Join(configDir, "mongodb.yml")
|
||||||
|
if err := loadYAMLFile(mongoConfigFile, &config.MongodbConfig); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load mongodb config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load shared Discovery configuration
|
||||||
|
discoveryConfigFile := filepath.Join(configDir, "discovery.yml")
|
||||||
|
if err := loadYAMLFile(discoveryConfigFile, &config.Discovery); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load discovery config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadYAMLFile loads a YAML file into the given struct
|
||||||
|
func loadYAMLFile(filename string, out interface{}) error {
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return yaml.Unmarshal(data, out)
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
# Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# RPC Configuration (following OpenIM standard format)
|
||||||
|
rpc:
|
||||||
|
# API or other RPCs can access this RPC through this IP; if left blank, the internal network IP is obtained by default
|
||||||
|
registerIP:
|
||||||
|
# Listening IP; 0.0.0.0 means both internal and external IPs are listened to, if blank, the internal network IP is automatically obtained by default
|
||||||
|
listenIP: 0.0.0.0
|
||||||
|
# autoSetPorts indicates whether to automatically set the ports
|
||||||
|
# if you use in kubernetes, set it to false
|
||||||
|
autoSetPorts: true
|
||||||
|
# List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports
|
||||||
|
# It will only take effect when autoSetPorts is set to false.
|
||||||
|
ports: [ 10800 ]
|
||||||
|
|
||||||
|
prometheus:
|
||||||
|
# Whether to enable prometheus
|
||||||
|
enable: true
|
||||||
|
# Prometheus listening ports, must be consistent with the number of rpc.ports
|
||||||
|
# It will only take effect when autoSetPorts is set to false.
|
||||||
|
ports: [ 20800 ]
|
||||||
|
|
||||||
|
# Encryption Configuration
|
||||||
|
encryption:
|
||||||
|
# Encryption mode: "aes", "signal", "hybrid"
|
||||||
|
mode: "aes" # Default to AES for compatibility
|
||||||
|
|
||||||
|
# AES encryption settings (existing)
|
||||||
|
aes:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Signal Protocol settings
|
||||||
|
signal:
|
||||||
|
enabled: false # Set to true when ready to use Signal Protocol
|
||||||
|
preKeyBatch: 100 # Max one-time prekeys per upload
|
||||||
|
keyRotationInterval: "168h" # 7 days
|
||||||
|
sessionCleanupInterval: "720h" # 30 days
|
||||||
|
prekeyCleanupInterval: "168h" # 7 days
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
maxOneTimePreKeys: 100
|
||||||
|
maxSessionsPerDevice: 1000
|
||||||
|
|
||||||
|
# Validation settings
|
||||||
|
validateSignatures: true
|
||||||
|
requireIdentityKeys: true
|
||||||
|
|
@ -0,0 +1,111 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package encryption
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrometheusConfig contains Prometheus configuration
|
||||||
|
type PrometheusConfig struct {
|
||||||
|
Enable bool `yaml:"enable"`
|
||||||
|
Ports []int `yaml:"ports"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config represents the configuration for encryption service
|
||||||
|
type Config struct {
|
||||||
|
RpcConfig config.RPC `yaml:"rpc"`
|
||||||
|
MongodbConfig config.Mongo `yaml:"mongo"`
|
||||||
|
Discovery config.Discovery `yaml:"discovery"`
|
||||||
|
PrometheusConfig PrometheusConfig `yaml:"prometheus"`
|
||||||
|
EncryptionConfig EncryptionConfig `yaml:"encryption"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptionConfig contains encryption-specific configuration
|
||||||
|
type EncryptionConfig struct {
|
||||||
|
Mode string `yaml:"mode"` // "aes", "signal", "hybrid"
|
||||||
|
AES AESConfig `yaml:"aes"`
|
||||||
|
Signal SignalConfig `yaml:"signal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AESConfig contains AES encryption configuration
|
||||||
|
type AESConfig struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignalConfig contains Signal Protocol configuration
|
||||||
|
type SignalConfig struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
PreKeyBatch int `yaml:"preKeyBatch"`
|
||||||
|
KeyRotationInterval time.Duration `yaml:"keyRotationInterval"`
|
||||||
|
SessionCleanupInterval time.Duration `yaml:"sessionCleanupInterval"`
|
||||||
|
PrekeyCleanupInterval time.Duration `yaml:"prekeyCleanupInterval"`
|
||||||
|
|
||||||
|
// Security settings
|
||||||
|
MaxOneTimePreKeys int `yaml:"maxOneTimePreKeys"`
|
||||||
|
MaxSessionsPerDevice int `yaml:"maxSessionsPerDevice"`
|
||||||
|
|
||||||
|
// Validation settings
|
||||||
|
ValidateSignatures bool `yaml:"validateSignatures"`
|
||||||
|
RequireIdentityKeys bool `yaml:"requireIdentityKeys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEncryptionMode returns the current encryption mode
|
||||||
|
func (c *Config) GetEncryptionMode() string {
|
||||||
|
if c.EncryptionConfig.Mode == "" {
|
||||||
|
return "aes" // default to AES for compatibility
|
||||||
|
}
|
||||||
|
return c.EncryptionConfig.Mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSignalEnabled returns true if Signal Protocol is enabled
|
||||||
|
func (c *Config) IsSignalEnabled() bool {
|
||||||
|
return c.EncryptionConfig.Signal.Enabled &&
|
||||||
|
(c.EncryptionConfig.Mode == "signal" || c.EncryptionConfig.Mode == "hybrid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAESEnabled returns true if AES encryption is enabled
|
||||||
|
func (c *Config) IsAESEnabled() bool {
|
||||||
|
return c.EncryptionConfig.AES.Enabled ||
|
||||||
|
c.EncryptionConfig.Mode == "aes" ||
|
||||||
|
c.EncryptionConfig.Mode == "hybrid"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignalConfig returns Signal Protocol configuration
|
||||||
|
func (c *Config) GetSignalConfig() *SignalConfig {
|
||||||
|
// Set defaults if not specified
|
||||||
|
if c.EncryptionConfig.Signal.PreKeyBatch == 0 {
|
||||||
|
c.EncryptionConfig.Signal.PreKeyBatch = 100
|
||||||
|
}
|
||||||
|
if c.EncryptionConfig.Signal.KeyRotationInterval == 0 {
|
||||||
|
c.EncryptionConfig.Signal.KeyRotationInterval = 7 * 24 * time.Hour // 7 days
|
||||||
|
}
|
||||||
|
if c.EncryptionConfig.Signal.SessionCleanupInterval == 0 {
|
||||||
|
c.EncryptionConfig.Signal.SessionCleanupInterval = 30 * 24 * time.Hour // 30 days
|
||||||
|
}
|
||||||
|
if c.EncryptionConfig.Signal.PrekeyCleanupInterval == 0 {
|
||||||
|
c.EncryptionConfig.Signal.PrekeyCleanupInterval = 7 * 24 * time.Hour // 7 days
|
||||||
|
}
|
||||||
|
if c.EncryptionConfig.Signal.MaxOneTimePreKeys == 0 {
|
||||||
|
c.EncryptionConfig.Signal.MaxOneTimePreKeys = 100
|
||||||
|
}
|
||||||
|
if c.EncryptionConfig.Signal.MaxSessionsPerDevice == 0 {
|
||||||
|
c.EncryptionConfig.Signal.MaxSessionsPerDevice = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
return &c.EncryptionConfig.Signal
|
||||||
|
}
|
@ -0,0 +1,348 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package encryption
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/openimsdk/tools/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Request/Response structures for HTTP API
|
||||||
|
type GetPreKeysResponse struct {
|
||||||
|
IdentityKey *IdentityKeyInfo `json:"identityKey"`
|
||||||
|
SignedPreKey *SignedPreKeyInfo `json:"signedPreKey"`
|
||||||
|
OneTimePreKey *PreKeyInfo `json:"oneTimePreKey,omitempty"`
|
||||||
|
RegistrationID int32 `json:"registrationId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdentityKeyInfo struct {
|
||||||
|
IdentityKey string `json:"identityKey"`
|
||||||
|
RegistrationID int32 `json:"registrationId"`
|
||||||
|
CreatedTime int64 `json:"createdTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreKeyInfo struct {
|
||||||
|
KeyID uint32 `json:"keyId"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignedPreKeyInfo struct {
|
||||||
|
KeyID uint32 `json:"keyId"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
CreatedTime int64 `json:"createdTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetPreKeysRequest struct {
|
||||||
|
IdentityKey string `json:"identityKey,omitempty"`
|
||||||
|
SignedPreKey *SignedPreKeyInfo `json:"signedPreKey,omitempty"`
|
||||||
|
OneTimePreKeys []PreKeyInfo `json:"oneTimePreKeys,omitempty"`
|
||||||
|
RegistrationID int32 `json:"registrationId,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPreKeys handles GET /api/v1/encryption/prekeys/:user_id/:device_id
|
||||||
|
func (s *Server) GetPreKeys(c *gin.Context) {
|
||||||
|
userID := c.Param("user_id")
|
||||||
|
deviceIDStr := c.Param("device_id")
|
||||||
|
|
||||||
|
deviceID, err := strconv.ParseInt(deviceIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, APIResponse{
|
||||||
|
Code: 400,
|
||||||
|
Message: "Invalid device_id",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.ZInfo(c.Request.Context(), "GetPreKeys", "userID", userID, "deviceID", deviceID)
|
||||||
|
|
||||||
|
// Get identity key
|
||||||
|
identityKey, err := s.keysManager.GetIdentityKey(c.Request.Context(), userID, int32(deviceID))
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(c.Request.Context(), "failed to get identity key", err)
|
||||||
|
c.JSON(http.StatusNotFound, APIResponse{
|
||||||
|
Code: 404,
|
||||||
|
Message: "Identity key not found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get signed prekey
|
||||||
|
signedPreKey, err := s.keysManager.GetActiveSignedPreKey(c.Request.Context(), userID, int32(deviceID))
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(c.Request.Context(), "failed to get signed prekey", err)
|
||||||
|
c.JSON(http.StatusNotFound, APIResponse{
|
||||||
|
Code: 404,
|
||||||
|
Message: "Signed prekey not found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get one-time prekey (optional)
|
||||||
|
oneTimePreKey, err := s.keysManager.GetOneTimePreKey(c.Request.Context(), userID, int32(deviceID))
|
||||||
|
if err != nil {
|
||||||
|
log.ZWarn(c.Request.Context(), "no one-time prekey available", err)
|
||||||
|
oneTimePreKey = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &GetPreKeysResponse{
|
||||||
|
IdentityKey: &IdentityKeyInfo{
|
||||||
|
IdentityKey: base64.StdEncoding.EncodeToString(identityKey.IdentityKey),
|
||||||
|
RegistrationID: identityKey.RegistrationID,
|
||||||
|
CreatedTime: identityKey.CreatedTime.Unix(),
|
||||||
|
},
|
||||||
|
SignedPreKey: &SignedPreKeyInfo{
|
||||||
|
KeyID: signedPreKey.KeyID,
|
||||||
|
PublicKey: base64.StdEncoding.EncodeToString(signedPreKey.PublicKey),
|
||||||
|
Signature: base64.StdEncoding.EncodeToString(signedPreKey.Signature),
|
||||||
|
CreatedTime: signedPreKey.CreatedTime.Unix(),
|
||||||
|
},
|
||||||
|
RegistrationID: identityKey.RegistrationID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if oneTimePreKey != nil {
|
||||||
|
response.OneTimePreKey = &PreKeyInfo{
|
||||||
|
KeyID: oneTimePreKey.KeyID,
|
||||||
|
PublicKey: base64.StdEncoding.EncodeToString(oneTimePreKey.PublicKey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, APIResponse{
|
||||||
|
Code: 0,
|
||||||
|
Message: "success",
|
||||||
|
Data: response,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPreKeys handles POST /api/v1/encryption/prekeys/:user_id/:device_id
|
||||||
|
func (s *Server) SetPreKeys(c *gin.Context) {
|
||||||
|
userID := c.Param("user_id")
|
||||||
|
deviceIDStr := c.Param("device_id")
|
||||||
|
|
||||||
|
deviceID, err := strconv.ParseInt(deviceIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, APIResponse{
|
||||||
|
Code: 400,
|
||||||
|
Message: "Invalid device_id",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req SetPreKeysRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, APIResponse{
|
||||||
|
Code: 400,
|
||||||
|
Message: "Invalid request body",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.ZInfo(c.Request.Context(), "SetPreKeys", "userID", userID, "deviceID", deviceID)
|
||||||
|
|
||||||
|
// Set identity key if provided
|
||||||
|
if req.IdentityKey != "" {
|
||||||
|
identityKeyBytes, err := base64.StdEncoding.DecodeString(req.IdentityKey)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, APIResponse{
|
||||||
|
Code: 400,
|
||||||
|
Message: "Invalid identity key encoding",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.keysManager.SetIdentityKey(c.Request.Context(), userID, int32(deviceID), identityKeyBytes, req.RegistrationID)
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(c.Request.Context(), "failed to set identity key", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, APIResponse{
|
||||||
|
Code: 500,
|
||||||
|
Message: "Failed to set identity key",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set signed prekey if provided
|
||||||
|
if req.SignedPreKey != nil {
|
||||||
|
publicKeyBytes, err := base64.StdEncoding.DecodeString(req.SignedPreKey.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, APIResponse{
|
||||||
|
Code: 400,
|
||||||
|
Message: "Invalid signed prekey public key encoding",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
signatureBytes, err := base64.StdEncoding.DecodeString(req.SignedPreKey.Signature)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, APIResponse{
|
||||||
|
Code: 400,
|
||||||
|
Message: "Invalid signed prekey signature encoding",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
signedPreKeyData := &SignedPreKeyResponse{
|
||||||
|
KeyId: req.SignedPreKey.KeyID,
|
||||||
|
PublicKey: publicKeyBytes,
|
||||||
|
Signature: signatureBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.keysManager.SetSignedPreKey(c.Request.Context(), userID, int32(deviceID), signedPreKeyData)
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(c.Request.Context(), "failed to set signed prekey", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, APIResponse{
|
||||||
|
Code: 500,
|
||||||
|
Message: "Failed to set signed prekey",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set one-time prekeys
|
||||||
|
if len(req.OneTimePreKeys) > 0 {
|
||||||
|
var preKeyData []*PreKeyResponse
|
||||||
|
for _, pk := range req.OneTimePreKeys {
|
||||||
|
publicKeyBytes, err := base64.StdEncoding.DecodeString(pk.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, APIResponse{
|
||||||
|
Code: 400,
|
||||||
|
Message: "Invalid one-time prekey public key encoding",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
preKeyData = append(preKeyData, &PreKeyResponse{
|
||||||
|
KeyId: pk.KeyID,
|
||||||
|
PublicKey: publicKeyBytes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptedCount, err := s.keysManager.SetOneTimePreKeys(c.Request.Context(), userID, int32(deviceID), preKeyData)
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(c.Request.Context(), "failed to set one-time prekeys", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, APIResponse{
|
||||||
|
Code: 500,
|
||||||
|
Message: "Failed to set one-time prekeys",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, APIResponse{
|
||||||
|
Code: 0,
|
||||||
|
Message: "success",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"preKeysAccepted": acceptedCount,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, APIResponse{
|
||||||
|
Code: 0,
|
||||||
|
Message: "success",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPreKeyCount handles GET /api/v1/encryption/prekeys/:user_id/:device_id/count
|
||||||
|
func (s *Server) GetPreKeyCount(c *gin.Context) {
|
||||||
|
userID := c.Param("user_id")
|
||||||
|
deviceIDStr := c.Param("device_id")
|
||||||
|
|
||||||
|
deviceID, err := strconv.ParseInt(deviceIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, APIResponse{
|
||||||
|
Code: 400,
|
||||||
|
Message: "Invalid device_id",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := s.keysManager.GetPreKeyCount(c.Request.Context(), userID, int32(deviceID))
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(c.Request.Context(), "failed to get prekey count", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, APIResponse{
|
||||||
|
Code: 500,
|
||||||
|
Message: "Failed to get prekey count",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
signedPreKeyExists, lastRotation, err := s.keysManager.GetSignedPreKeyInfo(c.Request.Context(), userID, int32(deviceID))
|
||||||
|
if err != nil {
|
||||||
|
signedPreKeyExists = false
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"oneTimePreKeyCount": count,
|
||||||
|
"signedPreKeyExists": signedPreKeyExists,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !lastRotation.IsZero() {
|
||||||
|
data["lastSignedPreKeyRotation"] = lastRotation.Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, APIResponse{
|
||||||
|
Code: 0,
|
||||||
|
Message: "success",
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIdentityKey handles GET /api/v1/encryption/identity/:user_id/:device_id
|
||||||
|
func (s *Server) GetIdentityKey(c *gin.Context) {
|
||||||
|
userID := c.Param("user_id")
|
||||||
|
deviceIDStr := c.Param("device_id")
|
||||||
|
|
||||||
|
deviceID, err := strconv.ParseInt(deviceIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, APIResponse{
|
||||||
|
Code: 400,
|
||||||
|
Message: "Invalid device_id",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
identityKey, err := s.keysManager.GetIdentityKey(c.Request.Context(), userID, int32(deviceID))
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(c.Request.Context(), "failed to get identity key", err)
|
||||||
|
c.JSON(http.StatusNotFound, APIResponse{
|
||||||
|
Code: 404,
|
||||||
|
Message: "Identity key not found",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &IdentityKeyInfo{
|
||||||
|
IdentityKey: base64.StdEncoding.EncodeToString(identityKey.IdentityKey),
|
||||||
|
RegistrationID: identityKey.RegistrationID,
|
||||||
|
CreatedTime: identityKey.CreatedTime.Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, APIResponse{
|
||||||
|
Code: 0,
|
||||||
|
Message: "success",
|
||||||
|
Data: response,
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,287 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package encryption
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/openimsdk/open-im-server/v3/internal/rpc/encryption/stores"
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model/signal"
|
||||||
|
"github.com/openimsdk/tools/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Maximum number of one-time prekeys that can be uploaded at once
|
||||||
|
MaxOneTimePreKeys = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
type KeysManager struct {
|
||||||
|
identityStore stores.IdentityStoreInterface
|
||||||
|
preKeyStore stores.PreKeyStoreInterface
|
||||||
|
signedPreKeyStore stores.SignedPreKeyStoreInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKeysManager(
|
||||||
|
identityStore stores.IdentityStoreInterface,
|
||||||
|
preKeyStore stores.PreKeyStoreInterface,
|
||||||
|
signedPreKeyStore stores.SignedPreKeyStoreInterface,
|
||||||
|
) *KeysManager {
|
||||||
|
return &KeysManager{
|
||||||
|
identityStore: identityStore,
|
||||||
|
preKeyStore: preKeyStore,
|
||||||
|
signedPreKeyStore: signedPreKeyStore,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIdentityKey retrieves the identity key for a user/device
|
||||||
|
func (km *KeysManager) GetIdentityKey(ctx context.Context, userID string, deviceID int32) (*signal.SignalIdentityKey, error) {
|
||||||
|
return km.identityStore.Get(ctx, userID, deviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIdentityKey sets the identity key for a user/device
|
||||||
|
func (km *KeysManager) SetIdentityKey(ctx context.Context, userID string, deviceID int32, identityKey []byte, registrationID int32) error {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Check if identity key already exists
|
||||||
|
existing, err := km.identityStore.Get(ctx, userID, deviceID)
|
||||||
|
if err == nil && existing != nil {
|
||||||
|
// Update existing identity key
|
||||||
|
existing.IdentityKey = identityKey
|
||||||
|
existing.RegistrationID = registrationID
|
||||||
|
existing.UpdatedTime = now
|
||||||
|
return km.identityStore.Update(ctx, userID, deviceID, existing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new identity key
|
||||||
|
identityKeyRecord := &signal.SignalIdentityKey{
|
||||||
|
UserID: userID,
|
||||||
|
DeviceID: deviceID,
|
||||||
|
IdentityKey: identityKey,
|
||||||
|
RegistrationID: registrationID,
|
||||||
|
CreatedTime: now,
|
||||||
|
UpdatedTime: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
return km.identityStore.Create(ctx, identityKeyRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveSignedPreKey retrieves the active signed prekey for a user/device
|
||||||
|
func (km *KeysManager) GetActiveSignedPreKey(ctx context.Context, userID string, deviceID int32) (*signal.SignalSignedPreKey, error) {
|
||||||
|
return km.signedPreKeyStore.GetActive(ctx, userID, deviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSignedPreKey sets a signed prekey for a user/device
|
||||||
|
func (km *KeysManager) SetSignedPreKey(ctx context.Context, userID string, deviceID int32, signedPreKey *SignedPreKeyResponse) error {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
signedPreKeyRecord := &signal.SignalSignedPreKey{
|
||||||
|
UserID: userID,
|
||||||
|
DeviceID: deviceID,
|
||||||
|
KeyID: signedPreKey.KeyId,
|
||||||
|
PublicKey: signedPreKey.PublicKey,
|
||||||
|
Signature: signedPreKey.Signature,
|
||||||
|
CreatedTime: now,
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deactivate existing signed prekeys and set this one as active
|
||||||
|
err := km.signedPreKeyStore.SetActive(ctx, userID, deviceID, signedPreKey.KeyId)
|
||||||
|
if err != nil {
|
||||||
|
log.ZWarn(ctx, "failed to deactivate existing signed prekeys", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update the signed prekey
|
||||||
|
existing, err := km.signedPreKeyStore.GetByKeyID(ctx, userID, deviceID, signedPreKey.KeyId)
|
||||||
|
if err == nil && existing != nil {
|
||||||
|
return km.signedPreKeyStore.Update(ctx, userID, deviceID, signedPreKey.KeyId, signedPreKeyRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
return km.signedPreKeyStore.Create(ctx, signedPreKeyRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOneTimePreKey retrieves an available one-time prekey and marks it as used
|
||||||
|
func (km *KeysManager) GetOneTimePreKey(ctx context.Context, userID string, deviceID int32) (*signal.SignalPreKey, error) {
|
||||||
|
preKey, err := km.preKeyStore.GetAvailable(ctx, userID, deviceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("no available one-time prekey: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the prekey as used
|
||||||
|
err = km.preKeyStore.MarkUsed(ctx, userID, deviceID, preKey.KeyID)
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(ctx, "failed to mark prekey as used", err, "userID", userID, "deviceID", deviceID, "keyID", preKey.KeyID)
|
||||||
|
// Don't fail the request, but log the error
|
||||||
|
}
|
||||||
|
|
||||||
|
return preKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOneTimePreKeys sets multiple one-time prekeys for a user/device
|
||||||
|
func (km *KeysManager) SetOneTimePreKeys(ctx context.Context, userID string, deviceID int32, preKeys []*PreKeyResponse) (int, error) {
|
||||||
|
if len(preKeys) > MaxOneTimePreKeys {
|
||||||
|
return 0, fmt.Errorf("too many one-time prekeys: %d (max: %d)", len(preKeys), MaxOneTimePreKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
var preKeyRecords []*signal.SignalPreKey
|
||||||
|
|
||||||
|
for _, preKey := range preKeys {
|
||||||
|
preKeyRecord := &signal.SignalPreKey{
|
||||||
|
UserID: userID,
|
||||||
|
DeviceID: deviceID,
|
||||||
|
KeyID: preKey.KeyId,
|
||||||
|
PublicKey: preKey.PublicKey,
|
||||||
|
Used: false,
|
||||||
|
CreatedTime: now,
|
||||||
|
}
|
||||||
|
preKeyRecords = append(preKeyRecords, preKeyRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := km.preKeyStore.CreateBatch(ctx, preKeyRecords)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to create one-time prekeys: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(preKeyRecords), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPreKeyCount returns the count of available one-time prekeys for a user/device
|
||||||
|
func (km *KeysManager) GetPreKeyCount(ctx context.Context, userID string, deviceID int32) (int64, error) {
|
||||||
|
return km.preKeyStore.CountAvailable(ctx, userID, deviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignedPreKeyInfo returns information about signed prekey existence and last rotation
|
||||||
|
func (km *KeysManager) GetSignedPreKeyInfo(ctx context.Context, userID string, deviceID int32) (exists bool, lastRotation time.Time, err error) {
|
||||||
|
signedPreKey, err := km.signedPreKeyStore.GetActive(ctx, userID, deviceID)
|
||||||
|
if err != nil {
|
||||||
|
return false, time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if signedPreKey != nil {
|
||||||
|
return true, signedPreKey.CreatedTime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, time.Time{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupExpiredKeys removes expired keys from storage
|
||||||
|
func (km *KeysManager) CleanupExpiredKeys(ctx context.Context) error {
|
||||||
|
// Cleanup used one-time prekeys older than 7 days
|
||||||
|
usedPreKeysCleanedCount, err := km.preKeyStore.CleanupUsed(ctx, 7*24*time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(ctx, "failed to cleanup used prekeys", err)
|
||||||
|
} else {
|
||||||
|
log.ZInfo(ctx, "cleaned up used prekeys", "count", usedPreKeysCleanedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup inactive signed prekeys older than 30 days
|
||||||
|
inactiveSignedPreKeysCount, err := km.signedPreKeyStore.CleanupInactive(ctx, 30*24*time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(ctx, "failed to cleanup inactive signed prekeys", err)
|
||||||
|
} else {
|
||||||
|
log.ZInfo(ctx, "cleaned up inactive signed prekeys", "count", inactiveSignedPreKeysCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSignedPreKey validates the signature of a signed prekey
|
||||||
|
func (km *KeysManager) ValidateSignedPreKey(ctx context.Context, userID string, deviceID int32, signedPreKey *SignedPreKeyResponse) error {
|
||||||
|
// Get the identity key to verify the signature
|
||||||
|
_, err := km.GetIdentityKey(ctx, userID, deviceID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get identity key for signature validation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement actual signature validation using Signal Protocol
|
||||||
|
// This would require integrating with the Signal Protocol library
|
||||||
|
// For now, we'll skip the validation
|
||||||
|
log.ZInfo(ctx, "signed prekey signature validation skipped (not implemented)",
|
||||||
|
"userID", userID, "deviceID", deviceID, "keyID", signedPreKey.KeyId)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RotateSignedPreKey creates a new signed prekey and deactivates the old one
|
||||||
|
func (km *KeysManager) RotateSignedPreKey(ctx context.Context, userID string, deviceID int32, newSignedPreKey *SignedPreKeyResponse) error {
|
||||||
|
// Validate the new signed prekey
|
||||||
|
err := km.ValidateSignedPreKey(ctx, userID, deviceID, newSignedPreKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("signed prekey validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the new signed prekey (this will automatically deactivate the old one)
|
||||||
|
return km.SetSignedPreKey(ctx, userID, deviceID, newSignedPreKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPreKeyBundleForUser retrieves all necessary keys for X3DH key agreement
|
||||||
|
func (km *KeysManager) GetPreKeyBundleForUser(ctx context.Context, userID string, deviceID int32) (map[string]interface{}, error) {
|
||||||
|
// Get identity key
|
||||||
|
identityKey, err := km.GetIdentityKey(ctx, userID, deviceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get identity key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get signed prekey
|
||||||
|
signedPreKey, err := km.GetActiveSignedPreKey(ctx, userID, deviceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get signed prekey: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get one-time prekey (optional)
|
||||||
|
oneTimePreKey, err := km.GetOneTimePreKey(ctx, userID, deviceID)
|
||||||
|
if err != nil {
|
||||||
|
log.ZWarn(ctx, "no one-time prekey available", err)
|
||||||
|
oneTimePreKey = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle := map[string]interface{}{
|
||||||
|
"identityKey": map[string]interface{}{
|
||||||
|
"identityKey": identityKey.IdentityKey,
|
||||||
|
"registrationId": identityKey.RegistrationID,
|
||||||
|
"createdTime": identityKey.CreatedTime.Unix(),
|
||||||
|
},
|
||||||
|
"signedPreKey": map[string]interface{}{
|
||||||
|
"keyId": signedPreKey.KeyID,
|
||||||
|
"publicKey": signedPreKey.PublicKey,
|
||||||
|
"signature": signedPreKey.Signature,
|
||||||
|
"createdTime": signedPreKey.CreatedTime.Unix(),
|
||||||
|
},
|
||||||
|
"registrationId": identityKey.RegistrationID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if oneTimePreKey != nil {
|
||||||
|
bundle["oneTimePreKey"] = map[string]interface{}{
|
||||||
|
"keyId": oneTimePreKey.KeyID,
|
||||||
|
"publicKey": oneTimePreKey.PublicKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundle, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response structures used in key manager
|
||||||
|
type SignedPreKeyResponse struct {
|
||||||
|
KeyId uint32 `json:"keyId"`
|
||||||
|
PublicKey []byte `json:"publicKey"`
|
||||||
|
Signature []byte `json:"signature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreKeyResponse struct {
|
||||||
|
KeyId uint32 `json:"keyId"`
|
||||||
|
PublicKey []byte `json:"publicKey"`
|
||||||
|
}
|
@ -0,0 +1,178 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package encryption
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/openimsdk/open-im-server/v3/internal/rpc/encryption/stores"
|
||||||
|
kdisc "github.com/openimsdk/open-im-server/v3/pkg/common/discovery"
|
||||||
|
"github.com/openimsdk/tools/db/mongoutil"
|
||||||
|
"github.com/openimsdk/tools/discovery"
|
||||||
|
"github.com/openimsdk/tools/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
*Config
|
||||||
|
keysManager *KeysManager
|
||||||
|
discoveryConn discovery.Conn
|
||||||
|
httpServer *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func Start(ctx context.Context, cfg *Config) error {
|
||||||
|
log.ZInfo(ctx, "encryption server start")
|
||||||
|
|
||||||
|
// Initialize service registry
|
||||||
|
client, err := kdisc.NewDiscoveryRegister(&cfg.Discovery, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize MongoDB
|
||||||
|
mongoClient, err := mongoutil.NewMongoDB(ctx, cfg.MongodbConfig.Build())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the specific database
|
||||||
|
db := mongoClient.GetDB()
|
||||||
|
|
||||||
|
// Initialize stores
|
||||||
|
identityStore := stores.NewIdentityStore(db)
|
||||||
|
preKeyStore := stores.NewPreKeyStore(db)
|
||||||
|
signedPreKeyStore := stores.NewSignedPreKeyStore(db)
|
||||||
|
|
||||||
|
// Initialize managers
|
||||||
|
keysManager := NewKeysManager(identityStore, preKeyStore, signedPreKeyStore)
|
||||||
|
|
||||||
|
server := &Server{
|
||||||
|
Config: cfg,
|
||||||
|
keysManager: keysManager,
|
||||||
|
discoveryConn: client,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup HTTP server
|
||||||
|
if err := server.setupHTTPServer(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start HTTP server
|
||||||
|
go func() {
|
||||||
|
if err := server.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.ZError(ctx, "HTTP server failed to start", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.ZInfo(ctx, "encryption server started successfully", "port", cfg.RpcConfig.Ports[0])
|
||||||
|
|
||||||
|
// Keep the service running
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return server.shutdown(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) setupHTTPServer() error {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
router := gin.Default()
|
||||||
|
|
||||||
|
// Add middleware
|
||||||
|
router.Use(gin.Recovery())
|
||||||
|
router.Use(s.corsMiddleware())
|
||||||
|
router.Use(s.loggingMiddleware())
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
api := router.Group("/api/v1/encryption")
|
||||||
|
{
|
||||||
|
// Key management endpoints
|
||||||
|
api.GET("/prekeys/:user_id/:device_id", s.GetPreKeys)
|
||||||
|
api.POST("/prekeys/:user_id/:device_id", s.SetPreKeys)
|
||||||
|
api.GET("/prekeys/:user_id/:device_id/count", s.GetPreKeyCount)
|
||||||
|
api.GET("/identity/:user_id/:device_id", s.GetIdentityKey)
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
api.GET("/health", s.HealthCheck)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create HTTP server
|
||||||
|
port := fmt.Sprintf(":%d", s.Config.RpcConfig.Ports[0])
|
||||||
|
s.httpServer = &http.Server{
|
||||||
|
Addr: port,
|
||||||
|
Handler: router,
|
||||||
|
ReadTimeout: 30 * time.Second,
|
||||||
|
WriteTimeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) shutdown(ctx context.Context) error {
|
||||||
|
log.ZInfo(ctx, "shutting down encryption server")
|
||||||
|
|
||||||
|
// Shutdown HTTP server
|
||||||
|
if s.httpServer != nil {
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := s.httpServer.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.ZError(ctx, "failed to shutdown HTTP server", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) corsMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Header("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
|
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||||
|
|
||||||
|
if c.Request.Method == "OPTIONS" {
|
||||||
|
c.AbortWithStatus(204)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) loggingMiddleware() gin.HandlerFunc {
|
||||||
|
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||||
|
return fmt.Sprintf("[%s] %s %s %d %s\n",
|
||||||
|
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
|
||||||
|
param.Method,
|
||||||
|
param.Path,
|
||||||
|
param.StatusCode,
|
||||||
|
param.Latency,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck handles GET /api/v1/encryption/health
|
||||||
|
func (s *Server) HealthCheck(c *gin.Context) {
|
||||||
|
c.JSON(200, APIResponse{
|
||||||
|
Code: 0,
|
||||||
|
Message: "success",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"status": "healthy",
|
||||||
|
"mode": s.Config.GetEncryptionMode(),
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package stores
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model/signal"
|
||||||
|
"github.com/openimsdk/tools/db/mongoutil"
|
||||||
|
"github.com/openimsdk/tools/errs"
|
||||||
|
"github.com/openimsdk/tools/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IdentityStore struct {
|
||||||
|
coll *mongo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIdentityStore(db *mongo.Database) IdentityStoreInterface {
|
||||||
|
coll := db.Collection(signal.SignalIdentityKeyCollection)
|
||||||
|
// Create indexes
|
||||||
|
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||||
|
{
|
||||||
|
Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "device_id", Value: 1}},
|
||||||
|
Options: options.Index().SetUnique(true),
|
||||||
|
},
|
||||||
|
{Keys: bson.D{{Key: "user_id", Value: 1}}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.ZWarn(context.Background(), "failed to create indexes for identity store", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &IdentityStore{coll: coll}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IdentityStore) Create(ctx context.Context, identityKey *signal.SignalIdentityKey) error {
|
||||||
|
return mongoutil.InsertOne(ctx, s.coll, identityKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IdentityStore) Update(ctx context.Context, userID string, deviceID int32, identityKey *signal.SignalIdentityKey) error {
|
||||||
|
filter := bson.M{"user_id": userID, "device_id": deviceID}
|
||||||
|
update := bson.M{"$set": identityKey}
|
||||||
|
return mongoutil.UpdateOne(ctx, s.coll, filter, update, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IdentityStore) Get(ctx context.Context, userID string, deviceID int32) (*signal.SignalIdentityKey, error) {
|
||||||
|
filter := bson.M{"user_id": userID, "device_id": deviceID}
|
||||||
|
identityKey, err := mongoutil.FindOne[*signal.SignalIdentityKey](ctx, s.coll, filter)
|
||||||
|
if err != nil {
|
||||||
|
if errs.ErrRecordNotFound.Is(err) {
|
||||||
|
return nil, fmt.Errorf("identity key not found for user %s device %d", userID, deviceID)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return identityKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IdentityStore) Delete(ctx context.Context, userID string, deviceID int32) error {
|
||||||
|
filter := bson.M{"user_id": userID, "device_id": deviceID}
|
||||||
|
return mongoutil.DeleteOne(ctx, s.coll, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IdentityStore) GetByUserID(ctx context.Context, userID string) ([]*signal.SignalIdentityKey, error) {
|
||||||
|
filter := bson.M{"user_id": userID}
|
||||||
|
return mongoutil.Find[*signal.SignalIdentityKey](ctx, s.coll, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IdentityStore) Exists(ctx context.Context, userID string, deviceID int32) (bool, error) {
|
||||||
|
filter := bson.M{"user_id": userID, "device_id": deviceID}
|
||||||
|
count, err := s.coll.CountDocuments(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package stores
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model/signal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IdentityStoreInterface defines the interface for identity key storage operations
|
||||||
|
type IdentityStoreInterface interface {
|
||||||
|
Create(ctx context.Context, identityKey *signal.SignalIdentityKey) error
|
||||||
|
Update(ctx context.Context, userID string, deviceID int32, identityKey *signal.SignalIdentityKey) error
|
||||||
|
Get(ctx context.Context, userID string, deviceID int32) (*signal.SignalIdentityKey, error)
|
||||||
|
Delete(ctx context.Context, userID string, deviceID int32) error
|
||||||
|
GetByUserID(ctx context.Context, userID string) ([]*signal.SignalIdentityKey, error)
|
||||||
|
Exists(ctx context.Context, userID string, deviceID int32) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreKeyStoreInterface defines the interface for one-time prekey storage operations
|
||||||
|
type PreKeyStoreInterface interface {
|
||||||
|
Create(ctx context.Context, prekey *signal.SignalPreKey) error
|
||||||
|
CreateBatch(ctx context.Context, prekeys []*signal.SignalPreKey) error
|
||||||
|
GetAvailable(ctx context.Context, userID string, deviceID int32) (*signal.SignalPreKey, error)
|
||||||
|
MarkUsed(ctx context.Context, userID string, deviceID int32, keyID uint32) error
|
||||||
|
Delete(ctx context.Context, userID string, deviceID int32, keyID uint32) error
|
||||||
|
DeleteAllByUserDevice(ctx context.Context, userID string, deviceID int32) error
|
||||||
|
CountAvailable(ctx context.Context, userID string, deviceID int32) (int64, error)
|
||||||
|
GetByKeyID(ctx context.Context, userID string, deviceID int32, keyID uint32) (*signal.SignalPreKey, error)
|
||||||
|
CleanupUsed(ctx context.Context, olderThan time.Duration) (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignedPreKeyStoreInterface defines the interface for signed prekey storage operations
|
||||||
|
type SignedPreKeyStoreInterface interface {
|
||||||
|
Create(ctx context.Context, signedPrekey *signal.SignalSignedPreKey) error
|
||||||
|
Update(ctx context.Context, userID string, deviceID int32, keyID uint32, signedPrekey *signal.SignalSignedPreKey) error
|
||||||
|
GetActive(ctx context.Context, userID string, deviceID int32) (*signal.SignalSignedPreKey, error)
|
||||||
|
GetByKeyID(ctx context.Context, userID string, deviceID int32, keyID uint32) (*signal.SignalSignedPreKey, error)
|
||||||
|
SetActive(ctx context.Context, userID string, deviceID int32, keyID uint32) error
|
||||||
|
Delete(ctx context.Context, userID string, deviceID int32, keyID uint32) error
|
||||||
|
GetAll(ctx context.Context, userID string, deviceID int32) ([]*signal.SignalSignedPreKey, error)
|
||||||
|
CleanupInactive(ctx context.Context, olderThan time.Duration) (int64, error)
|
||||||
|
Exists(ctx context.Context, userID string, deviceID int32) (bool, error)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package stores
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model/signal"
|
||||||
|
"github.com/openimsdk/tools/db/mongoutil"
|
||||||
|
"github.com/openimsdk/tools/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PreKeyStore struct {
|
||||||
|
coll *mongo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPreKeyStore(db *mongo.Database) PreKeyStoreInterface {
|
||||||
|
coll := db.Collection(signal.SignalPreKeyCollection)
|
||||||
|
// Create indexes
|
||||||
|
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||||
|
{
|
||||||
|
Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "device_id", Value: 1}, {Key: "key_id", Value: 1}},
|
||||||
|
Options: options.Index().SetUnique(true),
|
||||||
|
},
|
||||||
|
{Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "device_id", Value: 1}}},
|
||||||
|
{Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "device_id", Value: 1}, {Key: "used", Value: 1}}},
|
||||||
|
{Keys: bson.D{{Key: "created_time", Value: 1}}},
|
||||||
|
{Keys: bson.D{{Key: "used_time", Value: 1}}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.ZWarn(context.Background(), "failed to create indexes for prekey store", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PreKeyStore{coll: coll}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PreKeyStore) Create(ctx context.Context, prekey *signal.SignalPreKey) error {
|
||||||
|
return mongoutil.InsertOne(ctx, s.coll, prekey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PreKeyStore) CreateBatch(ctx context.Context, prekeys []*signal.SignalPreKey) error {
|
||||||
|
if len(prekeys) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return mongoutil.InsertMany(ctx, s.coll, prekeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PreKeyStore) GetAvailable(ctx context.Context, userID string, deviceID int32) (*signal.SignalPreKey, error) {
|
||||||
|
filter := bson.M{
|
||||||
|
"user_id": userID,
|
||||||
|
"device_id": deviceID,
|
||||||
|
"used": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get one available prekey, sorted by creation time (FIFO)
|
||||||
|
opts := options.FindOne().SetSort(bson.D{{Key: "created_time", Value: 1}})
|
||||||
|
prekey, err := mongoutil.FindOne[*signal.SignalPreKey](ctx, s.coll, filter, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return prekey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PreKeyStore) MarkUsed(ctx context.Context, userID string, deviceID int32, keyID uint32) error {
|
||||||
|
filter := bson.M{
|
||||||
|
"user_id": userID,
|
||||||
|
"device_id": deviceID,
|
||||||
|
"key_id": keyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
update := bson.M{
|
||||||
|
"$set": bson.M{
|
||||||
|
"used": true,
|
||||||
|
"used_time": now,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return mongoutil.UpdateOne(ctx, s.coll, filter, update, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PreKeyStore) Delete(ctx context.Context, userID string, deviceID int32, keyID uint32) error {
|
||||||
|
filter := bson.M{
|
||||||
|
"user_id": userID,
|
||||||
|
"device_id": deviceID,
|
||||||
|
"key_id": keyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return mongoutil.DeleteOne(ctx, s.coll, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PreKeyStore) DeleteAllByUserDevice(ctx context.Context, userID string, deviceID int32) error {
|
||||||
|
filter := bson.M{
|
||||||
|
"user_id": userID,
|
||||||
|
"device_id": deviceID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return mongoutil.DeleteMany(ctx, s.coll, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PreKeyStore) CountAvailable(ctx context.Context, userID string, deviceID int32) (int64, error) {
|
||||||
|
filter := bson.M{
|
||||||
|
"user_id": userID,
|
||||||
|
"device_id": deviceID,
|
||||||
|
"used": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.coll.CountDocuments(ctx, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PreKeyStore) GetByKeyID(ctx context.Context, userID string, deviceID int32, keyID uint32) (*signal.SignalPreKey, error) {
|
||||||
|
filter := bson.M{
|
||||||
|
"user_id": userID,
|
||||||
|
"device_id": deviceID,
|
||||||
|
"key_id": keyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
prekey, err := mongoutil.FindOne[*signal.SignalPreKey](ctx, s.coll, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return prekey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PreKeyStore) CleanupUsed(ctx context.Context, olderThan time.Duration) (int64, error) {
|
||||||
|
cutoffTime := time.Now().Add(-olderThan)
|
||||||
|
filter := bson.M{
|
||||||
|
"used": true,
|
||||||
|
"used_time": bson.M{"$lt": cutoffTime},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.coll.DeleteMany(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.DeletedCount, nil
|
||||||
|
}
|
@ -0,0 +1,187 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package stores
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model/signal"
|
||||||
|
"github.com/openimsdk/tools/db/mongoutil"
|
||||||
|
"github.com/openimsdk/tools/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SignedPreKeyStore struct {
|
||||||
|
coll *mongo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSignedPreKeyStore(db *mongo.Database) SignedPreKeyStoreInterface {
|
||||||
|
coll := db.Collection(signal.SignalSignedPreKeyCollection)
|
||||||
|
// Create indexes
|
||||||
|
_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
|
||||||
|
{
|
||||||
|
Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "device_id", Value: 1}, {Key: "key_id", Value: 1}},
|
||||||
|
Options: options.Index().SetUnique(true),
|
||||||
|
},
|
||||||
|
{Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "device_id", Value: 1}}},
|
||||||
|
{Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "device_id", Value: 1}, {Key: "active", Value: 1}}},
|
||||||
|
{Keys: bson.D{{Key: "active", Value: 1}, {Key: "created_time", Value: 1}}},
|
||||||
|
{Keys: bson.D{{Key: "created_time", Value: 1}}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.ZWarn(context.Background(), "failed to create indexes for signed prekey store", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SignedPreKeyStore{coll: coll}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignedPreKeyStore) Create(ctx context.Context, signedPrekey *signal.SignalSignedPreKey) error {
|
||||||
|
return mongoutil.InsertOne(ctx, s.coll, signedPrekey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignedPreKeyStore) Update(ctx context.Context, userID string, deviceID int32, keyID uint32, signedPrekey *signal.SignalSignedPreKey) error {
|
||||||
|
filter := bson.M{
|
||||||
|
"user_id": userID,
|
||||||
|
"device_id": deviceID,
|
||||||
|
"key_id": keyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
update := bson.M{"$set": signedPrekey}
|
||||||
|
return mongoutil.UpdateOne(ctx, s.coll, filter, update, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignedPreKeyStore) GetActive(ctx context.Context, userID string, deviceID int32) (*signal.SignalSignedPreKey, error) {
|
||||||
|
filter := bson.M{
|
||||||
|
"user_id": userID,
|
||||||
|
"device_id": deviceID,
|
||||||
|
"active": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the most recent active signed prekey
|
||||||
|
opts := options.FindOne().SetSort(bson.D{{Key: "created_time", Value: -1}})
|
||||||
|
signedPrekey, err := mongoutil.FindOne[*signal.SignalSignedPreKey](ctx, s.coll, filter, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return signedPrekey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignedPreKeyStore) GetByKeyID(ctx context.Context, userID string, deviceID int32, keyID uint32) (*signal.SignalSignedPreKey, error) {
|
||||||
|
filter := bson.M{
|
||||||
|
"user_id": userID,
|
||||||
|
"device_id": deviceID,
|
||||||
|
"key_id": keyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
signedPrekey, err := mongoutil.FindOne[*signal.SignalSignedPreKey](ctx, s.coll, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return signedPrekey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignedPreKeyStore) SetActive(ctx context.Context, userID string, deviceID int32, keyID uint32) error {
|
||||||
|
session, err := s.coll.Database().Client().StartSession()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer session.EndSession(ctx)
|
||||||
|
|
||||||
|
// Use transaction to ensure atomicity
|
||||||
|
_, err = session.WithTransaction(ctx, func(sc mongo.SessionContext) (interface{}, error) {
|
||||||
|
// First, deactivate all existing signed prekeys for this user/device
|
||||||
|
deactivateFilter := bson.M{
|
||||||
|
"user_id": userID,
|
||||||
|
"device_id": deviceID,
|
||||||
|
}
|
||||||
|
deactivateUpdate := bson.M{
|
||||||
|
"$set": bson.M{"active": false},
|
||||||
|
}
|
||||||
|
_, err := s.coll.UpdateMany(sc, deactivateFilter, deactivateUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, activate the specified signed prekey
|
||||||
|
activateFilter := bson.M{
|
||||||
|
"user_id": userID,
|
||||||
|
"device_id": deviceID,
|
||||||
|
"key_id": keyID,
|
||||||
|
}
|
||||||
|
activateUpdate := bson.M{
|
||||||
|
"$set": bson.M{"active": true},
|
||||||
|
}
|
||||||
|
return s.coll.UpdateOne(sc, activateFilter, activateUpdate)
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignedPreKeyStore) Delete(ctx context.Context, userID string, deviceID int32, keyID uint32) error {
|
||||||
|
filter := bson.M{
|
||||||
|
"user_id": userID,
|
||||||
|
"device_id": deviceID,
|
||||||
|
"key_id": keyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return mongoutil.DeleteOne(ctx, s.coll, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignedPreKeyStore) GetAll(ctx context.Context, userID string, deviceID int32) ([]*signal.SignalSignedPreKey, error) {
|
||||||
|
filter := bson.M{
|
||||||
|
"user_id": userID,
|
||||||
|
"device_id": deviceID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by creation time, newest first
|
||||||
|
opts := options.Find().SetSort(bson.D{{Key: "created_time", Value: -1}})
|
||||||
|
return mongoutil.Find[*signal.SignalSignedPreKey](ctx, s.coll, filter, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignedPreKeyStore) CleanupInactive(ctx context.Context, olderThan time.Duration) (int64, error) {
|
||||||
|
cutoffTime := time.Now().Add(-olderThan)
|
||||||
|
filter := bson.M{
|
||||||
|
"active": false,
|
||||||
|
"created_time": bson.M{"$lt": cutoffTime},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.coll.DeleteMany(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.DeletedCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignedPreKeyStore) Exists(ctx context.Context, userID string, deviceID int32) (bool, error) {
|
||||||
|
filter := bson.M{
|
||||||
|
"user_id": userID,
|
||||||
|
"device_id": deviceID,
|
||||||
|
"active": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := s.coll.CountDocuments(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package signal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SignalIdentityKeyCollection = "signal_identity_keys"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SignalIdentityKey represents the identity key for Signal protocol
|
||||||
|
type SignalIdentityKey struct {
|
||||||
|
UserID string `bson:"user_id" json:"userID"`
|
||||||
|
DeviceID int32 `bson:"device_id" json:"deviceID"`
|
||||||
|
IdentityKey []byte `bson:"identity_key" json:"identityKey"`
|
||||||
|
RegistrationID int32 `bson:"registration_id" json:"registrationID"`
|
||||||
|
CreatedTime time.Time `bson:"created_time" json:"createdTime"`
|
||||||
|
UpdatedTime time.Time `bson:"updated_time" json:"updatedTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignalIdentityKeyModelInterface interface {
|
||||||
|
// Create creates a new identity key record
|
||||||
|
Create(ctx context.Context, identityKey *SignalIdentityKey) error
|
||||||
|
|
||||||
|
// Update updates an existing identity key
|
||||||
|
Update(ctx context.Context, userID string, deviceID int32, identityKey *SignalIdentityKey) error
|
||||||
|
|
||||||
|
// Get retrieves an identity key by user ID and device ID
|
||||||
|
Get(ctx context.Context, userID string, deviceID int32) (*SignalIdentityKey, error)
|
||||||
|
|
||||||
|
// Delete removes an identity key
|
||||||
|
Delete(ctx context.Context, userID string, deviceID int32) error
|
||||||
|
|
||||||
|
// GetByUserID retrieves all identity keys for a user
|
||||||
|
GetByUserID(ctx context.Context, userID string) ([]*SignalIdentityKey, error)
|
||||||
|
|
||||||
|
// Exists checks if an identity key exists
|
||||||
|
Exists(ctx context.Context, userID string, deviceID int32) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SignalIdentityKey) TableName() string {
|
||||||
|
return SignalIdentityKeyCollection
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indexes returns the indexes for the collection
|
||||||
|
func (SignalIdentityKey) Indexes() []string {
|
||||||
|
return []string{
|
||||||
|
"user_id",
|
||||||
|
"user_id_device_id", // compound index for (user_id, device_id)
|
||||||
|
"created_time",
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package signal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SignalPreKeyCollection = "signal_prekeys"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SignalPreKey represents one-time prekeys for Signal protocol
|
||||||
|
type SignalPreKey struct {
|
||||||
|
UserID string `bson:"user_id" json:"userID"`
|
||||||
|
DeviceID int32 `bson:"device_id" json:"deviceID"`
|
||||||
|
KeyID uint32 `bson:"key_id" json:"keyID"`
|
||||||
|
PublicKey []byte `bson:"public_key" json:"publicKey"`
|
||||||
|
Used bool `bson:"used" json:"used"`
|
||||||
|
CreatedTime time.Time `bson:"created_time" json:"createdTime"`
|
||||||
|
UsedTime *time.Time `bson:"used_time,omitempty" json:"usedTime,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignalPreKeyModelInterface interface {
|
||||||
|
// Create creates a new prekey record
|
||||||
|
Create(ctx context.Context, prekey *SignalPreKey) error
|
||||||
|
|
||||||
|
// CreateBatch creates multiple prekey records in batch
|
||||||
|
CreateBatch(ctx context.Context, prekeys []*SignalPreKey) error
|
||||||
|
|
||||||
|
// GetAvailable retrieves an available (unused) prekey for a user/device
|
||||||
|
GetAvailable(ctx context.Context, userID string, deviceID int32) (*SignalPreKey, error)
|
||||||
|
|
||||||
|
// MarkUsed marks a prekey as used
|
||||||
|
MarkUsed(ctx context.Context, userID string, deviceID int32, keyID uint32) error
|
||||||
|
|
||||||
|
// Delete removes a prekey
|
||||||
|
Delete(ctx context.Context, userID string, deviceID int32, keyID uint32) error
|
||||||
|
|
||||||
|
// DeleteAllByUserDevice removes all prekeys for a user/device
|
||||||
|
DeleteAllByUserDevice(ctx context.Context, userID string, deviceID int32) error
|
||||||
|
|
||||||
|
// CountAvailable returns the count of available prekeys for a user/device
|
||||||
|
CountAvailable(ctx context.Context, userID string, deviceID int32) (int64, error)
|
||||||
|
|
||||||
|
// GetByKeyID retrieves a specific prekey by key ID
|
||||||
|
GetByKeyID(ctx context.Context, userID string, deviceID int32, keyID uint32) (*SignalPreKey, error)
|
||||||
|
|
||||||
|
// CleanupUsed removes used prekeys older than the specified duration
|
||||||
|
CleanupUsed(ctx context.Context, olderThan time.Duration) (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SignalPreKey) TableName() string {
|
||||||
|
return SignalPreKeyCollection
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indexes returns the indexes for the collection
|
||||||
|
func (SignalPreKey) Indexes() []string {
|
||||||
|
return []string{
|
||||||
|
"user_id",
|
||||||
|
"user_id_device_id", // compound index for (user_id, device_id)
|
||||||
|
"user_id_device_id_key_id", // compound unique index for (user_id, device_id, key_id)
|
||||||
|
"user_id_device_id_used", // compound index for (user_id, device_id, used)
|
||||||
|
"used_time",
|
||||||
|
"created_time",
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
// Copyright © 2024 OpenIM. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package signal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SignalSignedPreKeyCollection = "signal_signed_prekeys"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SignalSignedPreKey represents signed prekeys for Signal protocol
|
||||||
|
type SignalSignedPreKey struct {
|
||||||
|
UserID string `bson:"user_id" json:"userID"`
|
||||||
|
DeviceID int32 `bson:"device_id" json:"deviceID"`
|
||||||
|
KeyID uint32 `bson:"key_id" json:"keyID"`
|
||||||
|
PublicKey []byte `bson:"public_key" json:"publicKey"`
|
||||||
|
Signature []byte `bson:"signature" json:"signature"`
|
||||||
|
CreatedTime time.Time `bson:"created_time" json:"createdTime"`
|
||||||
|
Active bool `bson:"active" json:"active"` // Whether this is the active signed prekey
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignalSignedPreKeyModelInterface interface {
|
||||||
|
// Create creates a new signed prekey record
|
||||||
|
Create(ctx context.Context, signedPrekey *SignalSignedPreKey) error
|
||||||
|
|
||||||
|
// Update updates an existing signed prekey (for rotation)
|
||||||
|
Update(ctx context.Context, userID string, deviceID int32, keyID uint32, signedPrekey *SignalSignedPreKey) error
|
||||||
|
|
||||||
|
// GetActive retrieves the active signed prekey for a user/device
|
||||||
|
GetActive(ctx context.Context, userID string, deviceID int32) (*SignalSignedPreKey, error)
|
||||||
|
|
||||||
|
// GetByKeyID retrieves a specific signed prekey by key ID
|
||||||
|
GetByKeyID(ctx context.Context, userID string, deviceID int32, keyID uint32) (*SignalSignedPreKey, error)
|
||||||
|
|
||||||
|
// SetActive marks a signed prekey as active and deactivates others
|
||||||
|
SetActive(ctx context.Context, userID string, deviceID int32, keyID uint32) error
|
||||||
|
|
||||||
|
// Delete removes a signed prekey
|
||||||
|
Delete(ctx context.Context, userID string, deviceID int32, keyID uint32) error
|
||||||
|
|
||||||
|
// GetAll retrieves all signed prekeys for a user/device
|
||||||
|
GetAll(ctx context.Context, userID string, deviceID int32) ([]*SignalSignedPreKey, error)
|
||||||
|
|
||||||
|
// CleanupInactive removes inactive signed prekeys older than the specified duration
|
||||||
|
CleanupInactive(ctx context.Context, olderThan time.Duration) (int64, error)
|
||||||
|
|
||||||
|
// Exists checks if a signed prekey exists
|
||||||
|
Exists(ctx context.Context, userID string, deviceID int32) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SignalSignedPreKey) TableName() string {
|
||||||
|
return SignalSignedPreKeyCollection
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indexes returns the indexes for the collection
|
||||||
|
func (SignalSignedPreKey) Indexes() []string {
|
||||||
|
return []string{
|
||||||
|
"user_id",
|
||||||
|
"user_id_device_id", // compound index for (user_id, device_id)
|
||||||
|
"user_id_device_id_key_id", // compound unique index for (user_id, device_id, key_id)
|
||||||
|
"user_id_device_id_active", // compound index for (user_id, device_id, active)
|
||||||
|
"active_created_time", // compound index for (active, created_time)
|
||||||
|
"created_time",
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue