Skip to content

Commit

Permalink
feat: SetCipher (#99)
Browse files Browse the repository at this point in the history
  • Loading branch information
seborama committed Aug 28, 2022
1 parent c4379c3 commit ab75a40
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 14 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ vcr := govcr.NewVCR(

### Recipe: VCR with encrypted cassette - custom nonce generator

This is nearly identical to the previous recipe "VCR with encrypted cassette", except we pass our custom nonce generator.
This is nearly identical to the recipe ["VCR with encrypted cassette"](#recipe-vcr-with-encrypted-cassette), except we pass our custom nonce generator.

Example (this can also be achieved in the same way with the `ControlPanel`):

Expand Down Expand Up @@ -452,7 +452,17 @@ govcr decrypt -cassette-file my.cassette.json -key-file my.key

### Recipe: Changing cassette encryption

TODO
The cassette cipher can be changed for another with `SetCipher`.

For safety reasons, you cannot use `SetCipher` to remove encryption and decrypt the cassette. See the [cassette decryption recipe](#recipe-cassette-decryption) for that.

```go
vcr := govcr.NewVCR(...)
err := vcr.SetCipher(
encryption.NewChaCha20Poly1305WithRandomNonceGenerator,
"my_secret.key",
)
```

[(toc)](#table-of-content)

Expand Down Expand Up @@ -525,7 +535,7 @@ Recording and replaying track mutators are the same. The only difference is when

To set recording mutators, use `govcr.WithTrackRecordingMutators` when creating a new `VCR`, or use the `SetRecordingMutators` or `AddRecordingMutators` methods of the `ControlPanel` that is returned by `NewVCR`.

See the "VCR with a replaying Track Mutator" recipe for the general approach on creating a track mutator. You can also take a look at the "Remove Response TLS" recipe.
See the recipe ["VCR with a replaying Track Mutator"](#recipe-vcr-with-a-replaying-track-mutator) for the general approach on creating a track mutator. You can also take a look at the recipe ["Remove Response TLS"](#recipe-remove-response-tls).

[(toc)](#table-of-content)

Expand Down
9 changes: 9 additions & 0 deletions cassette/cassette.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,15 @@ func (k7 *Cassette) DecryptionFilter(data []byte) ([]byte, error) {
return Decrypt(data, k7.crypter)
}

// SetCrypter sets the cassette Crypter.
// This can be used to set a cipher when none is present (which already happens automatically
// when loading a cassette) or change the cipher when one is already present.
// The cassette is saved to persist the change with the new selected cipher.
func (k7 *Cassette) SetCrypter(crypter Crypter) error {
k7.crypter = crypter
return k7.save()
}

// Track retrieves the requested track number.
// '0' is the first track.
func (k7 *Cassette) Track(trackNumber int32) track.Track {
Expand Down
8 changes: 8 additions & 0 deletions controlpanel.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ func (controlPanel *ControlPanel) SetLiveOnlyMode() {
controlPanel.vcrTransport().SetLiveOnlyMode()
}

// SetCipher sets the cassette Cipher.
// This can be used to set a cipher when none is present (which already happens automatically
// when loading a cassette) or change the cipher when one is already present.
// The cassette is automatically saved with the new selected cipher.
func (controlPanel *ControlPanel) SetCipher(crypter CrypterProvider, keyFile string) error {
return controlPanel.vcrTransport().SetCipher(crypter, keyFile)
}

// AddRecordingMutators adds a set of recording Track Mutator's to the VCR.
func (controlPanel *ControlPanel) AddRecordingMutators(trackMutators ...track.Mutator) {
controlPanel.vcrTransport().AddRecordingMutators(trackMutators...)
Expand Down
34 changes: 23 additions & 11 deletions govcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"net/http"
"os"

"github.com/pkg/errors"

"github.com/seborama/govcr/v12/cassette"
"github.com/seborama/govcr/v12/encryption"
)
Expand Down Expand Up @@ -47,12 +49,7 @@ func (cb *CassetteLoader) WithCipher(crypter CrypterProvider, keyFile string) *C
// customer nonce generator.
// Using more than one WithCipher* on the same cassette is ambiguous.
func (cb *CassetteLoader) WithCipherCustomNonce(crypterNonce CrypterNonceProvider, keyFile string, nonceGenerator encryption.NonceGenerator) *CassetteLoader {
key, err := os.ReadFile(keyFile)
if err != nil {
panic(fmt.Sprintf("%+v", err))
}

cr, err := crypterNonce(key, nonceGenerator)
cr, err := makeCrypter(crypterNonce, keyFile, nonceGenerator)
if err != nil {
panic(fmt.Sprintf("%+v", err))
}
Expand All @@ -62,22 +59,37 @@ func (cb *CassetteLoader) WithCipherCustomNonce(crypterNonce CrypterNonceProvide
return cb
}

// WithCassette is an optional functional parameter to provide a VCR with
// a cassette to load.
// Cassette options may be provided (e.g. cryptography).
func (cb *CassetteLoader) make() *cassette.Cassette {
func (cb *CassetteLoader) load() *cassette.Cassette {
if cb == nil {
panic("please select a cassette for the VCR")
}

return cassette.LoadCassette(cb.cassetteName, cb.opts...)
}

func makeCrypter(crypterNonce CrypterNonceProvider, keyFile string, nonceGenerator encryption.NonceGenerator) (*encryption.Crypter, error) {
if crypterNonce == nil {
return nil, errors.New("a cipher must be supplied for encryption, `nil` is not permitted")
}

key, err := os.ReadFile(keyFile)
if err != nil {
return nil, errors.WithStack(err)
}

cr, err := crypterNonce(key, nonceGenerator)
if err != nil {
return nil, errors.WithStack(err)
}

return cr, nil
}

// NewVCR creates a new VCR.
func NewVCR(cassetteLoader *CassetteLoader, settings ...Setting) *ControlPanel {
var vcrSettings VCRSettings

vcrSettings.cassette = cassetteLoader.make()
vcrSettings.cassette = cassetteLoader.load()

for _, option := range settings {
option(&vcrSettings)
Expand Down
63 changes: 63 additions & 0 deletions govcr_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package govcr_test

import (
"bytes"
"fmt"
"io"
"math/rand"
"net/http"
"net/http/httptest"
"os"
Expand All @@ -15,6 +17,7 @@ import (
"github.com/stretchr/testify/suite"

"github.com/seborama/govcr/v12"
"github.com/seborama/govcr/v12/encryption"
"github.com/seborama/govcr/v12/stats"
)

Expand Down Expand Up @@ -93,6 +96,66 @@ func TestVCRControlPanel_HTTPClient(t *testing.T) {
assert.IsType(t, (*http.Client)(nil), unit)
}

func TestSetCrypto(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, "Hello: %d\n", rand.Intn(1e9))
}))

const cassetteName = "./temp-fixtures/TestSetCrypto.cassette"

_ = os.Remove(cassetteName)

// first, create an unencrypted cassette
vcr := govcr.NewVCR(govcr.NewCassetteLoader(cassetteName))

// add a track to the cassette to trigger its creation in the first place
resp, err := vcr.HTTPClient().Get(testServer.URL)
require.NoError(t, err)

_ = resp.Body.Close()

assert.Equal(t, "not encrypted", getCassetteCrypto(cassetteName))

// encrypt cassette with AESGCM
err = vcr.SetCipher(
encryption.NewAESGCMWithRandomNonceGenerator,
"test-fixtures/TestSetCrypto.1.key",
)
require.NoError(t, err)

assert.Equal(t, "aesgcm", getCassetteCrypto(cassetteName))

// re-encrypt cassette with ChaCha20Poly1305
err = vcr.SetCipher(
encryption.NewChaCha20Poly1305WithRandomNonceGenerator,
"test-fixtures/TestSetCrypto.2.key",
)
require.NoError(t, err)

assert.Equal(t, "chacha20poly1305", getCassetteCrypto(cassetteName))

// lastly, attempt to decrypt cassette - this is not permitted
err = vcr.SetCipher(nil, "")
require.Error(t, err)
}

func getCassetteCrypto(cassetteName string) string {
data, err := os.ReadFile(cassetteName)
if err != nil {
panic(err)
}

marker := "$ENC:V2$"

if !bytes.HasPrefix(data, []byte(marker)) {
return "not encrypted"
}

pos := len(marker)
cipherNameLen := int(data[len(marker)])
return string(data[pos+1 : pos+1+cipherNameLen])
}

type GoVCRTestSuite struct {
suite.Suite

Expand Down
3 changes: 3 additions & 0 deletions test-fixtures/TestSetCrypto.1.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.�ci(�����8�*��
��ш�eu��
cڏ�
1 change: 1 addition & 0 deletions test-fixtures/TestSetCrypto.2.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A��T<��B�!�e8�ƽ��GaJ�a9
27 changes: 27 additions & 0 deletions vcrtransport.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/seborama/govcr/v12/cassette"
"github.com/seborama/govcr/v12/cassette/track"
"github.com/seborama/govcr/v12/encryption"
govcrerr "github.com/seborama/govcr/v12/errors"
"github.com/seborama/govcr/v12/stats"
)
Expand Down Expand Up @@ -95,6 +96,32 @@ func (t *vcrTransport) SetLiveOnlyMode() {
t.pcb.SetLiveOnlyMode()
}

// SetCipher sets the cassette Cipher.
// This can be used to set a cipher when none is present (which already happens automatically
// when loading a cassette) or change the cipher when one is already present.
// The cassette is automatically saved with the new selected cipher.
func (t *vcrTransport) SetCipher(crypter CrypterProvider, keyFile string) error {
f := func(key []byte, nonceGenerator encryption.NonceGenerator) (*encryption.Crypter, error) {
// a "CrypterProvider" is a CrypterNonceProvider with a pre-defined / default nonceGenerator
return crypter(key)
}

return t.SetCipherCustomNonce(f, keyFile, nil)
}

// SetCipherCustomNonce sets the cassette Cipher.
// This can be used to set a cipher when none is present (which already happens automatically
// when loading a cassette) or change the cipher when one is already present.
// The cassette is automatically saved with the new selected cipher.
func (t *vcrTransport) SetCipherCustomNonce(crypter CrypterNonceProvider, keyFile string, nonceGenerator encryption.NonceGenerator) error {
cr, err := makeCrypter(crypter, keyFile, nonceGenerator)
if err != nil {
return err
}

return t.cassette.SetCrypter(cr)
}

// AddRecordingMutators adds a set of recording Track Mutator's to the VCR.
func (t *vcrTransport) AddRecordingMutators(mutators ...track.Mutator) {
t.pcb.AddRecordingMutators(mutators...)
Expand Down

0 comments on commit ab75a40

Please sign in to comment.