Skip to content
quarry Artifact [0f8257af85]
Login

Artifact [0f8257af85]

Artifact 0f8257af851a52d147a8681b46f4d15ff4bd65d558fd8b2055fb2a5a2e11be07:


// password.go holds the account password verifier. This is quarry's own
// choice and is deliberately NOT the sync protocol's credential: RFC §6.3
// fixes the push login as SHA1(project-code/user/password), a value the
// server must be able to recompute, which rules it out as a password
// store. An account password is verified here and never recovered, so it
// gets a memory-hard KDF instead.
package oltp

import (
	"crypto/rand"
	"crypto/subtle"
	"encoding/base64"
	"errors"
	"fmt"
	"strings"

	"golang.org/x/crypto/argon2"
)

// argon2id parameters. Cost lives IN the encoded hash, so raising these
// later leaves existing hashes verifiable — each one carries the
// parameters it was made with.
const (
	argonTime    = 1
	argonMemory  = 64 * 1024 // 64 MiB
	argonThreads = 4
	argonKeyLen  = 32
	argonSaltLen = 16
)

// HashPassword returns an encoded argon2id verifier in the standard
// PHC string format, salt and all.
func HashPassword(plain string) (string, error) {
	if plain == "" {
		return "", errors.New("oltp: empty password")
	}
	salt := make([]byte, argonSaltLen)
	if _, err := rand.Read(salt); err != nil {
		return "", err
	}
	key := argon2.IDKey([]byte(plain), salt, argonTime, argonMemory, argonThreads, argonKeyLen)
	return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
		argon2.Version, argonMemory, argonTime, argonThreads,
		base64.RawStdEncoding.EncodeToString(salt),
		base64.RawStdEncoding.EncodeToString(key)), nil
}

// VerifyPassword reports whether plain matches the encoded verifier. A
// malformed verifier is a mismatch, never a panic — the comparison is
// constant-time so a wrong password and a wrong length cost the same.
func VerifyPassword(encoded, plain string) bool {
	parts := strings.Split(encoded, "$")
	if len(parts) != 6 || parts[1] != "argon2id" {
		return false
	}
	var version int
	if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil || version != argon2.Version {
		return false
	}
	var memory uint32
	var time uint32
	var threads uint8
	if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads); err != nil {
		return false
	}
	salt, err := base64.RawStdEncoding.DecodeString(parts[4])
	if err != nil {
		return false
	}
	want, err := base64.RawStdEncoding.DecodeString(parts[5])
	if err != nil {
		return false
	}
	got := argon2.IDKey([]byte(plain), salt, time, memory, threads, uint32(len(want)))
	return subtle.ConstantTimeCompare(got, want) == 1
}