Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace scrypt with argon2id #471

Merged
merged 7 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Most recent version is listed first.

# v0.1.7
- Update go version; https://github.com/komuw/ong/pull/469
- ong/cry: Replace scrypt with argon2id: https://github.com/komuw/ong/pull/471

# v0.1.6
- Bump versions of dependencies used
Expand Down
36 changes: 15 additions & 21 deletions cry/enc.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ import (
cryptoRand "crypto/rand"
"encoding/base64"
"errors"
"runtime"
"slices"

"github.com/komuw/ong/internal/key"

"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/scrypt"
)

// Latacora recommends ChaCha20-Poly1305 for encryption.
// https://latacora.micro.blog/2018/04/03/cryptographic-right-answers.html
// https://www.latacora.com/blog/2024/07/29/crypto-right-answers-pq/
//
// The Go authors also recommend to use `crypto/cipher.NewGCM` or `XChaCha20-Poly1305`
// https://github.com/golang/crypto/blob/05595931fe9d3f8894ab063e1981d28e9873e2cb/tea/cipher.go#L13-L14
Expand All @@ -35,14 +35,15 @@ import (
const (
keyLen = chacha20poly1305.KeySize
saltLen = 8
)

var (
//
// The values recommended as of year 2017 are:
// n=32768, r=8 and p=1
// https://pkg.go.dev/golang.org/x/crypto/scrypt#Key
//
n = 32768 // CPU/memory cost parameter.
r = 8 // r and p must satisfy r * p < 2³⁰, else [scrypt.Key] returns an error.
p = 1
// The values recommended are:
// golang.org/x/crypto/argon2
time = uint32(1) //nolint:gochecknoglobals
memory = uint32(64 * 1024) //nolint:gochecknoglobals // 64MB
threads = uint8(runtime.NumCPU()) //nolint:gochecknoglobals
)

// Enc is an AEAD cipher mode providing authenticated encryption with associated data, ie [cipher.AEAD]
Expand All @@ -58,7 +59,7 @@ type Enc struct {
//
// It panics on error.
//
// It uses [scrypt] to derive the final key that will be used for encryption.
// It uses argon2id to derive the final key that will be used for encryption.
func New(secretKey string) Enc {
// I think it is okay for New to panic instead of returning an error.
// Since this is a crypto library, it is better to fail loudly than fail silently.
Expand All @@ -71,16 +72,12 @@ func New(secretKey string) Enc {
// derive a key.
password := []byte(secretKey)
salt := random(saltLen, saltLen) // should be random, 8 bytes is a good length.
derivedKey, err := deriveKey(password, salt)
if err != nil {
panic(err)
}
derivedKey := deriveKey(password, salt)

/*
Another option would be to use argon2.
import "golang.org/x/crypto/argon2"
salt := rand(16, 16) // 16bytes are recommended
key := argon2.Key( []byte("super-h@rd-Pas1word"), salt, 3, 32 * 1024, 4, keyLen)
import "golang.org/x/crypto/scrypt"
key := scrypt.Key("password", salt, 32768, 8, 1, keyLen)
*/

// xchacha20poly1305 takes a longer nonce, suitable to be generated randomly without risk of collisions.
Expand Down Expand Up @@ -136,10 +133,7 @@ func (e Enc) Decrypt(encryptedMsg []byte) (decryptedMsg []byte, err error) {
if !slices.Equal(salt, e.salt) {
// The encryptedMsg was encrypted using a different salt.
// So, we need to get the derived key for that salt and use it for decryption.
derivedKey, errK := scrypt.Key(e.key, salt, n, r, p, keyLen)
if errK != nil {
return nil, errK
}
derivedKey := deriveKey(e.key, salt)

aead, err = chacha20poly1305.NewX(derivedKey)
if err != nil {
Expand Down
7 changes: 2 additions & 5 deletions cry/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,9 @@ func ExampleEnc_EncryptEncode() {
func ExampleHash() {
password := "my NSA-hard password"
// it is okay to save hashedPasswd to the database, as an example.
hashedPasswd, err := cry.Hash(password)
if err != nil {
panic(err)
}
hashedPasswd := cry.Hash(password)

err = cry.Eql(password, hashedPasswd)
err := cry.Eql(password, hashedPasswd)
if err != nil {
panic(err)
}
Expand Down
31 changes: 10 additions & 21 deletions cry/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,28 @@ import (
"strconv"
"strings"

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

// Some of the code here is inspired by(or taken from):
// (a) https://github.com/elithrar/simple-scrypt whose license(MIT) can be found here: https://github.com/elithrar/simple-scrypt/blob/v1.3.0/LICENSE

const (
// this should be increased every time the parameters passed to [scrypt.Key] are changed.
// this should be increased every time the parameters passed to [argon2.IDKey] are changed.
version = 1
separator = "$"
)

func deriveKey(password, salt []byte) (derivedKey []byte, err error) {
derivedKey, err = scrypt.Key(password, salt, n, r, p, keyLen)
if err != nil {
return nil, err
}

return derivedKey, nil
func deriveKey(password, salt []byte) []byte {
// IDKey is Argon2id
return argon2.IDKey(password, salt, time, memory, threads, keyLen)
}

// Hash returns the scrypt hash of the password.
// Hash returns the argon2id hash of the password.
// It is safe to persist the result in your database instead of storing the actual password.
func Hash(password string) (string, error) {
func Hash(password string) string {
salt := random(saltLen, saltLen)
derivedKey, err := deriveKey([]byte(password), salt)
if err != nil {
return "", err
}
derivedKey := deriveKey([]byte(password), salt)

// Add version, salt to the derived key.
// The salt and the derived key are hex encoded.
Expand All @@ -47,7 +40,7 @@ func Hash(password string) (string, error) {
salt,
separator,
derivedKey,
), nil
)
}

// Eql performs a constant-time comparison between the password and the hash.
Expand Down Expand Up @@ -77,11 +70,7 @@ func Eql(password, hash string) error {
return err
}

dk, err := deriveKey([]byte(password), pSalt)
if err != nil {
return err
}

dk := deriveKey([]byte(password), pSalt)
if subtle.ConstantTimeCompare(dk, pDerivedKey) == 1 {
return nil
}
Expand Down
13 changes: 5 additions & 8 deletions cry/hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,30 @@ func TestHash(t *testing.T) {
t.Parallel()

password := "hey ok"
hash, err := Hash(password)
attest.Ok(t, err)
hash := Hash(password)
attest.NotZero(t, hash)
})

t.Run("eql success", func(t *testing.T) {
t.Parallel()

password := "hey ok"
hash, err := Hash(password)
attest.Ok(t, err)
hash := Hash(password)
attest.NotZero(t, hash)

err = Eql(password, hash)
err := Eql(password, hash)
attest.Ok(t, err)
})

t.Run("eql error", func(t *testing.T) {
t.Parallel()

password := "hey ok"
hash, err := Hash(password)
attest.Ok(t, err)
hash := Hash(password)
attest.NotZero(t, hash)

hash = strings.ReplaceAll(hash, separator, "-")
err = Eql(password, hash)
err := Eql(password, hash)
attest.Error(t, err)
})
}
6 changes: 1 addition & 5 deletions example/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,11 +269,7 @@ func (a app) login(secretKey string) http.HandlerFunc {
existingPasswdHash := a.db.Get("passwd")
if e := cry.Eql(password, existingPasswdHash); e != nil {
// passwd did not exist before.
hashedPasswd, errH := cry.Hash(password)
if errH != nil {
http.Error(w, errH.Error(), http.StatusInternalServerError)
return
}
hashedPasswd := cry.Hash(password)
a.db.Set("passwd", hashedPasswd)
}

Expand Down
Loading