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 }