This commit is contained in:
Mike Schwörer 2022-12-23 10:11:01 +01:00
parent 2224db8e85
commit 7f88a0726c
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
4 changed files with 284 additions and 5 deletions

270
cryptext/passHash.go Normal file
View File

@ -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<seed>(plaintext)
// - [v3]: seed | sha256<seed>(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
}

9
go.mod
View File

@ -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
)

6
go.sum
View File

@ -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=

View File

@ -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)