-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(BRIDGE-205): add AUTHENTICATE IMAP command.
- Loading branch information
Showing
10 changed files
with
407 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package command | ||
|
||
import ( | ||
"bytes" | ||
"encoding/base64" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/ProtonMail/gluon/rfcparser" | ||
) | ||
|
||
type Authenticate Login | ||
|
||
func (l Authenticate) String() string { | ||
return fmt.Sprintf("AUTHENTICATE '%v' '%v'", l.UserID, l.Password) | ||
} | ||
|
||
func (l Authenticate) SanitizedString() string { | ||
return fmt.Sprint("AUTHENTICATE <AUTH_DATA>") | ||
} | ||
|
||
type AuthenticateCommandParser struct{} | ||
|
||
const ( | ||
messageClientAbortedAuthentication = "client aborted authentication" | ||
messageInvalidBase64Content = "invalid base64 content" | ||
messageUnsupportedAuthenticationMechanism = "unsupported authentication mechanism" | ||
messageInvalidAuthenticationData = "invalid authentication data" //nolint:gosec | ||
) | ||
|
||
func (AuthenticateCommandParser) FromParser(p *rfcparser.Parser) (Payload, error) { | ||
// authenticate = "AUTHENTICATE" SP auth-type CRLF base64 | ||
// auth-type = atom | ||
// base64 = base64 encoded string | ||
if err := p.Consume(rfcparser.TokenTypeSP, "expected space after command"); err != nil { | ||
return nil, err | ||
} | ||
|
||
method, err := p.ParseAtom() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if !strings.EqualFold(method, "plain") { | ||
return nil, p.MakeError(messageUnsupportedAuthenticationMechanism) | ||
} | ||
|
||
return parseAuthInputString(p) | ||
} | ||
|
||
func parseAuthInputString(p *rfcparser.Parser) (*Authenticate, error) { | ||
// The continued response for the AUTHENTICATE can be whether | ||
// `*` , indicating the user aborted the authentication | ||
// a base64 encoded string of the form `identity\0userid\0password`. identity is ignored in IMAP. Some client (Thunderbird) will leave it empty), | ||
// other will use the userID (Apple Mail). | ||
parsed, err := p.ParseStringAfterContinuation() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
input := parsed.Value | ||
if input == "*" && p.Check(rfcparser.TokenTypeCR) { // behave like dovecot: no extra whitespaces allowed after * when cancelling. | ||
return nil, p.MakeError(messageClientAbortedAuthentication) | ||
} | ||
|
||
decoded, err := base64.StdEncoding.DecodeString(input) | ||
if err != nil { | ||
return nil, p.MakeError(messageInvalidBase64Content) | ||
} | ||
|
||
if len(decoded) < 2 { // min acceptable message be empty username and password (`\x00\x00`). | ||
return nil, p.MakeError(messageInvalidAuthenticationData) | ||
} | ||
|
||
split := bytes.Split(decoded[0:], []byte{0}) | ||
if len(split) != 3 { | ||
return nil, p.MakeError(messageInvalidAuthenticationData) | ||
} | ||
|
||
return &Authenticate{ | ||
UserID: string(split[1]), | ||
Password: string(split[2]), | ||
}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
package command | ||
|
||
import ( | ||
"bytes" | ||
"encoding/base64" | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/ProtonMail/gluon/rfcparser" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func continuationChecker(continued *bool) func() error { | ||
return func() error { *continued = true; return nil } | ||
} | ||
|
||
func TestParser_Authenticate(t *testing.T) { | ||
testData := []*Authenticate{ | ||
{UserID: "[email protected]", Password: "pass"}, | ||
{UserID: "[email protected]", Password: ""}, | ||
{UserID: "", Password: "pass"}, | ||
{UserID: "", Password: ""}, | ||
} | ||
|
||
for i, data := range testData { | ||
var continued bool | ||
|
||
tag := fmt.Sprintf("A%04d", i) | ||
authString := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("\x00%s\x00%s", data.UserID, data.Password))) | ||
input := toIMAPLine(tag+` AUTHENTICATE PLAIN`, authString) | ||
s := rfcparser.NewScanner(bytes.NewReader(input)) | ||
p := NewParserWithLiteralContinuationCb(s, continuationChecker(&continued)) | ||
cmd, err := p.Parse() | ||
message := fmt.Sprintf(" test failed for input %#v", data) | ||
|
||
require.NoError(t, err, "error"+message) | ||
require.True(t, continued, "continuation"+message) | ||
require.Equal(t, data, cmd.Payload, "payload"+message) | ||
require.Equal(t, "authenticate", p.LastParsedCommand(), "command"+message) | ||
require.Equal(t, tag, p.LastParsedTag(), "tag"+message) | ||
} | ||
} | ||
|
||
func TestParser_AuthenticationWithIdentity(t *testing.T) { | ||
var continued bool | ||
|
||
authString := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("identity\x00user\x00pass"))) | ||
s := rfcparser.NewScanner(bytes.NewReader(toIMAPLine(`A0001 authenticate plain`, authString))) | ||
p := NewParserWithLiteralContinuationCb(s, continuationChecker(&continued)) | ||
cmd, err := p.Parse() | ||
|
||
require.NoError(t, err, "error test failed") | ||
require.True(t, continued, "continuation test failed") | ||
require.Equal(t, &Authenticate{UserID: "user", Password: "pass"}, cmd.Payload, "payload test failed") | ||
require.Equal(t, "authenticate", p.LastParsedCommand(), "command test failed") | ||
require.Equal(t, "A0001", p.LastParsedTag(), "tag test failed") | ||
} | ||
|
||
func TestParser_AuthenticateFailures(t *testing.T) { | ||
testData := []struct { | ||
input []string | ||
expectedMessage string | ||
continuationExpected bool | ||
description string | ||
}{ | ||
{ | ||
input: []string{`A003 AUTHENTICATE PLAIN`, `*`}, | ||
expectedMessage: messageClientAbortedAuthentication, | ||
continuationExpected: true, | ||
description: "AUTHENTICATE abortion should return an error", | ||
}, | ||
{ | ||
input: []string{`A003 AUTHENTICATE NONE`, `*`}, | ||
expectedMessage: messageUnsupportedAuthenticationMechanism, | ||
continuationExpected: false, | ||
description: "AUTHENTICATE with unknown mechanism should fail", | ||
}, | ||
{ | ||
input: []string{`A003 AUTHENTICATE PLAIN GARBAGE`, `*`}, | ||
expectedMessage: "expected CR", | ||
continuationExpected: false, | ||
description: "AUTHENTICATE with garbage before CRLF should fail", | ||
}, | ||
{ | ||
input: []string{`A003 AUTHENTICATE PLAIN `, `*`}, | ||
expectedMessage: "expected CR", | ||
continuationExpected: false, | ||
description: "AUTHENTICATE with extra space before CRLF should fail", | ||
}, | ||
{ | ||
input: []string{`A003 AUTHENTICATE PLAIN`, `* `}, | ||
expectedMessage: messageInvalidBase64Content, | ||
continuationExpected: true, | ||
description: "AUTHENTICATE with extra space after the abort `*` should fail", | ||
}, | ||
{ | ||
input: []string{`A003 AUTHENTICATE PLAIN`, `* `}, | ||
expectedMessage: messageInvalidBase64Content, | ||
continuationExpected: true, | ||
description: "AUTHENTICATE with extra space after the abort `*` should fail", | ||
}, | ||
{ | ||
input: []string{`A003 AUTHENTICATE PLAIN`, `not-base64`}, | ||
expectedMessage: messageInvalidBase64Content, | ||
continuationExpected: true, | ||
description: "AUTHENTICATE with invalid base 64 message after continuation should fail", | ||
}, | ||
{ | ||
input: []string{`A003 AUTHENTICATE PLAIN`, base64.StdEncoding.EncodeToString([]byte("username+password"))}, | ||
expectedMessage: messageInvalidAuthenticationData, | ||
continuationExpected: true, | ||
description: "AUTHENTICATE with invalid decoded base64 content should fail", | ||
}, | ||
{ | ||
input: []string{`A003 AUTHENTICATE PLAIN`, base64.StdEncoding.EncodeToString([]byte("\x00username\x00password")) + " "}, | ||
expectedMessage: "expected CR", | ||
continuationExpected: true, | ||
description: "AUTHENTICATE with trailing spaces after a valid base64 message should fail", | ||
}, | ||
} | ||
|
||
for _, test := range testData { | ||
var continued bool | ||
|
||
s := rfcparser.NewScanner(bytes.NewReader(toIMAPLine(test.input...))) | ||
p := NewParserWithLiteralContinuationCb(s, continuationChecker(&continued)) | ||
_, err := p.Parse() | ||
failureDescription := fmt.Sprintf(" test failed for input %#v", test) | ||
|
||
var parserError *rfcparser.Error | ||
|
||
require.ErrorAs(t, err, &parserError, "error"+failureDescription) | ||
require.Equal(t, test.expectedMessage, parserError.Message, "error message"+failureDescription) | ||
require.Equal(t, test.continuationExpected, continued, "continuation"+failureDescription) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.