From 35e328b9c7ecb6dc565ed2003f63eec6773c6321 Mon Sep 17 00:00:00 2001 From: JPaja Date: Mon, 1 Dec 2025 01:16:19 +0100 Subject: [PATCH 1/6] Add: argon2id --- crypto.go | 33 +++++++++++ crypto_test.go | 148 +++++++++++++++++++++++++++++++++++++++++++++++++ docs/crypto.md | 15 +++++ functions.go | 1 + go.mod | 5 +- go.sum | 4 ++ 6 files changed, 204 insertions(+), 2 deletions(-) diff --git a/crypto.go b/crypto.go index b63623e8..33398374 100644 --- a/crypto.go +++ b/crypto.go @@ -33,6 +33,7 @@ import ( "strings" "github.com/google/uuid" + argon2_lib "golang.org/x/crypto/argon2" bcrypt_lib "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/scrypt" ) @@ -66,6 +67,38 @@ func bcrypt(input string) string { return string(hash) } +func argon2id(input string, time uint32, memory uint32, parallelism uint8, saltLen uint32, hashLen uint32) string { + // RFC 9106 + // SECOND RECOMMENDED option and is suggested as a default setting for memory-constrained environments. + // + if time == 0 { + time = 3 + } + if memory == 0 { + memory = 64 * 1024 + } + if parallelism == 0 { + parallelism = 1 + } + if saltLen == 0 { + saltLen = 16 + } + if hashLen == 0 { + hashLen = 32 + } + + salt := make([]byte, saltLen) + if _, err := rand.Read(salt); err != nil { + return fmt.Sprintf("failed to generate argon2 salt with len %d: %s", saltLen, err) + } + + key := argon2_lib.IDKey([]byte(input), salt, time, memory, parallelism, hashLen) + saltKey := base64.RawStdEncoding.EncodeToString([]byte(salt)) + hashKey := base64.RawStdEncoding.EncodeToString(key) + format := "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s" + return fmt.Sprintf(format, argon2_lib.Version, memory, time, parallelism, saltKey, hashKey) +} + func hashSha(password string) string { s := sha1.New() s.Write([]byte(password)) diff --git a/crypto_test.go b/crypto_test.go index fc34ee0a..364b3623 100644 --- a/crypto_test.go +++ b/crypto_test.go @@ -1,14 +1,17 @@ package sprig import ( + "bytes" "crypto/x509" "encoding/base64" "encoding/pem" + "errors" "fmt" "strings" "testing" "github.com/stretchr/testify/assert" + argon2_lib "golang.org/x/crypto/argon2" bcrypt_lib "golang.org/x/crypto/bcrypt" ) @@ -64,6 +67,151 @@ func TestBcrypt(t *testing.T) { } } +type Argon2idParameters struct { + Password string + Time uint32 + Memory uint32 + Parallelism uint8 + SaltLen uint32 + HashLen uint32 +} + +func TestArgon2idParameters(t *testing.T) { + expectations := []Argon2idParameters{ + { + Password: "abc", + Time: 1, + Memory: 131072, + Parallelism: 1, + SaltLen: 16, + HashLen: 32, + }, + { + Password: "abcefgh", + Time: 1, + Memory: 262144, + Parallelism: 4, + SaltLen: 32, + HashLen: 64, + }, + { + Password: "abcd", + Time: 0, + Memory: 0, + Parallelism: 0, + SaltLen: 0, + HashLen: 0, + }, + } + + for i, param := range expectations { + hash, err := runRaw( + `{{argon2id .Password .Time .Memory .Parallelism .SaltLen .HashLen}}`, + param, + ) + if err != nil { + t.Errorf("Test %d: failed to render template: %s", i, err) + continue + } + + if argon2idVerify(hash, param) != nil { + t.Errorf("Test %d: Generated hash %s for password %s is not valid: %s", i, hash, param.Password, err) + } + } +} + +func argon2idVerify(encodedHash string, param Argon2idParameters) error { + hashParts := strings.Split(encodedHash, "$") + if len(hashParts) != 6 { + return errors.New("invalid hash format") + } + + if hashParts[1] != "argon2id" { + return fmt.Errorf("invalid variant: %s", hashParts[1]) + } + + var version int + if _, err := fmt.Sscanf(hashParts[2], "v=%d", &version); err != nil { + return fmt.Errorf("invalid version format: %s", err) + } + if version != argon2_lib.Version { + return fmt.Errorf("incompatible version: %d", version) + } + + memPart := strings.Split(hashParts[3], ",") + if len(memPart) != 3 { + return errors.New("invalid memory/time/parallelism format") + } + + var memory uint32 + if _, err := fmt.Sscanf(memPart[0], "m=%d", &memory); err != nil { + return fmt.Errorf("invalid memory value: %s", err) + } + + var timeVal uint32 + if _, err := fmt.Sscanf(memPart[1], "t=%d", &timeVal); err != nil { + return fmt.Errorf("invalid time value: %s", err) + } + + var parallelism uint8 + if _, err := fmt.Sscanf(memPart[2], "p=%d", ¶llelism); err != nil { + return fmt.Errorf("invalid parallelism value: %s", err) + } + + salt, err := base64.RawStdEncoding.DecodeString(hashParts[4]) + if err != nil { + return fmt.Errorf("failed to decode salt: %s", err) + } + expectedHash, err := base64.RawStdEncoding.DecodeString(hashParts[5]) + if err != nil { + return fmt.Errorf("failed to decode hash: %s", err) + } + + paramTime := param.Time + if paramTime == 0 { + paramTime = 3 + } + paramMemory := param.Memory + if paramMemory == 0 { + paramMemory = 64 * 1024 + } + paramParallelism := param.Parallelism + if paramParallelism == 0 { + paramParallelism = 1 + } + paramSaltLen := param.SaltLen + if paramSaltLen == 0 { + paramSaltLen = uint32(len(salt)) + } + paramHashLen := param.HashLen + if paramHashLen == 0 { + paramHashLen = uint32(len(expectedHash)) + } + + if memory != paramMemory { + return fmt.Errorf("memory mismatch: hash has %d, expected %d", memory, paramMemory) + } + if timeVal != paramTime { + return fmt.Errorf("time mismatch: hash has %d, expected %d", timeVal, paramTime) + } + if parallelism != paramParallelism { + return fmt.Errorf("parallelism mismatch: hash has %d, expected %d", parallelism, paramParallelism) + } + if uint32(len(salt)) != paramSaltLen { + return fmt.Errorf("salt length mismatch: hash has %d, expected %d", len(salt), paramSaltLen) + } + if uint32(len(expectedHash)) != paramHashLen { + return fmt.Errorf("hash length mismatch: hash has %d, expected %d", len(expectedHash), paramHashLen) + } + + computed := argon2_lib.IDKey([]byte(param.Password), salt, paramTime, paramMemory, paramParallelism, paramHashLen) + if !bytes.Equal(computed, expectedHash) { + return errors.New("password does not match hash") + } + + return nil +} + type HtpasswdCred struct { Username string Password string diff --git a/docs/crypto.md b/docs/crypto.md index 35dbaa9c..597c9dad 100644 --- a/docs/crypto.md +++ b/docs/crypto.md @@ -47,6 +47,21 @@ The `bcrypt` function receives a string, and generates its `bcrypt` hash. bcrypt "myPassword" ``` +## argon2id + +The `argon2id` function takes a `password`, a `time`, a `memory`, a `parallelism`, a `saltLen`, and a `hashLen` and generates a `argon2id' hash. +Default values follows second reccomended values by [RFC 9106](https://datatracker.ietf.org/doc/rfc9106/) (t=3 and 64 MiB memory) as a default setting for memory-constrained environments, parallelism is 1 saltLen is 16 and hashLen is 32. +Argon2 is the winner of [Password Hashing Competition](https://www.password-hashing.net/) 2015 and is current reccomended hashing alghoritm for passwords. + +``` +argon2id "myPassword" [time [memory [parallelism [saltLen [hashLen]]]]] +``` + +### RFC 9106 First reccomended format (t=1 and 2 GiB memory) using 4 cores +``` +argon2id "myPassword" 1 2097152 4 +``` + ## htpasswd The `htpasswd` function takes a `username`, a `password`, and a `hashAlgorithm` and generates a `bcrypt` (recommended) or a base64 encoded and prefixed `sha` hash of the password. `hashAlgorithm` is optional and defaults to `bcrypt`. The result can be used for basic authentication on an [Apache HTTP Server](https://httpd.apache.org/docs/2.4/misc/password_encryptions.html#basic). diff --git a/functions.go b/functions.go index cda47d26..fbee3ba3 100644 --- a/functions.go +++ b/functions.go @@ -340,6 +340,7 @@ var genericMap = map[string]interface{}{ // Crypto: "bcrypt": bcrypt, + "argon2id": argon2id, "htpasswd": htpasswd, "genPrivateKey": generatePrivateKey, "derivePassword": derivePassword, diff --git a/go.mod b/go.mod index 2fdb8b89..44f0492a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Masterminds/sprig/v3 -go 1.23.0 +go 1.24.0 toolchain go1.24.4 @@ -14,7 +14,7 @@ require ( github.com/shopspring/decimal v1.4.0 github.com/spf13/cast v1.9.2 github.com/stretchr/testify v1.10.0 - golang.org/x/crypto v0.40.0 + golang.org/x/crypto v0.45.0 ) require ( @@ -22,6 +22,7 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.38.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9333cec1..a0d9c589 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,10 @@ golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 0aeaa1d978d8bd3d416ac50d2fffedd5fd3b57ff Mon Sep 17 00:00:00 2001 From: JPaja Date: Mon, 1 Dec 2025 01:18:59 +0100 Subject: [PATCH 2/6] Fix typo --- docs/crypto.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/crypto.md b/docs/crypto.md index 597c9dad..4449fbe9 100644 --- a/docs/crypto.md +++ b/docs/crypto.md @@ -51,7 +51,7 @@ bcrypt "myPassword" The `argon2id` function takes a `password`, a `time`, a `memory`, a `parallelism`, a `saltLen`, and a `hashLen` and generates a `argon2id' hash. Default values follows second reccomended values by [RFC 9106](https://datatracker.ietf.org/doc/rfc9106/) (t=3 and 64 MiB memory) as a default setting for memory-constrained environments, parallelism is 1 saltLen is 16 and hashLen is 32. -Argon2 is the winner of [Password Hashing Competition](https://www.password-hashing.net/) 2015 and is current reccomended hashing alghoritm for passwords. +Argon2 is the winner of [Password Hashing Competition](https://www.password-hashing.net/) 2015 and is current recommended hashing algorithm for passwords. ``` argon2id "myPassword" [time [memory [parallelism [saltLen [hashLen]]]]] From e845bf0865036f84f0f529d49303f19db1fcdd5c Mon Sep 17 00:00:00 2001 From: JPaja Date: Mon, 1 Dec 2025 01:20:52 +0100 Subject: [PATCH 3/6] fix typo --- docs/crypto.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/crypto.md b/docs/crypto.md index 4449fbe9..3f9044a3 100644 --- a/docs/crypto.md +++ b/docs/crypto.md @@ -50,14 +50,14 @@ bcrypt "myPassword" ## argon2id The `argon2id` function takes a `password`, a `time`, a `memory`, a `parallelism`, a `saltLen`, and a `hashLen` and generates a `argon2id' hash. -Default values follows second reccomended values by [RFC 9106](https://datatracker.ietf.org/doc/rfc9106/) (t=3 and 64 MiB memory) as a default setting for memory-constrained environments, parallelism is 1 saltLen is 16 and hashLen is 32. +Default values follows second recommended values by [RFC 9106](https://datatracker.ietf.org/doc/rfc9106/) (t=3 and 64 MiB memory) as a default setting for memory-constrained environments, parallelism is 1 saltLen is 16 and hashLen is 32. Argon2 is the winner of [Password Hashing Competition](https://www.password-hashing.net/) 2015 and is current recommended hashing algorithm for passwords. ``` argon2id "myPassword" [time [memory [parallelism [saltLen [hashLen]]]]] ``` -### RFC 9106 First reccomended format (t=1 and 2 GiB memory) using 4 cores +### RFC 9106 First recommended format (t=1 and 2 GiB memory) using 4 cores ``` argon2id "myPassword" 1 2097152 4 ``` From 2967d67f2fbaae565b1e06fa7dc563224ab5ee3a Mon Sep 17 00:00:00 2001 From: JPaja Date: Mon, 1 Dec 2025 02:06:00 +0100 Subject: [PATCH 4/6] Fix default args --- crypto.go | 38 ++++++++++++++++++++++---------------- crypto_test.go | 25 ++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/crypto.go b/crypto.go index 33398374..a11661ea 100644 --- a/crypto.go +++ b/crypto.go @@ -67,25 +67,31 @@ func bcrypt(input string) string { return string(hash) } -func argon2id(input string, time uint32, memory uint32, parallelism uint8, saltLen uint32, hashLen uint32) string { +func argon2id(input string, params ...uint32) string { // RFC 9106 // SECOND RECOMMENDED option and is suggested as a default setting for memory-constrained environments. // - if time == 0 { - time = 3 - } - if memory == 0 { - memory = 64 * 1024 - } - if parallelism == 0 { - parallelism = 1 - } - if saltLen == 0 { - saltLen = 16 - } - if hashLen == 0 { - hashLen = 32 - } + time := uint32(3) + memory := uint32(64 * 1024) + parallelism := uint8(1) + saltLen := uint32(16) + hashLen := uint32(32) + + if len(params) > 0 && params[0] != 0 { + time = params[0] + } + if len(params) > 1 && params[1] != 0 { + memory = params[1] + } + if len(params) > 2 && params[2] != 0 { + parallelism = uint8(params[2]) + } + if len(params) > 3 && params[3] != 0 { + saltLen = params[3] + } + if len(params) > 4 && params[4] != 0 { + hashLen = params[4] + } salt := make([]byte, saltLen) if _, err := rand.Read(salt); err != nil { diff --git a/crypto_test.go b/crypto_test.go index 364b3623..b96eca8e 100644 --- a/crypto_test.go +++ b/crypto_test.go @@ -71,7 +71,7 @@ type Argon2idParameters struct { Password string Time uint32 Memory uint32 - Parallelism uint8 + Parallelism uint32 SaltLen uint32 HashLen uint32 } @@ -114,10 +114,29 @@ func TestArgon2idParameters(t *testing.T) { continue } - if argon2idVerify(hash, param) != nil { + if err := argon2idVerify(hash, param); err != nil { t.Errorf("Test %d: Generated hash %s for password %s is not valid: %s", i, hash, param.Password, err) } } + + hash, err := runRaw(`{{argon2id "testPassword"}}`, nil) + if err != nil { + t.Errorf("failed to render template: %s", err) + return + } + + defaultParam := Argon2idParameters{ + Password: "testPassword", + Time: 3, + Memory: 64 * 1024, + Parallelism: 1, + SaltLen: 16, + HashLen: 32, + } + + if err := argon2idVerify(hash, defaultParam); err != nil { + t.Errorf("Generated hash %s for password %s is not valid: %s", hash, defaultParam.Password, err) + } } func argon2idVerify(encodedHash string, param Argon2idParameters) error { @@ -175,7 +194,7 @@ func argon2idVerify(encodedHash string, param Argon2idParameters) error { if paramMemory == 0 { paramMemory = 64 * 1024 } - paramParallelism := param.Parallelism + paramParallelism := uint8(param.Parallelism) if paramParallelism == 0 { paramParallelism = 1 } From 5d2a75250fa567898e09b1eb37582a1f4e7ea05e Mon Sep 17 00:00:00 2001 From: JPaja Date: Mon, 1 Dec 2025 02:08:30 +0100 Subject: [PATCH 5/6] fix fmt --- crypto.go | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/crypto.go b/crypto.go index a11661ea..f9934ff6 100644 --- a/crypto.go +++ b/crypto.go @@ -71,27 +71,27 @@ func argon2id(input string, params ...uint32) string { // RFC 9106 // SECOND RECOMMENDED option and is suggested as a default setting for memory-constrained environments. // - time := uint32(3) - memory := uint32(64 * 1024) - parallelism := uint8(1) - saltLen := uint32(16) - hashLen := uint32(32) - - if len(params) > 0 && params[0] != 0 { - time = params[0] - } - if len(params) > 1 && params[1] != 0 { - memory = params[1] - } - if len(params) > 2 && params[2] != 0 { - parallelism = uint8(params[2]) - } - if len(params) > 3 && params[3] != 0 { - saltLen = params[3] - } - if len(params) > 4 && params[4] != 0 { - hashLen = params[4] - } + time := uint32(3) + memory := uint32(64 * 1024) + parallelism := uint8(1) + saltLen := uint32(16) + hashLen := uint32(32) + + if len(params) > 0 && params[0] != 0 { + time = params[0] + } + if len(params) > 1 && params[1] != 0 { + memory = params[1] + } + if len(params) > 2 && params[2] != 0 { + parallelism = uint8(params[2]) + } + if len(params) > 3 && params[3] != 0 { + saltLen = params[3] + } + if len(params) > 4 && params[4] != 0 { + hashLen = params[4] + } salt := make([]byte, saltLen) if _, err := rand.Read(salt); err != nil { From e349cd06db357578841e7e3d595f18d982e33a6a Mon Sep 17 00:00:00 2001 From: JPaja Date: Mon, 1 Dec 2025 02:16:45 +0100 Subject: [PATCH 6/6] More tests --- crypto_test.go | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/crypto_test.go b/crypto_test.go index b96eca8e..13e615ec 100644 --- a/crypto_test.go +++ b/crypto_test.go @@ -125,7 +125,7 @@ func TestArgon2idParameters(t *testing.T) { return } - defaultParam := Argon2idParameters{ + param := Argon2idParameters{ Password: "testPassword", Time: 3, Memory: 64 * 1024, @@ -134,8 +134,28 @@ func TestArgon2idParameters(t *testing.T) { HashLen: 32, } - if err := argon2idVerify(hash, defaultParam); err != nil { - t.Errorf("Generated hash %s for password %s is not valid: %s", hash, defaultParam.Password, err) + if err := argon2idVerify(hash, param); err != nil { + t.Errorf("Generated hash %s for password %s is not valid: %s", hash, param.Password, err) + return + } + + hash2, err := runRaw(`{{argon2id "myPassword" 1 2097152 4}}`, nil) + if err != nil { + t.Errorf("failed to render template: %s", err) + return + } + + param2 := Argon2idParameters{ + Password: "myPassword", + Time: 1, + Memory: 2097152, + Parallelism: 4, + SaltLen: 16, + HashLen: 32, + } + + if err := argon2idVerify(hash2, param2); err != nil { + t.Errorf("Generated hash %s for password %s is not valid: %s", hash2, param2.Password, err) } }