Skip to content

Commit

Permalink
issues/133: Add password hashing capabilities. (#137)
Browse files Browse the repository at this point in the history
What:
- Add password hashing capabilities.

Why:
- Fixes: #133
  • Loading branch information
komuw authored Sep 26, 2022
1 parent 1c6a5fc commit 93eabba
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 15 deletions.
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

0 comments on commit 93eabba

Please sign in to comment.