1
\$\begingroup\$

I am writing a function to decode Argon2 hashed string to extract parameters. I need to handle many decode errors, and handle those errors lead to write many repeating if err == nil check code. Is there a better way to do this kind of check?

The encodedHash string follows the format, separated by $

"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s"

so an example string looks like

$argon2id$v=19$m=65536,t=3,p=4$ruDgwQK24h0wGXI87+lVWAbHmgNidUNPVSTdSloOlfM$py20HR7L4K6LllGsZNDbkrbh89x2tIF8JCIG0DAaoi8

What the decode function do is convert the string into this struct

type Argon2Params struct {
 Memory uint32
 Time uint32
 Threads uint8
 SaltLength uint32
 KeyLength uint32
}

Here is the decode function, I need to do a serious check during the decoding process. Every filed need to be checked to make sure no error rises.

func decodeHash(encodedHash string) (p *Params, salt, hash []byte, err error) {
 fileds := strings.Split(encodedHash, "$")
 if len(fileds) != 6 {
 return nil, nil, nil, errors.New("The encoded hash is not in the correct format")
 }
 var version int
// error check
 if _, err = fmt.Sscanf(fileds[2], "v=%d", &version); err != nil {
 return
 }
// error check
 if version != argon2.Version {
 return nil, nil, nil, errors.New("Argon2 version not match")
 }
 p = &Params{}
 _, err = fmt.Sscanf(fileds[3], "m=%d,t=%d,p=%d", &p.memory, &p.time, &p.threads)
// error check
 if err != nil {
 return
 }
 salt, p.saltLength, err = decodeBase64WithLength(fileds[4])
// error check
 if err != nil {
 return
 }
// error check
 hash, p.keyLength, err = decodeBase64WithLength(fileds[5])
 return
}
// a helper function
func decodeBase64WithLength(encodeString string) (str []byte, length uint32, err error) {
 str, err = base64.RawStdEncoding.DecodeString(encodeString)
 length = uint32(len(str))
 return
}
asked Apr 28, 2019 at 14:14
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Because you use named parameters, the error checking can indeed be simplified.

While you critique the use of if err != nil, I enjoy the consistent error handling, and prefer it to the concepts used in many other languages.

Returning errors

Rather than explicitly returning values, such as

return nil, nil, nil, errors.New("...")

You can instead set the named return value err:

if len(fields) != 6 {
 err = errors.New("incorrect hash format")
 return
}

This does not set the other values to nil, and that shouldn't matter. In terms of consuming this API, it would be bad practice to use any other return values if err is not nil. It would be the equivalent in C of using the return value of strtoul() without checking errno.

Having the comment // error check each time is too verbose.

Misspelling

The variable fileds is misspelled, it should be fields.

Helper function

Using named return values in your helper function is superfluous. You can easily return those variables directly.

func decodeBase64WithLength(encStr string) ([]byte, uint32, error) {
 s, err := base64.RawStdEncoding.DecodeString(encStr)
 return s, uint32(len(s)), err
}

But now that the function has been cleaned up, it's easy to see that it really isn't needed. If this is the only place you use the function, then doing something small twice doesn't justify a helper function in my opinion.

Params

You reference Argon2Params as Params, and both are exported types, but you refer to its members as unexported types. I'll assume it's exported, and capitalize the member variables.

Conclusion

Here is the code I ended up with:

package main
import (
 "encoding/base64"
 "errors"
 "fmt"
 "strings"
 "golang.org/x/crypto/argon2"
)
// Argon2Params are ...
type Argon2Params struct {
 Memory uint32
 Time uint32
 Threads uint8
 SaltLength uint32
 KeyLength uint32
}
func decodeHash(encodedHash string) (p *Argon2Params, salt, hash []byte, err error) {
 fields := strings.Split(encodedHash, "$")
 if len(fields) != 6 {
 err = errors.New("incorrect hash format")
 return
 }
 var version int
 if _, err = fmt.Sscanf(fields[2], "v=%d", &version); err != nil {
 return
 }
 if version != argon2.Version {
 err = errors.New("argon2 version mismatch")
 return
 }
 p = &Argon2Params{}
 if _, err = fmt.Sscanf(fields[3], "m=%d,t=%d,p=%d", &p.Memory, &p.Time,
 &p.Threads); err != nil {
 return
 }
 salt, err = base64.RawStdEncoding.DecodeString(fields[4])
 p.SaltLength = uint32(len(salt))
 if err != nil {
 return
 }
 hash, err = base64.RawStdEncoding.DecodeString(fields[5])
 p.KeyLength = uint32(len(hash))
 return
}
func main() {
 decodeHash("$argon2id$v=19$m=65536,t=3,p=4$ruDgwQK24h0wGXI87+lVWAbHmgNidUNPVSTdSloOlfM$py20HR7L4K6LllGsZNDbkrbh89x2tIF8JCIG0DAaoi8")
}

Hope this helps!

answered May 1, 2019 at 5:02
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Wow! such a detailed answer! Yes the Params is same as Argon2Params . \$\endgroup\$ Commented May 1, 2019 at 5:56

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.