From 710c257c649bfc1f1141f1f5302d6a88aa7d66a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Tue, 18 Jul 2023 13:34:54 +0200 Subject: [PATCH] v0.0.171 --- cryptext/passHash.go | 80 +++++++++++++-- cryptext/passHash_test.go | 210 ++++++++++++++++++++++++++++++++++++++ goextVersion.go | 4 +- tst/assertions.go | 11 +- 4 files changed, 293 insertions(+), 12 deletions(-) create mode 100644 cryptext/passHash_test.go diff --git a/cryptext/passHash.go b/cryptext/passHash.go index 379b2b8..fdd0407 100644 --- a/cryptext/passHash.go +++ b/cryptext/passHash.go @@ -3,6 +3,7 @@ package cryptext import ( "crypto/rand" "crypto/sha256" + "crypto/sha512" "encoding/base64" "encoding/hex" "errors" @@ -14,14 +15,15 @@ import ( "strings" ) -const LatestPassHashVersion = 4 +const LatestPassHashVersion = 5 // PassHash -// - [v0]: plaintext password ( `0|...` ) -// - [v1]: sha256(plaintext) -// - [v2]: seed | sha256(plaintext) -// - [v3]: seed | sha256(plaintext) | [hex(totp)] -// - [v4]: bcrypt(plaintext) | [hex(totp)] +// - [v0]: plaintext password ( `0|...` ) // simple, used to write PW's directly in DB +// - [v1]: sha256(plaintext) // simple hashing +// - [v2]: seed | sha256(plaintext) // add seed +// - [v3]: seed | sha256(plaintext) | [hex(totp)] // add TOTP support +// - [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 func (ph PassHash) Valid() bool { @@ -109,7 +111,21 @@ func (ph PassHash) Data() (_version int, _seed []byte, _payload []byte, _totp bo totp := false totpsecret := make([]byte, 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 } 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 } @@ -209,6 +233,12 @@ func (ph PassHash) ClearTOTP() (PassHash, error) { 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") } @@ -242,6 +272,12 @@ func (ph PassHash) WithTOTP(totpSecret []byte) (PassHash, error) { 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") } @@ -271,6 +307,10 @@ func (ph PassHash) Change(newPlainPass string) (PassHash, error) { return HashPasswordV4(newPlainPass, langext.Conditional(hastotp, totpsecret, nil)) } + if version == 5 { + return HashPasswordV5(newPlainPass, langext.Conditional(hastotp, totpsecret, nil)) + } + return "", errors.New("unknown version") } @@ -279,7 +319,24 @@ func (ph PassHash) String() string { } 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) { @@ -340,6 +397,13 @@ func HashPasswordV0(plainpass string) (PassHash, error) { 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 { h := sha256.New() h.Write([]byte(s)) diff --git a/cryptext/passHash_test.go b/cryptext/passHash_test.go new file mode 100644 index 0000000..782583b --- /dev/null +++ b/cryptext/passHash_test.go @@ -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)) +} diff --git a/goextVersion.go b/goextVersion.go index 1e262c2..fece942 100644 --- a/goextVersion.go +++ b/goextVersion.go @@ -1,5 +1,5 @@ package goext -const GoextVersion = "0.0.170" +const GoextVersion = "0.0.171" -const GoextVersionTimestamp = "2023-07-17T12:42:49+0200" +const GoextVersionTimestamp = "2023-07-18T13:34:54+0200" diff --git a/tst/assertions.go b/tst/assertions.go index ef814bb..7df18e8 100644 --- a/tst/assertions.go +++ b/tst/assertions.go @@ -2,6 +2,7 @@ package tst import ( "encoding/hex" + "runtime/debug" "testing" ) @@ -54,12 +55,18 @@ func AssertHexEqual(t *testing.T, expected string, actual []byte) { func AssertTrue(t *testing.T, value bool) { 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) { 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())) } }