Skip to content

Commit

Permalink
Merge pull request #338 from wneessen/feature/no_auth_logging
Browse files Browse the repository at this point in the history
Redact logging of SMTP authentication data
  • Loading branch information
wneessen authored Oct 15, 2024
2 parents 73663f6 + 3234c13 commit bb2fd0f
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 5 deletions.
42 changes: 39 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ type (
// isEncrypted indicates wether the Client connection is encrypted or not.
isEncrypted bool

// logAuthData indicates whether authentication-related data should be logged.
logAuthData bool

// logger is a logger that satisfies the log.Logger interface.
logger log.Logger

Expand Down Expand Up @@ -364,9 +367,10 @@ func WithSSLPort(fallback bool) Option {
// WithDebugLog enables debug logging for the Client.
//
// This function activates debug logging, which logs incoming and outgoing communication between the
// Client and the SMTP server to os.Stderr. Be cautious when using this option, as the logs may include
// unencrypted authentication data, depending on the SMTP authentication method in use, which could
// pose a data protection risk.
// Client and the SMTP server to os.Stderr. By default the debug logging will redact any kind of SMTP
// authentication data. If you need access to the actual authentication data in your logs, you can
// enable authentication data logging with the WithLogAuthData option or by setting it with the
// Client.SetLogAuthData method.
//
// Returns:
// - An Option function that enables debug logging for the Client.
Expand Down Expand Up @@ -671,6 +675,22 @@ func WithDialContextFunc(dialCtxFunc DialContextFunc) Option {
}
}

// WithLogAuthData enables logging of authentication data.
//
// This function sets the logAuthData field of the Client to true, enabling the logging of authentication data.
//
// Be cautious when using this option, as the logs may include unencrypted authentication data, depending on
// the SMTP authentication method in use, which could pose a data protection risk.
//
// Returns:
// - An Option function that configures the Client to enable authentication data logging.
func WithLogAuthData() Option {
return func(c *Client) error {
c.logAuthData = true
return nil
}
}

// TLSPolicy returns the TLSPolicy that is currently set on the Client as a string.
//
// This method retrieves the current TLSPolicy configured for the Client and returns it as a string representation.
Expand Down Expand Up @@ -865,6 +885,19 @@ func (c *Client) SetSMTPAuthCustom(smtpAuth smtp.Auth) {
c.smtpAuthType = SMTPAuthCustom
}

// SetLogAuthData sets or overrides the logging of SMTP authentication data for the Client.
//
// This function sets the logAuthData field of the Client to true, enabling the logging of authentication data.
//
// Be cautious when using this option, as the logs may include unencrypted authentication data, depending on
// the SMTP authentication method in use, which could pose a data protection risk.
//
// Parameters:
// - logAuth: Set wether or not to log SMTP authentication data for the Client.
func (c *Client) SetLogAuthData(logAuth bool) {
c.logAuthData = logAuth
}

// DialWithContext establishes a connection to the server using the provided context.Context.
//
// This function adds a deadline based on the Client's timeout to the provided context.Context
Expand Down Expand Up @@ -921,6 +954,9 @@ func (c *Client) DialWithContext(dialCtx context.Context) error {
if c.useDebugLog {
c.smtpClient.SetDebugLog(true)
}
if c.logAuthData {
c.smtpClient.SetLogAuthData()
}
if err = c.smtpClient.Hello(c.helo); err != nil {
return err
}
Expand Down
18 changes: 18 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ func TestNewClientWithOptions(t *testing.T) {
{"WithoutNoop()", WithoutNoop(), false},
{"WithDebugLog()", WithDebugLog(), false},
{"WithLogger()", WithLogger(log.New(os.Stderr, log.LevelDebug)), false},
{"WithLogger()", WithLogAuthData(), false},
{"WithDialContextFunc()", WithDialContextFunc(func(ctx context.Context, network, address string) (net.Conn, error) {
return nil, nil
}), false},
Expand Down Expand Up @@ -578,6 +579,23 @@ func TestWithoutNoop(t *testing.T) {
}
}

func TestClient_SetLogAuthData(t *testing.T) {
c, err := NewClient(DefaultHost, WithLogAuthData())
if err != nil {
t.Errorf("failed to create new client: %s", err)
return
}
if !c.logAuthData {
t.Errorf("WithLogAuthData failed. c.logAuthData expected to be: %t, got: %t", true,
c.logAuthData)
}
c.SetLogAuthData(false)
if c.logAuthData {
t.Errorf("SetLogAuthData failed. c.logAuthData expected to be: %t, got: %t", false,
c.logAuthData)
}
}

// TestSetSMTPAuthCustom tests the SetSMTPAuthCustom method for the Client object
func TestSetSMTPAuthCustom(t *testing.T) {
tests := []struct {
Expand Down
45 changes: 43 additions & 2 deletions smtp/smtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ type Client struct {
// auth supported auth mechanisms
auth []string

// authIsActive indicates that the Client is currently during SMTP authentication
authIsActive bool

// keep a reference to the connection so it can be used to create a TLS connection later
conn net.Conn

Expand All @@ -78,6 +81,9 @@ type Client struct {
// isConnected indicates if the Client has an active connection
isConnected bool

// logAuthData indicates if the Client should include SMTP authentication data in the logs
logAuthData bool

// localName is the name to use in HELO/EHLO
localName string // the name to use in HELO/EHLO

Expand Down Expand Up @@ -174,15 +180,29 @@ func (c *Client) Hello(localName string) error {
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
c.mutex.Lock()

c.debugLog(log.DirClientToServer, format, args...)
var logMsg []interface{}
logMsg = args
logFmt := format
if c.authIsActive {
logMsg = []interface{}{"<SMTP auth data redacted>"}
logFmt = "%s"
}
c.debugLog(log.DirClientToServer, logFmt, logMsg...)

id, err := c.Text.Cmd(format, args...)
if err != nil {
c.mutex.Unlock()
return 0, "", err
}
c.Text.StartResponse(id)
code, msg, err := c.Text.ReadResponse(expectCode)
c.debugLog(log.DirServerToClient, "%d %s", code, msg)

logMsg = []interface{}{code, msg}
if c.authIsActive && code >= 300 && code <= 400 {
logMsg = []interface{}{code, "<SMTP auth data redacted>"}
}
c.debugLog(log.DirServerToClient, "%d %s", logMsg...)

c.Text.EndResponse(id)
c.mutex.Unlock()
return code, msg, err
Expand Down Expand Up @@ -256,6 +276,20 @@ func (c *Client) Auth(a Auth) error {
if err := c.hello(); err != nil {
return err
}

c.mutex.Lock()
if !c.logAuthData {
c.authIsActive = true
}
c.mutex.Unlock()
defer func() {
c.mutex.Lock()
if !c.logAuthData {
c.authIsActive = false
}
c.mutex.Unlock()
}()

encoding := base64.StdEncoding
mech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth})
if err != nil {
Expand Down Expand Up @@ -556,6 +590,13 @@ func (c *Client) SetLogger(l log.Logger) {
c.logger = l
}

// SetLogAuthData enables logging of authentication data in the Client.
func (c *Client) SetLogAuthData() {
c.mutex.Lock()
c.logAuthData = true
c.mutex.Unlock()
}

// SetDSNMailReturnOption sets the DSN mail return option for the Mail method
func (c *Client) SetDSNMailReturnOption(d string) {
c.dsnmrtype = d
Expand Down
26 changes: 26 additions & 0 deletions smtp/smtp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,32 @@ func TestClient_SetLogger(t *testing.T) {
c.logger.Debugf(log.Log{Direction: log.DirServerToClient, Format: "%s", Messages: []interface{}{"test"}})
}

func TestClient_SetLogAuthData(t *testing.T) {
server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n")

var cmdbuf strings.Builder
bcmdbuf := bufio.NewWriter(&cmdbuf)
out := func() string {
if err := bcmdbuf.Flush(); err != nil {
t.Errorf("failed to flush: %s", err)
}
return cmdbuf.String()
}
var fake faker
fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
c, err := NewClient(fake, "fake.host")
if err != nil {
t.Fatalf("NewClient: %v\n(after %v)", err, out())
}
defer func() {
_ = c.Close()
}()
c.SetLogAuthData()
if !c.logAuthData {
t.Error("Expected logAuthData to be true but received false")
}
}

var newClientServer = `220 hello world
250-mx.google.com at your service
250-SIZE 35651584
Expand Down

0 comments on commit bb2fd0f

Please sign in to comment.