Skip to content

Instantly share code, notes, and snippets.

@mukunda-
Created March 25, 2022 22:15
Show Gist options
  • Save mukunda-/e3cb797adf459716188a7b34f211ae82 to your computer and use it in GitHub Desktop.
Save mukunda-/e3cb797adf459716188a7b34f211ae82 to your computer and use it in GitHub Desktop.
Argon2 password hashing in Go
// passhash.go
// * Argon2id password hashing.
//
// Copyright 2022 Mukunda Johnson
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific prior
// written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//////////////////////////////////////////////////////////////////////////////////////////
package passhash
import (
"bytes"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
//////////////////////////////////////////////////////////////////////////////////////////
// Argon2 authors recommend 16 bytes for the digest for "most applications". A 16-byte
// salt should be fine too. 16 seems like a small amount of characters, but these are
// full bytes, not a subset.
// Recommended memory and time is 64M, 1. Not sure about threads, but we'll use 4, as per
// the examples on https://pkg.go.dev/golang.org/x/crypto/argon2
const saltSize = 16
const a2Tag = "argon2id"
const a2KeySize = 16
const a2MemSize = 64*1024
const a2Time = 1
const a2Threads = 4
//----------------------------------------------------------------------------------------
// Radix64 codec uses a different alphabet than base64.
const r64alphabet = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
var r64codec = base64.NewEncoding(r64alphabet)
//----------------------------------------------------------------------------------------
// Invalid key occurs when there is a parsing error with the key input, so the key is
// malformed.
var InvalidKey = errors.New("invalid key")
// VersionMismatch is from the version in the key string not being the expected
// argon2.Version, so we will not attempt to decode it.
var VersionMismatch = errors.New("the argon2 implementation version does not match")
//----------------------------------------------------------------------------------------
// These functions encode/decode radix64 and exclude the "=" padding. I'm not sure if
// there is any standard that dictates that keystrings should not include padding, but
// it is what I've seen in practice (a bad practice, in my opinion, as it adds
// unnecessary complications).
func r64encode(input []byte) string {
length := r64codec.EncodedLen(len(input))
output := make([]byte, length)
r64codec.Encode(output, input)
return strings.ReplaceAll(string(output), "=", "")
}
func r64decode(input string) ([]byte, error) {
input += strings.Repeat("=", 4 - (len(input) % 4))
length := r64codec.DecodedLen(len(input))
output := make([]byte, length)
n, err := r64codec.Decode(output, []byte(input))
return output[:n], err
}
//----------------------------------------------------------------------------------------
// Holds all of the components of a decoded keystring.
type a2idKey struct {
Tag string
Version int
MemSize uint32
Time uint32
Threads uint8
Salt []byte
Digest []byte
}
//----------------------------------------------------------------------------------------
// Parses a string coded keystring into the separated components.
func parseKey(key string) (a2idKey, error) {
parts := strings.Split(key, "$")
var err error
var result a2idKey
if len(parts) != 6 {return a2idKey{}, InvalidKey}
result.Tag = parts[1]
if result.Tag != a2Tag {
// Not an argon2id key. Don't parse it.
return a2idKey{}, InvalidKey
}
// Version
if n, err := fmt.Sscanf(parts[2], "v=%d", &result.Version);
err != nil || n != 1 {
return a2idKey{}, InvalidKey
}
// Note: we are expecting a specific order of parameters here. Is that standard
// compliant?
if n, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d",
&result.MemSize, &result.Time, &result.Threads);
err != nil || n != 3 {
return a2idKey{}, InvalidKey
}
result.Salt, err = r64decode(parts[4])
if err != nil {return a2idKey{}, InvalidKey}
result.Digest, err = r64decode(parts[5])
if err != nil {return a2idKey{}, InvalidKey}
return result, nil
}
//----------------------------------------------------------------------------------------
// Convert key components into a coded key string.
func (key *a2idKey) String() string {
return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
key.Version, key.MemSize, key.Time, key.Threads,
r64encode(key.Salt), r64encode(key.Digest))
}
//----------------------------------------------------------------------------------------
// Uses crypto/rand to generate a secure salt.
func generateSecureSalt() []byte {
salt := make([]byte, saltSize)
_, err := rand.Read(salt)
if err != nil {
// This should never error, so let's panic. I can only imagine this being an error
// when there is a problem with the given distribution, and this should be treated
// as a severe error.
panic("could not read from crypto/rand!")
}
return salt
}
//----------------------------------------------------------------------------------------
// Takes a UTF-8 string and hashes it.
//
// Returns key string for CheckPassword `key`. Error if there is an error (but errors
// should be quite rare system problems).
func Hash(password string) (string) {
salt := generateSecureSalt()
digest := argon2.IDKey([]byte(password), salt,
a2Time, a2MemSize, a2Threads, a2KeySize)
key := a2idKey{
Tag: "argon2id",
Version: argon2.Version,
MemSize: a2MemSize,
Time: a2Time,
Threads: a2Threads,
Salt: salt,
Digest: digest,
}
return key.String()
}
//----------------------------------------------------------------------------------------
// Verifies if a key holds the given password.
// Returns false and error on any input/parsing errors.
func CheckPassword(password string, key string) (bool, error) {
pkey, err := parseKey(key)
if err != nil {
// Error in the given key, maybe an old format or something we didn't expect.
return false, err
}
// If the version doesn't match exactly, then let's not try to check the password. The
// undefined behavior could result in a security breach.
if pkey.Version != argon2.Version {return false, VersionMismatch}
test := argon2.IDKey([]byte(password), pkey.Salt,
pkey.Time, pkey.MemSize, pkey.Threads, uint32(len(pkey.Digest)))
return bytes.Compare(pkey.Digest, test) == 0, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment