diff --git a/auth-new/password.go b/auth-new/password.go index a7efa92..ad22503 100644 --- a/auth-new/password.go +++ b/auth-new/password.go @@ -1,12 +1,14 @@ package auth import ( + "bytes" + "crypto/rand" "time" "git.mstar.dev/mstar/goutils/other" "git.mstar.dev/mstar/goutils/sliceutils" "github.com/google/uuid" - "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/argon2" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -14,12 +16,31 @@ import ( "git.mstar.dev/mstar/linstrom/storage-new/models" ) +const saltLen = 32 + +func generateSalt(length int) ([]byte, error) { + salt := make([]byte, length) + if _, err := rand.Read(salt); err != nil { + return nil, err + } + return salt, nil +} + func hashPassword(password string) ([]byte, error) { - return bcrypt.GenerateFromPassword([]byte(password), 14) + salt, err := generateSalt(saltLen) + if err != nil { + return nil, err + } + hash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32) + hash = append(hash, salt...) + // return bcrypt.GenerateFromPassword([]byte(password), 14) + return hash, nil } func comparePassword(password string, hash []byte) bool { - return bcrypt.CompareHashAndPassword(hash, []byte(password)) == nil + salt := hash[len(hash)-saltLen:] + + return bytes.Equal(argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32), hash) } // Start a login process with a username (NOT account ID) and password diff --git a/auth-new/totp.go b/auth-new/totp.go new file mode 100644 index 0000000..c89c1c4 --- /dev/null +++ b/auth-new/totp.go @@ -0,0 +1,106 @@ +package auth + +import ( + "time" + + "git.mstar.dev/mstar/goutils/other" + "git.mstar.dev/mstar/goutils/sliceutils" + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" + "gorm.io/gorm/clause" + + "git.mstar.dev/mstar/linstrom/config" + "git.mstar.dev/mstar/linstrom/storage-new/dbgen" + "git.mstar.dev/mstar/linstrom/storage-new/models" +) + +const totpUnverifiedSuffix = "-NOT_VERIFIED" + +func (a *Authenticator) PerformTotpLogin( + username string, + totpToken string, +) (LoginNextState, string, error) { + if ok, err := a.canUsernameLogin(username); !ok { + return 0, "", other.Error("auth", "user may not login", err) + } + acc, err := dbgen.User.Where(dbgen.User.Username.Eq(username)).First() + if err != nil { + return LoginNextFailure, "", other.Error("auth", "failed to find account", err) + } + secrets := sliceutils.Map( + sliceutils.Filter(acc.AuthMethods, func(t models.UserAuthMethod) bool { + return t.AuthMethod == models.AuthMethodGAuth + }), + func(t models.UserAuthMethod) string { + return string(t.Token) + }, + ) + found := false + for _, secret := range secrets { + if totp.Validate(totpToken, secret) { + found = true + break + } + } + if !found { + return LoginNextFailure, "", other.Error( + "auth", + "no fitting credential found", + ErrInvalidCombination, + ) + } + token := models.AccessToken{ + User: *acc, + UserId: acc.ID, + ExpiresAt: time.Now().Add(time.Hour * 24 * 365), + } + err = dbgen.AccessToken. + // technically, the chance of a conflict is so incredibly low that it should never ever occur + // since both the username and a randomly generated uuid would need to be created the same. Twice + // But, just in case, do this + Clauses(clause.OnConflict{DoNothing: true}). + Omit(dbgen.AccessToken.Token). + Create(&token) + if err != nil { + return LoginNextFailure, "", other.Error( + "auth", + "failed to create new access token", + err, + ) + } + return LoginNextSucess, token.Token, nil +} + +func (a *Authenticator) StartTotpRegistration( + username string, + tokenName string, +) (*otp.Key, error) { + if ok, err := a.canUsernameLogin(username); !ok { + return nil, other.Error("auth", "user may not login", err) + } + acc, err := dbgen.User.Where(dbgen.User.Username.Eq(username)).First() + if err != nil { + return nil, other.Error("auth", "failed to find account", err) + } + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: config.GlobalConfig.General.GetFullDomain(), + AccountName: username, + SecretSize: 160, + }) + if err != nil { + return nil, err + } + secret := key.Secret() + authToken := models.UserAuthMethod{ + UserId: acc.ID, + User: *acc, + Token: []byte(secret), + AuthMethod: models.AuthMethodGAuth, + Name: tokenName + totpUnverifiedSuffix, + } + err = dbgen.UserAuthMethod.Create(&authToken) + if err != nil { + return nil, err + } + return key, nil +} diff --git a/go.mod b/go.mod index 0dcdab0..3796f90 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/datainq/xml-date-time v0.0.0-20170820214645-2292f08baa38 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -68,6 +69,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect + github.com/pquerna/otp v1.4.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.37.0 // indirect diff --git a/go.sum b/go.sum index eae244d..95f1c3f 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ= github.com/bsm/ginkgo/v2 v2.5.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8= @@ -272,6 +274,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= +github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= diff --git a/storage-new/helpers.go b/storage-new/helpers.go new file mode 100644 index 0000000..d212d92 --- /dev/null +++ b/storage-new/helpers.go @@ -0,0 +1,65 @@ +package storage + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" +) + +// Copied and adjusted from: https://bruinsslot.jp/post/golang-crypto/ + +func Encrypt(key, data []byte) ([]byte, error) { + // key, salt, err := DeriveKey(key, nil) + // if err != nil { + // return nil, err + // } + // + blockCipher, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(blockCipher) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err = rand.Read(nonce); err != nil { + return nil, err + } + + ciphertext := gcm.Seal(nonce, nonce, data, nil) + + // ciphertext = append(ciphertext, salt...) + + return ciphertext, nil +} + +func Decrypt(key, data []byte) ([]byte, error) { + // salt, data := data[len(data)-32:], data[:len(data)-32] + + // key, _, err := DeriveKey(key, salt) + // if err != nil { + // return nil, err + // } + // + blockCipher, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(blockCipher) + if err != nil { + return nil, err + } + + nonce, ciphertext := data[:gcm.NonceSize()], data[gcm.NonceSize():] + + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +}