Skip to content

Commit

Permalink
refactor crypto into its own package and add a helper to message stor…
Browse files Browse the repository at this point in the history
…es to do enc/dec
  • Loading branch information
ivarprudnikov committed Apr 27, 2024
1 parent f8d4d36 commit 7b90af2
Show file tree
Hide file tree
Showing 14 changed files with 206 additions and 203 deletions.
2 changes: 1 addition & 1 deletion cypress/e2e/auth.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,6 @@ describe('auth spec', () => {
cy.get('#password').type('joe')
cy.get('#password2').type('joe')
cy.get('.btn-primary').click()
cy.contains('username is not available').should('be.visible')
cy.contains('failed to create account').should('be.visible')
})
})
92 changes: 92 additions & 0 deletions internal/crypto/aes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package crypto

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
)

type EntityEncryptHelper struct{}

func (e EntityEncryptHelper) Encrypt(text, pass, salt string) (string, error) {
// derive a key from the pass
key, err := StrongKey(pass, salt)
if err != nil {
return "", err
}
ciphertext, err := EncryptAES(key, text)
if err != nil {
return "", err
}
return ciphertext, nil
}

// Decrypt cipher text with a given PIN which will be used to derive a key
func (e EntityEncryptHelper) Decrypt(ciphertext, pass, salt string) (string, error) {
// derive a key from the pass
key, err := StrongKey(pass, salt)
if err != nil {
return "", err
}
plaintext, err := DecryptAES(key, ciphertext)
if err != nil {
return "", err
}
return plaintext, nil
}

// Encrypts the plain text with a given key
// see StrongKey() to create one
func EncryptAES(key []byte, plaintext string) (string, error) {
cipherBlock, err := aes.NewCipher(key)
if err != nil {
return "", fmt.Errorf("failed to create cipher: %w", err)
}

gcm, err := cipher.NewGCM(cipherBlock)
if err != nil {
return "", fmt.Errorf("failed to wrap cipher: %w", err)
}

// Never use more than 2^32 random nonces with a given key because of the risk of a repeat.
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", fmt.Errorf("failed create nonce: %w", err)
}

// ciphertext here is actually nonce+ciphertext
// So that when we decrypt, just knowing the nonce size
// is enough to separate it from the ciphertext.
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)

return hex.EncodeToString(ciphertext), nil
}

// Decrypt the cipher text using the key provided
func DecryptAES(key []byte, ct string) (string, error) {
ciphertext, _ := hex.DecodeString(ct)
cipherBlock, err := aes.NewCipher(key)
if err != nil {
return "", fmt.Errorf("failed to create cipher: %w", err)
}

gcm, err := cipher.NewGCM(cipherBlock)
if err != nil {
return "", fmt.Errorf("failed to wrap cipher: %w", err)
}

// Since we know the ciphertext is actually nonce+ciphertext
// And len(nonce) == NonceSize(). We can separate the two.
nonceSize := gcm.NonceSize()
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]

plaintext, err := gcm.Open(nil, []byte(nonce), []byte(ciphertext), nil)
if err != nil {
return "", fmt.Errorf("failed to decrypt: %w", err)
}

return string(plaintext), nil
}
118 changes: 1 addition & 117 deletions internal/storage/hash.go → internal/crypto/argon.go
Original file line number Diff line number Diff line change
@@ -1,51 +1,15 @@
package storage
package crypto

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
"strings"

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

const keySizeBytes = 16 // 128-bit key

// simple pin number generator
// returns 4-5 digits
func MakePin() (string, error) {
b, err := generateRandomBytes(16)
if err != nil {
return "", err
}
pin := binary.BigEndian.Uint16(b)
return fmt.Sprintf("%d", pin), nil
}

// simple random token generator (url encoded)
func MakeToken() (string, error) {
b, err := generateRandomBytes(32)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}

// simple text hashing
func HashText(text string) string {
textHash := sha256.Sum256([]byte(text))
return hex.EncodeToString(textHash[:])
}

// Uses argon2id to hash and salt the given clear text value
// returns the encoded string to be used for storage
func HashPass(password string) (string, error) {
Expand Down Expand Up @@ -76,77 +40,6 @@ func CompareHashToPass(hash, password string) error {
return nil
}

// Use HMAC Key Derivation Function (HKDF) to derive a strong key (RFC5869)
// This function is useful to derive a key from some small password text the user knows
func StrongKey(passText string, saltText string) ([]byte, error) {
hashFn := sha256.New
hashSize := hashFn().Size()
passBytes := []byte(passText)
if len(saltText) != hashSize {
return nil, fmt.Errorf("invalid salt length, must be %d", hashSize)
}
saltBytes := []byte(saltText)
hkdf := hkdf.New(hashFn, passBytes, saltBytes, nil)
key := make([]byte, keySizeBytes)
if _, err := io.ReadFull(hkdf, key); err != nil {
return nil, err
}
return key, nil
}

// Encrypts the plain text with a given key
// see StrongKey() to create one
func EncryptAES(key []byte, plaintext string) (string, error) {
cipherBlock, err := aes.NewCipher(key)
if err != nil {
return "", fmt.Errorf("failed to create cipher: %w", err)
}

gcm, err := cipher.NewGCM(cipherBlock)
if err != nil {
return "", fmt.Errorf("failed to wrap cipher: %w", err)
}

// Never use more than 2^32 random nonces with a given key because of the risk of a repeat.
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", fmt.Errorf("failed create nonce: %w", err)
}

// ciphertext here is actually nonce+ciphertext
// So that when we decrypt, just knowing the nonce size
// is enough to separate it from the ciphertext.
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)

return hex.EncodeToString(ciphertext), nil
}

// Decrypt the cipher text using the key provided
func DecryptAES(key []byte, ct string) (string, error) {
ciphertext, _ := hex.DecodeString(ct)
cipherBlock, err := aes.NewCipher(key)
if err != nil {
return "", fmt.Errorf("failed to create cipher: %w", err)
}

gcm, err := cipher.NewGCM(cipherBlock)
if err != nil {
return "", fmt.Errorf("failed to wrap cipher: %w", err)
}

// Since we know the ciphertext is actually nonce+ciphertext
// And len(nonce) == NonceSize(). We can separate the two.
nonceSize := gcm.NonceSize()
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]

plaintext, err := gcm.Open(nil, []byte(nonce), []byte(ciphertext), nil)
if err != nil {
return "", fmt.Errorf("failed to decrypt: %w", err)
}

return string(plaintext), nil
}

const argonHashVariantName = "argon2id"

// The parameters to be used in the secure hash generation
Expand Down Expand Up @@ -218,12 +111,3 @@ func decodeArgon2Hash(hash string) (params *argon2Params, salt, key []byte, err
params.keyLength = uint32(len(key))
return params, salt, key, nil
}

// Generate cryptographically secure random of a given length
func generateRandomBytes(size uint32) ([]byte, error) {
bucket := make([]byte, size)
if _, err := rand.Read(bucket); err != nil {
return nil, err
}
return bucket, nil
}
68 changes: 68 additions & 0 deletions internal/crypto/crypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package crypto

import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"fmt"
"io"

"golang.org/x/crypto/hkdf"
)

// simple pin number generator
// returns 4-5 digits
func MakePin() (string, error) {
b, err := generateRandomBytes(16)
if err != nil {
return "", err
}
pin := binary.BigEndian.Uint16(b)
return fmt.Sprintf("%d", pin), nil
}

// simple random token generator (url encoded)
func MakeToken() (string, error) {
b, err := generateRandomBytes(32)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}

// simple text hashing
func HashText(text string) string {
textHash := sha256.Sum256([]byte(text))
return hex.EncodeToString(textHash[:])
}

const keySizeBytes = 16 // 128-bit key

// Use HMAC Key Derivation Function (HKDF) to derive a strong key (RFC5869)
// This function is useful to derive a key from some small password text the user knows
func StrongKey(passText string, saltText string) ([]byte, error) {
hashFn := sha256.New
hashSize := hashFn().Size()
passBytes := []byte(passText)
if len(saltText) != hashSize {
return nil, fmt.Errorf("invalid salt length, must be %d", hashSize)
}
saltBytes := []byte(saltText)
hkdf := hkdf.New(hashFn, passBytes, saltBytes, nil)
key := make([]byte, keySizeBytes)
if _, err := io.ReadFull(hkdf, key); err != nil {
return nil, err
}
return key, nil
}

// Generate cryptographically secure random of a given length
func generateRandomBytes(size uint32) ([]byte, error) {
bucket := make([]byte, size)
if _, err := rand.Read(bucket); err != nil {
return nil, err
}
return bucket, nil
}
20 changes: 10 additions & 10 deletions internal/storage/hash_test.go → internal/crypto/crypto_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package storage_test
package crypto_test

import (
"regexp"
"slices"
"strings"
"testing"

"github.com/ivarprudnikov/secretshare/internal/storage"
"github.com/ivarprudnikov/secretshare/internal/crypto"
)

func TestHash_MakePin(t *testing.T) {
pin, err := storage.MakePin()
pin, err := crypto.MakePin()
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
Expand All @@ -25,12 +25,12 @@ func TestHash_MakePin(t *testing.T) {

func TestHash_StrongKey(t *testing.T) {
salt := "12345678901234567890123456789012"
key1, err := storage.StrongKey("1234", salt)
key1, err := crypto.StrongKey("1234", salt)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

key2, err := storage.StrongKey("1234", salt)
key2, err := crypto.StrongKey("1234", salt)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
Expand All @@ -42,14 +42,14 @@ func TestHash_StrongKey(t *testing.T) {

func TestHash_EncryptDecrypt(t *testing.T) {
key := []byte("1234567890123456")
cipher, err := storage.EncryptAES(key, "abc")
cipher, err := crypto.EncryptAES(key, "abc")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if cipher == "abc" {
t.Fatal("ciphertext should not be the same as input")
}
plaintext, err := storage.DecryptAES(key, cipher)
plaintext, err := crypto.DecryptAES(key, cipher)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
Expand All @@ -59,22 +59,22 @@ func TestHash_EncryptDecrypt(t *testing.T) {
}

func TestHash_HashText(t *testing.T) {
digest := storage.HashText("foobar")
digest := crypto.HashText("foobar")
if digest != "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2" {
t.Fatalf("unexpected digest %s", digest)
}
}

func TestHash_HashPass_ThenCompare(t *testing.T) {
hashed, err := storage.HashPass("foobar")
hashed, err := crypto.HashPass("foobar")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !strings.HasPrefix(hashed, "$argon2id$v=") {
t.Fatalf("unexpected value %s", hashed)
}

err = storage.CompareHashToPass(hashed, "foobar")
err = crypto.CompareHashToPass(hashed, "foobar")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
Expand Down
Loading

0 comments on commit 7b90af2

Please sign in to comment.