Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
710c257c64
|
|||
c320bb3d90 | |||
2f01a1d50f
|
|||
ffc57b7e89
|
@@ -3,6 +3,7 @@ package cryptext
|
|||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -14,14 +15,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const LatestPassHashVersion = 4
|
const LatestPassHashVersion = 5
|
||||||
|
|
||||||
// PassHash
|
// PassHash
|
||||||
// - [v0]: plaintext password ( `0|...` )
|
// - [v0]: plaintext password ( `0|...` ) // simple, used to write PW's directly in DB
|
||||||
// - [v1]: sha256(plaintext)
|
// - [v1]: sha256(plaintext) // simple hashing
|
||||||
// - [v2]: seed | sha256<seed>(plaintext)
|
// - [v2]: seed | sha256<seed>(plaintext) // add seed
|
||||||
// - [v3]: seed | sha256<seed>(plaintext) | [hex(totp)]
|
// - [v3]: seed | sha256<seed>(plaintext) | [hex(totp)] // add TOTP support
|
||||||
// - [v4]: bcrypt(plaintext) | [hex(totp)]
|
// - [v4]: bcrypt(plaintext) | [hex(totp)] // use proper bcrypt
|
||||||
|
// - [v5]: bcrypt(sha512(plaintext)) | [hex(totp)] // hash pw before bcrypt (otherwise max pw-len = 72)
|
||||||
type PassHash string
|
type PassHash string
|
||||||
|
|
||||||
func (ph PassHash) Valid() bool {
|
func (ph PassHash) Valid() bool {
|
||||||
@@ -109,7 +111,21 @@ func (ph PassHash) Data() (_version int, _seed []byte, _payload []byte, _totp bo
|
|||||||
totp := false
|
totp := false
|
||||||
totpsecret := make([]byte, 0)
|
totpsecret := make([]byte, 0)
|
||||||
if split[2] != "0" {
|
if split[2] != "0" {
|
||||||
totpsecret, err = hex.DecodeString(split[3])
|
totpsecret, err = hex.DecodeString(split[2])
|
||||||
|
totp = true
|
||||||
|
}
|
||||||
|
return int(version), nil, payload, totp, totpsecret, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if version == 5 {
|
||||||
|
if len(split) != 3 {
|
||||||
|
return -1, nil, nil, false, nil, false
|
||||||
|
}
|
||||||
|
payload := []byte(split[1])
|
||||||
|
totp := false
|
||||||
|
totpsecret := make([]byte, 0)
|
||||||
|
if split[2] != "0" {
|
||||||
|
totpsecret, err = hex.DecodeString(split[2])
|
||||||
totp = true
|
totp = true
|
||||||
}
|
}
|
||||||
return int(version), nil, payload, totp, totpsecret, true
|
return int(version), nil, payload, totp, totpsecret, true
|
||||||
@@ -156,6 +172,14 @@ func (ph PassHash) Verify(plainpass string, totp *string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if version == 5 {
|
||||||
|
if !hastotp {
|
||||||
|
return bcrypt.CompareHashAndPassword(payload, hash512(plainpass)) == nil
|
||||||
|
} else {
|
||||||
|
return bcrypt.CompareHashAndPassword(payload, hash512(plainpass)) == nil && totpext.Validate(totpsecret, *totp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +233,12 @@ func (ph PassHash) ClearTOTP() (PassHash, error) {
|
|||||||
return PassHash(strings.Join(split, "|")), nil
|
return PassHash(strings.Join(split, "|")), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if version == 5 {
|
||||||
|
split := strings.Split(string(ph), "|")
|
||||||
|
split[2] = "0"
|
||||||
|
return PassHash(strings.Join(split, "|")), nil
|
||||||
|
}
|
||||||
|
|
||||||
return "", errors.New("unknown version")
|
return "", errors.New("unknown version")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +272,12 @@ func (ph PassHash) WithTOTP(totpSecret []byte) (PassHash, error) {
|
|||||||
return PassHash(strings.Join(split, "|")), nil
|
return PassHash(strings.Join(split, "|")), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if version == 5 {
|
||||||
|
split := strings.Split(string(ph), "|")
|
||||||
|
split[2] = hex.EncodeToString(totpSecret)
|
||||||
|
return PassHash(strings.Join(split, "|")), nil
|
||||||
|
}
|
||||||
|
|
||||||
return "", errors.New("unknown version")
|
return "", errors.New("unknown version")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +307,10 @@ func (ph PassHash) Change(newPlainPass string) (PassHash, error) {
|
|||||||
return HashPasswordV4(newPlainPass, langext.Conditional(hastotp, totpsecret, nil))
|
return HashPasswordV4(newPlainPass, langext.Conditional(hastotp, totpsecret, nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if version == 5 {
|
||||||
|
return HashPasswordV5(newPlainPass, langext.Conditional(hastotp, totpsecret, nil))
|
||||||
|
}
|
||||||
|
|
||||||
return "", errors.New("unknown version")
|
return "", errors.New("unknown version")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +319,24 @@ func (ph PassHash) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func HashPassword(plainpass string, totpSecret []byte) (PassHash, error) {
|
func HashPassword(plainpass string, totpSecret []byte) (PassHash, error) {
|
||||||
return HashPasswordV4(plainpass, totpSecret)
|
return HashPasswordV5(plainpass, totpSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HashPasswordV5(plainpass string, totpSecret []byte) (PassHash, error) {
|
||||||
|
var strtotp string
|
||||||
|
|
||||||
|
if totpSecret == nil {
|
||||||
|
strtotp = "0"
|
||||||
|
} else {
|
||||||
|
strtotp = hex.EncodeToString(totpSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := bcrypt.GenerateFromPassword(hash512(plainpass), bcrypt.MinCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return PassHash(fmt.Sprintf("5|%s|%s", string(payload), strtotp)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func HashPasswordV4(plainpass string, totpSecret []byte) (PassHash, error) {
|
func HashPasswordV4(plainpass string, totpSecret []byte) (PassHash, error) {
|
||||||
@@ -340,6 +397,13 @@ func HashPasswordV0(plainpass string) (PassHash, error) {
|
|||||||
return PassHash(fmt.Sprintf("0|%s", plainpass)), nil
|
return PassHash(fmt.Sprintf("0|%s", plainpass)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hash512(s string) []byte {
|
||||||
|
h := sha512.New()
|
||||||
|
h.Write([]byte(s))
|
||||||
|
bs := h.Sum(nil)
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
|
||||||
func hash256(s string) []byte {
|
func hash256(s string) []byte {
|
||||||
h := sha256.New()
|
h := sha256.New()
|
||||||
h.Write([]byte(s))
|
h.Write([]byte(s))
|
||||||
|
210
cryptext/passHash_test.go
Normal file
210
cryptext/passHash_test.go
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
package cryptext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/totpext"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/tst"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPassHash1(t *testing.T) {
|
||||||
|
ph, err := HashPassword("test123", nil)
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertFalse(t, ph.HasTOTP())
|
||||||
|
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassHashTOTP(t *testing.T) {
|
||||||
|
sec, err := totpext.GenerateSecret()
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
ph, err := HashPassword("test123", sec)
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertTrue(t, ph.HasTOTP())
|
||||||
|
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertFalse(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec))))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassHashUpgrade_V0(t *testing.T) {
|
||||||
|
ph, err := HashPasswordV0("test123")
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertFalse(t, ph.HasTOTP())
|
||||||
|
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
|
||||||
|
ph, err = ph.Upgrade("test123")
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertFalse(t, ph.HasTOTP())
|
||||||
|
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassHashUpgrade_V1(t *testing.T) {
|
||||||
|
ph, err := HashPasswordV1("test123")
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertFalse(t, ph.HasTOTP())
|
||||||
|
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
|
||||||
|
ph, err = ph.Upgrade("test123")
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertFalse(t, ph.HasTOTP())
|
||||||
|
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassHashUpgrade_V2(t *testing.T) {
|
||||||
|
ph, err := HashPasswordV2("test123")
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertFalse(t, ph.HasTOTP())
|
||||||
|
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
|
||||||
|
ph, err = ph.Upgrade("test123")
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertFalse(t, ph.HasTOTP())
|
||||||
|
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassHashUpgrade_V3(t *testing.T) {
|
||||||
|
ph, err := HashPasswordV3("test123", nil)
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertFalse(t, ph.HasTOTP())
|
||||||
|
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
|
||||||
|
ph, err = ph.Upgrade("test123")
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertFalse(t, ph.HasTOTP())
|
||||||
|
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassHashUpgrade_V3_TOTP(t *testing.T) {
|
||||||
|
sec, err := totpext.GenerateSecret()
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
ph, err := HashPasswordV3("test123", sec)
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertTrue(t, ph.HasTOTP())
|
||||||
|
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertFalse(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec))))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
|
||||||
|
ph, err = ph.Upgrade("test123")
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertTrue(t, ph.HasTOTP())
|
||||||
|
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertFalse(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec))))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassHashUpgrade_V4(t *testing.T) {
|
||||||
|
ph, err := HashPasswordV4("test123", nil)
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertFalse(t, ph.HasTOTP())
|
||||||
|
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
|
||||||
|
ph, err = ph.Upgrade("test123")
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertFalse(t, ph.HasTOTP())
|
||||||
|
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassHashUpgrade_V4_TOTP(t *testing.T) {
|
||||||
|
sec, err := totpext.GenerateSecret()
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
ph, err := HashPasswordV4("test123", sec)
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertTrue(t, ph.HasTOTP())
|
||||||
|
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertFalse(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec))))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
|
||||||
|
ph, err = ph.Upgrade("test123")
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
tst.AssertTrue(t, ph.Valid())
|
||||||
|
tst.AssertTrue(t, ph.HasTOTP())
|
||||||
|
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
|
||||||
|
|
||||||
|
tst.AssertFalse(t, ph.Verify("test123", nil))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec))))
|
||||||
|
tst.AssertFalse(t, ph.Verify("test124", nil))
|
||||||
|
}
|
10
go.mod
10
go.mod
@@ -5,14 +5,14 @@ go 1.19
|
|||||||
require (
|
require (
|
||||||
github.com/jmoiron/sqlx v1.3.5
|
github.com/jmoiron/sqlx v1.3.5
|
||||||
go.mongodb.org/mongo-driver v1.12.0
|
go.mongodb.org/mongo-driver v1.12.0
|
||||||
golang.org/x/crypto v0.10.0
|
golang.org/x/crypto v0.11.0
|
||||||
golang.org/x/sys v0.9.0
|
golang.org/x/sys v0.10.0
|
||||||
golang.org/x/term v0.9.0
|
golang.org/x/term v0.10.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/klauspost/compress v1.16.6 // indirect
|
github.com/klauspost/compress v1.16.7 // indirect
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
@@ -20,5 +20,5 @@ require (
|
|||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
|
||||||
golang.org/x/sync v0.3.0 // indirect
|
golang.org/x/sync v0.3.0 // indirect
|
||||||
golang.org/x/text v0.10.0 // indirect
|
golang.org/x/text v0.11.0 // indirect
|
||||||
)
|
)
|
||||||
|
10
go.sum
10
go.sum
@@ -17,6 +17,8 @@ github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/d
|
|||||||
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
|
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
|
||||||
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||||
|
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
@@ -70,6 +72,8 @@ golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
|||||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||||
|
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||||
|
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -97,6 +101,8 @@ golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
|||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||||
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||||
@@ -105,6 +111,8 @@ golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
|
|||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
|
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
|
||||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||||
|
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
||||||
|
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -117,6 +125,8 @@ golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
|||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||||
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
package goext
|
package goext
|
||||||
|
|
||||||
const GoextVersion = "0.0.167"
|
const GoextVersion = "0.0.171"
|
||||||
|
|
||||||
const GoextVersionTimestamp = "2023-06-22T17:33:56+0200"
|
const GoextVersionTimestamp = "2023-07-18T13:34:54+0200"
|
||||||
|
@@ -2,6 +2,7 @@ package tst
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"runtime/debug"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,12 +55,18 @@ func AssertHexEqual(t *testing.T, expected string, actual []byte) {
|
|||||||
|
|
||||||
func AssertTrue(t *testing.T, value bool) {
|
func AssertTrue(t *testing.T, value bool) {
|
||||||
if !value {
|
if !value {
|
||||||
t.Error("value should be true")
|
t.Error("value should be true\n" + string(debug.Stack()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AssertFalse(t *testing.T, value bool) {
|
func AssertFalse(t *testing.T, value bool) {
|
||||||
if value {
|
if value {
|
||||||
t.Error("value should be false")
|
t.Error("value should be false\n" + string(debug.Stack()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertNoErr(t *testing.T, anerr error) {
|
||||||
|
if anerr != nil {
|
||||||
|
t.Error("Function returned an error: " + anerr.Error() + "\n" + string(debug.Stack()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ package wmo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.mongodb.org/mongo-driver/mongo/options"
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
)
|
)
|
||||||
@@ -36,3 +37,20 @@ func (c *Coll[TData]) AggregateOneOpt(ctx context.Context, pipeline mongo.Pipeli
|
|||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Coll[TData]) AggregateOne(ctx context.Context, pipeline mongo.Pipeline, opts ...*options.AggregateOptions) (TData, error) {
|
||||||
|
cursor, err := c.coll.Aggregate(ctx, pipeline, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return *new(TData), err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cursor.Next(ctx) {
|
||||||
|
v, err := c.decodeSingle(ctx, cursor)
|
||||||
|
if err != nil {
|
||||||
|
return *new(TData), err
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return *new(TData), errors.New("no document in result")
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user