Skip to content

Commit

Permalink
Improved test coverage and error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
dwin committed Jun 6, 2018
1 parent e6fcf68 commit e4df73c
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 54 deletions.
4 changes: 2 additions & 2 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ var (
// ErrDecodingSalt indicates there was an issue converting the expected base64 salt to bytes
ErrDecodingSalt = errors.New("Unable to decode salt base64 to byte")

// ErrDecodingHash indicates there was an issue converting the expected base64 hash digest to bytes
ErrDecodingHash = errors.New("Unable to decode passhash base64 to byte")
// ErrDecodingDigest indicates there was an issue converting the expected base64 hash digest to bytes
ErrDecodingDigest = errors.New("Unable to decode passhash digest base64 to byte")

// ErrParseTime indicates there was an issue parsing the time parameter from the hash
// input string, possibly was not expected integer value
Expand Down
97 changes: 59 additions & 38 deletions password.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"encoding/base64"
"fmt"
"io"
"log"
"regexp"
"strconv"
"strings"
Expand Down Expand Up @@ -75,10 +76,7 @@ func Hash(pass string, customParams ...ArgonParams) (string, error) {
}

// Generate random salt
salt, err := generateSalt(params.SaltSize)
if err != nil {
return "", err
}
salt := generateSalt(params.SaltSize)

// Generate hash
passHash, err := generateHash([]byte(pass), salt, params)
Expand All @@ -95,43 +93,25 @@ func Hash(pass string, customParams ...ArgonParams) (string, error) {
// $argon2{function(i or id)}$v={version}$m={memory},t={time},p={parallelism}${salt(base64)}${digest(base64)}
// example: $argon2id$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
hashOut := fmt.Sprintf("$%s$v=%v$m=%v,t=%v,p=%v$%s$%s", params.Function, currentVersion, params.Memory, params.Time, params.Parallelism, encodedSalt, encodedHash)
// Check valid output
if err := checkHashFormat(hashOut); err != nil {
return "", fmt.Errorf("Hash output failed validation check parameters if custom, error: %s", err)
}

return hashOut, nil
}

// Verify regenerates the hash using the supplied pass and compares the value returning an error if the password
// is invalid or another error occurs. Any error should be considered a validation failure.
func Verify(pass, hash string) error {
// Check valid input
if err := checkHashFormat(hash); err != nil {
return err
}

// Split hash into parts
part := strings.Split(hash, "$")

// Get Parameters
hashParams, err := parseParams(part[3])
hashParams, err := GetParams(hash)
if err != nil {
return err
}

// Check hash function
switch part[1] {
case argon2i:
hashParams.Function = argon2i
case argon2id:
hashParams.Function = argon2id
}
// Split hash into parts
part := strings.Split(hash, "$")

// Get & Check Version
hashVersion, err := strconv.Atoi(strings.Trim(part[2], "v="))
if err != nil {
return ErrVersion
}
hashVersion, _ := strconv.Atoi(strings.Trim(part[2], "v="))

// Verify version is not greater than current version or less than 0
if hashVersion > currentVersion || hashVersion < 0 {
return ErrVersion
Expand All @@ -146,11 +126,11 @@ func Verify(pass, hash string) error {
// Get argon digest
decodedHash, err := base64.StdEncoding.DecodeString(part[5])
if err != nil {
return ErrDecodingHash
return ErrDecodingDigest
}

// Get size of existing hash
hashParams.OutputSize = uint32(len(decodedHash))
//hashParams.OutputSize = uint32(len(decodedHash))

// Generate hash for comparison using user input with stored parameters
comparisonHash, err := generateHash([]byte(pass), salt, hashParams)
Expand All @@ -171,23 +151,66 @@ func Verify(pass, hash string) error {

}

// GetParams takes hash sting as input and returns parameters as ArgonParams along with error
func GetParams(hash string) (hashParams ArgonParams, err error) {
// Check valid input
if err = checkHashFormat(hash); err != nil {
return
}

// Split hash into parts
part := strings.Split(hash, "$")

// Get Parameters
hashParams, err = parseParams(part[3])
if err != nil {
return
}

// Check hash function
switch part[1] {
case argon2i:
hashParams.Function = argon2i
case argon2id:
hashParams.Function = argon2id
}

// Get salt size
salt, err := base64.StdEncoding.DecodeString(part[4])
if err != nil {
return hashParams, ErrDecodingSalt
}
hashParams.SaltSize = uint8(len(salt))

// Get argon digest size
decodedHash, err := base64.StdEncoding.DecodeString(part[5])
if err != nil {
return hashParams, ErrDecodingDigest
}
hashParams.OutputSize = uint32(len(decodedHash))

return
}

// checkHashFormat uses regex to validate hash string pattern and returns error
func checkHashFormat(hash string) error {
// Check valid input
valid := regexp.MustCompile(`[$]argon2(?:id|i)[$]v=\d\d[$]m=\d{3,12},t=\d{1,4},p=\d{1,2}[$][^$]{1,100}[$][^$]{1,768}`)
valid := regexp.MustCompile(`[$]argon2(?:id|i)[$]v=\d{1,3}[$]m=\d{3,20},t=\d{1,4},p=\d{1,2}[$][^$]{1,100}[$][^$]{1,768}`)
if !valid.MatchString(hash) {
return ErrInvalidHashFormat
}
return nil
}

// generateSalt uses int input to return a random a salt for use in crypto operations
func generateSalt(saltLen uint8) ([]byte, error) {
func generateSalt(saltLen uint8) []byte {
salt := make([]byte, saltLen)
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
return salt, fmt.Errorf("Unable to generate random salt needed for crypto operations, error: %s", err)
fmt.Printf("Unable to generate random salt needed for crypto operations, error: %s\n", err)
log.Printf("Unable to generate random salt needed for crypto operations, error: %s\n", err)
return nil
}
return salt, nil
return salt
}

// generateHash takes passphrase and salt as bytes with parameters to provide Argon2 digest output
Expand Down Expand Up @@ -254,9 +277,7 @@ func checkParams(params ArgonParams) ArgonParams {
if params.Parallelism > maxParallelism {
params.Parallelism = maxParallelism
}
if params.Function != argon2i && params.Function != argon2id {
params.Function = argon2id
}

return params
}

Expand All @@ -265,7 +286,7 @@ func Benchmark(params ArgonParams) (elapsed float64, err error) {
pass := "benchmarkpass"
start := time.Now()

salt, err := generateSalt(params.SaltSize)
salt := generateSalt(params.SaltSize)
_, err = generateHash([]byte(pass), salt, params)

t := time.Now()
Expand Down
78 changes: 64 additions & 14 deletions password_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ func TestHash(t *testing.T) {
assert.EqualError(t, err, ErrCustomParameters.Error())

// Test below min custom params
out, err := Hash("password", ArgonParams{})
out, err := Hash("password", ArgonParams{Function: argon2i})
assert.NoError(t, err)
assert.Contains(t, out, "$argon2id$v=19$m=1024,t=1,p=1")
assert.Contains(t, out, "$argon2i$v=19$m=1024,t=1,p=1")

// Test above max params, should be forced to max
out, err = Hash("password", ArgonParams{SaltSize: 100, OutputSize: 600})
out, err = Hash("password", ArgonParams{SaltSize: 100, OutputSize: 600, Function: argon2i})
assert.NoError(t, err)
if err != nil {
t.FailNow()
Expand All @@ -63,6 +63,11 @@ func TestHash(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, decodedHash, maxOutputSize)

// Test invalid function choice
hash, err := Hash("password", ArgonParams{Time: 1, Memory: 16 * 1024, Parallelism: 4, OutputSize: 32, Function: "argon2b"})
assert.EqualError(t, err, ErrFunctionMismatch.Error())
assert.Empty(t, hash)

fmt.Println(" - " + t.Name() + " complete - ")
}

Expand All @@ -88,37 +93,79 @@ func TestVerify(t *testing.T) {
assert.EqualError(t, err, ErrHashMismatch.Error())
}

// Test Verify with bad salt
err := Verify("password", "$argon2i$v=19$m=65536,t=5,p=4$=$m6zc3AIQbGZOSv3grFtlquTUXXKdyfmCvrmKJ4cQf7E=")
assert.EqualError(t, err, ErrDecodingSalt.Error())
assert.NotNil(t, err)

// Test Verify with bad digest
err = Verify("password", "$argon2i$v=19$m=65536,t=5,p=4$MrcQyTq/if2OH2G5+YPKig==$=")
assert.EqualError(t, err, ErrDecodingDigest.Error())
assert.NotNil(t, err)

// Test Verify with bad hash string input
err := Verify("password", "$argon2id$v=19$m=65536,t=10$p=4$wusfaUEXfbhsz9R3+PI9nQ==$54an1yiYbCEfTtUzE0Lb536IcyP5CGpEvsO1agp2aZQ=")
err = Verify("password", "$argon2id$v=19$m=65536,t=10$p=4$wusfaUEXfbhsz9R3+PI9nQ==$54an1yiYbCEfTtUzE0Lb536IcyP5CGpEvsO1agp2aZQ=")
assert.EqualError(t, err, ErrInvalidHashFormat.Error())
assert.NotNil(t, err)

// Test Verify with Invalid hash function
err = Verify("password", "$argon2bi$v=19$m=65536,t=10,p=4$wusfaUEXfbhsz9R3+PI9nQ==$54an1yiYbCEfTtUzE0Lb536IcyP5CGpEvsO1agp2aZQ=")
assert.EqualError(t, err, ErrInvalidHashFormat.Error())
assert.NotNil(t, err)

// Test Verify with Invalid version
err = Verify("password", "$argon2i$v=99$m=65536,t=10,p=4$wusfaUEXfbhsz9R3+PI9nQ==$54an1yiYbCEfTtUzE0Lb536IcyP5CGpEvsO1agp2aZQ=")
assert.EqualError(t, err, ErrVersion.Error())
assert.NotNil(t, err)

// Test Verify with malformed/invalid salt
err = Verify("password", "$argon2i$v=19$m=65536,t=10,p=4$wusfaUEXf@hsz9R3+PI9nQ==$54an1yiYbCEfTtUzE0Lb536IcyP5CGpEvsO1agp2aZQ=")
assert.EqualError(t, err, ErrDecodingSalt.Error())
assert.NotNil(t, err)

// Test Verify with malformed/invalid salt
err = Verify("password", "$argon2i$v=19$m=65536,t=5,p=4$MrcQyTq/if?OH2G5+YPKig==$m6zc3AIQbGZOSv3grFtlquTUXXKdyfmCvrmKJ4cQf7E=")
assert.EqualError(t, err, ErrDecodingSalt.Error())
assert.NotNil(t, err)

// Test Verify with malformed/invalid digest
err = Verify("password", "$argon2i$v=19$m=65536,t=10,p=4$wusfaUEXfbhsz9R3+PI9nQ==$54an1yiYbCEfTtUzE0Lb53#IcyP5CGpEvsO1agp2aZQ=")
assert.EqualError(t, err, ErrDecodingHash.Error())
assert.EqualError(t, err, ErrDecodingDigest.Error())
assert.NotNil(t, err)

// Test Verify with malformed/invalid digest
err = Verify("password", "$argon2i$v=19$m=65536,t=5,p=4$MrcQyTq/ifOH2G5+YPKig==$m6zc3AIQbGZOSv3grFtlquTUX*XKdyfmCvrmKJ4cQf7E=")
assert.EqualError(t, err, ErrDecodingSalt.Error())
assert.NotNil(t, err)

fmt.Println(" - " + t.Name() + " complete - ")
}

func TestGetParams(t *testing.T) {
// Test GetParams using testdata hashes
for _, hash := range testdata {
params, err := GetParams(hash)
assert.NoError(t, err)
assert.NotZero(t, params.Memory)
assert.NotZero(t, params.Parallelism)
assert.NotZero(t, params.Time)
assert.NotZero(t, params.OutputSize)
assert.NotZero(t, params.SaltSize)
}

fmt.Println(" - " + t.Name() + " complete - ")
}
func TestCheckParams(t *testing.T) {
params := checkParams(ArgonParams{SaltSize: 100, OutputSize: 600})
assert.EqualValues(t, maxSaltSize, params.SaltSize)
assert.EqualValues(t, maxOutputSize, params.OutputSize)
assert.EqualValues(t, minMemory, params.Memory)
assert.EqualValues(t, minTime, params.Time)
assert.EqualValues(t, argon2id, params.Function)
assert.EqualValues(t, minParallelism, params.Parallelism)
assert.Empty(t, params.Function)
// Check Max Parameters
params = checkParams(ArgonParams{Parallelism: 100})
assert.EqualValues(t, maxParallelism, params.Parallelism)
}
func TestCheckHashFormat(t *testing.T) {
// Check bad hash format
Expand All @@ -139,22 +186,25 @@ func TestGenerateHash(t *testing.T) {
assert.EqualValues(t, "+iExTQDCJnO4fErO61zMAeC24R3utWMk8tW85saXOBU=", base64.StdEncoding.EncodeToString(out))

// Test invalid function choice
_, err = generateHash(testpass, salt, ArgonParams{Time: 12, Memory: 64 * 1024, Parallelism: 4, OutputSize: 32, Function: "argon2b"})
hash, err := generateHash(testpass, salt, ArgonParams{Time: 1, Memory: 16 * 1024, Parallelism: 4, OutputSize: 32, Function: "argon2b"})
assert.EqualError(t, err, ErrFunctionMismatch.Error())
assert.Empty(t, hash)

fmt.Println(" - " + t.Name() + " complete - ")
}

func TestGenerateSalt(t *testing.T) {
expectedLen := 20
salt, err := generateSalt(uint8(expectedLen))
assert.NoError(t, err)
assert.Len(t, salt, expectedLen)
// Generate random salts from minSaltSize to maxSaltSize
for i := minSaltSize; i < maxSaltSize; i++ {
expectedLen := i
salt := generateSalt(uint8(expectedLen))
assert.Len(t, salt, expectedLen)
}
}

func TestHashAndVerify(t *testing.T) {
// Hash & Verify various lengths from 8 chars up to 256 chars with default params
for i := 8; i < 256; i *= 3 {
for i := 8; i < 256; i *= 8 {
pass := randSeq(i)
out, err := Hash(pass)
assert.NoError(t, err)
Expand All @@ -164,7 +214,7 @@ func TestHashAndVerify(t *testing.T) {
}

// Hash & Verify with Custom Params
for i := 8; i < 256; i *= 3 {
for i := 8; i < 256; i *= 8 {
pass := randSeq(i)
out, err := Hash(pass, ArgonParams{Time: 12, Memory: 64 * 1024, Parallelism: 4, OutputSize: 32, Function: "argon2id"})
assert.NoError(t, err)
Expand Down Expand Up @@ -199,7 +249,7 @@ func TestParseParams(t *testing.T) {
func TestBenchmark(t *testing.T) {
var count int
var totalDuration float64
for totalDuration < 5 {
for totalDuration < 3 {
singleDuration, err := Benchmark(defaultParams)
assert.NoError(t, err)
totalDuration += singleDuration
Expand Down

0 comments on commit e4df73c

Please sign in to comment.