Adding local argon2 hashing

Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
This commit is contained in:
Ethan Wellenreiter 2025-04-18 02:17:24 -04:00
parent aa1b8d1d1b
commit 3a72e93cc8
4 changed files with 129 additions and 76 deletions

View File

@ -1,3 +1,8 @@
module git.ewellenr.ca/receipt_indexer/backend
go 1.24.2
require (
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/sys v0.32.0 // indirect
)

4
backend/go.sum Normal file
View File

@ -0,0 +1,4 @@
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

View File

@ -0,0 +1,80 @@
package lcrypto
import (
"encoding/base64"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
var (
ErrInvalidHash = errors.New("the encoded hash is not in the correct format")
ErrIncompatibleVersion = errors.New("incompatible argon version")
ErrWrongHashAlgo = errors.New("wrong hash algorithm")
algorithmName = "argon2id"
)
type Argon2id struct {
Memory uint32
Iterations uint32
Parallelism uint8
}
func (a *Argon2id) encodeHashString(hash []byte, salt []byte) (string, error) {
// Base64 encode the salt and hashed password.
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
// Return a string using the standard encoded hash representation.
encoding := fmt.Sprintf("$%s$v=%d$m=%d,t=%d,p=%d$%s$%s", algorithmName, argon2.Version, a.Memory, a.Iterations, a.Parallelism, b64Salt, b64Hash)
return encoding, nil
}
func (a *Argon2id) decodeHashString(encodedString string) (algo Argon2id, hash []byte, salt []byte, err error) {
vals := strings.Split(encodedString, "$")
if len(vals) != 6 {
return algo, nil, nil, ErrInvalidHash
}
var name string
if _, err = fmt.Sscanf(vals[1], "%s", &name); err != nil {
return algo, nil, nil, err
}
if name != algorithmName {
return algo, nil, nil, ErrWrongHashAlgo
}
var version int
_, err = fmt.Sscanf(vals[2], "v=%d", &version)
if err != nil {
return algo, nil, nil, err
}
if version != argon2.Version {
return algo, nil, nil, ErrIncompatibleVersion
}
_, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &algo.Memory, &algo.Iterations, &algo.Parallelism)
if err != nil {
return algo, nil, nil, err
}
salt, err = base64.RawStdEncoding.Strict().DecodeString(vals[4])
if err != nil {
return algo, nil, nil, err
}
hash, err = base64.RawStdEncoding.Strict().DecodeString(vals[5])
if err != nil {
return algo, nil, nil, err
}
return algo, salt, hash, nil
}
func (a *Argon2id) hashBytes(pass string, salt []byte, keyLen uint) ([]byte, error) {
hash := argon2.IDKey([]byte(pass), salt, a.Iterations, a.Memory, a.Parallelism, uint32(keyLen))
return hash, nil
}

View File

@ -3,31 +3,49 @@ package lcrypto
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
var (
ErrInvalidHash = errors.New("the encoded hash is not in the correct format")
ErrIncompatibleVersion = errors.New("incompatible hash version")
ErrWrongHashAlgo = errors.New("wrong hash algorithm")
)
type Hasher interface {
CheckPass(pass string, encoded string) (bool, error)
hash(pass string) ([]byte, error)
Hash(password string) (string, error)
}
type HashAlgo interface {
name() string
version() uint
serialize() ([]byte, error)
deserialize(serialization []byte) error
encodeHashString(hash []byte, salt []byte) (string, error)
decodeHashString(encodedString string) (algo HashAlgo, hash []byte, salt []byte, err error)
hashBytes(pass string, salt []byte, keyLength uint) ([]byte, error)
// hashString(pass string) (string, error)
}
type Hasher struct {
Algo HashAlgo
KeyLength uint
SaltLength uint
}
func (h *Hasher) CheckPass(pass string, encoded string) (bool, error) {
algo, oghash, salt, err := h.Algo.decodeHashString(encoded)
if err != nil {
return false, err
}
newhash, err := algo.hashBytes(pass, salt, h.KeyLength)
if err != nil {
return false, err
}
equal, err := compareHash(oghash, newhash)
return equal, err
}
func (h *Hasher) Hash(password string) (string, error) {
salt, err := GenerateRandomBytes(h.SaltLength)
if err != nil {
return "", err
}
hash, err := h.Algo.hashBytes(password, salt, h.KeyLength)
if err != nil {
return "", err
}
return h.Algo.encodeHashString(hash, salt)
}
func compareHash(h1 []byte, h2 []byte) (bool, error) {
@ -49,57 +67,3 @@ func GenerateRandomBytes(saltLen uint) ([]byte, error) {
return bytes, nil
}
func encodeHashString(hashalgo string, version uint, hash []byte, salt []byte, params HashAlgo) (string, error) {
// Base64 encode the salt and hashed password.
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
paramEncode, err := params.serialize()
if err != nil {
return "", err
}
b64Params := base64.RawStdEncoding.EncodeToString(paramEncode)
encoding := fmt.Sprintf("$%s$v=%d$p=%s$%s$%s", hashalgo, version, b64Params, b64Salt, b64Hash)
return encoding, nil
}
func decodeHashString(encodedString string) (encodedHash string, hash []byte, salt []byte, err error) {
vals := strings.Split(encodedString, "$")
if len(vals) != 5 {
return "", nil, nil, ErrInvalidHash
}
var version int
_, err = fmt.Sscanf(vals[2], "v=%d", &version)
if err != nil {
return "", nil, nil, err
}
if version != argon2.Version {
return nil, nil, nil, ErrIncompatibleVersion
}
p = &Argon2params{}
_, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &p.Memory, &p.Iterations, &p.Parallelism)
if err != nil {
return nil, nil, nil, err
}
salt, err = base64.RawStdEncoding.Strict().DecodeString(vals[4])
if err != nil {
return "", nil, nil, err
}
p.SaltLength = uint32(len(salt))
hash, err = base64.RawStdEncoding.Strict().DecodeString(vals[5])
if err != nil {
return "", nil, nil, err
}
p.KeyLength = uint32(len(hash))
return p, salt, hash, nil
return "", nil, nil, nil
}