package main import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "strings" "golang.org/x/crypto/argon2" ) const ( // Hash formats used by the database hashFmtPlaintext int = 0 hashFmtArgon2i_v1 int = 1 hashFmtPreferred = hashFmtArgon2i_v1 // Constant parameters for our v1 version of this argon2_time = 1 argon2_memory = 16 * 1024 // 16 MiB argon2_want_output_bytes = 32 // 256-bit ) // verifyPassword checks if a provided plaintext password is valid for a known // hashed password. // If the verification is successful, but the used format is not the current // preferred format, you may want to re-hash the password with the current // preferred format. func verifyPassword(format int, hashValue, testValue string) (bool, error) { switch format { case hashFmtPlaintext: return subtle.ConstantTimeCompare([]byte(hashValue), []byte(testValue)) == 1, nil case hashFmtArgon2i_v1: // base64(salt) $ base64(hash) tParts := strings.SplitN(hashValue, `$`, 2) if len(tParts) != 2 { return false, fmt.Errorf("malformed hash value (expected 2 segments, got %d)", len(tParts)) } salt, err := base64.StdEncoding.DecodeString(tParts[0]) if err != nil { return false, errors.New(`malformed hash value (malformed base64 of salt)`) } existingHashBytes, err := base64.StdEncoding.DecodeString(tParts[1]) if err != nil { return false, errors.New(`malformed hash value (malformed base64 of hash)`) } newHash := argon2.Key([]byte(testValue), salt, argon2_time, argon2_memory, 1, argon2_want_output_bytes) return subtle.ConstantTimeCompare(existingHashBytes, newHash) == 1, nil default: return false, fmt.Errorf("unrecognised password hash format %d", format) } } // hashPassword converts the provided plaintext password into a hash format. // It is recommended to pass in `hashFmtPreferred` as the format. func hashPassword(format int, newValue string) (string, error) { switch format { case hashFmtPlaintext: return newValue, nil case hashFmtArgon2i_v1: salt := make([]byte, 32) n, err := rand.Read(salt) if err != nil { return "", err } if n != len(salt) { return "", fmt.Errorf(`short read from urandom (got %d expected %d)`, n, len(salt)) } newHash := argon2.Key([]byte(newValue), salt, argon2_time, argon2_memory, 1, argon2_want_output_bytes) // base64(salt) $ base64(hash) return base64.StdEncoding.EncodeToString(salt) + `$` + base64.StdEncoding.EncodeToString(newHash), nil default: return "", fmt.Errorf("unrecognised password hash format %d", format) } }