diff --git a/CHANGELOG.md b/CHANGELOG.md index 571387ec..5b076031 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Most recent version is listed first. + +## v0.0.9 +- Add password hashing capabilities: https://github.com/komuw/ong/pull/137 + ## v0.0.8 - Improve documentation. diff --git a/enc/enc.go b/cry/enc.go similarity index 97% rename from enc/enc.go rename to cry/enc.go index b1fd77a3..c723cf2f 100644 --- a/enc/enc.go +++ b/cry/enc.go @@ -1,7 +1,7 @@ -// Package enc provides utilities to carry out encryption and decryption. +// Package cry provides utilities for cryptography. // This library has not been vetted and people are discouraged from using it. // Instead use the crypto facilities in the Go standard library and/or golang.org/x/crypto -package enc +package cry import ( "crypto/cipher" @@ -69,9 +69,9 @@ func New(key string) Enc { } // derive a key. - salt := random(saltLen, saltLen) // should be random, 8 bytes is a good length. password := []byte(key) - derivedKey, err := scrypt.Key(password, salt, n, r, p, keyLen) + salt := random(saltLen, saltLen) // should be random, 8 bytes is a good length. + derivedKey, err := deriveKey(password, salt) if err != nil { panic(err) } diff --git a/enc/enc_test.go b/cry/enc_test.go similarity index 99% rename from enc/enc_test.go rename to cry/enc_test.go index 83ac0f73..ded608b5 100644 --- a/enc/enc_test.go +++ b/cry/enc_test.go @@ -1,4 +1,4 @@ -package enc +package cry import ( "sync" diff --git a/enc/example_test.go b/cry/example_test.go similarity index 63% rename from enc/example_test.go rename to cry/example_test.go index 5d4fd270..49174284 100644 --- a/enc/example_test.go +++ b/cry/example_test.go @@ -1,14 +1,14 @@ -package enc_test +package cry_test import ( "fmt" - "github.com/komuw/ong/enc" + "github.com/komuw/ong/cry" ) func ExampleEnc_Encrypt() { key := "hard-passwd" - e := enc.New(key) + e := cry.New(key) plainTextMsg := "Muziki asili yake - Remmy Ongala." // English: `What is the origin of music by Remmy Ongala` encryptedMsg := e.Encrypt(plainTextMsg) @@ -19,7 +19,7 @@ func ExampleEnc_Encrypt() { func ExampleEnc_EncryptEncode() { key := "hard-passwd" - e := enc.New(key) + e := cry.New(key) originalPlainTextMsg := "three little birds." encryptedEncodedMsg := e.EncryptEncode(originalPlainTextMsg) @@ -37,3 +37,18 @@ func ExampleEnc_EncryptEncode() { // Output: three little birds. } + +func ExampleHash() { + password := "my NSA-hard password" + hashedPasswd, err := cry.Hash(password) // save hashedPasswd to the database. + if err != nil { + panic(err) + } + + err = cry.Eql(password, hashedPasswd) // retrieve hashedPasswd from database. + if err != nil { + panic(err) + } + + fmt.Println(hashedPasswd) +} diff --git a/cry/hash.go b/cry/hash.go new file mode 100644 index 00000000..720d5bcd --- /dev/null +++ b/cry/hash.go @@ -0,0 +1,90 @@ +package cry + +import ( + "crypto/subtle" + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + + "golang.org/x/crypto/scrypt" +) + +// Most of the code here is insipired 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. + 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 +} + +// Hash returns the scrypt 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) { + salt := random(saltLen, saltLen) + derivedKey, err := deriveKey([]byte(password), salt) + if err != nil { + return "", err + } + + // Add version, salt to the derived key. + // The salt and the derived key are hex encoded. + return fmt.Sprintf( + `%d%s%x%s%x`, + version, + separator, + salt, + separator, + derivedKey, + ), nil +} + +// Eql performs a constant-time comparison between the password and the hash. +// The hash ought to have been produced by [Hash] +func Eql(password, hash string) error { + params := strings.Split(hash, "$") + + if len(params) != 3 { + return errors.New("unable to parse") + } + + pVer, err := strconv.Atoi(params[0]) + if err != nil { + return err + } + if pVer != version { + return errors.New("version mismatch") + } + + pSalt, err := hex.DecodeString(params[1]) + if err != nil { + return err + } + + pDerivedKey, err := hex.DecodeString(params[2]) + if err != nil { + return err + } + + dk, err := deriveKey([]byte(password), pSalt) + if err != nil { + return err + } + + if subtle.ConstantTimeCompare(dk, pDerivedKey) == 1 { + return nil + } + + return errors.New("password mismatch") +} diff --git a/cry/hash_test.go b/cry/hash_test.go new file mode 100644 index 00000000..5ad11dc9 --- /dev/null +++ b/cry/hash_test.go @@ -0,0 +1,46 @@ +package cry + +import ( + "strings" + "testing" + + "github.com/akshayjshah/attest" +) + +func TestHash(t *testing.T) { + t.Parallel() + + t.Run("hash success", func(t *testing.T) { + t.Parallel() + + password := "hey ok" + hash, err := Hash(password) + attest.Ok(t, err) + 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) + attest.NotZero(t, 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) + attest.NotZero(t, hash) + + hash = strings.ReplaceAll(hash, separator, "-") + err = Eql(password, hash) + attest.Error(t, err) + }) +} diff --git a/middleware/csrf.go b/middleware/csrf.go index f2877274..a494ec17 100644 --- a/middleware/csrf.go +++ b/middleware/csrf.go @@ -7,7 +7,7 @@ import ( "net/http" "time" - "github.com/komuw/ong/enc" + "github.com/komuw/ong/cry" "github.com/komuw/ong/id" "github.com/komuw/ong/cookie" @@ -52,7 +52,7 @@ const ( // Csrf is a middleware that provides protection against Cross Site Request Forgeries. // If a csrf token is not provided(or is not valid), when it ought to have been; this middleware will issue a http GET redirect to the same url. func Csrf(wrappedHandler http.HandlerFunc, secretKey, domain string) http.HandlerFunc { - enc := enc.New(secretKey) + enc := cry.New(secretKey) msgToEncrypt := id.Random(16) return func(w http.ResponseWriter, r *http.Request) { diff --git a/middleware/csrf_test.go b/middleware/csrf_test.go index bdca6a13..d0484e26 100644 --- a/middleware/csrf_test.go +++ b/middleware/csrf_test.go @@ -9,9 +9,8 @@ import ( "sync" "testing" - "github.com/komuw/ong/enc" - "github.com/akshayjshah/attest" + "github.com/komuw/ong/cry" "github.com/komuw/ong/id" ) @@ -299,7 +298,7 @@ func TestCsrf(t *testing.T) { wrappedHandler := Csrf(someCsrfHandler(msg), getSecretKey(), domain) key := getSecretKey() - enc := enc.New(key) + enc := cry.New(key) reqCsrfTok := enc.EncryptEncode("msgToEncrypt") { @@ -398,7 +397,7 @@ func TestCsrf(t *testing.T) { wrappedHandler := Csrf(someCsrfHandler(msg), getSecretKey(), domain) key := getSecretKey() - enc := enc.New(key) + enc := cry.New(key) reqCsrfTok := enc.EncryptEncode("msgToEncrypt") {