2021-12-25 02:16:58 +01:00
|
|
|
package auth
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/rand"
|
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
2022-07-24 22:02:55 +02:00
|
|
|
"net/mail"
|
2022-07-22 03:19:24 +02:00
|
|
|
"strings"
|
2021-12-25 02:16:58 +01:00
|
|
|
"time"
|
2022-07-13 18:32:48 +02:00
|
|
|
|
|
|
|
"golang.org/x/crypto/scrypt"
|
2021-12-25 02:16:58 +01:00
|
|
|
)
|
|
|
|
|
2022-06-07 19:25:14 +02:00
|
|
|
type UserId int32
|
2022-07-22 03:19:24 +02:00
|
|
|
type NormalizedEmail string // Should always contain a normalized value
|
2022-06-07 19:25:14 +02:00
|
|
|
type Email string
|
|
|
|
type DeviceId string
|
|
|
|
type Password string
|
2022-07-15 21:36:11 +02:00
|
|
|
type KDFKey string // KDF output
|
|
|
|
type ClientSaltSeed string // part of client-side KDF input along with root password
|
|
|
|
type ServerSalt string // server-side KDF input for accounts
|
2022-06-09 23:04:49 +02:00
|
|
|
type TokenString string
|
2021-12-25 02:16:58 +01:00
|
|
|
type AuthScope string
|
|
|
|
|
|
|
|
const ScopeFull = AuthScope("*")
|
|
|
|
|
|
|
|
// For test stubs
|
|
|
|
type AuthInterface interface {
|
2022-06-07 19:25:14 +02:00
|
|
|
// TODO maybe have a "refresh token" thing if the client won't have email available all the time?
|
|
|
|
NewToken(UserId, DeviceId, AuthScope) (*AuthToken, error)
|
2021-12-25 02:16:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type Auth struct{}
|
|
|
|
|
|
|
|
// Note that everything here is given to anybody who presents a valid
|
|
|
|
// downloadKey and associated email. Currently these fields are safe to give
|
|
|
|
// at that low security level, but keep this in mind as we change this struct.
|
|
|
|
type AuthToken struct {
|
2022-06-09 23:04:49 +02:00
|
|
|
Token TokenString `json:"token"`
|
|
|
|
DeviceId DeviceId `json:"deviceId"`
|
|
|
|
Scope AuthScope `json:"scope"`
|
|
|
|
UserId UserId `json:"userId"`
|
|
|
|
Expiration *time.Time `json:"expiration"`
|
2021-12-25 02:16:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const AuthTokenLength = 32
|
|
|
|
|
2022-06-07 19:25:14 +02:00
|
|
|
func (a *Auth) NewToken(userId UserId, deviceId DeviceId, scope AuthScope) (*AuthToken, error) {
|
2021-12-25 02:16:58 +01:00
|
|
|
b := make([]byte, AuthTokenLength)
|
2022-06-07 19:25:14 +02:00
|
|
|
// TODO - Is this is a secure random function? (Maybe audit)
|
2021-12-25 02:16:58 +01:00
|
|
|
if _, err := rand.Read(b); err != nil {
|
|
|
|
return nil, fmt.Errorf("Error generating token: %+v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &AuthToken{
|
2022-06-09 23:04:49 +02:00
|
|
|
Token: TokenString(hex.EncodeToString(b)),
|
2022-06-07 19:25:14 +02:00
|
|
|
DeviceId: deviceId,
|
|
|
|
Scope: scope,
|
|
|
|
UserId: userId,
|
2021-12-25 02:16:58 +01:00
|
|
|
// TODO add Expiration here instead of putting it in store.go. and thus redo store.go. d'oh.
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE - not stubbing methods of structs like this. more convoluted than it's worth right now
|
|
|
|
func (at *AuthToken) ScopeValid(required AuthScope) bool {
|
2022-06-08 00:41:32 +02:00
|
|
|
// So far * is the only scope issued. Used to have more, didn't want to
|
|
|
|
// delete this feature yet in case we add more again. We'll delete it if it's
|
|
|
|
// of no use and ends up complicating anything.
|
|
|
|
return at.Scope == ScopeFull || at.Scope == required
|
2021-12-25 02:16:58 +01:00
|
|
|
}
|
2022-06-07 19:25:14 +02:00
|
|
|
|
2022-07-15 21:36:11 +02:00
|
|
|
const ServerSaltLength = 16
|
|
|
|
const ClientSaltSeedLength = 32
|
2022-07-13 18:32:48 +02:00
|
|
|
|
|
|
|
// https://words.filippo.io/the-scrypt-parameters/
|
|
|
|
func passwordScrypt(p Password, saltBytes []byte) ([]byte, error) {
|
|
|
|
scryptN := 32768
|
|
|
|
scryptR := 8
|
|
|
|
scryptP := 1
|
|
|
|
keyLen := 32
|
|
|
|
return scrypt.Key([]byte(p), saltBytes, scryptN, scryptR, scryptP, keyLen)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Given a password (in the same format submitted via request), generate a
|
|
|
|
// random salt, run the password and salt thorugh the KDF, and return the salt
|
|
|
|
// and kdf output. The result generally goes into a database.
|
2022-07-15 21:36:11 +02:00
|
|
|
func (p Password) Create() (key KDFKey, salt ServerSalt, err error) {
|
|
|
|
saltBytes := make([]byte, ServerSaltLength)
|
2022-07-13 18:32:48 +02:00
|
|
|
if _, err := rand.Read(saltBytes); err != nil {
|
|
|
|
return "", "", fmt.Errorf("Error generating salt: %+v", err)
|
|
|
|
}
|
|
|
|
keyBytes, err := passwordScrypt(p, saltBytes)
|
|
|
|
if err == nil {
|
|
|
|
key = KDFKey(hex.EncodeToString(keyBytes[:]))
|
2022-07-15 21:36:11 +02:00
|
|
|
salt = ServerSalt(hex.EncodeToString(saltBytes[:]))
|
2022-07-13 18:32:48 +02:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Given a password (in the same format submitted via request), a salt, and an
|
|
|
|
// expected kdf output, run the password and salt thorugh the KDF, and return
|
|
|
|
// whether the result kdf output matches the kdf test output.
|
|
|
|
// The salt and test kdf output generally come out of the database, and is used
|
|
|
|
// to check a submitted password.
|
2022-07-15 21:36:11 +02:00
|
|
|
func (p Password) Check(checkKey KDFKey, salt ServerSalt) (match bool, err error) {
|
2022-07-13 18:32:48 +02:00
|
|
|
saltBytes, err := hex.DecodeString(string(salt))
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("Error decoding salt from hex: %+v", err)
|
|
|
|
}
|
|
|
|
keyBytes, err := passwordScrypt(p, saltBytes)
|
|
|
|
if err == nil {
|
|
|
|
match = KDFKey(hex.EncodeToString(keyBytes[:])) == checkKey
|
|
|
|
}
|
|
|
|
return
|
2022-06-07 19:25:14 +02:00
|
|
|
}
|
2022-07-22 03:19:24 +02:00
|
|
|
|
2022-07-24 22:02:55 +02:00
|
|
|
func (e Email) Validate() bool {
|
|
|
|
email, err := mail.ParseAddress(string(e))
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
// "Joe <joe@example.com>" is valid according to ParseAddress. Likewise
|
|
|
|
// " joe@example.com". Etc. We only want the exact address, "joe@example.com"
|
|
|
|
// to be valid. ParseAddress will extract the exact address as
|
|
|
|
// parsed.Address. So we'll take the input email, put it through
|
|
|
|
// ParseAddress, see if it parses successfully, and then compare the input
|
|
|
|
// email to parsed.Address to make sure that it was an exact address to begin
|
|
|
|
// with.
|
|
|
|
return string(e) == email.Address
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c ClientSaltSeed) Validate() bool {
|
|
|
|
_, err := hex.DecodeString(string(c))
|
|
|
|
const seedHexLength = ClientSaltSeedLength * 2
|
|
|
|
return len(c) == seedHexLength && err == nil
|
|
|
|
}
|
|
|
|
|
2022-07-22 03:19:24 +02:00
|
|
|
// TODO consider unicode. Also some providers might be case sensitive, and/or
|
|
|
|
// may have other ways of having email addresses be equivalent (which we may
|
|
|
|
// not care about though)
|
|
|
|
func (e Email) Normalize() NormalizedEmail {
|
|
|
|
return NormalizedEmail(strings.ToLower(string(e)))
|
|
|
|
}
|