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

Secret Manager Integration #6

Merged
merged 2 commits into from
Jul 15, 2021
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
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ module github.com/rotationalio/whisper
go 1.16

require (
cloud.google.com/go v0.87.0
github.com/dn365/gin-zerolog v0.0.0-20171227063204-b43714b00db1
github.com/gin-gonic/gin v1.7.2
github.com/golang/protobuf v1.5.2 // indirect
github.com/joho/godotenv v1.3.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/kr/pretty v0.1.0 // indirect
github.com/rs/zerolog v1.23.0
github.com/stretchr/testify v1.7.0
github.com/urfave/cli/v2 v2.3.0
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
google.golang.org/genproto v0.0.0-20210714021259-044028024a4f
google.golang.org/grpc v1.39.0
google.golang.org/protobuf v1.27.1
gopkg.in/yaml.v2 v2.4.0 // indirect
)
474 changes: 471 additions & 3 deletions go.sum

Large diffs are not rendered by default.

38 changes: 0 additions & 38 deletions pkg/models.go

This file was deleted.

192 changes: 69 additions & 123 deletions pkg/secrets.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package whisper

import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
Expand All @@ -21,9 +22,6 @@ const DefaultSecretLifetime = time.Hour * 24 * 7
// DefaultSecretAccesses ensures that once the secret is fetched it is destroyed
const DefaultSecretAccesses = 1

var tmpSecretsStore = make(map[string]string)
var tmpSecretsMeta = make(map[string]*SecretMetadata)

// CreateSecret handles an incoming CreateSecretRequest and attempts to create a new
// secret that will only be displayed when the correct link is retrieved.
func (s *Server) CreateSecret(c *gin.Context) {
Expand All @@ -35,16 +33,25 @@ func (s *Server) CreateSecret(c *gin.Context) {
return
}

// Create the secret metadata
meta := &SecretMetadata{
Filename: req.Filename,
IsBase64: req.IsBase64,
Created: time.Now(),
// Make a random URL to store the secret in
var (
err error
token string
)
if token, err = s.GenerateUniqueURL(context.TODO()); err != nil {
log.Error().Err(err).Msg("could not generate unique token for secret")
c.JSON(http.StatusInternalServerError, ErrorResponse(err))
return
}

// Create the secret context
meta := s.vault.With(token)
meta.Filename = req.Filename
meta.IsBase64 = req.IsBase64
meta.Created = time.Now()

// Store the password as a derived key
var err error
if meta.Password, err = CreateDerivedKey(req.Password); err != nil {
if err = meta.SetPassword(req.Password); err != nil {
log.Error().Err(err).Msg("could not create derived key")
c.JSON(http.StatusInternalServerError, ErrorResponse(err))
return
Expand All @@ -53,164 +60,98 @@ func (s *Server) CreateSecret(c *gin.Context) {
// Compute the number of accesses for the secret
if req.Accesses == 0 {
meta.Accesses = DefaultSecretAccesses
log.Debug().Int("accesses", meta.Accesses).Msg("using default number of accesses")
} else {
meta.Accesses = req.Accesses
log.Debug().Int("accesses", meta.Accesses).Msg("using user supplied number of accesses")
}

// Compute the expiration time from the request
if req.Lifetime == v1.Duration(0) {
meta.Expires = meta.Created.Add(DefaultSecretLifetime)
log.Debug().Dur("ttl", DefaultSecretLifetime).Msg("using default secret lifetime")
} else {
meta.Expires = meta.Created.Add(time.Duration(req.Lifetime))
log.Debug().Dur("ttl", time.Duration(req.Lifetime)).Msg("using user supplied secret lifetime")
}

// Create the reply back to the user
rep := &v1.CreateSecretReply{
Expires: meta.Expires,
}

// Make a random URL to store the secret in
if rep.Token, err = GenerateUniqueURL(); err != nil {
log.Error().Err(err).Msg("could not generate unique token for secret")
// Create the secret in the vault.
if err = meta.New(context.TODO(), req.Secret); err != nil {
log.Error().Err(err).Msg("could not create new secret in vault")
c.JSON(http.StatusInternalServerError, ErrorResponse(err))
return
}

// Store the password in the database
tmpSecretsStore[rep.Token] = req.Secret
tmpSecretsMeta[rep.Token] = meta

// Return success
c.JSON(http.StatusCreated, rep)
// Return successful reply back to the user
c.JSON(http.StatusCreated, &v1.CreateSecretReply{
Token: token,
Expires: meta.Expires,
})
}

// FetchSecret handles an incoming fetch secret request and attempts to retrieve the
// secret from the database and return it to the user. This function also handles the
// password and ensures that a 404 is returned to obfuscate the existence of the secret
// on bad requests.
func (s *Server) FetchSecret(c *gin.Context) {
// Fetch the meta with the token
// Prepare to fetch the meta with the token and password from the request
token := c.Param("token")
meta, ok := tmpSecretsMeta[token]
if !ok {
c.JSON(http.StatusNotFound, ErrorResponse("secret not found"))
return
}

// Check the secret is valid prior to returning a response (in case a sidechannel
// retrieval or race condition failed to destroy the password).
if !meta.Valid() {
log.Warn().Msg("race condition or invalid secret metadata fetched, destroying")
delete(tmpSecretsMeta, token)
delete(tmpSecretsStore, token)
c.JSON(http.StatusNotFound, ErrorResponse("secret not found"))
return
}

// Check the password if it is required
if meta.Password != "" {
// A password is required as an Authorization: Bearer <token> header where the
// token is the base64 encoded password. Basic auth does not apply here since
// there is no username associated with the secret.
password := ParseBearerToken(c.GetHeader("Authorization"))

// If no password is specified return unauthorized
if password == "" {
c.JSON(http.StatusUnauthorized, ErrorResponse("password required for secret"))
return
}

// Use derived key algorithm to perform a password verification
verified, err := VerifyDerivedKey(meta.Password, password)
if err != nil {
log.Error().Err(err).Msg("could not verify dervied key")
meta := s.vault.With(token)
password := ParseBearerToken(c.GetHeader("Authorization"))
log.Debug().Bool("authorization", password != "").Msg("beginning fetch")

// Attempt to retrieve the secret from the database
secret, err := meta.Fetch(context.TODO(), password)
if err != nil {
switch err {
case ErrSecretNotFound:
c.JSON(http.StatusNotFound, ErrorResponse(err))
case ErrNotAuthorized:
c.JSON(http.StatusUnauthorized, ErrorResponse(err))
default:
log.Error().Err(err).Msg("could not fetch secret")
c.JSON(http.StatusInternalServerError, ErrorResponse(err))
return
}
if !verified {
c.JSON(http.StatusUnauthorized, ErrorResponse("invalid password"))
return
}
return
}

// Update metadata with the access info
meta.Access()

// Create the secret reply
rep := v1.FetchSecretReply{
Secret: secret,
Filename: meta.Filename,
IsBase64: meta.IsBase64,
Created: meta.Created,
Accesses: meta.Retrievals,
}

// Fetch the secret with the token
if rep.Secret, ok = tmpSecretsStore[token]; !ok {
c.JSON(http.StatusInternalServerError, ErrorResponse("unhandled secret store error"))
return
}

// Cleanup if necessary
if !meta.Valid() {
delete(tmpSecretsMeta, token)
delete(tmpSecretsStore, token)
}

// Return the successful reply
c.JSON(http.StatusOK, rep)
}

// DestroySecret handles an incoming destroy secret request and attempts to delete the
// secret from the database. This RPC is password protected in the same way fetch is.
func (s *Server) DestroySecret(c *gin.Context) {
// Fetch the meta with the token
// Prepare to fetch the meta with the token and password from the request
token := c.Param("token")
meta, ok := tmpSecretsMeta[token]
if !ok {
c.JSON(http.StatusNotFound, ErrorResponse("secret not found"))
return
}

// Check the secret is valid prior to returning a response (in case a sidechannel
// retrieval or race condition failed to destroy the password).
if !meta.Valid() {
log.Warn().Msg("race condition or invalid secret metadata fetched, destroying")
delete(tmpSecretsMeta, token)
delete(tmpSecretsStore, token)
c.JSON(http.StatusNotFound, ErrorResponse("secret not found"))
return
}
meta := s.vault.With(token)
password := ParseBearerToken(c.GetHeader("Authorization"))
log.Debug().Bool("authorization", password != "").Msg("beginning destroy")

// Check the password if it is required
if meta.Password != "" {
// A password is required as an Authorization: Bearer <token> header where the
// token is the base64 encoded password. Basic auth does not apply here since
// there is no username associated with the secret.
password := ParseBearerToken(c.GetHeader("Authorization"))

// If no password is specified return unauthorized
if password == "" {
c.JSON(http.StatusUnauthorized, ErrorResponse("password required for secret"))
return
}

// Use derived key algorithm to perform a password verification
verified, err := VerifyDerivedKey(meta.Password, password)
if err != nil {
log.Error().Err(err).Msg("could not verify dervied key")
// Delete the secret from the database
// Attempt to retrieve the secret from the database
err := meta.Destroy(context.TODO(), password)
if err != nil {
switch err {
case ErrSecretNotFound:
c.JSON(http.StatusNotFound, ErrorResponse(err))
case ErrNotAuthorized:
c.JSON(http.StatusUnauthorized, ErrorResponse(err))
default:
log.Error().Err(err).Msg("could not destroy secret")
c.JSON(http.StatusInternalServerError, ErrorResponse(err))
return
}
if !verified {
c.JSON(http.StatusUnauthorized, ErrorResponse("invalid password"))
return
}
return
}

// Delete the secret from the database
delete(tmpSecretsMeta, token)
delete(tmpSecretsStore, token)

// Return the successful reply
c.JSON(http.StatusOK, &v1.DestroySecretReply{Destroyed: true})
}
Expand All @@ -224,7 +165,7 @@ const (
// URL-safe string and determines if it is in the database or not. If it finds a
// collision it attempts to find a unique string for a fixed number of attempts before
// quitting.
func GenerateUniqueURL() (token string, err error) {
func (s *Server) GenerateUniqueURL(ctx context.Context) (token string, err error) {
for i := 0; i < generateUniqueAttempts; i++ {
// Create a random array of bytes
buf := make([]byte, generateUniqueLength)
Expand All @@ -236,7 +177,12 @@ func GenerateUniqueURL() (token string, err error) {
token := base64.RawURLEncoding.EncodeToString(buf)

// Check if the token exists already in the database
if _, ok := tmpSecretsStore[token]; !ok {
var exists bool
if exists, err = s.vault.Check(ctx, token); err != nil {
return "", err
}

if !exists {
return token, nil
}
}
Expand Down
17 changes: 0 additions & 17 deletions pkg/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,6 @@ import (
"github.com/stretchr/testify/require"
)

func TestGenerateUniqueURL(t *testing.T) {
tokens := make(map[string]struct{})
for i := 0; i < 48; i++ {
// Generate token
token, err := GenerateUniqueURL()
require.NoError(t, err)
require.Len(t, token, 43)

// Make sure token is unique
_, ok := tokens[token]
require.False(t, ok)

// Add token to unique set
tokens[token] = struct{}{}
}
}

func TestParseBearerToken(t *testing.T) {
password := base64.URLEncoding.EncodeToString([]byte("supersecretsquirrel"))
tt := []struct {
Expand Down
Loading