From 565359ed80b11803d95f79b54da28cc00d86a579 Mon Sep 17 00:00:00 2001 From: Ghislain Bourgeois Date: Tue, 28 May 2024 15:35:11 -0400 Subject: [PATCH 1/3] chore: Copy http2_util, logger_conf and logger_util to util project --- http2_util/server.go | 50 ++++++++ http2_util/server_debug.go | 42 +++++++ logger_conf/conf.go | 65 ++++++++++ logger_util/log_level.go | 80 ++++++++++++ logger_util/logger_util.go | 252 +++++++++++++++++++++++++++++++++++++ 5 files changed, 489 insertions(+) create mode 100644 http2_util/server.go create mode 100644 http2_util/server_debug.go create mode 100644 logger_conf/conf.go create mode 100644 logger_util/log_level.go create mode 100644 logger_util/logger_util.go diff --git a/http2_util/server.go b/http2_util/server.go new file mode 100644 index 0000000..f180ab1 --- /dev/null +++ b/http2_util/server.go @@ -0,0 +1,50 @@ +// Copyright 2019 Communication Service/Software Laboratory, National Chiao Tung University (free5gc.org) +// +// SPDX-License-Identifier: Apache-2.0 + +//go:build !debug +// +build !debug + +package http2_util + +import ( + "crypto/tls" + "fmt" + "net/http" + "os" + "time" + + "github.com/pkg/errors" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +// NewServer returns a server instance with HTTP/2.0 and HTTP/2.0 cleartext support +// If this function cannot open or create the secret log file, +// **it still returns server instance** but without the secret log and error indication +func NewServer(bindAddr string, preMasterSecretLogPath string, handler http.Handler) (server *http.Server, err error) { + if handler == nil { + return nil, errors.New("server needs handler to handle request") + } + + h2Server := &http2.Server{ + // TODO: extends the idle time after re-use openapi client + IdleTimeout: 1 * time.Millisecond, + } + server = &http.Server{ + Addr: bindAddr, + Handler: h2c.NewHandler(handler, h2Server), + } + + if preMasterSecretLogPath != "" { + preMasterSecretFile, err := os.OpenFile(preMasterSecretLogPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return server, fmt.Errorf("create pre-master-secret log [%s] fail: %s", preMasterSecretLogPath, err) + } + server.TLSConfig = &tls.Config{ + KeyLogWriter: preMasterSecretFile, + } + } + + return +} diff --git a/http2_util/server_debug.go b/http2_util/server_debug.go new file mode 100644 index 0000000..272364b --- /dev/null +++ b/http2_util/server_debug.go @@ -0,0 +1,42 @@ +// Copyright 2019 Communication Service/Software Laboratory, National Chiao Tung University (free5gc.org) +// +// SPDX-License-Identifier: Apache-2.0 + +//+build debug + +package http2_util + +import ( + "crypto/tls" + "github.com/pkg/errors" + "net/http" + "os" +) + +type ZeroSource struct{} + +func (ZeroSource) Read(b []byte) (n int, err error) { + for i := range b { + b[i] = 0 + } + return len(b), nil +} + +func NewServer(bindAddr string, tlskeylog string, handler http.Handler) (server *http.Server, err error) { + keylogFile, err := os.OpenFile(tlskeylog, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return nil, err + } + if handler == nil { + return nil, errors.New("server need handler") + } + server = &http.Server{ + Addr: bindAddr, + TLSConfig: &tls.Config{ + KeyLogWriter: keylogFile, + Rand: ZeroSource{}, + }, + Handler: handler, + } + return +} diff --git a/logger_conf/conf.go b/logger_conf/conf.go new file mode 100644 index 0000000..364c0f4 --- /dev/null +++ b/logger_conf/conf.go @@ -0,0 +1,65 @@ +// Copyright 2019 Communication Service/Software Laboratory, National Chiao Tung University (free5gc.org) +// +// SPDX-License-Identifier: Apache-2.0 + +package logger_conf + +import ( + "log" + "os" + "strconv" + + "github.com/omec-project/util/path_util" +) + +var Free5gcLogDir string = path_util.Free5gcPath("free5gc/log") + "/" +var LibLogDir string = Free5gcLogDir + "lib/" +var NfLogDir string = Free5gcLogDir + "nf/" + +var Free5gcLogFile string = Free5gcLogDir + "free5gc.log" + +func init() { + if err := os.MkdirAll(LibLogDir, 0775); err != nil { + log.Printf("Mkdir %s failed: %+v", LibLogDir, err) + } + if err := os.MkdirAll(NfLogDir, 0775); err != nil { + log.Printf("Mkdir %s failed: %+v", NfLogDir, err) + } + + // Create log file or if it already exist, check if user can access it + f, fileOpenErr := os.OpenFile(Free5gcLogFile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666) + if fileOpenErr != nil { + // user cannot access it. + log.Printf("Cannot Open %s\n", Free5gcLogFile) + } else { + // user can access it + if err := f.Close(); err != nil { + log.Printf("File %s cannot been closed\n", Free5gcLogFile) + } + } + + sudoUID, errUID := strconv.Atoi(os.Getenv("SUDO_UID")) + sudoGID, errGID := strconv.Atoi(os.Getenv("SUDO_GID")) + + if errUID == nil && errGID == nil { + // if using sudo to run the program, errUID will be nil and sudoUID will get the uid who run sudo + // else errUID will not be nil and sudoUID will be nil + // If user using sudo to run the program and create log file, log will own by root, + // here we change own to user so user can view and reuse the file + if err := os.Chown(Free5gcLogDir, sudoUID, sudoGID); err != nil { + log.Printf("Dir %s chown to %d:%d error: %v\n", Free5gcLogDir, sudoUID, sudoGID, err) + } + if err := os.Chown(LibLogDir, sudoUID, sudoGID); err != nil { + log.Printf("Dir %s chown to %d:%d error: %v\n", LibLogDir, sudoUID, sudoGID, err) + } + if err := os.Chown(NfLogDir, sudoUID, sudoGID); err != nil { + log.Printf("Dir %s chown to %d:%d error: %v\n", NfLogDir, sudoUID, sudoGID, err) + } + + if fileOpenErr == nil { + if err := os.Chown(Free5gcLogFile, sudoUID, sudoGID); err != nil { + log.Printf("File %s chown to %d:%d error: %v\n", Free5gcLogFile, sudoUID, sudoGID, err) + } + } + } +} diff --git a/logger_util/log_level.go b/logger_util/log_level.go new file mode 100644 index 0000000..e4aaedc --- /dev/null +++ b/logger_util/log_level.go @@ -0,0 +1,80 @@ +// Copyright 2019 Communication Service/Software Laboratory, National Chiao Tung University (free5gc.org) +// +// SPDX-License-Identifier: Apache-2.0 + +package logger_util + +import ( + "reflect" + + "github.com/asaskevich/govalidator" +) + +type Logger struct { + AMF *LogSetting `yaml:"AMF" valid:"optional"` + AUSF *LogSetting `yaml:"AUSF" valid:"optional"` + N3IWF *LogSetting `yaml:"N3IWF" valid:"optional"` + NRF *LogSetting `yaml:"NRF" valid:"optional"` + NSSF *LogSetting `yaml:"NSSF" valid:"optional"` + PCF *LogSetting `yaml:"PCF" valid:"optional"` + SMF *LogSetting `yaml:"SMF" valid:"optional"` + UDM *LogSetting `yaml:"UDM" valid:"optional"` + UDR *LogSetting `yaml:"UDR" valid:"optional"` + UPF *LogSetting `yaml:"UPF" valid:"optional"` + NEF *LogSetting `yaml:"NEF" valid:"optional"` + WEBUI *LogSetting `yaml:"WEBUI" valid:"optional"` + + Aper *LogSetting `yaml:"Aper" valid:"optional"` + CommonConsumerTest *LogSetting `yaml:"CommonConsumerTest" valid:"optional"` + FSM *LogSetting `yaml:"FSM" valid:"optional"` + MongoDBLibrary *LogSetting `yaml:"MongoDBLibrary" valid:"optional"` + NAS *LogSetting `yaml:"NAS" valid:"optional"` + NGAP *LogSetting `yaml:"NGAP" valid:"optional"` + OpenApi *LogSetting `yaml:"OpenApi" valid:"optional"` + NamfCommunication *LogSetting `yaml:"NamfCommunication" valid:"optional"` + NamfEventExposure *LogSetting `yaml:"NamfEventExposure" valid:"optional"` + NnssfNSSAIAvailability *LogSetting `yaml:"NnssfNSSAIAvailability" valid:"optional"` + NnssfNSSelection *LogSetting `yaml:"NnssfNSSelection" valid:"optional"` + NsmfEventExposure *LogSetting `yaml:"NsmfEventExposure" valid:"optional"` + NsmfPDUSession *LogSetting `yaml:"NsmfPDUSession" valid:"optional"` + NudmEventExposure *LogSetting `yaml:"NudmEventExposure" valid:"optional"` + NudmParameterProvision *LogSetting `yaml:"NudmParameterProvision" valid:"optional"` + NudmSubscriberDataManagement *LogSetting `yaml:"NudmSubscriberDataManagement" valid:"optional"` + NudmUEAuthentication *LogSetting `yaml:"NudmUEAuthentication" valid:"optional"` + NudmUEContextManagement *LogSetting `yaml:"NudmUEContextManagement" valid:"optional"` + NudrDataRepository *LogSetting `yaml:"NudrDataRepository" valid:"optional"` + PathUtil *LogSetting `yaml:"PathUtil" valid:"optional"` + PFCP *LogSetting `yaml:"PFCP" valid:"optional"` +} + +func (l *Logger) Validate() (bool, error) { + logger := reflect.ValueOf(l).Elem() + for i := 0; i < logger.NumField(); i++ { + if logSetting := logger.Field(i).Interface().(*LogSetting); logSetting != nil { + result, err := logSetting.validate() + return result, err + } + } + + result, err := govalidator.ValidateStruct(l) + return result, err +} + +type LogSetting struct { + DebugLevel string `yaml:"debugLevel" valid:"debugLevel"` + ReportCaller bool `yaml:"ReportCaller" valid:"type(bool)"` +} + +func (l *LogSetting) validate() (bool, error) { + govalidator.TagMap["debugLevel"] = govalidator.Validator(func(str string) bool { + if str == "panic" || str == "fatal" || str == "error" || str == "warn" || + str == "info" || str == "debug" || str == "trace" { + return true + } else { + return false + } + }) + + result, err := govalidator.ValidateStruct(l) + return result, err +} diff --git a/logger_util/logger_util.go b/logger_util/logger_util.go new file mode 100644 index 0000000..ad53770 --- /dev/null +++ b/logger_util/logger_util.go @@ -0,0 +1,252 @@ +// Copyright 2019 Communication Service/Software Laboratory, National Chiao Tung University (free5gc.org) +// +// SPDX-License-Identifier: Apache-2.0 + +package logger_util + +import ( + "fmt" + "log" + "net" + "net/http" + "net/http/httputil" + "os" + "path/filepath" + "runtime/debug" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +type FileHook struct { + file *os.File + flag int + chmod os.FileMode + formatter *logrus.TextFormatter +} + +func NewFileHook(file string, flag int, chmod os.FileMode) (*FileHook, error) { + plainFormatter := &logrus.TextFormatter{DisableColors: true} + logFile, err := os.OpenFile(file, flag, chmod) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to write file on filehook %v\n", err) + return nil, err + } + + return &FileHook{logFile, flag, chmod, plainFormatter}, nil +} + +func CreateFree5gcLogFile(file string) (string, error) { + // Because free5gc log file will be used by multiple NFs, it is not recommended to rename. + return createLogFile(file, "", false) +} + +func CreateNfLogFile(file string, defaultName string) (string, error) { + return createLogFile(file, defaultName, true) +} + +/* + * createLogFile + * @param file, The full file path from arguments input by user. + * @param defaultName, Default log file name (if it is empty, it means no default log file will be created) + * @param rename, Modify the file name if the file exists + * @return error, fullPath + */ +func createLogFile(file string, defaultName string, rename bool) (string, error) { + var fullPath string + directory, fileName := filepath.Split(file) + + if directory == "" || fileName == "" { + directory = "./log/" + fileName = defaultName + } + + if fileName == "" { + return "", nil + } + + fullPath = filepath.Join(directory, fileName) + + if rename { + if err := renameOldLogFile(fullPath); err != nil { + return "", err + } + } + + if err := os.MkdirAll(directory, 0775); err != nil { + return "", fmt.Errorf("make directory %s failed: %v", directory, err) + } + + sudoUID, errUID := strconv.Atoi(os.Getenv("SUDO_UID")) + sudoGID, errGID := strconv.Atoi(os.Getenv("SUDO_GID")) + if errUID == nil && errGID == nil { + // if using sudo to run the program, errUID will be nil and sudoUID will get the uid who run sudo + // else errUID will not be nil and sudoUID will be nil + // If user using sudo to run the program and create log file, log will own by root, + // here we change own to user so user can view and reuse the file + if err := os.Chown(directory, sudoUID, sudoGID); err != nil { + return "", fmt.Errorf("directory %s chown to %d:%d error: %v", directory, sudoUID, sudoGID, err) + } + + // Create log file or if it already exist, check if user can access it + if f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666); err != nil { + // user cannot access it. + return "", fmt.Errorf("cannot Open %s error: %v", fullPath, err) + } else { + // user can access it + if err := f.Close(); err != nil { + return "", fmt.Errorf("file %s cannot been closed", fullPath) + } + if err := os.Chown(fullPath, sudoUID, sudoGID); err != nil { + return "", fmt.Errorf("file %s chown to %d:%d error: %v", fullPath, sudoUID, sudoGID, err) + } + } + } + + return fullPath, nil +} + +func renameOldLogFile(fullPath string) error { + _, err := os.Stat(fullPath) + + if os.IsNotExist(err) { + return nil + } + + counter := 0 + sep := "." + fileDir, fileName := filepath.Split(fullPath) + + if contents, err := os.ReadDir(fileDir); err != nil { + return fmt.Errorf("reads the directory error %v", err) + } else { + for _, content := range contents { + if !content.IsDir() { + if strings.Contains(content.Name(), (fileName + sep)) { + counter++ + } + } + } + } + + newFullPath := fmt.Sprintf("%s%s%s%d", fileDir, fileName, sep, (counter + 1)) + if err := os.Rename(fullPath, newFullPath); err != nil { + return fmt.Errorf("unable to rename file %v", err) + } + + return nil +} + +// Fire event +func (hook *FileHook) Fire(entry *logrus.Entry) error { + var line string + if plainformat, err := hook.formatter.Format(entry); err != nil { + log.Printf("Formatter error: %+v", err) + return err + } else { + line = string(plainformat) + } + if _, err := hook.file.WriteString(line); err != nil { + fmt.Fprintf(os.Stderr, "unable to write file on filehook(entry.String)%v\n", err) + return err + } + + return nil +} + +func (hook *FileHook) Levels() []logrus.Level { + return []logrus.Level{ + logrus.PanicLevel, + logrus.FatalLevel, + logrus.TraceLevel, + logrus.ErrorLevel, + logrus.WarnLevel, + logrus.InfoLevel, + logrus.DebugLevel, + } +} + +// The Middleware will write the Gin logs to logrus. +func ginToLogrus(log *logrus.Entry) gin.HandlerFunc { + return func(c *gin.Context) { + path := c.Request.URL.Path + raw := c.Request.URL.RawQuery + + // Process request + c.Next() + + clientIP := c.ClientIP() + method := c.Request.Method + statusCode := c.Writer.Status() + errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String() + + if raw != "" { + path = path + "?" + raw + } + + log.Infof("| %3d | %15s | %-7s | %s | %s", + statusCode, clientIP, method, path, errorMessage) + } +} + +// The Middleware will recover the Gin panic to logrus. +func ginRecover(log *logrus.Entry) gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if p := recover(); p != nil { + // Check for a broken connection, as it is not really a condition that warrants a panic stack trace. + var brokenPipe bool + if ne, ok := p.(*net.OpError); ok { + if se, ok := ne.Err.(*os.SyscallError); ok { + if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || + strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { + brokenPipe = true + } + } + } + + if log != nil { + stack := string(debug.Stack()) + if httpRequest, err := httputil.DumpRequest(c.Request, false); err != nil { + log.Errorf("Dump http request error: %v\n", err) + } else { + headers := strings.Split(string(httpRequest), "\r\n") + for idx, header := range headers { + current := strings.Split(header, ":") + if current[0] == "Authorization" { + headers[idx] = current[0] + ": *" + } + } + + // changing Fatalf to Errorf to let program not be exited + if brokenPipe { + log.Errorf("%v\n%s", p, string(httpRequest)) + } else if gin.IsDebugging() { + log.Errorf("[Debugging] panic:\n%s\n%v\n%s", strings.Join(headers, "\r\n"), p, stack) + } else { + log.Errorf("panic: %v\n%s", p, stack) + } + } + } + + // If the connection is dead, we can't write a status to it. + if brokenPipe { + c.Error(p.(error)) // nolint: errcheck + c.Abort() + } else { + c.AbortWithStatus(http.StatusInternalServerError) + } + } + }() + c.Next() + } +} + +// NewGinWithLogrus - returns an Engine instance with the ginToLogrus and Recovery middleware already attached. +func NewGinWithLogrus(log *logrus.Entry) *gin.Engine { + engine := gin.New() + engine.Use(ginToLogrus(log), ginRecover(log)) + return engine +} From 09afeb5ff882af267ebeb25adac20dcd46dc5e54 Mon Sep 17 00:00:00 2001 From: Ghislain Bourgeois Date: Tue, 28 May 2024 20:20:00 -0400 Subject: [PATCH 2/3] Standardize on logger instead of logger_util --- logger/logger_config.go | 26 +++- logger_util/log_level.go | 80 ------------ logger_util/logger_util.go | 252 ------------------------------------- 3 files changed, 21 insertions(+), 337 deletions(-) delete mode 100644 logger_util/log_level.go delete mode 100644 logger_util/logger_util.go diff --git a/logger/logger_config.go b/logger/logger_config.go index 5e20147..406ae42 100644 --- a/logger/logger_config.go +++ b/logger/logger_config.go @@ -28,11 +28,27 @@ type Logger struct { NWDAF *LogSetting `yaml:"NWDAF" valid:"optional"` WEBUI *LogSetting `yaml:"WEBUI" valid:"optional"` - Aper *LogSetting `yaml:"Aper" valid:"optional"` - FSM *LogSetting `yaml:"FSM" valid:"optional"` - NAS *LogSetting `yaml:"NAS" valid:"optional"` - NGAP *LogSetting `yaml:"NGAP" valid:"optional"` - PFCP *LogSetting `yaml:"PFCP" valid:"optional"` + Aper *LogSetting `yaml:"Aper" valid:"optional"` + CommonConsumerTest *LogSetting `yaml:"CommonConsumerTest" valid:"optional"` + FSM *LogSetting `yaml:"FSM" valid:"optional"` + MongoDBLibrary *LogSetting `yaml:"MongoDBLibrary" valid:"optional"` + NAS *LogSetting `yaml:"NAS" valid:"optional"` + NGAP *LogSetting `yaml:"NGAP" valid:"optional"` + OpenApi *LogSetting `yaml:"OpenApi" valid:"optional"` + NamfCommunication *LogSetting `yaml:"NamfCommunication" valid:"optional"` + NamfEventExposure *LogSetting `yaml:"NamfEventExposure" valid:"optional"` + NnssfNSSAIAvailability *LogSetting `yaml:"NnssfNSSAIAvailability" valid:"optional"` + NnssfNSSelection *LogSetting `yaml:"NnssfNSSelection" valid:"optional"` + NsmfEventExposure *LogSetting `yaml:"NsmfEventExposure" valid:"optional"` + NsmfPDUSession *LogSetting `yaml:"NsmfPDUSession" valid:"optional"` + NudmEventExposure *LogSetting `yaml:"NudmEventExposure" valid:"optional"` + NudmParameterProvision *LogSetting `yaml:"NudmParameterProvision" valid:"optional"` + NudmSubscriberDataManagement *LogSetting `yaml:"NudmSubscriberDataManagement" valid:"optional"` + NudmUEAuthentication *LogSetting `yaml:"NudmUEAuthentication" valid:"optional"` + NudmUEContextManagement *LogSetting `yaml:"NudmUEContextManagement" valid:"optional"` + NudrDataRepository *LogSetting `yaml:"NudrDataRepository" valid:"optional"` + PathUtil *LogSetting `yaml:"PathUtil" valid:"optional"` + PFCP *LogSetting `yaml:"PFCP" valid:"optional"` } func (l *Logger) Validate() (bool, error) { diff --git a/logger_util/log_level.go b/logger_util/log_level.go deleted file mode 100644 index e4aaedc..0000000 --- a/logger_util/log_level.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2019 Communication Service/Software Laboratory, National Chiao Tung University (free5gc.org) -// -// SPDX-License-Identifier: Apache-2.0 - -package logger_util - -import ( - "reflect" - - "github.com/asaskevich/govalidator" -) - -type Logger struct { - AMF *LogSetting `yaml:"AMF" valid:"optional"` - AUSF *LogSetting `yaml:"AUSF" valid:"optional"` - N3IWF *LogSetting `yaml:"N3IWF" valid:"optional"` - NRF *LogSetting `yaml:"NRF" valid:"optional"` - NSSF *LogSetting `yaml:"NSSF" valid:"optional"` - PCF *LogSetting `yaml:"PCF" valid:"optional"` - SMF *LogSetting `yaml:"SMF" valid:"optional"` - UDM *LogSetting `yaml:"UDM" valid:"optional"` - UDR *LogSetting `yaml:"UDR" valid:"optional"` - UPF *LogSetting `yaml:"UPF" valid:"optional"` - NEF *LogSetting `yaml:"NEF" valid:"optional"` - WEBUI *LogSetting `yaml:"WEBUI" valid:"optional"` - - Aper *LogSetting `yaml:"Aper" valid:"optional"` - CommonConsumerTest *LogSetting `yaml:"CommonConsumerTest" valid:"optional"` - FSM *LogSetting `yaml:"FSM" valid:"optional"` - MongoDBLibrary *LogSetting `yaml:"MongoDBLibrary" valid:"optional"` - NAS *LogSetting `yaml:"NAS" valid:"optional"` - NGAP *LogSetting `yaml:"NGAP" valid:"optional"` - OpenApi *LogSetting `yaml:"OpenApi" valid:"optional"` - NamfCommunication *LogSetting `yaml:"NamfCommunication" valid:"optional"` - NamfEventExposure *LogSetting `yaml:"NamfEventExposure" valid:"optional"` - NnssfNSSAIAvailability *LogSetting `yaml:"NnssfNSSAIAvailability" valid:"optional"` - NnssfNSSelection *LogSetting `yaml:"NnssfNSSelection" valid:"optional"` - NsmfEventExposure *LogSetting `yaml:"NsmfEventExposure" valid:"optional"` - NsmfPDUSession *LogSetting `yaml:"NsmfPDUSession" valid:"optional"` - NudmEventExposure *LogSetting `yaml:"NudmEventExposure" valid:"optional"` - NudmParameterProvision *LogSetting `yaml:"NudmParameterProvision" valid:"optional"` - NudmSubscriberDataManagement *LogSetting `yaml:"NudmSubscriberDataManagement" valid:"optional"` - NudmUEAuthentication *LogSetting `yaml:"NudmUEAuthentication" valid:"optional"` - NudmUEContextManagement *LogSetting `yaml:"NudmUEContextManagement" valid:"optional"` - NudrDataRepository *LogSetting `yaml:"NudrDataRepository" valid:"optional"` - PathUtil *LogSetting `yaml:"PathUtil" valid:"optional"` - PFCP *LogSetting `yaml:"PFCP" valid:"optional"` -} - -func (l *Logger) Validate() (bool, error) { - logger := reflect.ValueOf(l).Elem() - for i := 0; i < logger.NumField(); i++ { - if logSetting := logger.Field(i).Interface().(*LogSetting); logSetting != nil { - result, err := logSetting.validate() - return result, err - } - } - - result, err := govalidator.ValidateStruct(l) - return result, err -} - -type LogSetting struct { - DebugLevel string `yaml:"debugLevel" valid:"debugLevel"` - ReportCaller bool `yaml:"ReportCaller" valid:"type(bool)"` -} - -func (l *LogSetting) validate() (bool, error) { - govalidator.TagMap["debugLevel"] = govalidator.Validator(func(str string) bool { - if str == "panic" || str == "fatal" || str == "error" || str == "warn" || - str == "info" || str == "debug" || str == "trace" { - return true - } else { - return false - } - }) - - result, err := govalidator.ValidateStruct(l) - return result, err -} diff --git a/logger_util/logger_util.go b/logger_util/logger_util.go deleted file mode 100644 index ad53770..0000000 --- a/logger_util/logger_util.go +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright 2019 Communication Service/Software Laboratory, National Chiao Tung University (free5gc.org) -// -// SPDX-License-Identifier: Apache-2.0 - -package logger_util - -import ( - "fmt" - "log" - "net" - "net/http" - "net/http/httputil" - "os" - "path/filepath" - "runtime/debug" - "strconv" - "strings" - - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" -) - -type FileHook struct { - file *os.File - flag int - chmod os.FileMode - formatter *logrus.TextFormatter -} - -func NewFileHook(file string, flag int, chmod os.FileMode) (*FileHook, error) { - plainFormatter := &logrus.TextFormatter{DisableColors: true} - logFile, err := os.OpenFile(file, flag, chmod) - if err != nil { - fmt.Fprintf(os.Stderr, "unable to write file on filehook %v\n", err) - return nil, err - } - - return &FileHook{logFile, flag, chmod, plainFormatter}, nil -} - -func CreateFree5gcLogFile(file string) (string, error) { - // Because free5gc log file will be used by multiple NFs, it is not recommended to rename. - return createLogFile(file, "", false) -} - -func CreateNfLogFile(file string, defaultName string) (string, error) { - return createLogFile(file, defaultName, true) -} - -/* - * createLogFile - * @param file, The full file path from arguments input by user. - * @param defaultName, Default log file name (if it is empty, it means no default log file will be created) - * @param rename, Modify the file name if the file exists - * @return error, fullPath - */ -func createLogFile(file string, defaultName string, rename bool) (string, error) { - var fullPath string - directory, fileName := filepath.Split(file) - - if directory == "" || fileName == "" { - directory = "./log/" - fileName = defaultName - } - - if fileName == "" { - return "", nil - } - - fullPath = filepath.Join(directory, fileName) - - if rename { - if err := renameOldLogFile(fullPath); err != nil { - return "", err - } - } - - if err := os.MkdirAll(directory, 0775); err != nil { - return "", fmt.Errorf("make directory %s failed: %v", directory, err) - } - - sudoUID, errUID := strconv.Atoi(os.Getenv("SUDO_UID")) - sudoGID, errGID := strconv.Atoi(os.Getenv("SUDO_GID")) - if errUID == nil && errGID == nil { - // if using sudo to run the program, errUID will be nil and sudoUID will get the uid who run sudo - // else errUID will not be nil and sudoUID will be nil - // If user using sudo to run the program and create log file, log will own by root, - // here we change own to user so user can view and reuse the file - if err := os.Chown(directory, sudoUID, sudoGID); err != nil { - return "", fmt.Errorf("directory %s chown to %d:%d error: %v", directory, sudoUID, sudoGID, err) - } - - // Create log file or if it already exist, check if user can access it - if f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666); err != nil { - // user cannot access it. - return "", fmt.Errorf("cannot Open %s error: %v", fullPath, err) - } else { - // user can access it - if err := f.Close(); err != nil { - return "", fmt.Errorf("file %s cannot been closed", fullPath) - } - if err := os.Chown(fullPath, sudoUID, sudoGID); err != nil { - return "", fmt.Errorf("file %s chown to %d:%d error: %v", fullPath, sudoUID, sudoGID, err) - } - } - } - - return fullPath, nil -} - -func renameOldLogFile(fullPath string) error { - _, err := os.Stat(fullPath) - - if os.IsNotExist(err) { - return nil - } - - counter := 0 - sep := "." - fileDir, fileName := filepath.Split(fullPath) - - if contents, err := os.ReadDir(fileDir); err != nil { - return fmt.Errorf("reads the directory error %v", err) - } else { - for _, content := range contents { - if !content.IsDir() { - if strings.Contains(content.Name(), (fileName + sep)) { - counter++ - } - } - } - } - - newFullPath := fmt.Sprintf("%s%s%s%d", fileDir, fileName, sep, (counter + 1)) - if err := os.Rename(fullPath, newFullPath); err != nil { - return fmt.Errorf("unable to rename file %v", err) - } - - return nil -} - -// Fire event -func (hook *FileHook) Fire(entry *logrus.Entry) error { - var line string - if plainformat, err := hook.formatter.Format(entry); err != nil { - log.Printf("Formatter error: %+v", err) - return err - } else { - line = string(plainformat) - } - if _, err := hook.file.WriteString(line); err != nil { - fmt.Fprintf(os.Stderr, "unable to write file on filehook(entry.String)%v\n", err) - return err - } - - return nil -} - -func (hook *FileHook) Levels() []logrus.Level { - return []logrus.Level{ - logrus.PanicLevel, - logrus.FatalLevel, - logrus.TraceLevel, - logrus.ErrorLevel, - logrus.WarnLevel, - logrus.InfoLevel, - logrus.DebugLevel, - } -} - -// The Middleware will write the Gin logs to logrus. -func ginToLogrus(log *logrus.Entry) gin.HandlerFunc { - return func(c *gin.Context) { - path := c.Request.URL.Path - raw := c.Request.URL.RawQuery - - // Process request - c.Next() - - clientIP := c.ClientIP() - method := c.Request.Method - statusCode := c.Writer.Status() - errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String() - - if raw != "" { - path = path + "?" + raw - } - - log.Infof("| %3d | %15s | %-7s | %s | %s", - statusCode, clientIP, method, path, errorMessage) - } -} - -// The Middleware will recover the Gin panic to logrus. -func ginRecover(log *logrus.Entry) gin.HandlerFunc { - return func(c *gin.Context) { - defer func() { - if p := recover(); p != nil { - // Check for a broken connection, as it is not really a condition that warrants a panic stack trace. - var brokenPipe bool - if ne, ok := p.(*net.OpError); ok { - if se, ok := ne.Err.(*os.SyscallError); ok { - if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || - strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { - brokenPipe = true - } - } - } - - if log != nil { - stack := string(debug.Stack()) - if httpRequest, err := httputil.DumpRequest(c.Request, false); err != nil { - log.Errorf("Dump http request error: %v\n", err) - } else { - headers := strings.Split(string(httpRequest), "\r\n") - for idx, header := range headers { - current := strings.Split(header, ":") - if current[0] == "Authorization" { - headers[idx] = current[0] + ": *" - } - } - - // changing Fatalf to Errorf to let program not be exited - if brokenPipe { - log.Errorf("%v\n%s", p, string(httpRequest)) - } else if gin.IsDebugging() { - log.Errorf("[Debugging] panic:\n%s\n%v\n%s", strings.Join(headers, "\r\n"), p, stack) - } else { - log.Errorf("panic: %v\n%s", p, stack) - } - } - } - - // If the connection is dead, we can't write a status to it. - if brokenPipe { - c.Error(p.(error)) // nolint: errcheck - c.Abort() - } else { - c.AbortWithStatus(http.StatusInternalServerError) - } - } - }() - c.Next() - } -} - -// NewGinWithLogrus - returns an Engine instance with the ginToLogrus and Recovery middleware already attached. -func NewGinWithLogrus(log *logrus.Entry) *gin.Engine { - engine := gin.New() - engine.Use(ginToLogrus(log), ginRecover(log)) - return engine -} From 3598e36b82501ea8ab188338c8169b0702d00772 Mon Sep 17 00:00:00 2001 From: Ghislain Bourgeois Date: Tue, 28 May 2024 20:42:28 -0400 Subject: [PATCH 3/3] Copy util_3gpp to the util project --- go.mod | 4 +- go.sum | 4 + util_3gpp/3gpp_type.go | 21 ++ util_3gpp/logger/logger.go | 59 ++++++ util_3gpp/str_conv.go | 18 ++ util_3gpp/suci/toSupi.go | 381 +++++++++++++++++++++++++++++++++++++ 6 files changed, 486 insertions(+), 1 deletion(-) create mode 100644 util_3gpp/3gpp_type.go create mode 100644 util_3gpp/logger/logger.go create mode 100644 util_3gpp/str_conv.go create mode 100644 util_3gpp/suci/toSupi.go diff --git a/go.mod b/go.mod index 3972d7f..fe73f1c 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,14 @@ require ( github.com/evanphx/json-patch v4.11.0+incompatible github.com/gin-gonic/gin v1.7.3 github.com/mitchellh/mapstructure v1.4.1 + github.com/omec-project/openapi v1.2.0 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.0 github.com/smartystreets/goconvey v1.6.4 github.com/stretchr/testify v1.8.0 github.com/thakurajayL/go-ipam v0.0.5-dev go.mongodb.org/mongo-driver v1.10.1 + golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b ) @@ -30,6 +32,7 @@ require ( github.com/go-playground/validator/v10 v10.4.1 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt v3.2.1+incompatible // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect @@ -58,7 +61,6 @@ require ( go.uber.org/zap v1.23.0 // indirect go4.org/intern v0.0.0-20220617035311-6925f38cc365 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect - golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect golang.org/x/text v0.3.7 // indirect diff --git a/go.sum b/go.sum index 22af254..f306d9a 100644 --- a/go.sum +++ b/go.sum @@ -99,6 +99,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= +github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -199,6 +201,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/omec-project/openapi v1.2.0 h1:7Wvi0HLvhvxMyQtqGcqtMCPC/0QCGAFP5htrXCfWxRc= +github.com/omec-project/openapi v1.2.0/go.mod h1:hjU13MB1m9MHTko87JfsUNCdeD6/m6VkNZDD8Vq5U9M= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= diff --git a/util_3gpp/3gpp_type.go b/util_3gpp/3gpp_type.go new file mode 100644 index 0000000..10b6492 --- /dev/null +++ b/util_3gpp/3gpp_type.go @@ -0,0 +1,21 @@ +// Copyright 2019 Communication Service/Software Laboratory, National Chiao Tung University (free5gc.org) +// +// SPDX-License-Identifier: Apache-2.0 + +package util_3gpp + +type Dnn []uint8 + +func (d *Dnn) MarshalBinary() (data []byte, err error) { + + data = append(data, uint8(len(*d))) + data = append(data, (*d)...) + + return data, nil +} + +func (d *Dnn) UnmarshalBinary(data []byte) error { + + (*d) = data[1:] + return nil +} diff --git a/util_3gpp/logger/logger.go b/util_3gpp/logger/logger.go new file mode 100644 index 0000000..85e19b8 --- /dev/null +++ b/util_3gpp/logger/logger.go @@ -0,0 +1,59 @@ +// Copyright 2019 Communication Service/Software Laboratory, National Chiao Tung University (free5gc.org) +// +// SPDX-License-Identifier: Apache-2.0 + +package logger + +import ( + "os" + "time" + + formatter "github.com/antonfisher/nested-logrus-formatter" + "github.com/sirupsen/logrus" + + "github.com/omec-project/util/logger_conf" + "github.com/omec-project/util/logger" +) + +var log *logrus.Logger + +// Util3GPPLog : Log entry of util_3gpp +var Util3GPPLog *logrus.Entry + +func init() { + log = logrus.New() + log.SetReportCaller(false) + + log.Formatter = &formatter.Formatter{ + TimestampFormat: time.RFC3339, + TrimMessages: true, + NoFieldsSpace: true, + HideKeys: true, + FieldsOrder: []string{"component", "category"}, + } + + free5gcLogHook, err := logger.NewFileHook(logger_conf.Free5gcLogFile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666) + if err == nil { + log.Hooks.Add(free5gcLogHook) + } + + selfLogHook, err := logger.NewFileHook(logger_conf.LibLogDir+"util_3gpp.log", + os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666) + if err == nil { + log.Hooks.Add(selfLogHook) + } + + Util3GPPLog = log.WithFields(logrus.Fields{"component": "LIB", "category": "3GPP"}) +} + +// SetLogLevel : set the log level (panic|fatal|error|warn|info|debug|trace) +func SetLogLevel(level logrus.Level) { + Util3GPPLog.Infoln("set log level :", level) + log.SetLevel(level) +} + +// SetReportCaller : Set whether shows the filePath and functionName on loggers +func SetReportCaller(bool bool) { + Util3GPPLog.Infoln("set report call :", bool) + log.SetReportCaller(bool) +} diff --git a/util_3gpp/str_conv.go b/util_3gpp/str_conv.go new file mode 100644 index 0000000..392e76e --- /dev/null +++ b/util_3gpp/str_conv.go @@ -0,0 +1,18 @@ +// Copyright 2019 Communication Service/Software Laboratory, National Chiao Tung University (free5gc.org) +// +// SPDX-License-Identifier: Apache-2.0 + +package util_3gpp + +import ( + "fmt" + + "github.com/omec-project/openapi/models" +) + +func SNssaiToString(snssai *models.Snssai) (str string) { + if snssai.Sd == "" { + return fmt.Sprintf("%d-%s", snssai.Sst, snssai.Sd) + } + return fmt.Sprintf("%d", snssai.Sst) +} diff --git a/util_3gpp/suci/toSupi.go b/util_3gpp/suci/toSupi.go new file mode 100644 index 0000000..5bf5186 --- /dev/null +++ b/util_3gpp/suci/toSupi.go @@ -0,0 +1,381 @@ +// Copyright 2019 Communication Service/Software Laboratory, National Chiao Tung University (free5gc.org) +// +// SPDX-License-Identifier: Apache-2.0 + +package suci + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/elliptic" + "crypto/hmac" + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "fmt" + "log" + "math" + "math/big" + "math/bits" + "strconv" + "strings" + + "golang.org/x/crypto/curve25519" + + "github.com/omec-project/util/util_3gpp/logger" +) + +type SuciProfile struct { + ProtectionScheme string `yaml:"ProtectionScheme,omitempty"` + PrivateKey string `yaml:"PrivateKey,omitempty"` + PublicKey string `yaml:"PublicKey,omitempty"` +} + +// profile A. +const ProfileAMacKeyLen = 32 // octets +const ProfileAEncKeyLen = 16 // octets +const ProfileAIcbLen = 16 // octets +const ProfileAMacLen = 8 // octets +const ProfileAHashLen = 32 // octets + +// profile B. +const ProfileBMacKeyLen = 32 // octets +const ProfileBEncKeyLen = 16 // octets +const ProfileBIcbLen = 16 // octets +const ProfileBMacLen = 8 // octets +const ProfileBHashLen = 32 // octets + +func CompressKey(uncompressed []byte, y *big.Int) []byte { + compressed := uncompressed[0:33] + if y.Bit(0) == 1 { // 0x03 + compressed[0] = 0x03 + } else { // 0x02 + compressed[0] = 0x02 + } + // fmt.Printf("compressed: %x\n", compressed) + return compressed +} + +// modified from https://stackoverflow.com/questions/46283760/ +// how-to-uncompress-a-single-x9-62-compressed-point-on-an-ecdh-p256-curve-in-go. +func uncompressKey(compressedBytes []byte, priv []byte) (*big.Int, *big.Int) { + // Split the sign byte from the rest + signByte := uint(compressedBytes[0]) + xBytes := compressedBytes[1:] + + x := new(big.Int).SetBytes(xBytes) + three := big.NewInt(3) + + // The params for P256 + c := elliptic.P256().Params() + + // The equation is y^2 = x^3 - 3x + b + // x^3, mod P + xCubed := new(big.Int).Exp(x, three, c.P) + + // 3x, mod P + threeX := new(big.Int).Mul(x, three) + threeX.Mod(threeX, c.P) + + // x^3 - 3x + b mod P + ySquared := new(big.Int).Sub(xCubed, threeX) + ySquared.Add(ySquared, c.B) + ySquared.Mod(ySquared, c.P) + + // find the square root mod P + y := new(big.Int).ModSqrt(ySquared, c.P) + if y == nil { + // If this happens then you're dealing with an invalid point. + logger.Util3GPPLog.Errorln("Uncompressed key with invalid point") + return nil, nil + } + + // Finally, check if you have the correct root. If not you want -y mod P + if y.Bit(0) != signByte&1 { + y.Neg(y) + y.Mod(y, c.P) + } + // fmt.Printf("xUncom: %x\nyUncon: %x\n", x, y) + return x, y +} + +func HmacSha256(input, macKey []byte, macLen int) []byte { + h := hmac.New(sha256.New, macKey) + if _, err := h.Write(input); err != nil { + log.Printf("HMAC SHA256 error %+v", err) + } + macVal := h.Sum(nil) + macTag := macVal[:macLen] + // fmt.Printf("macVal: %x\nmacTag: %x\n", macVal, macTag) + return macTag +} + +func Aes128ctr(input, encKey, icb []byte) []byte { + output := make([]byte, len(input)) + block, err := aes.NewCipher(encKey) + if err != nil { + log.Printf("AES128 CTR error %+v", err) + } + stream := cipher.NewCTR(block, icb) + stream.XORKeyStream(output, input) + // fmt.Printf("aes input: %x %x %x\naes output: %x\n", input, encKey, icb, output) + return output +} + +func AnsiX963KDF(sharedKey, publicKey []byte, profileEncKeyLen, profileMacKeyLen, profileHashLen int) []byte { + var counter uint32 = 0x00000001 + var kdfKey []byte + kdfRounds := int(math.Ceil(float64(profileEncKeyLen+profileMacKeyLen) / float64(profileHashLen))) + for i := 1; i <= kdfRounds; i++ { + counterBytes := make([]byte, 4) + binary.BigEndian.PutUint32(counterBytes, counter) + // fmt.Printf("counterBytes: %x\n", counterBytes) + tmpK := sha256.Sum256(append(append(sharedKey, counterBytes...), publicKey...)) + sliceK := tmpK[:] + kdfKey = append(kdfKey, sliceK...) + // fmt.Printf("kdfKey in round %d: %x\n", i, kdfKey) + counter++ + } + return kdfKey +} + +func swapNibbles(input []byte) []byte { + output := make([]byte, len(input)) + for i, b := range input { + output[i] = bits.RotateLeft8(b, 4) + } + return output +} + +func calcSchemeResult(decryptPlainText []byte, supiType string) string { + var schemeResult string + if supiType == typeIMSI { + schemeResult = hex.EncodeToString(swapNibbles(decryptPlainText)) + if schemeResult[len(schemeResult)-1] == 'f' { + schemeResult = schemeResult[:len(schemeResult)-1] + } + } else { + schemeResult = hex.EncodeToString(decryptPlainText) + } + return schemeResult +} + +func profileA(input, supiType, privateKey string) (string, error) { + logger.Util3GPPLog.Infoln("SuciToSupi Profile A") + s, hexDecodeErr := hex.DecodeString(input) + if hexDecodeErr != nil { + logger.Util3GPPLog.Errorln("hex DecodeString error") + return "", hexDecodeErr + } + + // for X25519(profile A), q (The number of elements in the field Fq) = 2^255 - 19 + // len(pubkey) is therefore ceil((log2q)/8+1) = 32octets + ProfileAPubKeyLen := 32 + if len(s) < ProfileAPubKeyLen+ProfileAMacLen { + logger.Util3GPPLog.Errorln("len of input data is too short!") + return "", fmt.Errorf("suci input too short\n") + } + + decryptMac := s[len(s)-ProfileAMacLen:] + decryptPublicKey := s[:ProfileAPubKeyLen] + decryptCipherText := s[ProfileAPubKeyLen : len(s)-ProfileAMacLen] + // fmt.Printf("dePub: %x\ndeCiph: %x\ndeMac: %x\n", decryptPublicKey, decryptCipherText, decryptMac) + + // test data from TS33.501 Annex C.4 + // aHNPriv, _ := hex.DecodeString("c53c2208b61860b06c62e5406a7b330c2b577aa5558981510d128247d38bd1d") + var aHNPriv []byte + if aHNPrivTmp, err := hex.DecodeString(privateKey); err != nil { + log.Printf("Decode error: %+v", err) + } else { + aHNPriv = aHNPrivTmp + } + var decryptSharedKey []byte + if decryptSharedKeyTmp, err := curve25519.X25519(aHNPriv, []byte(decryptPublicKey)); err != nil { + log.Printf("X25519 error: %+v", err) + } else { + decryptSharedKey = decryptSharedKeyTmp + } + // fmt.Printf("deShared: %x\n", decryptSharedKey) + + kdfKey := AnsiX963KDF(decryptSharedKey, decryptPublicKey, ProfileAEncKeyLen, ProfileAMacKeyLen, ProfileAHashLen) + decryptEncKey := kdfKey[:ProfileAEncKeyLen] + decryptIcb := kdfKey[ProfileAEncKeyLen : ProfileAEncKeyLen+ProfileAIcbLen] + decryptMacKey := kdfKey[len(kdfKey)-ProfileAMacKeyLen:] + // fmt.Printf("\ndeEncKey(size%d): %x\ndeMacKey: %x\ndeIcb: %x\n", len(decryptEncKey), decryptEncKey, decryptMacKey, + // decryptIcb) + + decryptMacTag := HmacSha256(decryptCipherText, decryptMacKey, ProfileAMacLen) + if bytes.Equal(decryptMacTag, decryptMac) { + logger.Util3GPPLog.Infoln("decryption MAC match") + } else { + logger.Util3GPPLog.Errorln("decryption MAC failed") + return "", fmt.Errorf("decryption MAC failed\n") + } + + decryptPlainText := Aes128ctr(decryptCipherText, decryptEncKey, decryptIcb) + + return calcSchemeResult(decryptPlainText, supiType), nil +} + +func profileB(input, supiType, privateKey string) (string, error) { + logger.Util3GPPLog.Infoln("SuciToSupi Profile B") + s, hexDecodeErr := hex.DecodeString(input) + if hexDecodeErr != nil { + logger.Util3GPPLog.Errorln("hex DecodeString error") + return "", hexDecodeErr + } + + var ProfileBPubKeyLen int // p256, module q = 2^256 - 2^224 + 2^192 + 2^96 - 1 + var uncompressed bool + if s[0] == 0x02 || s[0] == 0x03 { + ProfileBPubKeyLen = 33 // ceil(log(2, q)/8) + 1 = 33 + uncompressed = false + } else if s[0] == 0x04 { + ProfileBPubKeyLen = 65 // 2*ceil(log(2, q)/8) + 1 = 65 + uncompressed = true + } else { + logger.Util3GPPLog.Errorln("input error") + return "", fmt.Errorf("suci input error\n") + } + + // fmt.Printf("len:%d %d\n", len(s), ProfileBPubKeyLen + ProfileBMacLen) + if len(s) < ProfileBPubKeyLen+ProfileBMacLen { + logger.Util3GPPLog.Errorln("len of input data is too short!") + return "", fmt.Errorf("suci input too short\n") + } + decryptPublicKey := s[:ProfileBPubKeyLen] + decryptMac := s[len(s)-ProfileBMacLen:] + decryptCipherText := s[ProfileBPubKeyLen : len(s)-ProfileBMacLen] + // fmt.Printf("dePub: %x\ndeCiph: %x\ndeMac: %x\n", decryptPublicKey, decryptCipherText, decryptMac) + + // test data from TS33.501 Annex C.4 + // bHNPriv, _ := hex.DecodeString("F1AB1074477EBCC7F554EA1C5FC368B1616730155E0041AC447D6301975FECDA") + var bHNPriv []byte + if bHNPrivTmp, err := hex.DecodeString(privateKey); err != nil { + log.Printf("Decode error: %+v", err) + } else { + bHNPriv = bHNPrivTmp + } + + var xUncompressed, yUncompressed *big.Int + if uncompressed { + xUncompressed = new(big.Int).SetBytes(decryptPublicKey[1:(ProfileBPubKeyLen/2 + 1)]) + yUncompressed = new(big.Int).SetBytes(decryptPublicKey[(ProfileBPubKeyLen/2 + 1):]) + } else { + xUncompressed, yUncompressed = uncompressKey(decryptPublicKey, bHNPriv) + if xUncompressed == nil || yUncompressed == nil { + logger.Util3GPPLog.Errorln("Uncompressed key has invalid point") + return "", fmt.Errorf("Key uncompression error\n") + } + } + // fmt.Printf("xUncom: %x\nyUncom: %x\n", xUncompressed, yUncompressed) + + // x-coordinate is the shared key + decryptSharedKey, _ := elliptic.P256().ScalarMult(xUncompressed, yUncompressed, bHNPriv) + // fmt.Printf("deShared: %x\n", decryptSharedKey.Bytes()) + + decryptPublicKeyForKDF := decryptPublicKey + if uncompressed { + decryptPublicKeyForKDF = CompressKey(decryptPublicKey, yUncompressed) + } + + kdfKey := AnsiX963KDF(decryptSharedKey.Bytes(), decryptPublicKeyForKDF, ProfileBEncKeyLen, ProfileBMacKeyLen, + ProfileBHashLen) + // fmt.Printf("kdfKey: %x\n", kdfKey) + decryptEncKey := kdfKey[:ProfileBEncKeyLen] + decryptIcb := kdfKey[ProfileBEncKeyLen : ProfileBEncKeyLen+ProfileBIcbLen] + decryptMacKey := kdfKey[len(kdfKey)-ProfileBMacKeyLen:] + // fmt.Printf("\ndeEncKey(size%d): %x\ndeMacKey: %x\ndeIcb: %x\n", len(decryptEncKey), decryptEncKey, decryptMacKey, + // decryptIcb) + + decryptMacTag := HmacSha256(decryptCipherText, decryptMacKey, ProfileBMacLen) + if bytes.Equal(decryptMacTag, decryptMac) { + logger.Util3GPPLog.Infoln("decryption MAC match") + } else { + logger.Util3GPPLog.Errorln("decryption MAC failed") + return "", fmt.Errorf("decryption MAC failed\n") + } + + decryptPlainText := Aes128ctr(decryptCipherText, decryptEncKey, decryptIcb) + + return calcSchemeResult(decryptPlainText, supiType), nil +} + +// suci-0(SUPI type)-mcc-mnc-routingIndentifier-protectionScheme-homeNetworkPublicKeyIdentifier-schemeOutput. +const supiTypePlace = 1 +const mccPlace = 2 +const mncPlace = 3 +const schemePlace = 5 +const HNPublicKeyIDPlace = 6 + +const typeIMSI = "0" +const imsiPrefix = "imsi-" +const nullScheme = "0" +const profileAScheme = "1" +const profileBScheme = "2" + +func ToSupi(suci string, suciProfiles []SuciProfile) (string, error) { + suciPart := strings.Split(suci, "-") + logger.Util3GPPLog.Infof("suciPart: %+v", suciPart) + + suciPrefix := suciPart[0] + if suciPrefix == "imsi" || suciPrefix == "nai" { + logger.Util3GPPLog.Infof("Got supi\n") + return suci, nil + + } else if suciPrefix == "suci" { + if len(suciPart) < 6 { + return "", fmt.Errorf("Suci with wrong format\n") + } + + } else { + return "", fmt.Errorf("Unknown suciPrefix [%s]", suciPrefix) + } + + logger.Util3GPPLog.Infof("scheme %s\n", suciPart[schemePlace]) + scheme := suciPart[schemePlace] + mccMnc := suciPart[mccPlace] + suciPart[mncPlace] + + supiPrefix := imsiPrefix + if suciPrefix == "suci" && suciPart[supiTypePlace] == typeIMSI { + supiPrefix = imsiPrefix + logger.Util3GPPLog.Infof("SUPI type is IMSI\n") + } + + if scheme == nullScheme { // NULL scheme + return supiPrefix + mccMnc + suciPart[len(suciPart)-1], nil + } + + // (HNPublicKeyID-1) is the index of "suciProfiles" slices + keyIndex, err := strconv.Atoi(suciPart[HNPublicKeyIDPlace]) + if err != nil { + return "", fmt.Errorf("Parse HNPublicKeyID error: %+v", err) + } + if keyIndex > len(suciProfiles) { + return "", fmt.Errorf("keyIndex(%d) out of range(%d)", keyIndex, len(suciProfiles)) + } + + protectScheme := suciProfiles[keyIndex-1].ProtectionScheme + privateKey := suciProfiles[keyIndex-1].PrivateKey + + if scheme != protectScheme { + return "", fmt.Errorf("Protect Scheme mismatch [%s:%s]", scheme, protectScheme) + } + + if scheme == profileAScheme { + if profileAResult, err := profileA(suciPart[len(suciPart)-1], suciPart[supiTypePlace], privateKey); err != nil { + return "", err + } else { + return supiPrefix + mccMnc + profileAResult, nil + } + } else if scheme == profileBScheme { + if profileBResult, err := profileB(suciPart[len(suciPart)-1], suciPart[supiTypePlace], privateKey); err != nil { + return "", err + } else { + return supiPrefix + mccMnc + profileBResult, nil + } + } else { + return "", fmt.Errorf("Protect Scheme (%s) is not supported", scheme) + } +}