1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2026-01-09 17:55:57 +00:00

Implement AUTH

This patch implements the AUTH SMTP command, using per-domain user databases.

Note that we don't really use or check the validation for anything, this is
just implementing the command itself.
This commit is contained in:
Alberto Bertogli
2016-07-16 12:43:29 +01:00
parent ff103c18c3
commit 21e69aa42f
4 changed files with 361 additions and 5 deletions

110
internal/auth/auth.go Normal file
View File

@@ -0,0 +1,110 @@
package auth
import (
"bytes"
"encoding/base64"
"fmt"
"math/rand"
"strings"
"time"
"unicode/utf8"
"blitiri.com.ar/go/chasquid/internal/userdb"
)
// DecodeResponse decodes a plain auth response.
//
// It must be a a base64-encoded string of the form:
// <authorization id> NUL <authentication id> NUL <password>
//
// https://tools.ietf.org/html/rfc4954#section-4.1.
//
// Either both ID match, or one of them is empty.
// We expect the ID to be "user@domain", which is NOT an RFC requirement but
// our own.
func DecodeResponse(response string) (user, domain, passwd string, err error) {
buf, err := base64.StdEncoding.DecodeString(response)
if err != nil {
return
}
bufsp := bytes.SplitN(buf, []byte{0}, 3)
if len(bufsp) != 3 {
err = fmt.Errorf("Response pieces != 3, as per RFC")
return
}
identity := ""
passwd = string(bufsp[2])
{
// We don't make the distinction between the two IDs, as long as one is
// empty, or they're the same.
z := string(bufsp[0])
c := string(bufsp[1])
// If neither is empty, then they must be the same.
if (z != "" && c != "") && (z != c) {
err = fmt.Errorf("Auth IDs do not match")
return
}
if z != "" {
identity = z
}
if c != "" {
identity = c
}
}
if identity == "" {
err = fmt.Errorf("Empty identity, must be in the form user@domain")
return
}
// Identity must be in the form "user@domain".
// This is NOT an RFC requirement, it's our own.
idsp := strings.SplitN(identity, "@", 2)
if len(idsp) != 2 {
err = fmt.Errorf("Identity must be in the form user@domain")
return
}
user = idsp[0]
domain = idsp[1]
// TODO: Quedamos aca. Validar dominio no (solo) como utf8, sino ver que
// no contenga ni "/" ni "..". Podemos usar golang.org/x/net/idna para
// convertirlo a unicode primero, o al reves. No se que queremos.
if !utf8.ValidString(user) || !utf8.ValidString(domain) {
err = fmt.Errorf("User/domain is not valid UTF-8")
return
}
return
}
// How long Authenticate calls should last, approximately.
// This will be applied both for successful and unsuccessful attempts.
// We will increase this number by 0-20%.
var AuthenticateTime = 100 * time.Millisecond
// Authenticate user/password on the given database.
func Authenticate(udb *userdb.DB, user, passwd string) bool {
defer func(start time.Time) {
elapsed := time.Since(start)
delay := AuthenticateTime - elapsed
if delay > 0 {
maxDelta := int64(float64(delay) * 0.2)
delay += time.Duration(rand.Int63n(maxDelta))
time.Sleep(delay)
}
}(time.Now())
// Note that the database CAN be nil, to simplify callers.
if udb == nil {
return false
}
return udb.Authenticate(user, passwd)
}

View File

@@ -0,0 +1,93 @@
package auth
import (
"encoding/base64"
"testing"
"time"
"blitiri.com.ar/go/chasquid/internal/userdb"
)
func TestDecodeResponse(t *testing.T) {
// Successful cases. Note we hard-code the response for extra assurance.
cases := []struct {
response, user, domain, passwd string
}{
{"dUBkAHVAZABwYXNz", "u", "d", "pass"}, // u@d\0u@d\0pass
{"dUBkAABwYXNz", "u", "d", "pass"}, // u@d\0\0pass
{"AHVAZABwYXNz", "u", "d", "pass"}, // \0u@d\0pass
{"dUBkAABwYXNz/w==", "u", "d", "pass\xff"}, // u@d\0\0pass\xff
// "ñaca@ñeque\0\0clavaré"
{"w7FhY2FAw7FlcXVlAABjbGF2YXLDqQ==", "ñaca", "ñeque", "clavaré"},
}
for _, c := range cases {
u, d, p, err := DecodeResponse(c.response)
if err != nil {
t.Errorf("Error in case %v: %v", c, err)
}
if u != c.user || d != c.domain || p != c.passwd {
t.Errorf("Expected %q %q %q ; got %q %q %q",
c.user, c.domain, c.passwd, u, d, p)
}
}
_, _, _, err := DecodeResponse("this is not base64 encoded")
if err == nil {
t.Errorf("invalid base64 did not fail as expected")
}
failedCases := []string{
"", "\x00", "\x00\x00", "\x00\x00\x00", "\x00\x00\x00\x00",
"a\x00b", "a\x00b\x00c", "a@a\x00b@b\x00pass", "a\x00a\x00pass",
"\xffa@b\x00\xffa@b\x00pass",
}
for _, c := range failedCases {
r := base64.StdEncoding.EncodeToString([]byte(c))
_, _, _, err := DecodeResponse(r)
if err == nil {
t.Errorf("Expected case %q to fail, but succeeded", c)
} else {
t.Logf("OK: %q failed with %v", c, err)
}
}
}
func TestAuthenticate(t *testing.T) {
db := userdb.New("/dev/null")
db.AddUser("user", "password")
// Test the correct case first
ts := time.Now()
if !Authenticate(db, "user", "password") {
t.Errorf("failed valid authentication for user/password")
}
if time.Since(ts) < AuthenticateTime {
t.Errorf("authentication was too fast")
}
// Incorrect cases.
cases := []struct{ user, password string }{
{"user", "incorrect"},
{"invalid", "p"},
}
for _, c := range cases {
ts = time.Now()
if Authenticate(db, c.user, c.password) {
t.Errorf("successful auth on %v", c)
}
if time.Since(ts) < AuthenticateTime {
t.Errorf("authentication was too fast")
}
}
// And the special case of a nil userdb.
ts = time.Now()
if Authenticate(nil, "user", "password") {
t.Errorf("successful auth on a nil userdb")
}
if time.Since(ts) < AuthenticateTime {
t.Errorf("authentication was too fast")
}
}