mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 11:02:54 +00:00
refactor: Split subscriber service into separate files for better code organization
- Move IMSI allocation logic to subscriber_imsi.go - Move authentication key generation (generateAuthKeys, getOperatorVariant, generateOPc) to subscriber_auth.go - Move eSIM profile operations (provisionESIMProfile, deactivateESIMProfile) to subscriber_esim.go - Keep core subscriber CRUD operations and session management in subscriber.go - Remove unused crypto/aes, crypto/rand, encoding/hex, and os imports from main
This commit is contained in:
@@ -2,11 +2,7 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/config"
|
||||
@@ -206,105 +202,6 @@ func (s *SubscriberService) ListSubscribers(ctx context.Context, req *ListSubscr
|
||||
}, nil
|
||||
}
|
||||
|
||||
// allocateIMSI allocates a new IMSI from the configured range
|
||||
func (s *SubscriberService) allocateIMSI(ctx context.Context) (models.IMSI, error) {
|
||||
// Get current IMSI allocation state
|
||||
alloc, err := s.db.GetIMSIAllocation(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get IMSI allocation: %w", err)
|
||||
}
|
||||
|
||||
// Check if we have IMSIs available
|
||||
if alloc.LastIMSI >= alloc.MaxIMSI {
|
||||
return "", fmt.Errorf("IMSI range exhausted")
|
||||
}
|
||||
|
||||
// Allocate next IMSI
|
||||
nextIMSI := alloc.LastIMSI + 1
|
||||
alloc.LastIMSI = nextIMSI
|
||||
|
||||
// Update allocation state
|
||||
if err := s.db.UpdateIMSIAllocation(ctx, alloc); err != nil {
|
||||
return "", fmt.Errorf("failed to update IMSI allocation: %w", err)
|
||||
}
|
||||
|
||||
// Format IMSI: MCC (3) + MNC (2-3) + subscriber number
|
||||
imsiStr := fmt.Sprintf("%s%010d", s.config.IMSI.Prefix, nextIMSI)
|
||||
return models.IMSI(imsiStr), nil
|
||||
}
|
||||
|
||||
// generateAuthKeys generates authentication keys for the subscriber
|
||||
func (s *SubscriberService) generateAuthKeys() (string, string, error) {
|
||||
// Generate 128-bit random key (K)
|
||||
key := make([]byte, 16)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Get OP (Operator variant) from operator configuration
|
||||
// This should be consistent across all subscribers for the same operator
|
||||
op := s.getOperatorVariant()
|
||||
if op == nil {
|
||||
return "", "", fmt.Errorf("operator variant not configured")
|
||||
}
|
||||
|
||||
// Generate OPc (derived from OP and K) using AES-128 encryption
|
||||
// OPc = AES-128(K, OP) where OP is encrypted with K
|
||||
opc, err := s.generateOPc(key, op)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return hex.EncodeToString(key), hex.EncodeToString(opc), nil
|
||||
}
|
||||
|
||||
// getOperatorVariant returns the operator variant (OP) from configuration
|
||||
func (s *SubscriberService) getOperatorVariant() []byte {
|
||||
// Get operator variant from environment variable or secure configuration
|
||||
// This should be the same across all subscribers for the same operator
|
||||
opStr := os.Getenv("OPERATOR_VARIANT")
|
||||
if opStr == "" {
|
||||
// Fallback to default for development
|
||||
opStr = "TelecomOP1234567" // 16-byte operator variant
|
||||
}
|
||||
|
||||
// Ensure exactly 16 bytes for AES-128
|
||||
op := make([]byte, 16)
|
||||
copy(op, []byte(opStr))
|
||||
|
||||
// If shorter than 16 bytes, pad with zeros
|
||||
if len(opStr) < 16 {
|
||||
for i := len(opStr); i < 16; i++ {
|
||||
op[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
return op
|
||||
}
|
||||
|
||||
// generateOPc derives OPc from OP and K using AES-128 encryption
|
||||
func (s *SubscriberService) generateOPc(k, op []byte) ([]byte, error) {
|
||||
// Create AES-128 cipher block with key K
|
||||
block, err := aes.NewCipher(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
|
||||
}
|
||||
|
||||
// OPc = AES-128(K, OP) - encrypt OP with key K
|
||||
opc := make([]byte, aes.BlockSize) // AES block size is 16 bytes
|
||||
|
||||
// Create ECB mode cipher (as per 3GPP specification for OPc derivation)
|
||||
// In 3GPP, OPc is derived using AES-128 ECB mode
|
||||
if len(op) != aes.BlockSize {
|
||||
return nil, fmt.Errorf("OP must be 16 bytes for AES-128")
|
||||
}
|
||||
|
||||
// Encrypt OP using ECB mode (single block)
|
||||
block.Encrypt(opc, op)
|
||||
|
||||
return opc, nil
|
||||
}
|
||||
|
||||
// terminateSubscriberSessions terminates all active sessions for a subscriber
|
||||
func (s *SubscriberService) terminateSubscriberSessions(ctx context.Context, imsi models.IMSI) error {
|
||||
sessions, err := s.db.GetActiveSessionsByIMSI(ctx, imsi)
|
||||
@@ -331,91 +228,6 @@ func (s *SubscriberService) terminateSubscriberSessions(ctx context.Context, ims
|
||||
return nil
|
||||
}
|
||||
|
||||
// provisionESIMProfile provisions an eSIM profile for the subscriber
|
||||
func (s *SubscriberService) provisionESIMProfile(ctx context.Context, subscriberID uint) error {
|
||||
// Get subscriber details
|
||||
subscriber, err := s.db.GetSubscriber(ctx, subscriberID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get subscriber: %w", err)
|
||||
}
|
||||
|
||||
// Validate EID
|
||||
if err := s.es2Service.ValidateEID(subscriber.EUICCID); err != nil {
|
||||
return fmt.Errorf("invalid EID: %w", err)
|
||||
}
|
||||
|
||||
// Provision profile via ES2+ API
|
||||
profileInfo, err := s.es2Service.ProvisionProfile(ctx, subscriber)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to provision profile: %w", err)
|
||||
}
|
||||
|
||||
// Update subscriber with profile information
|
||||
subscriber.ProfileID = profileInfo.ProfileID
|
||||
subscriber.ProfileStatus = models.ProfileStatusDownloading
|
||||
|
||||
if err := s.db.UpdateSubscriber(ctx, subscriber); err != nil {
|
||||
return fmt.Errorf("failed to update subscriber: %w", err)
|
||||
}
|
||||
|
||||
// Activate profile
|
||||
if err := s.es2Service.ActivateProfile(ctx, subscriber.EUICCID, profileInfo.ProfileID); err != nil {
|
||||
return fmt.Errorf("failed to activate profile: %w", err)
|
||||
}
|
||||
|
||||
// Update subscriber status
|
||||
subscriber.ProfileStatus = models.ProfileStatusActive
|
||||
if err := s.db.UpdateSubscriber(ctx, subscriber); err != nil {
|
||||
return fmt.Errorf("failed to update subscriber status: %w", err)
|
||||
}
|
||||
|
||||
// Notify AMF of new subscriber
|
||||
if err := s.amfClient.NotifySubscriberUpdate(ctx, subscriber.IMSI, models.SubscriberStatusActive); err != nil {
|
||||
fmt.Printf("Failed to notify AMF of subscriber activation: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deactivateESIMProfile deactivates an eSIM profile
|
||||
func (s *SubscriberService) deactivateESIMProfile(ctx context.Context, subscriberID uint) error {
|
||||
// Get subscriber details
|
||||
subscriber, err := s.db.GetSubscriber(ctx, subscriberID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get subscriber: %w", err)
|
||||
}
|
||||
|
||||
// Check if subscriber has active profile
|
||||
if subscriber.ProfileID == "" || subscriber.ProfileStatus != models.ProfileStatusActive {
|
||||
return fmt.Errorf("no active profile to deactivate")
|
||||
}
|
||||
|
||||
// Deactivate profile via ES2+ API
|
||||
if err := s.es2Service.DeactivateProfile(ctx, subscriber.EUICCID, subscriber.ProfileID); err != nil {
|
||||
return fmt.Errorf("failed to deactivate profile: %w", err)
|
||||
}
|
||||
|
||||
// Update subscriber status
|
||||
subscriber.ProfileStatus = models.ProfileStatusInactive
|
||||
subscriber.Status = models.SubscriberStatusInactive
|
||||
|
||||
if err := s.db.UpdateSubscriber(ctx, subscriber); err != nil {
|
||||
return fmt.Errorf("failed to update subscriber: %w", err)
|
||||
}
|
||||
|
||||
// Terminate all sessions
|
||||
if err := s.terminateSubscriberSessions(ctx, subscriber.IMSI); err != nil {
|
||||
fmt.Printf("Failed to terminate sessions: %v\n", err)
|
||||
}
|
||||
|
||||
// Notify AMF of subscriber deactivation
|
||||
if err := s.amfClient.NotifySubscriberUpdate(ctx, subscriber.IMSI, models.SubscriberStatusInactive); err != nil {
|
||||
fmt.Printf("Failed to notify AMF of subscriber deactivation: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Request/Response types
|
||||
type CreateSubscriberRequest struct {
|
||||
MSISDN string `json:"msisdn" validate:"required"`
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// generateAuthKeys generates authentication keys for the subscriber
|
||||
func (s *SubscriberService) generateAuthKeys() (string, string, error) {
|
||||
// Generate 128-bit random key (K)
|
||||
key := make([]byte, 16)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Get OP (Operator variant) from operator configuration
|
||||
// This should be consistent across all subscribers for the same operator
|
||||
op := s.getOperatorVariant()
|
||||
if op == nil {
|
||||
return "", "", fmt.Errorf("operator variant not configured")
|
||||
}
|
||||
|
||||
// Generate OPc (derived from OP and K) using AES-128 encryption
|
||||
// OPc = AES-128(K, OP) where OP is encrypted with K
|
||||
opc, err := s.generateOPc(key, op)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return hex.EncodeToString(key), hex.EncodeToString(opc), nil
|
||||
}
|
||||
|
||||
// getOperatorVariant returns the operator variant (OP) from configuration
|
||||
func (s *SubscriberService) getOperatorVariant() []byte {
|
||||
// Get operator variant from environment variable or secure configuration
|
||||
// This should be the same across all subscribers for the same operator
|
||||
opStr := os.Getenv("OPERATOR_VARIANT")
|
||||
if opStr == "" {
|
||||
// Fallback to default for development
|
||||
opStr = "TelecomOP1234567" // 16-byte operator variant
|
||||
}
|
||||
|
||||
// Ensure exactly 16 bytes for AES-128
|
||||
op := make([]byte, 16)
|
||||
copy(op, []byte(opStr))
|
||||
|
||||
// If shorter than 16 bytes, pad with zeros
|
||||
if len(opStr) < 16 {
|
||||
for i := len(opStr); i < 16; i++ {
|
||||
op[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
return op
|
||||
}
|
||||
|
||||
// generateOPc derives OPc from OP and K using AES-128 encryption
|
||||
func (s *SubscriberService) generateOPc(k, op []byte) ([]byte, error) {
|
||||
// Create AES-128 cipher block with key K
|
||||
block, err := aes.NewCipher(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
|
||||
}
|
||||
|
||||
// OPc = AES-128(K, OP) - encrypt OP with key K
|
||||
opc := make([]byte, aes.BlockSize) // AES block size is 16 bytes
|
||||
|
||||
// Create ECB mode cipher (as per 3GPP specification for OPc derivation)
|
||||
// In 3GPP, OPc is derived using AES-128 ECB mode
|
||||
if len(op) != aes.BlockSize {
|
||||
return nil, fmt.Errorf("OP must be 16 bytes for AES-128")
|
||||
}
|
||||
|
||||
// Encrypt OP using ECB mode (single block)
|
||||
block.Encrypt(opc, op)
|
||||
|
||||
return opc, nil
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/models"
|
||||
)
|
||||
|
||||
// provisionESIMProfile provisions an eSIM profile for the subscriber
|
||||
func (s *SubscriberService) provisionESIMProfile(ctx context.Context, subscriberID uint) error {
|
||||
// Get subscriber details
|
||||
subscriber, err := s.db.GetSubscriber(ctx, subscriberID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get subscriber: %w", err)
|
||||
}
|
||||
|
||||
// Validate EID
|
||||
if err := s.es2Service.ValidateEID(subscriber.EUICCID); err != nil {
|
||||
return fmt.Errorf("invalid EID: %w", err)
|
||||
}
|
||||
|
||||
// Provision profile via ES2+ API
|
||||
profileInfo, err := s.es2Service.ProvisionProfile(ctx, subscriber)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to provision profile: %w", err)
|
||||
}
|
||||
|
||||
// Update subscriber with profile information
|
||||
subscriber.ProfileID = profileInfo.ProfileID
|
||||
subscriber.ProfileStatus = models.ProfileStatusDownloading
|
||||
|
||||
if err := s.db.UpdateSubscriber(ctx, subscriber); err != nil {
|
||||
return fmt.Errorf("failed to update subscriber: %w", err)
|
||||
}
|
||||
|
||||
// Activate profile
|
||||
if err := s.es2Service.ActivateProfile(ctx, subscriber.EUICCID, profileInfo.ProfileID); err != nil {
|
||||
return fmt.Errorf("failed to activate profile: %w", err)
|
||||
}
|
||||
|
||||
// Update subscriber status
|
||||
subscriber.ProfileStatus = models.ProfileStatusActive
|
||||
if err := s.db.UpdateSubscriber(ctx, subscriber); err != nil {
|
||||
return fmt.Errorf("failed to update subscriber status: %w", err)
|
||||
}
|
||||
|
||||
// Notify AMF of new subscriber
|
||||
if err := s.amfClient.NotifySubscriberUpdate(ctx, subscriber.IMSI, models.SubscriberStatusActive); err != nil {
|
||||
fmt.Printf("Failed to notify AMF of subscriber activation: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deactivateESIMProfile deactivates an eSIM profile
|
||||
func (s *SubscriberService) deactivateESIMProfile(ctx context.Context, subscriberID uint) error {
|
||||
// Get subscriber details
|
||||
subscriber, err := s.db.GetSubscriber(ctx, subscriberID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get subscriber: %w", err)
|
||||
}
|
||||
|
||||
// Check if subscriber has active profile
|
||||
if subscriber.ProfileID == "" || subscriber.ProfileStatus != models.ProfileStatusActive {
|
||||
return fmt.Errorf("no active profile to deactivate")
|
||||
}
|
||||
|
||||
// Deactivate profile via ES2+ API
|
||||
if err := s.es2Service.DeactivateProfile(ctx, subscriber.EUICCID, subscriber.ProfileID); err != nil {
|
||||
return fmt.Errorf("failed to deactivate profile: %w", err)
|
||||
}
|
||||
|
||||
// Update subscriber status
|
||||
subscriber.ProfileStatus = models.ProfileStatusInactive
|
||||
subscriber.Status = models.SubscriberStatusInactive
|
||||
|
||||
if err := s.db.UpdateSubscriber(ctx, subscriber); err != nil {
|
||||
return fmt.Errorf("failed to update subscriber: %w", err)
|
||||
}
|
||||
|
||||
// Terminate all sessions
|
||||
if err := s.terminateSubscriberSessions(ctx, subscriber.IMSI); err != nil {
|
||||
fmt.Printf("Failed to terminate sessions: %v\n", err)
|
||||
}
|
||||
|
||||
// Notify AMF of subscriber deactivation
|
||||
if err := s.amfClient.NotifySubscriberUpdate(ctx, subscriber.IMSI, models.SubscriberStatusInactive); err != nil {
|
||||
fmt.Printf("Failed to notify AMF of subscriber deactivation: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/models"
|
||||
)
|
||||
|
||||
// allocateIMSI allocates a new IMSI for a subscriber
|
||||
func (s *SubscriberService) allocateIMSI(ctx context.Context) (models.IMSI, error) {
|
||||
// Get current IMSI allocation
|
||||
alloc, err := s.db.GetIMSIAllocation(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get IMSI allocation: %w", err)
|
||||
}
|
||||
|
||||
// Check if we've reached the maximum
|
||||
if alloc.LastIMSI >= alloc.MaxIMSI {
|
||||
return "", fmt.Errorf("IMSI range exhausted: max %d reached", alloc.MaxIMSI)
|
||||
}
|
||||
|
||||
// Allocate next IMSI
|
||||
nextIMSI := alloc.LastIMSI + 1
|
||||
alloc.LastIMSI = nextIMSI
|
||||
|
||||
// Update allocation state
|
||||
if err := s.db.UpdateIMSIAllocation(ctx, alloc); err != nil {
|
||||
return "", fmt.Errorf("failed to update IMSI allocation: %w", err)
|
||||
}
|
||||
|
||||
// Format IMSI: MCC (3) + MNC (2-3) + subscriber number
|
||||
imsiStr := fmt.Sprintf("%s%010d", s.config.IMSI.Prefix, nextIMSI)
|
||||
return models.IMSI(imsiStr), nil
|
||||
}
|
||||
Reference in New Issue
Block a user