diff --git a/cryptext/passHash.go b/cryptext/passHash.go new file mode 100644 index 0000000..be32661 --- /dev/null +++ b/cryptext/passHash.go @@ -0,0 +1,270 @@ +package cryptext + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "gogs.mikescher.com/BlackForestBytes/goext/totpext" + "golang.org/x/crypto/bcrypt" + "strconv" + "strings" +) + +const LatestPassHashVersion = 4 + +// PassHash +// - [v0]: plaintext password ( `0|...` ) +// - [v1]: sha256(plaintext) +// - [v2]: seed | sha256(plaintext) +// - [v3]: seed | sha256(plaintext) | [hex(totp)] +// - [v4]: bcrypt(plaintext) | [hex(totp)] +type PassHash string + +func (ph PassHash) Valid() bool { + _, _, _, _, _, valid := ph.Data() + return valid +} + +func (ph PassHash) HasTOTP() bool { + _, _, _, otp, _, _ := ph.Data() + return otp +} + +func (ph PassHash) Data() (_version int, _seed []byte, _payload []byte, _totp bool, _totpsecret []byte, _valid bool) { + + split := strings.Split(string(ph), "|") + if len(split) == 0 { + return -1, nil, nil, false, nil, false + } + + version, err := strconv.ParseInt(split[0], 10, 32) + if err != nil { + return -1, nil, nil, false, nil, false + } + + if version == 0 { + if len(split) != 2 { + return -1, nil, nil, false, nil, false + } + return int(version), nil, []byte(split[1]), false, nil, true + } + + if version == 1 { + if len(split) != 2 { + return -1, nil, nil, false, nil, false + } + payload, err := base64.RawStdEncoding.DecodeString(split[1]) + if err != nil { + return -1, nil, nil, false, nil, false + } + return int(version), nil, payload, false, nil, true + } + + // + if version == 2 { + if len(split) != 3 { + return -1, nil, nil, false, nil, false + } + seed, err := base64.RawStdEncoding.DecodeString(split[1]) + if err != nil { + return -1, nil, nil, false, nil, false + } + payload, err := base64.RawStdEncoding.DecodeString(split[2]) + if err != nil { + return -1, nil, nil, false, nil, false + } + return int(version), seed, payload, false, nil, true + } + + if version == 3 { + if len(split) != 4 { + return -1, nil, nil, false, nil, false + } + seed, err := base64.RawStdEncoding.DecodeString(split[1]) + if err != nil { + return -1, nil, nil, false, nil, false + } + payload, err := base64.RawStdEncoding.DecodeString(split[2]) + if err != nil { + return -1, nil, nil, false, nil, false + } + totp := false + totpsecret := make([]byte, 0) + if split[3] != "0" { + totpsecret, err = hex.DecodeString(split[3]) + totp = true + } + return int(version), seed, payload, totp, totpsecret, true + } + + if version == 4 { + 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[3]) + totp = true + } + return int(version), nil, payload, totp, totpsecret, true + } + + return -1, nil, nil, false, nil, false +} + +func (ph PassHash) Verify(plainpass string, totp *string) bool { + version, seed, payload, hastotp, totpsecret, valid := ph.Data() + if !valid { + return false + } + + if hastotp && totp == nil { + return false + } + + if version == 0 { + return langext.ArrEqualsExact([]byte(plainpass), payload) + } + + if version == 1 { + return langext.ArrEqualsExact(hash256(plainpass), payload) + } + + if version == 2 { + return langext.ArrEqualsExact(hash256Seeded(plainpass, seed), payload) + } + + if version == 3 { + if !hastotp { + return langext.ArrEqualsExact(hash256Seeded(plainpass, seed), payload) + } else { + return langext.ArrEqualsExact(hash256Seeded(plainpass, seed), payload) && totpext.Validate(totpsecret, *totp) + } + } + + if version == 4 { + if !hastotp { + return bcrypt.CompareHashAndPassword(payload, []byte(plainpass)) != nil + } else { + return bcrypt.CompareHashAndPassword(payload, []byte(plainpass)) != nil && totpext.Validate(totpsecret, *totp) + } + } + + return false +} + +func (ph PassHash) NeedsPasswordUpgrade() bool { + version, _, _, _, _, valid := ph.Data() + return valid && version < LatestPassHashVersion +} + +func (ph PassHash) Upgrade(plainpass string) (PassHash, error) { + version, _, _, hastotp, totpsecret, valid := ph.Data() + if !valid { + return "", errors.New("invalid password") + } + if version == LatestPassHashVersion { + return ph, nil + } + if hastotp { + return HashPassword(plainpass, totpsecret) + } else { + return HashPassword(plainpass, nil) + } +} + +func (ph PassHash) String() string { + return string(ph) +} + +func HashPassword(plainpass string, totpSecret []byte) (PassHash, error) { + return HashPasswordV4(plainpass, totpSecret) +} + +func HashPasswordV4(plainpass string, totpSecret []byte) (PassHash, error) { + var strtotp string + + if totpSecret == nil { + strtotp = "0" + } else { + strtotp = hex.EncodeToString(totpSecret) + } + + payload, err := bcrypt.GenerateFromPassword([]byte(plainpass), bcrypt.MinCost) + if err != nil { + return "", err + } + + return PassHash(fmt.Sprintf("4|%s|%s", string(payload), strtotp)), nil +} + +func HashPasswordV3(plainpass string, totpSecret []byte) (PassHash, error) { + var strtotp string + + if totpSecret == nil { + strtotp = "0" + } else { + strtotp = hex.EncodeToString(totpSecret) + } + + seed, err := newSeed() + if err != nil { + return "", err + } + + checksum := hash256Seeded(plainpass, seed) + + return PassHash(fmt.Sprintf("3|%s|%s|%s", + base64.RawStdEncoding.EncodeToString(seed), + base64.RawStdEncoding.EncodeToString(checksum), + strtotp)), nil +} + +func HashPasswordV2(plainpass string) (PassHash, error) { + seed, err := newSeed() + if err != nil { + return "", err + } + + checksum := hash256Seeded(plainpass, seed) + + return PassHash(fmt.Sprintf("2|%s|%s", base64.RawStdEncoding.EncodeToString(seed), base64.RawStdEncoding.EncodeToString(checksum))), nil +} + +func HashPasswordV1(plainpass string) (PassHash, error) { + return PassHash(fmt.Sprintf("1|%s", base64.RawStdEncoding.EncodeToString(hash256(plainpass)))), nil +} + +func HashPasswordV0(plainpass string) (PassHash, error) { + return PassHash(fmt.Sprintf("0|%s", plainpass)), nil +} + +func hash256(s string) []byte { + h := sha256.New() + h.Write([]byte(s)) + bs := h.Sum(nil) + return bs +} + +func hash256Seeded(s string, seed []byte) []byte { + h := sha256.New() + h.Write(seed) + h.Write([]byte(s)) + bs := h.Sum(nil) + return bs +} + +func newSeed() ([]byte, error) { + secret := make([]byte, 32) + _, err := rand.Read(secret) + if err != nil { + return nil, err + } + return secret, nil +} diff --git a/go.mod b/go.mod index 57df7ed..18a66a5 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,11 @@ module gogs.mikescher.com/BlackForestBytes/goext go 1.19 require ( - golang.org/x/sys v0.1.0 - golang.org/x/term v0.1.0 + golang.org/x/sys v0.3.0 + golang.org/x/term v0.3.0 ) -require github.com/jmoiron/sqlx v1.3.5 // indirect +require ( + github.com/jmoiron/sqlx v1.3.5 // indirect + golang.org/x/crypto v0.4.0 // indirect +) diff --git a/go.sum b/go.sum index 7fab807..894d7b9 100644 --- a/go.sum +++ b/go.sum @@ -3,7 +3,13 @@ github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= diff --git a/sq/structscanner.go b/sq/structscanner.go index b28808c..f834692 100644 --- a/sq/structscanner.go +++ b/sq/structscanner.go @@ -51,7 +51,7 @@ func (r *StructScanner) Start(dest any) error { return nil } -// StructScan forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go +// StructScanExt forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go // does also wok with nullabel structs (from LEFT JOIN's) func (r *StructScanner) StructScanExt(dest any) error { v := reflect.ValueOf(dest) @@ -148,7 +148,7 @@ func (r *StructScanner) StructScanExt(dest any) error { return r.rows.Err() } -// StructScan forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go +// StructScanBase forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go // without (relevant) changes func (r *StructScanner) StructScanBase(dest any) error { v := reflect.ValueOf(dest)