From a335c6754e75bfa38edb9c2b6eab48cb53bf5981 Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Tue, 14 Apr 2026 16:22:22 +0300 Subject: [PATCH] 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 --- .../internal/services/subscriber.go | 188 ------------------ .../internal/services/subscriber_auth.go | 81 ++++++++ .../internal/services/subscriber_esim.go | 93 +++++++++ .../internal/services/subscriber_imsi.go | 35 ++++ 4 files changed, 209 insertions(+), 188 deletions(-) create mode 100644 apps/api-server/internal/services/subscriber_auth.go create mode 100644 apps/api-server/internal/services/subscriber_esim.go create mode 100644 apps/api-server/internal/services/subscriber_imsi.go diff --git a/apps/api-server/internal/services/subscriber.go b/apps/api-server/internal/services/subscriber.go index b0d7755..b4c34c0 100644 --- a/apps/api-server/internal/services/subscriber.go +++ b/apps/api-server/internal/services/subscriber.go @@ -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"` diff --git a/apps/api-server/internal/services/subscriber_auth.go b/apps/api-server/internal/services/subscriber_auth.go new file mode 100644 index 0000000..e17d240 --- /dev/null +++ b/apps/api-server/internal/services/subscriber_auth.go @@ -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 +} diff --git a/apps/api-server/internal/services/subscriber_esim.go b/apps/api-server/internal/services/subscriber_esim.go new file mode 100644 index 0000000..dcf0a22 --- /dev/null +++ b/apps/api-server/internal/services/subscriber_esim.go @@ -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 +} diff --git a/apps/api-server/internal/services/subscriber_imsi.go b/apps/api-server/internal/services/subscriber_imsi.go new file mode 100644 index 0000000..a451138 --- /dev/null +++ b/apps/api-server/internal/services/subscriber_imsi.go @@ -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 +}