Created
March 25, 2022 22:15
-
-
Save mukunda-/e3cb797adf459716188a7b34f211ae82 to your computer and use it in GitHub Desktop.
Argon2 password hashing in Go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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