Skip to content

Commit

Permalink
Merge pull request #1 from gabriel-samfira/add-sio
Browse files Browse the repository at this point in the history
Add functions to Seal/Unseal using minio/sio
  • Loading branch information
gabriel-samfira authored Aug 19, 2023
2 parents 1e24ecc + f0951f7 commit 234018c
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 55 deletions.
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ require (
github.com/google/uuid v1.3.0
github.com/gorilla/handlers v1.5.1
github.com/mattn/go-isatty v0.0.19
github.com/minio/sio v0.3.1
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.8.4
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569
golang.org/x/crypto v0.7.0
golang.org/x/sys v0.8.0
Expand All @@ -18,9 +20,11 @@ require (
require (
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7N
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
Expand All @@ -23,8 +25,14 @@ github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/sio v0.3.1 h1:d59r5RTHb1OsQaSl1EaTWurzMMDRLA5fgNmjzD4eVu4=
github.com/minio/sio v0.3.1/go.mod h1:S0ovgVgc+sTlQyhiXA1ppBLv7REM7TYi5yyq2qL/Y6o=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569 h1:xzABM9let0HLLqFypcxvLmlvEciCHL7+Lv+4vwZqecI=
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569/go.mod h1:2Ly+NIftZN4de9zRmENdYbvPQeaVIYKWpLFStLFEBgI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
Expand Down
156 changes: 156 additions & 0 deletions util/seal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package util

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"fmt"
"io"

"github.com/minio/sio"
"github.com/pkg/errors"
"golang.org/x/crypto/hkdf"
)

type Envelope struct {
Nonce [32]byte `json:"nonce"`
Data []byte `json:"data"`
}

// Seal will encrypt the given data using a derived key from the given passphrase.
// This function is meant to be used with small datasets like passwords, keys and
// secrets of any type, before they are saved to disk.
func Seal(data []byte, passphrase []byte) ([]byte, error) {
if len(passphrase) != 32 {
return nil, fmt.Errorf("invalid passphrase length (expected length 32 characters)")
}

var nonce [32]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
return nil, fmt.Errorf("failed to read random data: %w", err)
}

// derive an encryption key from the master key and the nonce
var key [32]byte
kdf := hkdf.New(sha256.New, passphrase, nonce[:], nil)
if _, err := io.ReadFull(kdf, key[:]); err != nil {
return nil, fmt.Errorf("failed to derive encryption key: %w", err)
}

input := bytes.NewReader(data)
output := bytes.NewBuffer(nil)

if _, err := sio.Encrypt(output, input, sio.Config{Key: key[:]}); err != nil {
return nil, fmt.Errorf("failed to encrypt data: %w", err)
}
envelope := Envelope{
Data: output.Bytes(),
Nonce: nonce,
}
asJs, err := json.Marshal(envelope)
if err != nil {
return nil, fmt.Errorf("failed to marshal envelope: %w", err)
}
return asJs, nil
}

// Unseal will decrypt the given data using a derived key from the given passphrase.
// This function is meant to be used with small datasets like passwords, keys and
// secrets of any type, after they are read from disk.
func Unseal(data []byte, passphrase []byte) ([]byte, error) {
if len(passphrase) != 32 {
return nil, fmt.Errorf("invalid passphrase length (expected length 32 characters)")
}

var envelope Envelope
if err := json.Unmarshal(data, &envelope); err != nil {
return Aes256Decode(data, string(passphrase))
}

// derive an encryption key from the master key and the nonce
var key [32]byte
kdf := hkdf.New(sha256.New, passphrase, envelope.Nonce[:], nil)
if _, err := io.ReadFull(kdf, key[:]); err != nil {
return nil, fmt.Errorf("failed to derive encryption key: %w", err)
}

input := bytes.NewReader(envelope.Data)
output := bytes.NewBuffer(nil)

if _, err := sio.Decrypt(output, input, sio.Config{Key: key[:]}); err != nil {
return nil, fmt.Errorf("failed to decrypt data: %w", err)
}

return output.Bytes(), nil
}

func Aes256Encode(target []byte, passphrase string) ([]byte, error) {
if len(passphrase) != 32 {
return nil, fmt.Errorf("invalid passphrase length (expected length 32 characters)")
}

block, err := aes.NewCipher([]byte(passphrase))
if err != nil {
return nil, errors.Wrap(err, "creating cipher")
}

aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, errors.Wrap(err, "creating new aead")
}

nonce := make([]byte, aesgcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, errors.Wrap(err, "creating nonce")
}

ciphertext := aesgcm.Seal(nonce, nonce, target, nil)
return ciphertext, nil
}

func Aes256EncodeString(target string, passphrase string) ([]byte, error) {
if len(passphrase) != 32 {
return nil, fmt.Errorf("invalid passphrase length (expected length 32 characters)")
}

return Aes256Encode([]byte(target), passphrase)
}

func Aes256Decode(target []byte, passphrase string) ([]byte, error) {
if len(passphrase) != 32 {
return nil, fmt.Errorf("invalid passphrase length (expected length 32 characters)")
}

block, err := aes.NewCipher([]byte(passphrase))
if err != nil {
return nil, errors.Wrap(err, "creating cipher")
}

aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, errors.Wrap(err, "creating new aead")
}

nonceSize := aesgcm.NonceSize()
if len(target) < nonceSize {
return nil, fmt.Errorf("failed to decrypt text")
}

nonce, ciphertext := target[:nonceSize], target[nonceSize:]
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("failed to decrypt text")
}
return plaintext, nil
}

func Aes256DecodeString(target []byte, passphrase string) (string, error) {
data, err := Aes256Decode(target, passphrase)
if err != nil {
return "", err
}
return string(data), nil
}
60 changes: 60 additions & 0 deletions util/seal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package util

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"
)

var (
EncryptionPassphrase = "bocyasicgatEtenOubwonIbsudNutDom"
WeakEncryptionPassphrase = "test123"
)

func TestSealUnseal(t *testing.T) {
data, err := Seal([]byte("test"), []byte(EncryptionPassphrase))
require.NoError(t, err)

// Data should unmarshal in an Envelope
var envelope Envelope
err = json.Unmarshal(data, &envelope)
require.NoError(t, err)

// Should be able to decrypt
decrypted, err := Unseal(data, []byte(EncryptionPassphrase))
require.NoError(t, err)
require.Equal(t, []byte("test"), decrypted)
}

func TestAes256EncodeDecode(t *testing.T) {
// Should be able to encrypt
encrypted, err := Aes256Encode([]byte("test"), EncryptionPassphrase)
require.NoError(t, err)

// Should be able to decrypt
decrypted, err := Aes256Decode(encrypted, EncryptionPassphrase)
require.NoError(t, err)
require.Equal(t, []byte("test"), decrypted)
}

func TestUnsealAes256EncodedData(t *testing.T) {
encrypted, err := Aes256Encode([]byte("test"), EncryptionPassphrase)
require.NoError(t, err)

// Should be able to decrypt
decrypted, err := Unseal(encrypted, []byte(EncryptionPassphrase))
require.NoError(t, err)
require.Equal(t, []byte("test"), decrypted)
}

func TestSealUnsealWeakSecret(t *testing.T) {
_, err := Seal([]byte("test"), []byte(WeakEncryptionPassphrase))
require.NotNil(t, err)
require.EqualError(t, err, "invalid passphrase length (expected length 32 characters)")

// The data is irelevant. We expect to error out on the passphrase length.
_, err = Unseal([]byte("test"), []byte(WeakEncryptionPassphrase))
require.NotNil(t, err)
require.EqualError(t, err, "invalid passphrase length (expected length 32 characters)")
}
55 changes: 0 additions & 55 deletions util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ package util
import (
"bytes"
"compress/gzip"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/binary"
Expand Down Expand Up @@ -244,59 +242,6 @@ func GetRandomString(n int) (string, error) {
return string(data), nil
}

func Aes256EncodeString(target string, passphrase string) ([]byte, error) {
if len(passphrase) != 32 {
return nil, fmt.Errorf("invalid passphrase length (expected length 32 characters)")
}

toEncrypt := []byte(target)
block, err := aes.NewCipher([]byte(passphrase))
if err != nil {
return nil, errors.Wrap(err, "creating cipher")
}

aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, errors.Wrap(err, "creating new aead")
}

nonce := make([]byte, aesgcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, errors.Wrap(err, "creating nonce")
}

ciphertext := aesgcm.Seal(nonce, nonce, toEncrypt, nil)
return ciphertext, nil
}

func Aes256DecodeString(target []byte, passphrase string) (string, error) {
if len(passphrase) != 32 {
return "", fmt.Errorf("invalid passphrase length (expected length 32 characters)")
}

block, err := aes.NewCipher([]byte(passphrase))
if err != nil {
return "", errors.Wrap(err, "creating cipher")
}

aesgcm, err := cipher.NewGCM(block)
if err != nil {
return "", errors.Wrap(err, "creating new aead")
}

nonceSize := aesgcm.NonceSize()
if len(target) < nonceSize {
return "", fmt.Errorf("failed to decrypt text")
}

nonce, ciphertext := target[:nonceSize], target[nonceSize:]
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", fmt.Errorf("failed to decrypt text")
}
return string(plaintext), nil
}

// PaswsordToBcrypt returns a bcrypt hash of the specified password using the default cost
func PaswsordToBcrypt(password string) (string, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
Expand Down

0 comments on commit 234018c

Please sign in to comment.