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

issues/133: Add password hashing capabilities. #137

Merged
merged 12 commits into from
Sep 26, 2022
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
8 changes: 4 additions & 4 deletions enc/enc.go → cry/enc.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion enc/enc_test.go → cry/enc_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package enc
package cry

import (
"sync"
Expand Down
23 changes: 19 additions & 4 deletions enc/example_test.go → cry/example_test.go
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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)
Expand All @@ -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)
}
90 changes: 90 additions & 0 deletions cry/hash.go
Original file line number Diff line number Diff line change
@@ -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")
}
46 changes: 46 additions & 0 deletions cry/hash_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
4 changes: 2 additions & 2 deletions middleware/csrf.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 3 additions & 4 deletions middleware/csrf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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")

{
Expand Down Expand Up @@ -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")

{
Expand Down