diff --git a/cassette/cassette.go b/cassette/cassette.go index a6e0e58..7ddead2 100644 --- a/cassette/cassette.go +++ b/cassette/cassette.go @@ -332,7 +332,6 @@ func getEncryptionMarker(data []byte) string { // Decrypt is a utility function that decrypts the cassette raw data // with the use of the supplied crypter. -// TODO: move this method to the encryption package?? func Decrypt(data []byte, crypter Crypter) ([]byte, error) { encMarker := getEncryptionMarker(data) markerLen := len(encMarker) diff --git a/cassette/track/http.go b/cassette/track/http.go index 759b6ff..b14704a 100644 --- a/cassette/track/http.go +++ b/cassette/track/http.go @@ -184,6 +184,8 @@ type Response struct { Body []byte ContentLength int64 TransferEncoding []string + Close bool + Uncompressed bool Trailer http.Header TLS *tls.ConnectionState @@ -219,6 +221,8 @@ func ToResponse(httpResponse *http.Response) *Response { Body: bodyClone, ContentLength: httpResponse.ContentLength, TransferEncoding: tsfEncodingClone, + Close: httpResponse.Close, + Uncompressed: httpResponse.Uncompressed, Trailer: trailerClone, TLS: tlsClone, } diff --git a/cassette/track/track.go b/cassette/track/track.go index dff28ba..5fd7643 100644 --- a/cassette/track/track.go +++ b/cassette/track/track.go @@ -137,7 +137,8 @@ func (trk Track) ToHTTPResponse() *http.Response { httpResponse.Body = bodyReadCloser httpResponse.ContentLength = trk.Response.ContentLength httpResponse.TransferEncoding = trk.Response.TransferEncoding - // TODO: Close, Uncompressed ? + httpResponse.Close = trk.Response.Close + httpResponse.Uncompressed = trk.Response.Uncompressed httpResponse.Trailer = trk.Response.Trailer httpResponse.TLS = trk.Response.TLS diff --git a/cassette/track/track_test.go b/cassette/track/track_test.go index 6732af2..5367ffd 100644 --- a/cassette/track/track_test.go +++ b/cassette/track/track_test.go @@ -77,6 +77,8 @@ func TestTrack_ToHTTPResponse(t *testing.T) { Body: []byte("resp body"), ContentLength: 826, TransferEncoding: []string{"resp tsf_encoding"}, + Close: true, + Uncompressed: true, Trailer: map[string][]string{ "resptr1": {"resptrva11", "resptrva22"}, }, @@ -104,8 +106,8 @@ func TestTrack_ToHTTPResponse(t *testing.T) { Body: nil, // we assert Body separately because it's an io.Reader ContentLength: 826, TransferEncoding: []string{"resp tsf_encoding"}, - Close: false, - Uncompressed: false, + Close: true, + Uncompressed: true, Trailer: map[string][]string{ "resptr1": {"resptrva11", "resptrva22"}, }, diff --git a/encryption/aesgcm.go b/encryption/aesgcm.go index f0e1d09..4e13dbb 100644 --- a/encryption/aesgcm.go +++ b/encryption/aesgcm.go @@ -4,6 +4,8 @@ import ( "crypto/aes" "crypto/cipher" + "github.com/pkg/errors" + cryptoerr "github.com/seborama/govcr/v12/encryption/errors" ) @@ -21,8 +23,6 @@ func NewAESGCMWithRandomNonceGenerator(key []byte) (*Crypter, error) { // // If you want to convert a passphrase to a key, use a suitable // package like bcrypt or scrypt. -// -// TODO: add a nonceGenerator validator i.e. call it 1000 times, ensures no dupes. func NewAESGCM(key []byte, nonceGenerator NonceGenerator) (*Crypter, error) { if len(key) != 16 && len(key) != 32 { return nil, cryptoerr.NewErrCrypto("key size is not 16 or 32 bytes") @@ -42,5 +42,9 @@ func NewAESGCM(key []byte, nonceGenerator NonceGenerator) (*Crypter, error) { nonceGenerator = NewRandomNonceGenerator(aesgcm.NonceSize()) } + if err = validateNonceGenerator(nonceGenerator); err != nil { + return nil, errors.Wrap(err, "nonce generator is not valid") + } + return NewCrypter(aesgcm, "aesgcm", nonceGenerator), nil } diff --git a/encryption/chacha20poly1305.go b/encryption/chacha20poly1305.go index 3f0c37d..928cdce 100644 --- a/encryption/chacha20poly1305.go +++ b/encryption/chacha20poly1305.go @@ -1,6 +1,7 @@ package encryption import ( + "github.com/pkg/errors" "golang.org/x/crypto/chacha20poly1305" ) @@ -17,8 +18,6 @@ func NewChaCha20Poly1305WithRandomNonceGenerator(key []byte) (*Crypter, error) { // The key should be 32 bytes long. // // If you want to convert a passphrase to a key, you can use a function such as Argon2. -// -// TODO: add a nonceGenerator validator i.e. call it 1000 times, ensures no dupes. func NewChaCha20Poly1305(key []byte, nonceGenerator NonceGenerator) (*Crypter, error) { cc20px, err := chacha20poly1305.NewX(key) if err != nil { @@ -29,5 +28,9 @@ func NewChaCha20Poly1305(key []byte, nonceGenerator NonceGenerator) (*Crypter, e nonceGenerator = NewRandomNonceGenerator(cc20px.NonceSize()) } + if err = validateNonceGenerator(nonceGenerator); err != nil { + return nil, errors.Wrap(err, "nonce generator is not valid") + } + return NewCrypter(cc20px, "chacha20poly1305", nonceGenerator), nil } diff --git a/encryption/nonce.go b/encryption/nonce.go index 04843de..5a25b4c 100644 --- a/encryption/nonce.go +++ b/encryption/nonce.go @@ -2,7 +2,10 @@ package encryption import ( "crypto/rand" + "fmt" "io" + + "github.com/pkg/errors" ) // RandomNonceGenerator is a random generator of nonce of the specified size. @@ -28,3 +31,23 @@ func (ng RandomNonceGenerator) Generate() ([]byte, error) { return nonce, nil } + +func validateNonceGenerator(nonceGenerator NonceGenerator) error { + nonces := make(map[string]struct{}) + + for i := 1; i <= 1000; i++ { + n, err := nonceGenerator.Generate() + if err != nil { + return errors.Wrap(err, "nonceGenerator failure") + } + + nStr := fmt.Sprintf("%x", n) + if _, ok := nonces[nStr]; ok { + return errors.New("nonceGenerator produces frequent duplicates") + } + + nonces[nStr] = struct{}{} + } + + return nil +} diff --git a/encryption/nonce_wb_test.go b/encryption/nonce_wb_test.go new file mode 100644 index 0000000..33fd6c0 --- /dev/null +++ b/encryption/nonce_wb_test.go @@ -0,0 +1,37 @@ +package encryption + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func Test_validateNonceGenerator_Passes(t *testing.T) { + err := validateNonceGenerator(NewRandomNonceGenerator(32)) + assert.NoError(t, err) +} + +func Test_validateNonceGenerator_Fails_NonceGenErr(t *testing.T) { + err := validateNonceGenerator(brokenNonceGenerator{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "nonceGenerator failure: broken nonce") +} + +func Test_validateNonceGenerator_Fails_WeakNonceGen(t *testing.T) { + err := validateNonceGenerator(weakNonceGenerator{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "nonceGenerator produces frequent duplicates") +} + +type brokenNonceGenerator struct{} + +func (ng brokenNonceGenerator) Generate() ([]byte, error) { + return nil, errors.New("broken nonce") +} + +type weakNonceGenerator struct{} + +func (ng weakNonceGenerator) Generate() ([]byte, error) { + return []byte("static nonce"), nil +} diff --git a/pcb.go b/pcb.go index 6ccb57a..c5da6cb 100644 --- a/pcb.go +++ b/pcb.go @@ -155,10 +155,6 @@ func (pcb *PrintedCircuitBoard) ClearReplayingMutators() { // RequestMatcher is an interface that exposes the method to perform request comparison. // request comparison involves the HTTP request and the track request recorded on cassette. -// -// TODO: -// there could be a case to have RequestMatchers (plural) that would work akin to track.Mutators. -// I.e. they could be chained and conditional. type RequestMatcher interface { Match(httpRequest, trackRequest *track.Request) bool }