github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/crypto/scrypt.go (about)

     1  package crypto
     2  
     3  // Params describes the input parameters to the scrypt
     4  import (
     5  	"bytes"
     6  	"crypto/subtle"
     7  	"encoding/hex"
     8  	"errors"
     9  	"fmt"
    10  	"runtime/debug"
    11  	"strconv"
    12  
    13  	"golang.org/x/crypto/scrypt"
    14  )
    15  
    16  // The code below is heavily inspired by https://github.com/elithrar/simple-scrypt
    17  
    18  // Scrypt params, this set of parameters is recommended in
    19  // - https://www.tarsnap.com/scrypt/scrypt-slides.pdf
    20  // - https://pkg.go.dev/golang.org/x/crypto/scrypt#Key
    21  // - https://pkg.go.dev/github.com/elithrar/simple-scrypt#DefaultParams
    22  // - https://blog.filippo.io/the-scrypt-parameters/
    23  // - https://github.com/golang/go/issues/22082
    24  const defaultN = 32768
    25  const defaultR = 8
    26  const defaultP = 1
    27  
    28  // hash length
    29  const defaultDkLen = 32
    30  
    31  // salt length
    32  const defaultSaltLen = 16
    33  
    34  // Errors
    35  var (
    36  	ErrInvalidHash                 = errors.New("Invalid hash format")
    37  	ErrMismatchedHashAndPassphrase = errors.New("hash and password are different")
    38  	ErrNoUpdateNeeded              = errors.New("hash already has correct parameters")
    39  )
    40  
    41  var sep = []byte("$")
    42  
    43  type scryptHash struct {
    44  	n    int
    45  	r    int
    46  	p    int
    47  	salt []byte
    48  	dk   []byte
    49  }
    50  
    51  func (h *scryptHash) UnmarshalText(hashbytes []byte) error {
    52  	// Decode existing hash, retrieve params and salt.
    53  	vals := bytes.Split(hashbytes, sep)
    54  	// "scrypt", P, N, R, salt, scrypt derived key
    55  	if len(vals) != 6 {
    56  		return ErrInvalidHash
    57  	}
    58  	if string(vals[0]) != "scrypt" {
    59  		return ErrInvalidHash
    60  	}
    61  
    62  	var err error
    63  
    64  	h.n, err = strconv.Atoi(string(vals[1]))
    65  	if err != nil {
    66  		return ErrInvalidHash
    67  	}
    68  
    69  	h.r, err = strconv.Atoi(string(vals[2]))
    70  	if err != nil {
    71  		return ErrInvalidHash
    72  	}
    73  
    74  	h.p, err = strconv.Atoi(string(vals[3]))
    75  	if err != nil {
    76  		return ErrInvalidHash
    77  	}
    78  
    79  	h.salt = make([]byte, hex.DecodedLen(len(vals[4])))
    80  	_, err = hex.Decode(h.salt, vals[4])
    81  	if err != nil {
    82  		return ErrInvalidHash
    83  	}
    84  
    85  	h.dk = make([]byte, hex.DecodedLen(len(vals[5])))
    86  	_, err = hex.Decode(h.dk, vals[5])
    87  	if err != nil {
    88  		return ErrInvalidHash
    89  	}
    90  
    91  	return nil
    92  }
    93  
    94  func (h *scryptHash) MarshalText() ([]byte, error) {
    95  	s := fmt.Sprintf("scrypt$%d$%d$%d$%x$%x", h.n, h.r, h.p, h.salt, h.dk)
    96  	return []byte(s), nil
    97  }
    98  
    99  func (h *scryptHash) Compare(passphrase []byte) error {
   100  	// scrypt the cleartext passphrase with the same parameters and salt
   101  	other, err := scrypt.Key(passphrase, h.salt, h.n, h.r, h.p, len(h.dk))
   102  	if err != nil {
   103  		return err
   104  	}
   105  
   106  	// Constant time comparison
   107  	if subtle.ConstantTimeCompare(h.dk, other) == 1 {
   108  		return nil
   109  	}
   110  
   111  	return ErrMismatchedHashAndPassphrase
   112  }
   113  
   114  func (h *scryptHash) NeedUpdate() bool {
   115  	return h.n != defaultN || h.p != defaultP || h.r != defaultR ||
   116  		len(h.salt) != defaultSaltLen || len(h.dk) != defaultDkLen
   117  }
   118  
   119  // GenerateFromPassphrase returns the derived key of the passphrase using the
   120  // parameters provided. The parameters are prepended to the derived key and
   121  // separated by the "$" character (0x24).
   122  // If the parameters provided are less than the minimum acceptable values,
   123  // an error will be returned.
   124  func GenerateFromPassphrase(passphrase []byte) ([]byte, error) {
   125  	h := &scryptHash{n: defaultN, r: defaultR, p: defaultP}
   126  	var err error
   127  
   128  	h.salt = GenerateRandomBytes(defaultSaltLen)
   129  
   130  	// scrypt.Key returns the raw scrypt derived key.
   131  	h.dk, err = scrypt.Key(passphrase, h.salt, h.n, h.r, h.p, defaultDkLen)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  
   136  	return h.MarshalText()
   137  }
   138  
   139  // CompareHashAndPassphrase compares a derived key with the possible cleartext
   140  // equivalent. The parameters used in the provided derived key are used. The
   141  // comparison performed by this function is constant-time.
   142  //
   143  // It returns an error if the derived keys do not match. It also returns a
   144  // needUpdate boolean indicating whether or not the passphrase hash has
   145  // outdated parameters and should be recomputed.
   146  func CompareHashAndPassphrase(hash []byte, passphrase []byte) (needUpdate bool, err error) {
   147  	h := &scryptHash{}
   148  	if err = h.UnmarshalText(hash); err != nil {
   149  		return false, err
   150  	}
   151  	if err = h.Compare(passphrase); err != nil {
   152  		return false, err
   153  	}
   154  
   155  	// Force go to release the memory, as scrypt takes a lot of RAM by design
   156  	// and go has some GC heuristics that can keep it for some times.
   157  	// See https://github.com/golang/go/issues/20000
   158  	// and https://groups.google.com/forum/#!topic/golang-nuts/I9R9MKUS9bo
   159  	debug.FreeOSMemory()
   160  
   161  	return h.NeedUpdate(), nil
   162  }