Initial commit

This commit is contained in:
Kevin Kandlbinder 2023-10-19 11:44:03 +00:00
commit 036c1824a9
14 changed files with 1867 additions and 0 deletions

111
.gitignore vendored Normal file
View file

@ -0,0 +1,111 @@
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Go template
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
/wroofauth
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/

43
cmd/root.go Normal file
View file

@ -0,0 +1,43 @@
package cmd
import (
"os"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "wroofauth",
Short: "The auth system that might bite.",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.wroofauth.yaml)")
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

35
cmd/serve.go Normal file
View file

@ -0,0 +1,35 @@
package cmd
import (
"git.1in9.net/raider/wroofauth/internal/server"
"github.com/spf13/cobra"
)
// serveCmd represents the serve command
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Starts WroofAuth in Server mode",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
server.Serve()
},
}
func init() {
rootCmd.AddCommand(serveCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// serveCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// serveCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

55
go.mod Normal file
View file

@ -0,0 +1,55 @@
module git.1in9.net/raider/wroofauth
go 1.18
require (
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/improbable-eng/grpc-web v0.15.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pquerna/otp v1.4.0 // indirect
github.com/rs/cors v1.7.0 // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/cobra-cli v1.3.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.17.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.mongodb.org/mongo-driver v1.12.1 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
nhooyr.io/websocket v1.8.6 // indirect
)

1121
go.sum Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,50 @@
package database
import (
"context"
"github.com/spf13/viper"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)
var dbClient *mongo.Client
var MongoDatabase *mongo.Database
func Connect() {
cmdMonitor := &event.CommandMonitor{
Started: func(_ context.Context, evt *event.CommandStartedEvent) {
// TODO: Log
},
}
client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(viper.GetString("mongo.uri")).SetMonitor(cmdMonitor))
if err != nil {
// TODO: Log
panic(err)
}
err = client.Ping(context.TODO(), readpref.Nearest())
if err != nil {
// TODO: Log
panic(err)
}
dbClient = client
MongoDatabase = client.Database(viper.GetString("mongo.database"))
//UserCollection = Database.Collection(viper.GetString("mongo.collection.users"))
//ClientCollection = Database.Collection(viper.GetString("mongo.collection.clients"))
//GroupCollection = Database.Collection(viper.GetString("mongo.collection.groups"))
}
func Disconnect() {
err := dbClient.Disconnect(context.TODO())
if err != nil {
// TODO: Log
panic(err)
}
}

View file

@ -0,0 +1,8 @@
package entities
import "go.mongodb.org/mongo-driver/bson/primitive"
type Entity interface {
GetType() string
GetID() primitive.ObjectID
}

View file

@ -0,0 +1 @@
package user

View file

@ -0,0 +1,216 @@
package user
import (
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"strings"
"time"
"github.com/pquerna/otp/totp"
"github.com/spf13/viper"
"golang.org/x/crypto/argon2"
"git.1in9.net/raider/wroofauth/internal/parameters"
"go.mongodb.org/mongo-driver/bson/primitive"
)
var (
ErrInvalidHash = errors.New("the encoded hash is not in the correct format")
ErrIncompatibleVersion = errors.New("incompatible version of argon2")
ErrWrongPassword = errors.New("wrong password")
ErrPasswordLoginDeactivated = errors.New("password login deactivated for this user")
)
type UserSecondFactor struct {
Name string `bson:"name"`
Type string `bson:"type"`
Enabled bool `bson:"enabled"`
Arguments map[string]string `bson:"arguments"`
}
type User struct {
ID primitive.ObjectID `bson:"_id"`
Username *string `bson:"username,omitempty"`
Email *string `bson:"email,omitempty"`
PasswordHash *string `bson:"password,omitempty"`
PasswordChangeTimestamp time.Time `bson:"passwordChangeTimestamp,omitempty"`
SecondFactors []*UserSecondFactor `bson:"secondFactors"`
SecondFactorOverride bool `bson:"secondFactorOverride,omitempty"` // Can be used to temporarily disable 2fa
}
func (u *User) GetType() string {
return "user"
}
func (u *User) GetID() primitive.ObjectID {
return u.ID
}
func (u *User) GetFriendlyIdentifier() string {
if u.Username != nil {
return *u.Username
}
if u.Email != nil {
return *u.Email
}
return u.ID.Hex() // User with no identification, fall back to just echoing the ID
}
func (u *User) countActive2FA() int {
count := 0
for _, factor := range u.SecondFactors {
if !factor.Enabled {
continue
}
count++
}
return count
}
func (u *User) Needs2FA() bool {
return !u.SecondFactorOverride && u.countActive2FA() > 0
}
func (u *User) CheckPassword(password string) error {
if u.PasswordHash == nil {
return ErrPasswordLoginDeactivated
}
p, salt, hash, err := decodeHash(*u.PasswordHash)
if err != nil {
return err
}
otherHash := argon2.IDKey([]byte(password), salt, p.Iterations, p.Memory, p.Parallelism, p.KeyLength)
if subtle.ConstantTimeCompare(hash, otherHash) == 1 {
return nil
}
return ErrWrongPassword
}
func (u *User) SetPassword(password string) error {
p := parameters.GetPasswordParams()
salt, err := parameters.GenerateRandomBytes(p.SaltLength)
if err != nil {
return err
}
hash := argon2.IDKey([]byte(password), salt, p.Iterations, p.Memory, p.Parallelism, p.KeyLength)
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
encodedHash := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, p.Memory, p.Iterations, p.Parallelism, b64Salt, b64Hash)
u.PasswordHash = &encodedHash
u.PasswordChangeTimestamp = time.Now()
return nil
}
func (u *User) AddTOTP(name string) (string, error) {
key, err := totp.Generate(totp.GenerateOpts{
Issuer: viper.GetString("totp.issuer"),
AccountName: u.GetFriendlyIdentifier(),
})
if err != nil {
return "", err
}
u.SecondFactors = append(u.SecondFactors, &UserSecondFactor{
Name: name,
Type: "totp",
Enabled: false,
Arguments: map[string]string{
"secret": key.Secret(),
},
})
return key.String(), nil
}
func (u *User) EnableTOTP(code string) error {
for _, factor := range u.SecondFactors {
if factor.Enabled || factor.Type != "totp" {
continue
}
success := totp.Validate(code, factor.Arguments["secret"])
if !success {
continue
}
factor.Enabled = true
return nil
}
return errors.New("no such totp")
}
func (u *User) ValidateTOTP(code string) error {
for _, factor := range u.SecondFactors {
if !factor.Enabled || factor.Type != "totp" {
continue
}
success := totp.Validate(code, factor.Arguments["secret"])
if !success {
continue
}
return nil
}
return errors.New("no such totp")
}
func decodeHash(encodedHash string) (p *parameters.PasswordParams, salt, hash []byte, err error) {
values := strings.Split(encodedHash, "$")
if len(values) != 6 {
return nil, nil, nil, ErrInvalidHash
}
var version int
_, err = fmt.Sscanf(values[2], "v=%d", &version)
if err != nil {
return nil, nil, nil, err
}
if version != argon2.Version {
return nil, nil, nil, ErrIncompatibleVersion
}
p = &parameters.PasswordParams{}
_, err = fmt.Sscanf(values[3], "m=%d,t=%d,p=%d", &p.Memory, &p.Iterations, &p.Parallelism)
if err != nil {
return nil, nil, nil, err
}
salt, err = base64.RawStdEncoding.Strict().DecodeString(values[4])
if err != nil {
return nil, nil, nil, err
}
p.SaltLength = uint32(len(salt))
hash, err = base64.RawStdEncoding.Strict().DecodeString(values[5])
if err != nil {
return nil, nil, nil, err
}
p.KeyLength = uint32(len(hash))
return p, salt, hash, nil
}

View file

@ -0,0 +1,176 @@
package machines
import (
"errors"
"strings"
"git.1in9.net/raider/wroofauth/internal/entities/user"
)
var (
ErrIllegalStateAction = errors.New("illegal action for this state")
ErrIllegalState = errors.New("illegal state")
)
type SessionState string
const (
SessionState_EMPTY SessionState = "EMPTY"
SessionState_UNAUTHENTICATED SessionState = "UNAUTHENTICATED"
SessionState_AWAITING_FACTOR SessionState = "AWAITING_FACTOR"
SessionState_AUTHENTICATED_PENDING SessionState = "AUTHENTICATED_PENDING"
SessionState_AUTHENTICATED_FULLY SessionState = "AUTHENTICATED_FULLY"
SessionState_AUTHENTICATED_PASSWORD_CHANGE SessionState = "AUTHENTICATED_PASSWORD_CHANGE"
SessionState_AUTHENTICATED_2FA_ENROLL SessionState = "AUTHENTICATED_2FA_ENROLL"
SessionState_AUTHENTICATED_REVIEW_TOS SessionState = "AUTHENTICATED_REVIEW_TOS"
SessionState_AUTHENTICATED_REVIEW_RECOVERY SessionState = "AUTHENTICATED_REVIEW_RECOVERY"
)
type AuthenticationMethod string
const (
AuthenticationMethod_NONE = "NONE"
AuthenticationMethod_PASSWORD = "PASSWORD"
)
type SecondFactor string
const (
SecondFactor_NONE = "NONE"
SecondFactor_TOTP = "TOTP"
)
type Session struct {
State SessionState
AuthenticationMethod AuthenticationMethod
SecondFactor SecondFactor
User *user.User
}
func NewSession() *Session {
return &Session{
State: SessionState_EMPTY,
AuthenticationMethod: AuthenticationMethod_NONE,
SecondFactor: SecondFactor_NONE,
}
}
// s.Validate checks if the session is in a valid state
func (s *Session) Validate() error {
if s.IsAnyAuthenticated() {
if s.User == nil {
// We can only be here if a user is set
return ErrIllegalState
}
if s.AuthenticationMethod == AuthenticationMethod_NONE {
// We can not be authenticated without any way we got here
return ErrIllegalState
}
if s.SecondFactor == SecondFactor_NONE && s.User.Needs2FA() {
// User has either just enabled 2FA, or something went horribly wrong.
// Either way, throw away the session!
return ErrIllegalState
}
}
return nil
}
func (s *Session) IsAnyAuthenticated() bool {
return strings.HasPrefix(string(s.State), "AUTHENTICATED_")
}
func (s *Session) performPreflight() error {
// TODO: Do Preflight Checks.
// TODO: Do PASSWORD_EXPIRED check
// TODO: Do 2FA-Force-Enrolment check
// TODO: Do TOS check
// TODO: Do Reveiw Recovery check
// TODO: Do Flag check
// Preflight ok.
s.State = SessionState_AUTHENTICATED_FULLY
return nil
}
func (s *Session) HandleIdentification(identification string) error {
if s.State != SessionState_EMPTY {
return ErrIllegalStateAction // This step may only run on EMPTY sessions
}
// TODO: Handle Identification
return nil
}
func (s *Session) HandlePassword(password string) error {
if s.State != SessionState_UNAUTHENTICATED {
return ErrIllegalStateAction // This step may only run on UNAUTHENTICATED sessions
}
err := s.User.CheckPassword(password)
if err != nil {
return err
}
// Password Ok.
s.AuthenticationMethod = AuthenticationMethod_PASSWORD
if !s.User.Needs2FA() {
// No 2fa, jump to AUTHENTICATED_PENDING for preflight
s.State = SessionState_AUTHENTICATED_PENDING
return s.performPreflight()
}
s.State = SessionState_AWAITING_FACTOR
return nil
}
// TODO: Passkey action
func (s *Session) HandleTOTP(otp string) error {
if s.State != SessionState_AWAITING_FACTOR {
return ErrIllegalStateAction
}
err := s.User.ValidateTOTP(otp)
if err != nil {
return err
}
// OTP Ok.
s.SecondFactor = SecondFactor_TOTP
// Good to go for preflight.
s.State = SessionState_AUTHENTICATED_PENDING
return s.performPreflight()
}
func (s *Session) HandleLock() error {
if !s.IsAnyAuthenticated() {
return ErrIllegalStateAction
}
// TODO: Handle Lock
return nil
}
func (s *Session) HandleLogout() error {
if !s.IsAnyAuthenticated() {
return ErrIllegalStateAction
}
// TODO: Handle Logout
return nil
}
func (s *Session) Destroy() error {
// TODO: Destroy Session
return nil
}

View file

@ -0,0 +1,31 @@
package parameters
import "crypto/rand"
type PasswordParams struct {
Memory uint32
Iterations uint32
Parallelism uint8
SaltLength uint32
KeyLength uint32
}
func GetPasswordParams() *PasswordParams {
return &PasswordParams{
Memory: 64 * 1024,
Iterations: 3,
Parallelism: 2,
SaltLength: 16,
KeyLength: 32,
}
}
func GenerateRandomBytes(n uint32) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return nil, err
}
return b, nil
}

View file

@ -0,0 +1,4 @@
package server
func Serve() {
}

9
main.go Normal file
View file

@ -0,0 +1,9 @@
package main
import "git.1in9.net/raider/wroofauth/cmd"
//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative proto/session.proto
func main() {
cmd.Execute()
}

7
tools.go Normal file
View file

@ -0,0 +1,7 @@
//go:build tools
package main
import (
_ "github.com/spf13/cobra-cli"
)