package service import ( "bytes" "context" "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/gob" "errors" "io" "golang.org/x/crypto/bcrypt" "git.sr.ht/~gabrielgio/img/pkg/database/repository" ) type AuthController struct { authRepository repository.AuthRepository userRepository repository.UserRepository key []byte } var InvalidLogin = errors.New("Invalid login") func NewAuthController( authRepository repository.AuthRepository, userRepository repository.UserRepository, key []byte, ) *AuthController { return &AuthController{ authRepository: authRepository, userRepository: userRepository, key: key, } } func (c *AuthController) Login(ctx context.Context, username, password []byte) ([]byte, error) { id, err := c.authRepository.GetIDByUsername(ctx, string(username)) if errors.Is(err, repository.ErrRecordNotFound) { return nil, InvalidLogin } else if err != nil { return nil, err } hashedPassword, err := c.authRepository.GetPassword(ctx, id) if errors.Is(err, repository.ErrRecordNotFound) { return nil, InvalidLogin } else if err != nil { return nil, err } if err := bcrypt.CompareHashAndPassword(hashedPassword, password); err != nil { return nil, InvalidLogin } token := &Token{ UserID: id, Username: string(username), } return WriteToken(token, c.key) } // InitialRegister register a initial user, it will validate if there is another // user stored already. If so an error `InvlidaInput` will be returned func (c *AuthController) InitialRegister(ctx context.Context, username, password []byte, path []byte) error { exist, err := c.userRepository.Any(ctx) if err != nil { return err } if exist { return InvlidInput } hash, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost) if err != nil { return err } _, err = c.userRepository.Create(ctx, &repository.CreateUser{ Username: string(username), Password: hash, IsAdmin: true, Path: string(path), }) return err } func (u *AuthController) List(ctx context.Context) ([]*repository.User, error) { return u.userRepository.List(ctx) } func (u *AuthController) Get(ctx context.Context, id uint) (*repository.User, error) { return u.userRepository.Get(ctx, id) } func (u *AuthController) Delete(ctx context.Context, id uint) error { return u.userRepository.Delete(ctx, id) } func (u *AuthController) Upsert( ctx context.Context, id *uint, username string, name string, password []byte, isAdmin bool, path string, ) error { if id != nil { if err := u.userRepository.Update(ctx, *id, &repository.UpdateUser{ Username: username, Name: name, IsAdmin: isAdmin, Path: path, }); err != nil { return err } if len(password) > 0 { hash, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost) if err != nil { return err } return u.userRepository.UpdatePassword(ctx, *id, hash) } return nil } hash, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost) if err != nil { return err } _, err = u.userRepository.Create(ctx, &repository.CreateUser{ Username: username, Name: name, Password: hash, IsAdmin: isAdmin, Path: path, }) return err } type ( Token struct { UserID uint Username string } ) func ReadToken(data []byte, key []byte) (*Token, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } aesgcm, err := cipher.NewGCM(block) if err != nil { panic(err.Error()) } nonceSize := aesgcm.NonceSize() if len(data) < nonceSize { return nil, errors.New("nonce size greater than data's size") } nonce, ciphertext := data[:nonceSize], data[nonceSize:] plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) if err != nil { return nil, err } r := bytes.NewReader(plaintext) var token Token dec := gob.NewDecoder(r) if err = dec.Decode(&token); err != nil { return nil, err } return &token, nil } func WriteToken(token *Token, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } aesgcm, err := cipher.NewGCM(block) if err != nil { return nil, err } var buffer bytes.Buffer enc := gob.NewEncoder(&buffer) if err := enc.Encode(token); err != nil { return nil, err } nonce := make([]byte, aesgcm.NonceSize()) if _, err = io.ReadFull(rand.Reader, nonce); err != nil { return nil, err } ciphertext := aesgcm.Seal(nonce, nonce, buffer.Bytes(), nil) return ciphertext, nil }