Skip to content

Commit

Permalink
feat(database): add secret Encrypt() and Decrypt() (#160)
Browse files Browse the repository at this point in the history
* feat(database): add secret Encrypt() and Decrypt()

* chore: address linter feedback

* fix: ensure decoded secret value > nonce size
  • Loading branch information
jbrockopp authored Mar 4, 2021
1 parent abab84a commit 580e7ea
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 1 deletion.
108 changes: 108 additions & 0 deletions database/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
package database

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"database/sql"
"encoding/base64"
"errors"
"fmt"
"io"
"strings"

"github.com/go-vela/types/constants"
Expand Down Expand Up @@ -55,6 +61,108 @@ type Secret struct {
AllowCommand sql.NullBool `sql:"allow_command"`
}

// Decrypt will manipulate the existing secret value by
// base64 decoding that value. Then, a AES-256 cipher
// block is created from the encryption key in order to
// decrypt the base64 decoded secret value.
func (s *Secret) Decrypt(key string) error {
// base64 decode the encrypted secret value
decoded, err := base64.StdEncoding.DecodeString(s.Value.String)
if err != nil {
return err
}

// create a new cipher block from the encryption key
//
// the key should have a length of 64 bits to ensure
// we are using the AES-256 standard
//
// https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
block, err := aes.NewCipher([]byte(key))
if err != nil {
return err
}

// creates a new Galois Counter Mode cipher block
gcm, err := cipher.NewGCM(block)
if err != nil {
return err
}

// nonce is an arbitrary number used to to ensure that
// old communications cannot be reused in replay attacks.
//
// https://en.wikipedia.org/wiki/Cryptographic_nonce
nonceSize := gcm.NonceSize()

// verify the decoded secret length is greater than nonce
//
// if the base64 decoded secret value is less than the
// nonce size, then we can reasonably assume the secret
// hasn't been encrypted yet.
if len(decoded) < nonceSize {
return fmt.Errorf("invalid length for decoded secret value")
}

// capture nonce and ciphertext from decoded secret value
nonce, ciphertext := decoded[:nonceSize], decoded[nonceSize:]

// decrypt the decoded secret value from the ciphertext
decrypted, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return err
}

// set the decrypted secret value
s.Value = sql.NullString{String: string(decrypted), Valid: true}

return nil
}

// Encrypt will manipulate the existing secret value by
// creating a AES-256 cipher block from the encryption
// key in order to encrypt the secret value. Then, the
// secret value is base64 encoded for transport across
// network boundaries.
func (s *Secret) Encrypt(key string) error {
// create a new cipher block from the encryption key
//
// the key should have a length of 64 bits to ensure
// we are using the AES-256 standard
//
// https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
block, err := aes.NewCipher([]byte(key))
if err != nil {
return err
}

// creates a new Galois Counter Mode cipher block
gcm, err := cipher.NewGCM(block)
if err != nil {
return err
}

// nonce is an arbitrary number used to to ensure that
// old communications cannot be reused in replay attacks.
//
// https://en.wikipedia.org/wiki/Cryptographic_nonce
nonce := make([]byte, gcm.NonceSize())

// set nonce from a cryptographically secure random number generator
_, err = io.ReadFull(rand.Reader, nonce)
if err != nil {
return err
}

// encrypt the data with the randomly generated nonce
encrypted := gcm.Seal(nonce, nonce, []byte(s.Value.String), nil)

// base64 encode the encrypted secret data to make it network safe
s.Value = sql.NullString{String: base64.StdEncoding.EncodeToString(encrypted), Valid: true}

return nil
}

// Nullify ensures the valid flag for
// the sql.Null types are properly set.
//
Expand Down
105 changes: 105 additions & 0 deletions database/secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,117 @@ package database

import (
"database/sql"
"encoding/base64"
"reflect"
"testing"

"github.com/go-vela/types/library"
)

func TestDatabase_Secret_Decrypt(t *testing.T) {
// setup types

key := "C639A572E14D5075C526FDDD43E4ECF6"

s := testSecret()
err := s.Encrypt(key)
if err != nil {
t.Errorf("unable to encrypt secret: %v", err)
}

unencrypted := testSecret()
unencrypted.Value = sql.NullString{
String: base64.StdEncoding.EncodeToString([]byte("b")),
Valid: true,
}

// setup tests
tests := []struct {
failure bool
key string
secret Secret
}{
{
failure: false,
key: key,
secret: *s,
},
{
failure: true,
key: "",
secret: *s,
},
{
failure: true,
key: key,
secret: *testSecret(),
},
{
failure: true,
key: key,
secret: *unencrypted,
},
}

// run tests
for _, test := range tests {
err := test.secret.Decrypt(test.key)

if test.failure {
if err == nil {
t.Errorf("Decrypt should have returned err")
}

continue
}

if err != nil {
t.Errorf("Decrypt returned err: %v", err)
}
}
}

func TestDatabase_Secret_Encrypt(t *testing.T) {
// setup types

key := "C639A572E14D5075C526FDDD43E4ECF6"

// setup tests
tests := []struct {
failure bool
key string
secret *Secret
}{
{
failure: false,
key: key,
secret: testSecret(),
},
{
failure: true,
key: "",
secret: testSecret(),
},
}

// run tests
for _, test := range tests {
err := test.secret.Encrypt(test.key)

if test.failure {
if err == nil {
t.Errorf("Encrypt should have returned err")
}

continue
}

if err != nil {
t.Errorf("Encrypt returned err: %v", err)
}
}
}

func TestDatabase_Secret_Nullify(t *testing.T) {
// setup types
var s *Secret
Expand Down
1 change: 0 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down

0 comments on commit 580e7ea

Please sign in to comment.