From ab75a4095060e051af1355eaf16b0b4b60793f84 Mon Sep 17 00:00:00 2001 From: Seb C Date: Sun, 28 Aug 2022 16:14:43 +0100 Subject: [PATCH] feat: SetCipher (#99) --- README.md | 16 ++++++-- cassette/cassette.go | 9 +++++ controlpanel.go | 8 ++++ govcr.go | 34 +++++++++++------ govcr_test.go | 63 +++++++++++++++++++++++++++++++ test-fixtures/TestSetCrypto.1.key | 3 ++ test-fixtures/TestSetCrypto.2.key | 1 + vcrtransport.go | 27 +++++++++++++ 8 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 test-fixtures/TestSetCrypto.1.key create mode 100644 test-fixtures/TestSetCrypto.2.key diff --git a/README.md b/README.md index cda7762..0c388b3 100644 --- a/README.md +++ b/README.md @@ -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`): @@ -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) @@ -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) diff --git a/cassette/cassette.go b/cassette/cassette.go index 9e0784b..a6e0e58 100644 --- a/cassette/cassette.go +++ b/cassette/cassette.go @@ -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 { diff --git a/controlpanel.go b/controlpanel.go index bc9a7f5..1fca0f5 100644 --- a/controlpanel.go +++ b/controlpanel.go @@ -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...) diff --git a/govcr.go b/govcr.go index 7084fe6..fbd9037 100644 --- a/govcr.go +++ b/govcr.go @@ -5,6 +5,8 @@ import ( "net/http" "os" + "github.com/pkg/errors" + "github.com/seborama/govcr/v12/cassette" "github.com/seborama/govcr/v12/encryption" ) @@ -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)) } @@ -62,10 +59,7 @@ 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") } @@ -73,11 +67,29 @@ func (cb *CassetteLoader) make() *cassette.Cassette { 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) diff --git a/govcr_test.go b/govcr_test.go index bda6c30..6bca17c 100644 --- a/govcr_test.go +++ b/govcr_test.go @@ -1,8 +1,10 @@ package govcr_test import ( + "bytes" "fmt" "io" + "math/rand" "net/http" "net/http/httptest" "os" @@ -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" ) @@ -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 diff --git a/test-fixtures/TestSetCrypto.1.key b/test-fixtures/TestSetCrypto.1.key new file mode 100644 index 0000000..4aed2ea --- /dev/null +++ b/test-fixtures/TestSetCrypto.1.key @@ -0,0 +1,3 @@ +.ci(8* +шeu +cڏ \ No newline at end of file diff --git a/test-fixtures/TestSetCrypto.2.key b/test-fixtures/TestSetCrypto.2.key new file mode 100644 index 0000000..5122435 --- /dev/null +++ b/test-fixtures/TestSetCrypto.2.key @@ -0,0 +1 @@ +AT