From 428517fef5b901db7275d9f5a75eda89a4c28e08 Mon Sep 17 00:00:00 2001 From: Lukasz Klich Date: Mon, 16 Oct 2023 17:06:51 +0200 Subject: [PATCH] New crypto implementation (#155) Tests in this file have been order dependent. I broke those dependencies by introducing few additional lines of code in each an every one. And the initHistoryOpts now requires passing pn, so maybe whoever decide to write new tests will realize that it might be nice to create new pubnub and new config instead of reusing them across every test. Co-authored-by: Mateusz Dahlke <39696234+Xavrax@users.noreply.github.com> --- .pubnub.yml | 11 +- CHANGELOG.md | 9 + config.go | 54 +- crypto/aes_cbc_cryptor.go | 90 + crypto/aes_cbc_cryptor_test.go | 77 + crypto/cryptor.go | 25 + crypto/cryptor_header.go | 174 ++ crypto/cryptor_header_test.go | 29 + crypto/decrypting_reader.go | 134 ++ crypto/decrypting_reader_test.go | 53 + crypto/default_extended_cryptor.go | 55 + crypto/encrypting_reader.go | 86 + crypto/encrypting_reader_test.go | 42 + crypto/legacy_cryptor.go | 150 ++ crypto/legacy_cryptor_test.go | 72 + crypto/module.go | 174 ++ crypto/module_test.go | 22 + crypto/utils.go | 61 + fetch_request.go | 4 +- files_download_file.go | 36 +- files_send_file_to_s3.go | 25 +- fire_request.go | 34 +- fire_request_test.go | 2 +- go.mod | 1 + go.sum | 2 + history_request.go | 4 +- history_request_test.go | 76 +- publish_file_message.go | 4 +- publish_request.go | 18 +- publish_request_test.go | 22 +- pubnub.go | 34 +- pubnub_test.go | 27 + subscription_manager.go | 15 +- subscription_manager_test.go | 36 +- tests/contract/contract_test.go | 10 +- tests/contract/crypto_state_test.go | 52 + tests/contract/crypto_steps_test.go | 243 +++ tests/contract/steps_mapping_test.go | 18 + tests/contract/utils_test.go | 3 + tests/e2e/file_upload_dec_output.txt | 352 ---- tests/e2e/file_upload_sample_encrypted.txt | Bin 21904 -> 0 bytes tests/e2e/file_upload_test_output.txt | Bin 21904 -> 21879 bytes tests/e2e/files_test.go | 34 - tests/e2e/helper.go | 8 + tests/e2e/objectsV2_test.go | 1958 ++------------------ tests/e2e/publish_test.go | 6 +- utils.go | 120 ++ utils/crypto.go | 333 +--- utils/crypto_test.go | 19 + 49 files changed, 2141 insertions(+), 2673 deletions(-) create mode 100644 crypto/aes_cbc_cryptor.go create mode 100644 crypto/aes_cbc_cryptor_test.go create mode 100644 crypto/cryptor.go create mode 100644 crypto/cryptor_header.go create mode 100644 crypto/cryptor_header_test.go create mode 100644 crypto/decrypting_reader.go create mode 100644 crypto/decrypting_reader_test.go create mode 100644 crypto/default_extended_cryptor.go create mode 100644 crypto/encrypting_reader.go create mode 100644 crypto/encrypting_reader_test.go create mode 100644 crypto/legacy_cryptor.go create mode 100644 crypto/legacy_cryptor_test.go create mode 100644 crypto/module.go create mode 100644 crypto/module_test.go create mode 100644 crypto/utils.go create mode 100644 tests/contract/crypto_state_test.go create mode 100644 tests/contract/crypto_steps_test.go delete mode 100644 tests/e2e/file_upload_dec_output.txt delete mode 100644 tests/e2e/file_upload_sample_encrypted.txt create mode 100644 utils.go diff --git a/.pubnub.yml b/.pubnub.yml index c0647476..4d9ddc67 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,6 +1,13 @@ --- -version: v7.1.2 +version: v7.2.0 changelog: + - date: 2023-10-16 + version: v7.2.0 + changes: + - type: feature + text: "Update the crypto module structure and add enhanced AES-CBC cryptor." + - type: bug + text: "Improved security of crypto implementation by increasing the cipher key entropy by a factor of two." - date: 2023-05-11 version: v7.1.2 changes: @@ -733,7 +740,7 @@ sdks: distribution-type: package distribution-repository: GitHub package-name: Go - location: https://github.com/pubnub/go/releases/tag/v7.1.2 + location: https://github.com/pubnub/go/releases/tag/v7.2.0 requires: - name: "Go" diff --git a/CHANGELOG.md b/CHANGELOG.md index beed4af2..074c9bd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## v7.2.0 +October 16 2023 + +#### Added +- Update the crypto module structure and add enhanced AES-CBC cryptor. + +#### Fixed +- Improved security of crypto implementation by increasing the cipher key entropy by a factor of two. + ## v7.1.2 May 11 2023 diff --git a/config.go b/config.go index 67596b9e..03382cb6 100644 --- a/config.go +++ b/config.go @@ -2,6 +2,7 @@ package pubnub import ( "fmt" + "github.com/pubnub/go/v7/crypto" "log" "sync" ) @@ -26,30 +27,33 @@ type Config struct { // UUID to be used as a device identifier. // //Deprecated: please use SetUserId/GetUserId - UUID string - CipherKey string // If CipherKey is passed, all communications to/from PubNub will be encrypted. - Secure bool // True to use TLS - ConnectTimeout int // net.Dialer.Timeout - NonSubscribeRequestTimeout int // http.Client.Timeout for non-subscribe requests - SubscribeRequestTimeout int // http.Client.Timeout for subscribe requests only - FileUploadRequestTimeout int // http.Client.Timeout File Upload Request only - HeartbeatInterval int // The frequency of the pings to the server to state that the client is active - PresenceTimeout int // The time after which the server will send a timeout for the client - MaximumReconnectionRetries int // The config sets how many times to retry to reconnect before giving up. - MaximumLatencyDataAge int // Max time to store the latency data for telemetry - FilterExpression string // Feature to subscribe with a custom filter expression. - PNReconnectionPolicy ReconnectionPolicy // Reconnection policy selection - Log *log.Logger // Logger instance - SuppressLeaveEvents bool // When true the SDK doesn't send out the leave requests. - DisablePNOtherProcessing bool // PNOther processing looks for pn_other in the JSON on the recevied message - UseHTTP2 bool // HTTP2 Flag - MessageQueueOverflowCount int // When the limit is exceeded by the number of messages received in a single subscribe request, a status event PNRequestMessageCountExceededCategory is fired. - MaxIdleConnsPerHost int // Used to set the value of HTTP Transport's MaxIdleConnsPerHost. - MaxWorkers int // Number of max workers for Publish and Grant requests - UsePAMV3 bool // Use PAM version 2, Objects requets would still use PAM v3 - StoreTokensOnGrant bool // Will store grant v3 tokens in token manager for further use. - FileMessagePublishRetryLimit int // The number of tries made in case of Publish File Message failure. - UseRandomInitializationVector bool // When true the IV will be random for all requests and not just file upload. When false the IV will be hardcoded for all requests except File Upload + UUID string + //DEPRECATED: please use CryptoModule + CipherKey string // If CipherKey is passed, all communications to/from PubNub will be encrypted. + Secure bool // True to use TLS + ConnectTimeout int // net.Dialer.Timeout + NonSubscribeRequestTimeout int // http.Client.Timeout for non-subscribe requests + SubscribeRequestTimeout int // http.Client.Timeout for subscribe requests only + FileUploadRequestTimeout int // http.Client.Timeout File Upload Request only + HeartbeatInterval int // The frequency of the pings to the server to state that the client is active + PresenceTimeout int // The time after which the server will send a timeout for the client + MaximumReconnectionRetries int // The config sets how many times to retry to reconnect before giving up. + MaximumLatencyDataAge int // Max time to store the latency data for telemetry + FilterExpression string // Feature to subscribe with a custom filter expression. + PNReconnectionPolicy ReconnectionPolicy // Reconnection policy selection + Log *log.Logger // Logger instance + SuppressLeaveEvents bool // When true the SDK doesn't send out the leave requests. + DisablePNOtherProcessing bool // PNOther processing looks for pn_other in the JSON on the recevied message + UseHTTP2 bool // HTTP2 Flag + MessageQueueOverflowCount int // When the limit is exceeded by the number of messages received in a single subscribe request, a status event PNRequestMessageCountExceededCategory is fired. + MaxIdleConnsPerHost int // Used to set the value of HTTP Transport's MaxIdleConnsPerHost. + MaxWorkers int // Number of max workers for Publish and Grant requests + UsePAMV3 bool // Use PAM version 2, Objects requets would still use PAM v3 + StoreTokensOnGrant bool // Will store grant v3 tokens in token manager for further use. + FileMessagePublishRetryLimit int // The number of tries made in case of Publish File Message failure. + //DEPRECATED: please use CryptoModule + UseRandomInitializationVector bool // When true the IV will be random for all requests and not just file upload. When false the IV will be hardcoded for all requests except File Upload + CryptoModule crypto.CryptoModule // A cryptography module used for encryption and decryption } // NewDemoConfig initiates the config with demo keys, for tests only. @@ -90,7 +94,7 @@ func NewConfigWithUserId(userId UserId) *Config { return &c } -//Deprecated: Please use NewConfigWithUserId +// Deprecated: Please use NewConfigWithUserId func NewConfig(uuid string) *Config { return NewConfigWithUserId(UserId(uuid)) } diff --git a/crypto/aes_cbc_cryptor.go b/crypto/aes_cbc_cryptor.go new file mode 100644 index 00000000..98673aa4 --- /dev/null +++ b/crypto/aes_cbc_cryptor.go @@ -0,0 +1,90 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "errors" + "fmt" + "io" +) + +type aesCbcCryptor struct { + block cipher.Block +} + +func NewAesCbcCryptor(cipherKey string) (ExtendedCryptor, error) { + block, e := aesCipher(cipherKey) + if e != nil { + return nil, e + } + + return &aesCbcCryptor{ + block: block, + }, nil +} + +var crivId = "ACRH" + +func (c *aesCbcCryptor) Id() string { + return crivId +} + +func (c *aesCbcCryptor) Encrypt(message []byte) (*EncryptedData, error) { + message = padWithPKCS7(message) + iv := generateIV(aes.BlockSize) + blockmode := cipher.NewCBCEncrypter(c.block, iv) + + encryptedBytes := make([]byte, len(message)) + blockmode.CryptBlocks(encryptedBytes, message) + + return &EncryptedData{ + Metadata: iv, + Data: encryptedBytes, + }, nil +} + +func (c *aesCbcCryptor) Decrypt(encryptedData *EncryptedData) (r []byte, e error) { + decrypter := cipher.NewCBCDecrypter(c.block, encryptedData.Metadata) + //to handle decryption errors + defer func() { + if rec := recover(); rec != nil { + r, e = nil, fmt.Errorf("%s", rec) + } + }() + + if len(encryptedData.Data)%16 != 0 { + return nil, fmt.Errorf("encrypted data length should be divisible by block size") + } + + decrypted := make([]byte, len(encryptedData.Data)) + decrypter.CryptBlocks(decrypted, encryptedData.Data) + return unpadPKCS7(decrypted) +} + +func (c *aesCbcCryptor) EncryptStream(reader io.Reader) (*EncryptedStreamData, error) { + iv := generateIV(aes.BlockSize) + + return &EncryptedStreamData{ + Metadata: iv, + Reader: newBlockModeEncryptingReader(reader, cipher.NewCBCEncrypter(c.block, iv)), + }, nil +} + +func (c *aesCbcCryptor) DecryptStream(encryptedData *EncryptedStreamData) (io.Reader, error) { + if encryptedData.Metadata == nil { + return nil, errors.New("missing metadata") + } + return newBlockModeDecryptingReader(encryptedData.Reader, cipher.NewCBCDecrypter(c.block, encryptedData.Metadata)), nil +} + +func aesCipher(cipherKey string) (cipher.Block, error) { + hash := sha256.New() + hash.Write([]byte(cipherKey)) + + block, err := aes.NewCipher(hash.Sum(nil)) + if err != nil { + return nil, err + } + return block, nil +} diff --git a/crypto/aes_cbc_cryptor_test.go b/crypto/aes_cbc_cryptor_test.go new file mode 100644 index 00000000..ea17d909 --- /dev/null +++ b/crypto/aes_cbc_cryptor_test.go @@ -0,0 +1,77 @@ +package crypto + +import ( + "bytes" + "io" + "testing" + "testing/quick" +) + +var defaultPropertyTestConfig = &quick.Config{ + MaxCount: 1000, +} + +func canDecryptEncryptStreamResult(in []byte) bool { + cryptor, e := NewAesCbcCryptor("enigma") + if e != nil { + return false + } + + output, err := cryptor.EncryptStream(bytes.NewReader(in)) + if err != nil { + return false + } + + encrData, err := io.ReadAll(output.Reader) + + decrypted, err := cryptor.Decrypt(&EncryptedData{ + Data: encrData, + Metadata: output.Metadata, + }) + if err != nil { + return false + } + return bytes.Equal(in, decrypted) +} + +func canDecryptStreamEncryptResult(in []byte) bool { + cryptor, e := NewAesCbcCryptor("enigma") + if e != nil { + return false + } + + output, err := cryptor.Encrypt(in) + if err != nil { + println(err.Error()) + return false + } + + decryptingReader, err := cryptor.DecryptStream(&EncryptedStreamData{ + Reader: bytes.NewReader(output.Data), + Metadata: output.Metadata, + }) + if err != nil { + println(err.Error()) + return false + } + + decrypted, err := io.ReadAll(decryptingReader) + if err != nil { + println(err.Error()) + return false + } + + return bytes.Equal(in, decrypted) +} + +func Test_AesCBC_EncryptStream(t *testing.T) { + if err := quick.Check(canDecryptEncryptStreamResult, defaultPropertyTestConfig); err != nil { + t.Error(err) + } +} + +func Test_AesCBC_DecryptStream(t *testing.T) { + if err := quick.Check(canDecryptStreamEncryptResult, defaultPropertyTestConfig); err != nil { + t.Error(err) + } +} diff --git a/crypto/cryptor.go b/crypto/cryptor.go new file mode 100644 index 00000000..3eb1c434 --- /dev/null +++ b/crypto/cryptor.go @@ -0,0 +1,25 @@ +package crypto + +import "io" + +type EncryptedData struct { + Metadata []byte + Data []byte +} + +type Cryptor interface { + Id() string + Encrypt(message []byte) (*EncryptedData, error) + Decrypt(encryptedData *EncryptedData) ([]byte, error) +} + +type EncryptedStreamData struct { + Metadata []byte + Reader io.Reader +} + +type ExtendedCryptor interface { + Cryptor + EncryptStream(reader io.Reader) (*EncryptedStreamData, error) + DecryptStream(encryptedData *EncryptedStreamData) (io.Reader, error) +} diff --git a/crypto/cryptor_header.go b/crypto/cryptor_header.go new file mode 100644 index 00000000..f391f50e --- /dev/null +++ b/crypto/cryptor_header.go @@ -0,0 +1,174 @@ +package crypto + +import ( + "bufio" + "bytes" + "encoding/binary" + "fmt" + "io" + math "math" + "strconv" +) + +const versionPosition = 4 +const versionV1 = 1 +const cryptorIdPosition = 5 +const cryptorIdLength = 4 +const sizePosition = 9 +const shortSizeLength = 1 +const longSizeLength = 3 +const longSizeIndicator = 0xFF +const maxShortSize = 254 +const sentinelLength = 4 + +var sentinel = [sentinelLength]byte{0x50, 0x4E, 0x45, 0x44} + +func headerV1(cryptorId string, metadata []byte) ([]byte, error) { + cryptorDataSize := len(metadata) + var cryptorDataBytesSize int + + if cryptorDataSize < longSizeIndicator { + cryptorDataBytesSize = shortSizeLength + } else if cryptorDataSize < math.MaxUint16 { + cryptorDataBytesSize = longSizeLength + } else { + return nil, fmt.Errorf("size of cryptor metadata too large %d", cryptorDataSize) + } + r := make([]byte, 0, len(sentinel)+1+cryptorIdLength+cryptorDataBytesSize+cryptorDataSize) + + buffer := bytes.NewBuffer(r) + _, e := buffer.Write(sentinel[:]) + if e != nil { + return nil, e + } + e = buffer.WriteByte(versionV1) + if e != nil { + return nil, e + } + + _, e = buffer.Write([]byte(cryptorId)) + if e != nil { + return nil, e + } + if cryptorDataBytesSize == shortSizeLength { + e = buffer.WriteByte(byte(cryptorDataSize)) + if e != nil { + return nil, e + } + } else { + e = buffer.WriteByte(longSizeIndicator) + if e != nil { + return nil, e + } + sizeBytes := make([]byte, 2) + binary.BigEndian.PutUint16(sizeBytes, uint16(cryptorDataSize)) + _, e = buffer.Write(sizeBytes) + if e != nil { + return nil, e + } + } + _, e = buffer.Write(metadata) + if e != nil { + return nil, e + } + return buffer.Bytes(), nil +} + +func peekHeaderCryptorId(data []byte) (cryptorId *string, e error) { + if len(data) < len(sentinel) || !bytes.Equal(data[:len(sentinel)], sentinel[:]) { + return &legacyId, nil + } + + if data[versionPosition] != versionV1 { + return nil, unsupportedHeaderVersion(int(data[versionPosition])) + } + + id := string(data[cryptorIdPosition : cryptorIdPosition+cryptorIdLength]) + return &id, nil +} + +func parseHeader(data []byte) (cryptorId *string, encrData *EncryptedData, e error) { + id, err := peekHeaderCryptorId(data) + if err != nil { + return nil, nil, err + } + if (*id) == legacyId { + return id, &EncryptedData{Data: data, Metadata: nil}, nil + } + var headerSize int64 + position := int64(sizePosition) + if data[sizePosition] == longSizeIndicator { + position += longSizeLength + + headerSize, err = strconv.ParseInt(string(data[sizePosition:sizePosition+longSizeLength]), 10, 32) + if err != nil { + return nil, nil, err + } + } else { + position += shortSizeLength + headerSize = int64(data[sizePosition]) + } + + metadata := data[position : position+headerSize] + position += int64(len(metadata)) + + if int64(len(data)) < position { + return nil, nil, fmt.Errorf("decryption error: %w", e) + } + + return id, &EncryptedData{Data: data[position:], Metadata: metadata}, nil +} + +func parseHeaderStream(bufData *bufio.Reader) (cryptorId *string, encrypted *EncryptedStreamData, e error) { + peeked, err := bufData.Peek(sentinelLength + 1 + cryptorIdLength + longSizeLength) + if err != nil { + return nil, nil, fmt.Errorf("decryption error: %w", err) + } + + id, err := peekHeaderCryptorId(peeked) + if err != nil { + return nil, nil, err + } + + if (*id) == legacyId { + return id, &EncryptedStreamData{ + Reader: bufData, + Metadata: nil, + }, nil + } + + var metadataSize int64 + position := int64(sizePosition) + if peeked[sizePosition] == longSizeIndicator { + position += longSizeLength + var e error + metadataSize, e = strconv.ParseInt(string(peeked[sizePosition:sizePosition+longSizeLength]), 10, 32) + if e != nil { + return nil, nil, e + } + } else { + position += shortSizeLength + metadataSize = int64(peeked[sizePosition]) + } + if metadataSize > 254 { + _, e := bufData.Discard(sentinelLength + 1 + cryptorIdLength + longSizeLength) + if e != nil { + return nil, nil, e + } + } else { + _, e := bufData.Discard(sentinelLength + 1 + cryptorIdLength + shortSizeLength) + if e != nil { + return nil, nil, e + } + } + m := make([]byte, metadataSize) + _, e = io.ReadFull(bufData, m) + if e != nil { + return nil, nil, e + } + + return id, &EncryptedStreamData{ + Reader: bufData, + Metadata: m, + }, nil +} diff --git a/crypto/cryptor_header_test.go b/crypto/cryptor_header_test.go new file mode 100644 index 00000000..d2493ba4 --- /dev/null +++ b/crypto/cryptor_header_test.go @@ -0,0 +1,29 @@ +package crypto + +import ( + "bytes" + "github.com/stretchr/testify/assert" + "math" + "testing" +) + +func TestCryptorHeader_CreateHeaderWithLargeMetadata(t *testing.T) { + metadata := make([]byte, 512) + header, _ := headerV1("abcd", metadata) + cryptorDataSize := header[sentinelLength+1+cryptorIdLength+longSizeLength-2 : sentinelLength+1+cryptorIdLength+longSizeLength] + assert.True(t, bytes.Equal([]byte{0xff, 0x02, 0x00}, cryptorDataSize)) +} + +func TestCryptorHeader_CreateHeaderWithSmallMetadata(t *testing.T) { + metadata := make([]byte, 128) + header, _ := headerV1("abcd", metadata) + assert.Equal(t, byte(0x80), header[sizePosition]) +} + +func TestCryptorHeader_WithTooLargeMetadata(t *testing.T) { + metadata := make([]byte, math.MaxUint16+255) + _, e := headerV1("abcd", metadata) + if e == nil { + assert.Fail(t, "expected error") + } +} diff --git a/crypto/decrypting_reader.go b/crypto/decrypting_reader.go new file mode 100644 index 00000000..95bc71df --- /dev/null +++ b/crypto/decrypting_reader.go @@ -0,0 +1,134 @@ +package crypto + +import ( + "bufio" + "bytes" + "crypto/cipher" + "errors" + "io" +) + +func newBlockModeDecryptingReader(r io.Reader, mode cipher.BlockMode) io.Reader { + return &blockModeDecryptingReader{ + r: bufio.NewReader(r), + blockMode: mode, + buffer: bytes.NewBuffer(nil), + err: nil, + } +} + +type blockModeDecryptingReader struct { + r *bufio.Reader + blockMode cipher.BlockMode + buffer *bytes.Buffer + err error +} + +func (decryptingReader *blockModeDecryptingReader) readNextBlock() ([]byte, error) { + reader := decryptingReader.r + + output := make([]byte, decryptingReader.blockMode.BlockSize()) + sizeOfCurrentlyRead, readErr := io.ReadFull(reader, output) + if readErr != nil && readErr != io.EOF { + return nil, readErr + } + + if sizeOfCurrentlyRead == 0 && readErr == io.EOF { + return nil, io.EOF + } + + if _, e := reader.Peek(1); e == io.EOF { + return output[:sizeOfCurrentlyRead], io.EOF + } + + return output, nil +} + +func (decryptingReader *blockModeDecryptingReader) decryptUntilPFull(p []byte) (n int, err error) { + var copied int + var block []byte + var e error + alreadyWrote := 0 + for alreadyWrote < len(p) { + block, e = decryptingReader.readNextBlock() + decryptingReader.err = e + + if errors.Is(e, io.ErrUnexpectedEOF) { + return alreadyWrote, errors.New("encrypted data length is not a multiple of the block size") + } + + if e != nil && e != io.EOF { + return alreadyWrote, e + } + + decryptingReader.blockMode.CryptBlocks(block, block) + + if bytes.Equal(block, bytes.Repeat([]byte{byte(decryptingReader.blockMode.BlockSize())}, len(block))) { + break + } else if e == io.EOF { + unpadded, unpadErr := unpadPKCS7(block) + if len(unpadded) > 0 { + block = unpadded + copied = copy(p[alreadyWrote:], block) + alreadyWrote += copied + } + + if unpadErr != nil { + return alreadyWrote, unpadErr + } + if copied < len(block) { + decryptingReader.buffer.Write(block[copied:]) + } + break + } else { + copied = copy(p[alreadyWrote:], block) + alreadyWrote += copied + if copied < len(block) { + decryptingReader.buffer.Write(block[copied:]) + } + } + } + + return alreadyWrote, nil +} + +func readFromBufferUntilEmpty(buffer *bytes.Buffer, p []byte) int { + buffered := buffer.Next(len(p)) + if len(buffered) == 0 { + return 0 + } + return copy(p, buffered) +} + +func (decryptingReader *blockModeDecryptingReader) readFromBufferUntilEmpty(p []byte) (n int, err error) { + buffered := decryptingReader.buffer.Next(len(p)) + var alreadyWrote int + if len(buffered) > 0 { + alreadyWrote = copy(p, buffered) + } + + if decryptingReader.err != nil { + return alreadyWrote, decryptingReader.err + } + + return alreadyWrote, nil +} + +func (decryptingReader *blockModeDecryptingReader) Read(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, errors.New("cannot read into empty buffer") + } + + if (decryptingReader.err != nil) && decryptingReader.buffer.Len() == 0 { + return 0, decryptingReader.err + } + + alreadyWrote := readFromBufferUntilEmpty(decryptingReader.buffer, p) + + if decryptingReader.err != nil { + return alreadyWrote, nil + } + + n, e := decryptingReader.decryptUntilPFull(p[alreadyWrote:]) + return alreadyWrote + n, e +} diff --git a/crypto/decrypting_reader_test.go b/crypto/decrypting_reader_test.go new file mode 100644 index 00000000..0d5018bf --- /dev/null +++ b/crypto/decrypting_reader_test.go @@ -0,0 +1,53 @@ +package crypto + +import ( + "bytes" + "io" + "testing" + "testing/quick" +) + +type DoNothingBlockMode struct { +} + +func (d *DoNothingBlockMode) BlockSize() int { + return 16 +} + +func (d *DoNothingBlockMode) CryptBlocks(dst, src []byte) { + copy(dst, src) +} + +func canReadDifferentSizeOfChunks(in []byte, bufferSize uint8) bool { + inPadded := padWithPKCS7VarBlock(in, 16) + if bufferSize == 0 { + return true + } + decryptingReader := newBlockModeDecryptingReader(bytes.NewReader(inPadded), &DoNothingBlockMode{}) + readDataBuffer := bytes.NewBuffer(nil) + buffer := make([]byte, bufferSize) + numberOfReadBytes := 0 + + var e error + var readBytes int + + for e == nil { + readBytes, e = decryptingReader.Read(buffer) + numberOfReadBytes += readBytes + readDataBuffer.Write(buffer[:readBytes]) + } + + if e != nil && e != io.EOF { + return false + } + + out := readDataBuffer.Bytes()[:numberOfReadBytes] + + return bytes.Equal(in, out) +} + +func Test_DecryptingReader_ReadDifferentSizeOfBuffers(t *testing.T) { + if err := quick.Check(canReadDifferentSizeOfChunks, defaultPropertyTestConfig); err != nil { + t.Error(err) + } +} diff --git a/crypto/default_extended_cryptor.go b/crypto/default_extended_cryptor.go new file mode 100644 index 00000000..290d3454 --- /dev/null +++ b/crypto/default_extended_cryptor.go @@ -0,0 +1,55 @@ +package crypto + +import ( + "bytes" + "io" +) + +type defaultExtendedCryptor struct { + Cryptor +} + +func liftToExtendedCryptor(cryptor Cryptor) ExtendedCryptor { + if extendedCryptor, ok := cryptor.(ExtendedCryptor); ok { + return extendedCryptor + } else { + return newDefaultExtendedCryptor(cryptor) + } +} + +func newDefaultExtendedCryptor(cryptor Cryptor) ExtendedCryptor { + return &defaultExtendedCryptor{ + cryptor, + } +} + +func (c *defaultExtendedCryptor) EncryptStream(input io.Reader) (*EncryptedStreamData, error) { + inputBytes, e := io.ReadAll(input) + if e != nil { + return nil, e + } + + encryptedData, e := c.Cryptor.Encrypt(inputBytes) + if e != nil { + return nil, e + } + + return &EncryptedStreamData{ + Reader: bytes.NewReader(encryptedData.Data), + Metadata: encryptedData.Metadata, + }, nil +} + +func (c *defaultExtendedCryptor) DecryptStream(input *EncryptedStreamData) (io.Reader, error) { + inputBytes, e := io.ReadAll(input.Reader) + if e != nil { + return nil, e + } + + decryptedData, e := c.Cryptor.Decrypt(&EncryptedData{Data: inputBytes, Metadata: input.Metadata}) + if e != nil { + return nil, e + } + + return bytes.NewReader(decryptedData), nil +} diff --git a/crypto/encrypting_reader.go b/crypto/encrypting_reader.go new file mode 100644 index 00000000..f113cdfb --- /dev/null +++ b/crypto/encrypting_reader.go @@ -0,0 +1,86 @@ +package crypto + +import ( + "bufio" + "bytes" + "crypto/cipher" + "errors" + "io" +) + +func newBlockModeEncryptingReader(r io.Reader, mode cipher.BlockMode) io.Reader { + return &blockModeEncryptingReader{ + r: bufio.NewReader(r), + blockMode: mode, + buffer: bytes.NewBuffer(nil), + err: nil, + } +} + +type blockModeEncryptingReader struct { + r *bufio.Reader + blockMode cipher.BlockMode + buffer *bytes.Buffer + err error +} + +func (encryptingReader *blockModeEncryptingReader) readNextBlockPadded() (read []byte, err error) { + output := make([]byte, encryptingReader.blockMode.BlockSize()) + sizeOfCurrentlyRead, readErr := io.ReadFull(encryptingReader.r, output) + if readErr != nil && readErr != io.EOF && !errors.Is(readErr, io.ErrUnexpectedEOF) { + return nil, readErr + } + + if sizeOfCurrentlyRead == 0 && readErr == io.EOF { + return padWithPKCS7VarBlock(nil, encryptingReader.blockMode.BlockSize()), io.EOF + } + + if errors.Is(readErr, io.ErrUnexpectedEOF) { + return padWithPKCS7VarBlock(output[:sizeOfCurrentlyRead], encryptingReader.blockMode.BlockSize()), io.EOF + } + + return output, nil +} + +func (encryptingReader *blockModeEncryptingReader) encryptUntilPFull(p []byte) (int, error) { + var alreadyWrote int + for alreadyWrote <= len(p) { + block, e := encryptingReader.readNextBlockPadded() + if e != nil && e != io.EOF { + return alreadyWrote, e + } + encryptingReader.err = e + + encryptingReader.blockMode.CryptBlocks(block, block) + copied := copy(p[alreadyWrote:], block) + alreadyWrote += copied + if copied < len(block) { + encryptingReader.buffer.Write(block[copied:]) + } + if e == io.EOF { + break + } + } + + return alreadyWrote, nil +} + +func (encryptingReader *blockModeEncryptingReader) Read(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, errors.New("cannot read into empty buffer") + } + + if encryptingReader.err != nil && encryptingReader.buffer.Len() == 0 { + return 0, encryptingReader.err + } + + alreadyWrote := readFromBufferUntilEmpty(encryptingReader.buffer, p) + + if encryptingReader.err != nil { + return alreadyWrote, nil + } + + n, e := encryptingReader.encryptUntilPFull(p[alreadyWrote:]) + + return alreadyWrote + n, e +} diff --git a/crypto/encrypting_reader_test.go b/crypto/encrypting_reader_test.go new file mode 100644 index 00000000..45dfb3e3 --- /dev/null +++ b/crypto/encrypting_reader_test.go @@ -0,0 +1,42 @@ +package crypto + +import ( + "bytes" + "io" + "testing" + "testing/quick" +) + +func encryptingReaderCanReadDifferentSizeOfChunks(in []byte, bufferSize uint8) bool { + inPadded := padWithPKCS7VarBlock(in, 16) + if bufferSize == 0 { + return true + } + encryptingReader := newBlockModeEncryptingReader(bytes.NewReader(in), &DoNothingBlockMode{}) + readDataBuffer := bytes.NewBuffer(nil) + buffer := make([]byte, bufferSize) + numberOfReadBytes := 0 + + var e error + var readBytes int + + for e == nil { + readBytes, e = encryptingReader.Read(buffer) + numberOfReadBytes += readBytes + readDataBuffer.Write(buffer[:readBytes]) + } + + if e != nil && e != io.EOF { + return false + } + + out := readDataBuffer.Bytes()[:numberOfReadBytes] + + return bytes.Equal(inPadded, out) +} + +func Test_EncryptingReader_ReadDifferentSizeOfBuffers(t *testing.T) { + if err := quick.Check(encryptingReaderCanReadDifferentSizeOfChunks, defaultPropertyTestConfig); err != nil { + t.Error(err) + } +} diff --git a/crypto/legacy_cryptor.go b/crypto/legacy_cryptor.go new file mode 100644 index 00000000..a7226fb9 --- /dev/null +++ b/crypto/legacy_cryptor.go @@ -0,0 +1,150 @@ +package crypto + +import ( + "bufio" + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" +) + +// 16 byte constant legacy IV +var valIV = "0123456789012345" + +type legacyCryptor struct { + block cipher.Block + randomIv bool +} + +func NewLegacyCryptor(cipherKey string, useRandomIV bool) (ExtendedCryptor, error) { + block, e := legacyAesCipher(cipherKey) + if e != nil { + return nil, e + } + + return &legacyCryptor{ + block: block, + randomIv: useRandomIV, + }, nil +} + +var legacyId = string([]byte{0x00, 0x00, 0x00, 0x00}) + +func (c *legacyCryptor) Id() string { + return legacyId +} + +func (c *legacyCryptor) Encrypt(message []byte) (*EncryptedData, error) { + message = padWithPKCS7(message) + iv := make([]byte, aes.BlockSize) + if c.randomIv { + iv = generateIV(aes.BlockSize) + } else { + iv = []byte(valIV) + } + blockmode := cipher.NewCBCEncrypter(c.block, iv) + + encryptedBytes := make([]byte, len(message)) + blockmode.CryptBlocks(encryptedBytes, message) + + if c.randomIv { + return &EncryptedData{Data: append(iv, encryptedBytes...), Metadata: nil}, nil + } + return &EncryptedData{Data: encryptedBytes, Metadata: nil}, nil +} + +func (c *legacyCryptor) Decrypt(encryptedData *EncryptedData) (r []byte, e error) { + iv := make([]byte, aes.BlockSize) + data := encryptedData.Data + if c.randomIv { + iv = data[:aes.BlockSize] + data = data[aes.BlockSize:] + } else { + iv = []byte(valIV) + } + + if len(data)%16 != 0 { + return nil, fmt.Errorf("length of data to decrypt should be divisible by block size") + } + + decrypter := cipher.NewCBCDecrypter(c.block, iv) + //to handle decryption errors + defer func() { + if rec := recover(); rec != nil { + r, e = nil, fmt.Errorf("decrypt error: %s", rec) + } + }() + + decrypted := make([]byte, len(data)) + decrypter.CryptBlocks(decrypted, data) + val, err := unpadPKCS7(decrypted) + if err != nil { + return nil, fmt.Errorf("decrypt error: %s", err) + } + + return val, nil +} + +func (c *legacyCryptor) EncryptStream(reader io.Reader) (*EncryptedStreamData, error) { + iv := generateIV(aes.BlockSize) + + return &EncryptedStreamData{ + Metadata: nil, + Reader: io.MultiReader(bytes.NewReader(iv), newBlockModeEncryptingReader(reader, cipher.NewCBCEncrypter(c.block, iv))), + }, nil +} + +func (c *legacyCryptor) DecryptStream(encryptedData *EncryptedStreamData) (io.Reader, error) { + iv := make([]byte, aes.BlockSize) + _, err := io.ReadFull(encryptedData.Reader, iv) + if err != nil { + return nil, err + } + + bufReader := bufio.NewReader(encryptedData.Reader) + peeked, e := bufReader.Peek(1) + if len(peeked) == 0 { + return nil, errors.New("can't decrypt empty data") + } + + if e != nil { + return nil, e + } + + return newBlockModeDecryptingReader(encryptedData.Reader, cipher.NewCBCDecrypter(c.block, iv)), nil +} + +// EncryptCipherKey DEPRECATED +// EncryptCipherKey creates the 256 bit hex of the cipher key +// +// It accepts the following parameters: +// cipherKey: cipher key to use to decrypt. +// +// returns the 256 bit hex of the cipher key. +func EncryptCipherKey(cipherKey string) []byte { + hash := sha256.New() + hash.Write([]byte(cipherKey)) + + sha256String := hash.Sum(nil)[:16] + return []byte(hex.EncodeToString(sha256String)) +} + +// legacyAesCipher returns the cipher block +// +// It accepts the following parameters: +// cipherKey: cipher key. +// +// returns the cipher block, +// error if any. +func legacyAesCipher(cipherKey string) (cipher.Block, error) { + key := EncryptCipherKey(cipherKey) + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return block, nil +} diff --git a/crypto/legacy_cryptor_test.go b/crypto/legacy_cryptor_test.go new file mode 100644 index 00000000..f2fc0fd0 --- /dev/null +++ b/crypto/legacy_cryptor_test.go @@ -0,0 +1,72 @@ +package crypto + +import ( + "bytes" + "io" + "testing" + "testing/quick" +) + +func legacyCanDecryptEncryptStreamResult(in []byte) bool { + cryptor, e := NewLegacyCryptor("enigma", true) + if e != nil { + return false + } + output, err := cryptor.EncryptStream(bytes.NewReader(in)) + if err != nil { + return false + } + + encr, err := io.ReadAll(output.Reader) + + decrypted, err := cryptor.Decrypt(&EncryptedData{ + Data: encr, + Metadata: output.Metadata, + }) + if err != nil { + return false + } + return bytes.Equal(in, decrypted) +} + +func legacyCanDecryptStreamEncryptResult(in []byte) bool { + cryptor, e := NewLegacyCryptor("enigma", true) + if e != nil { + return false + } + + output, err := cryptor.Encrypt(in) + if err != nil { + println(err.Error()) + return false + } + + decryptingReader, err := cryptor.DecryptStream(&EncryptedStreamData{ + Reader: bytes.NewReader(output.Data), + Metadata: output.Metadata, + }) + if err != nil { + println(err.Error()) + return false + } + + decrypted, err := io.ReadAll(decryptingReader) + if err != nil { + println(err.Error()) + return false + } + + return bytes.Equal(in, decrypted) +} + +func Test_Legacy_EncryptStream(t *testing.T) { + if err := quick.Check(legacyCanDecryptEncryptStreamResult, defaultPropertyTestConfig); err != nil { + t.Error(err) + } +} + +func Test_Legacy_DecryptStream(t *testing.T) { + if err := quick.Check(legacyCanDecryptStreamEncryptResult, defaultPropertyTestConfig); err != nil { + t.Error(err) + } +} diff --git a/crypto/module.go b/crypto/module.go new file mode 100644 index 00000000..95229194 --- /dev/null +++ b/crypto/module.go @@ -0,0 +1,174 @@ +package crypto + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" +) + +// CryptoModule is an interface for encrypting and decrypting data. +type CryptoModule interface { + Encrypt(input []byte) ([]byte, error) + Decrypt(input []byte) ([]byte, error) + EncryptStream(input io.Reader) (io.Reader, error) + DecryptStream(input io.Reader) (io.Reader, error) +} + +type module struct { + encryptor ExtendedCryptor + decryptors map[string]ExtendedCryptor +} + +func NewLegacyCryptoModule(cipherKey string, randomIv bool) (CryptoModule, error) { + legacy, e := NewLegacyCryptor(cipherKey, randomIv) + if e != nil { + return nil, e + } + aesCbc, e := NewAesCbcCryptor(cipherKey) + + if e != nil { + return nil, e + } + + return NewCryptoModule(legacy, []Cryptor{aesCbc}), nil +} + +func NewAesCbcCryptoModule(cipherKey string, randomIv bool) (CryptoModule, error) { + aesCbc, e := NewAesCbcCryptor(cipherKey) + if e != nil { + return nil, e + } + + legacy, e := NewLegacyCryptor(cipherKey, randomIv) + if e != nil { + return nil, e + } + + return NewCryptoModule(aesCbc, []Cryptor{legacy}), nil +} + +func NewCryptoModule(defaultCryptor Cryptor, decryptors []Cryptor) CryptoModule { + + decryptorsMap := make(map[string]ExtendedCryptor, len(decryptors)+1) + for _, d := range decryptors { + decryptorsMap[d.Id()] = liftToExtendedCryptor(d) + } + + encryptor := liftToExtendedCryptor(defaultCryptor) + decryptorsMap[encryptor.Id()] = encryptor + + return &module{ + encryptor: encryptor, + decryptors: decryptorsMap, + } +} + +func (m *module) Encrypt(message []byte) ([]byte, error) { + if len(message) == 0 { + return nil, errors.New("encryption error: can't encrypt empty data") + } + encryptedData, e := m.encryptor.Encrypt(message) + if e != nil { + return nil, fmt.Errorf("encryption error: %s", e.Error()) + } + + if m.encryptor.Id() == legacyId { + return encryptedData.Data, nil + } + + header, e := headerV1(m.encryptor.Id(), encryptedData.Metadata) + if e != nil { + return nil, e + } + + return append(header, encryptedData.Data...), nil +} + +func (m *module) Decrypt(data []byte) ([]byte, error) { + if len(data) == 0 { + return nil, errors.New("decryption error: can't decrypt empty data") + } + + id, encryptedData, e := parseHeader(data) + if e != nil { + return nil, e + } + + decryptor := m.decryptors[*id] + + if decryptor == nil { + return nil, fmt.Errorf("unknown crypto error: unknown cryptor id %s", *id) + } + + if len(encryptedData.Data) == 0 { + return nil, errors.New("decryption error: can't decrypt empty data") + } + + var r []byte + if r, e = m.decryptors[*id].Decrypt(encryptedData); e != nil { + return nil, fmt.Errorf("decryption error: %s", e.Error()) + } + + return r, nil +} + +func (m *module) EncryptStream(input io.Reader) (io.Reader, error) { + bufferedReader := bufio.NewReader(input) + peekedBytes, e := bufferedReader.Peek(1) + if len(peekedBytes) == 0 { + return nil, errors.New("encryption error: can't encrypt empty data") + } + + encryptedStreamData, e := m.encryptor.EncryptStream(bufferedReader) + if e != nil { + return nil, e + } + if m.encryptor.Id() == legacyId { + return encryptedStreamData.Reader, nil + } + header, e := headerV1(m.encryptor.Id(), encryptedStreamData.Metadata) + if e != nil { + return nil, e + } + + headerReader := bytes.NewReader(header) + + return io.MultiReader(headerReader, encryptedStreamData.Reader), nil +} + +func (m *module) DecryptStream(input io.Reader) (io.Reader, error) { + bufData := bufio.NewReader(input) + + id, encryptedStreamData, e := parseHeaderStream(bufData) + if e != nil { + return nil, e + } + + peeked, e := bufData.Peek(1) + if e != nil { + return nil, fmt.Errorf("decryption error: %w", e) + } + + if len(peeked) == 0 { + return nil, errors.New("decryption error: can't decrypt empty data") + } + + decryptor := m.decryptors[*id] + + if decryptor == nil { + return nil, fmt.Errorf("unknown crypto error: unknown cryptor id %s", *id) + } + + if e != nil { + return nil, fmt.Errorf("decryption error: %s", e.Error()) + } + + var r io.Reader + if r, e = m.decryptors[*id].DecryptStream(encryptedStreamData); e != nil { + return nil, fmt.Errorf("decryption error: %s", e.Error()) + } + + return r, nil +} diff --git a/crypto/module_test.go b/crypto/module_test.go new file mode 100644 index 00000000..8f402e3e --- /dev/null +++ b/crypto/module_test.go @@ -0,0 +1,22 @@ +package crypto + +import ( + "encoding/base64" + "testing" +) + +func TestToProduceData(t *testing.T) { + cipherKey := "myCipherKey" + textToEncrypt := "Hello world encrypted with " + + legacyModuleStaticIv, _ := NewLegacyCryptoModule(cipherKey, false) + legacyModuleRandomIv, _ := NewLegacyCryptoModule(cipherKey, true) + aesCbcModule, _ := NewAesCbcCryptoModule(cipherKey, true) + r1, _ := legacyModuleStaticIv.Encrypt([]byte(textToEncrypt + "legacyModuleStaticIv")) + r2, _ := legacyModuleRandomIv.Encrypt([]byte(textToEncrypt + "legacyModuleRandomIv")) + r3, _ := aesCbcModule.Encrypt([]byte(textToEncrypt + "aesCbcModule")) + + println(base64.StdEncoding.EncodeToString(r1)) + println(base64.StdEncoding.EncodeToString(r2)) + println(base64.StdEncoding.EncodeToString(r3)) +} diff --git a/crypto/utils.go b/crypto/utils.go new file mode 100644 index 00000000..fdf9132e --- /dev/null +++ b/crypto/utils.go @@ -0,0 +1,61 @@ +package crypto + +import ( + "bytes" + "crypto/rand" + "fmt" +) + +func generateIV(blocksize int) []byte { + iv := make([]byte, blocksize) + if _, err := rand.Read(iv); err != nil { + panic(err) + } + return iv +} + +func padWithPKCS7VarBlock(data []byte, blocklen int) []byte { + padlen := 1 + for ((len(data) + padlen) % blocklen) != 0 { + padlen = padlen + 1 + } + + pad := bytes.Repeat([]byte{byte(padlen)}, padlen) + return append(data, pad...) +} + +// padWithPKCS7 pads the data as per the PKCS7 standard +// It accepts the following parameters: +// data: data to pad as byte array. +// returns the padded data as byte array. +func padWithPKCS7(data []byte) []byte { + return padWithPKCS7VarBlock(data, 16) +} + +// unpadPKCS7 unpads the data as per the PKCS7 standard +// It accepts the following parameters: +// data: data to unpad as byte array. +// returns the unpadded data as byte array. +func unpadPKCS7(data []byte) ([]byte, error) { + blocklen := 16 + if len(data)%blocklen != 0 || len(data) == 0 { + return nil, fmt.Errorf("invalid data len %d", len(data)) + } + padlen := int(data[len(data)-1]) + if padlen > blocklen || padlen == 0 { + return nil, fmt.Errorf("padding is invalid") + } + // check padding + pad := data[len(data)-padlen:] + for i := 0; i < padlen; i++ { + if pad[i] != byte(padlen) { + return nil, fmt.Errorf("padding is invalid") + } + } + + return data[:len(data)-padlen], nil +} + +func unsupportedHeaderVersion(version int) error { + return fmt.Errorf("unknown crypto error: unsupported crypto header version %d", version) +} diff --git a/fetch_request.go b/fetch_request.go index 93947617..879c547b 100644 --- a/fetch_request.go +++ b/fetch_request.go @@ -278,7 +278,7 @@ func (o *fetchOpts) parseMessageActions(actions interface{}) map[string]PNHistor return resp } -//{"status": 200, "error": false, "error_message": "", "channels": {"ch1":[{"message_type": "", "message": {"text": "hey"}, "timetoken": "15959610984115342", "meta": "", "uuid": "db9c5e39-7c95-40f5-8d71-125765b6f561"}]}} +// {"status": 200, "error": false, "error_message": "", "channels": {"ch1":[{"message_type": "", "message": {"text": "hey"}, "timetoken": "15959610984115342", "meta": "", "uuid": "db9c5e39-7c95-40f5-8d71-125765b6f561"}]}} func (o *fetchOpts) fetchMessages(channels map[string]interface{}) map[string][]FetchResponseItem { messages := make(map[string][]FetchResponseItem, len(channels)) @@ -290,7 +290,7 @@ func (o *fetchOpts) fetchMessages(channels map[string]interface{}) map[string][] for _, val := range histResponseMap { if histResponse, ok3 := val.(map[string]interface{}); ok3 { - msg, _ := parseCipherInterface(histResponse["message"], o.pubnub.Config) + msg, _ := parseCipherInterface(histResponse["message"], o.pubnub.Config, o.pubnub.getCryptoModule()) histItem := FetchResponseItem{ Message: msg, diff --git a/files_download_file.go b/files_download_file.go index 5c3e51ef..260dab2f 100644 --- a/files_download_file.go +++ b/files_download_file.go @@ -2,12 +2,10 @@ package pubnub import ( "fmt" + "github.com/pubnub/go/v7/crypto" "io" "net/http" "net/url" - "strconv" - - "github.com/pubnub/go/v7/utils" ) var emptyDownloadFileResponse *PNDownloadFileResponse @@ -93,32 +91,32 @@ func (b *downloadFileBuilder) Execute() (*PNDownloadFileResponse, StatusResponse stat.StatusCode = resp.StatusCode return nil, stat, err } - contentLenEnc, err := strconv.ParseInt(string(resp.Header.Get("Content-Length")), 10, 64) - if err != nil { - b.opts.pubnub.Config.Log.Printf("err in parsing content length %s", err) - return nil, stat, err - } var respDL *PNDownloadFileResponse - if b.opts.CipherKey != "" { - r, w := io.Pipe() - utils.DecryptFile(b.opts.CipherKey, contentLenEnc, resp.Body, w) + if b.opts.CipherKey == "" && b.opts.pubnub.getCryptoModule() == nil { respDL = &PNDownloadFileResponse{ - File: r, + File: resp.Body, + } + } else { + var e error + cryptoModule := b.opts.pubnub.getCryptoModule() + if b.opts.CipherKey != "" { + cryptoModule, e = crypto.NewLegacyCryptoModule(b.opts.CipherKey, true) + if e != nil { + return nil, stat, e + } } - } else if b.opts.pubnub.Config.CipherKey != "" { - r, w := io.Pipe() - utils.DecryptFile(b.opts.pubnub.Config.CipherKey, contentLenEnc, resp.Body, w) + r, e := cryptoModule.DecryptStream(resp.Body) + if e != nil { + return nil, stat, e + } respDL = &PNDownloadFileResponse{ File: r, } - } else { - respDL = &PNDownloadFileResponse{ - File: resp.Body, - } } + return respDL, stat, nil } diff --git a/files_send_file_to_s3.go b/files_send_file_to_s3.go index 442adca8..3d0df4a8 100644 --- a/files_send_file_to_s3.go +++ b/files_send_file_to_s3.go @@ -3,6 +3,7 @@ package pubnub import ( "bytes" "encoding/json" + "github.com/pubnub/go/v7/crypto" "io" "io/ioutil" "mime/multipart" @@ -11,7 +12,6 @@ import ( "os" "github.com/pubnub/go/v7/pnerr" - "github.com/pubnub/go/v7/utils" ) var emptySendFileToS3Response *PNSendFileToS3Response @@ -131,17 +131,30 @@ func (o *sendFileToS3Opts) buildBodyMultipartFileUpload() (bytes.Buffer, *multip return bytes.Buffer{}, writer, s, errFilePart } - if o.CipherKey != "" { - utils.EncryptFile(o.CipherKey, []byte{}, filePart, o.File) - } else if o.pubnub.Config.CipherKey != "" { - utils.EncryptFile(o.pubnub.Config.CipherKey, []byte{}, filePart, o.File) - } else { + if o.CipherKey == "" && o.pubnub.getCryptoModule() == nil { _, errIOCopy := io.Copy(filePart, o.File) if errIOCopy != nil { o.pubnub.Config.Log.Printf("ERROR: io Copy error: %s\n", errIOCopy.Error()) return bytes.Buffer{}, writer, s, errIOCopy } + } else { + var e error + cryptoModule := o.pubnub.getCryptoModule() + + if o.CipherKey != "" { + cryptoModule, e = crypto.NewLegacyCryptoModule(o.CipherKey, true) + if e != nil { + o.pubnub.Config.Log.Printf("ERROR: %s\n", e.Error()) + return bytes.Buffer{}, writer, s, e + } + } + + e = encryptStreamAndCopyTo(cryptoModule, o.File, filePart) + if e != nil { + o.pubnub.Config.Log.Printf("ERROR: %s\n", e.Error()) + return bytes.Buffer{}, writer, s, e + } } errWriterClose := writer.Close() diff --git a/fire_request.go b/fire_request.go index fbe0709d..5481c7b6 100644 --- a/fire_request.go +++ b/fire_request.go @@ -157,15 +157,18 @@ func (o *fireOpts) buildPath() (string, error) { var message []byte var err error - if cipherKey := o.pubnub.Config.CipherKey; cipherKey != "" { - msg := utils.EncryptString(cipherKey, string(message), o.pubnub.Config.UseRandomInitializationVector) - - o.Message = []byte(msg) - } - - message, err = utils.ValueAsString(o.Message) - if err != nil { - return "", err + if o.pubnub.getCryptoModule() != nil { + var msg string + if msg, err = serializeEncryptAndSerialize(o.pubnub.getCryptoModule(), o.Message, o.Serialize); err != nil { + o.pubnub.Config.Log.Printf("error in serializing: %v\n", err) + return "", err + } + message = []byte(msg) + } else { + message, err = utils.ValueAsString(o.Message) + if err != nil { + return "", err + } } return fmt.Sprintf(publishGetPath, @@ -212,7 +215,7 @@ func (o *fireOpts) buildBody() ([]byte, error) { if err != nil { return []byte{}, err } - msg = []byte(m) + msg = m } else { if s, ok := o.Message.(string); ok { msg = []byte(s) @@ -222,13 +225,16 @@ func (o *fireOpts) buildBody() ([]byte, error) { } } - if cipherKey := o.pubnub.Config.CipherKey; cipherKey != "" { - enc := utils.EncryptString(cipherKey, string(msg), o.pubnub.Config.UseRandomInitializationVector) - msg, err := utils.ValueAsString(enc) + if o.pubnub.getCryptoModule() != nil { + enc, err := encryptString(o.pubnub.getCryptoModule(), string(msg)) + if err != nil { + return []byte{}, err + } + m, err := utils.ValueAsString(enc) if err != nil { return []byte{}, err } - return []byte(msg), nil + return m, nil } return msg, nil } diff --git a/fire_request_test.go b/fire_request_test.go index 5f3d5e2a..5ad48b7b 100644 --- a/fire_request_test.go +++ b/fire_request_test.go @@ -308,7 +308,7 @@ func TestFireGetAllParameters(t *testing.T) { } func TestFireGetAllParametersCipher(t *testing.T) { message := "test" - AssertSuccessFireGetAllParameters(t, "%22c3dSanMrRnc4ZnNNT1BEaGFnZmd1QT09%22", message, "enigma") + AssertSuccessFireGetAllParameters(t, "%22%2B3AfkVAl8saHsXJdtOhRVQ%3D%3D%22", message, "enigma") } func TestFirePostAllParameters(t *testing.T) { diff --git a/go.mod b/go.mod index 38a328ec..00b3da7f 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/brianolson/cbor_go v1.0.0 github.com/cucumber/godog v0.12.0 github.com/google/uuid v1.3.0 + github.com/joho/godotenv v1.5.1 // indirect github.com/stretchr/testify v1.7.0 golang.org/x/net v0.8.0 golang.org/x/text v0.8.0 // indirect diff --git a/go.sum b/go.sum index e5b99115..242284dd 100644 --- a/go.sum +++ b/go.sum @@ -112,6 +112,8 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= diff --git a/history_request.go b/history_request.go index c6679c57..5c51e51d 100644 --- a/history_request.go +++ b/history_request.go @@ -224,7 +224,7 @@ func getHistoryItemsWithoutTimetoken(historyResponseRaw []byte, o *historyOpts, for i, v := range historyResponseItems { o.pubnub.Config.Log.Println(v) - items[i].Message, _ = parseCipherInterface(v, o.pubnub.Config) + items[i].Message, _ = parseCipherInterface(v, o.pubnub.Config, o.pubnub.getCryptoModule()) } return items, nil } @@ -237,7 +237,7 @@ func getHistoryItemsWithTimetoken(historyResponseItems []HistoryResponseItem, o for i, v := range historyResponseItems { if v.Message != nil { o.pubnub.Config.Log.Println(v.Message) - items[i].Message, _ = parseCipherInterface(v.Message, o.pubnub.Config) + items[i].Message, _ = parseCipherInterface(v.Message, o.pubnub.Config, o.pubnub.getCryptoModule()) o.pubnub.Config.Log.Println(v.Timetoken) items[i].Timetoken = v.Timetoken diff --git a/history_request_test.go b/history_request_test.go index 5ac1dbe1..8eb31d6b 100644 --- a/history_request_test.go +++ b/history_request_test.go @@ -15,7 +15,7 @@ var ( fakeResponseState = StatusResponse{} ) -func initHistoryOpts() *historyOpts { +func (pn *PubNub) initHistoryOpts() *historyOpts { opts := newHistoryOpts(pubnub, pubnub.ctx) opts.Channel = "ch" opts.Start = int64(100000) @@ -25,6 +25,7 @@ func initHistoryOpts() *historyOpts { opts.Reverse = false opts.Count = 3 opts.IncludeTimetoken = true + opts.pubnub = pn return opts } @@ -201,7 +202,9 @@ func TestHistoryResponseParsingStringMessages(t *testing.T) { jsonString := []byte(`[["hey-1","hey-two","hey-1","hey-1","hey-1","hey0","hey1","hey2","hey3","hey4","hey5","hey6","hey7","hey8","hey9","hey10","hey0","hey1","hey2","hey3","hey4","hey5","hey6","hey7","hey8","hey9","hey10","hey0","hey1","hey2","hey3","hey4","hey5","hey6","hey7","hey8","hey9","hey10"],14991775432719844,14991868111600528]`) - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + pn := NewPubNubDemo() + + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Nil(err) assert.Equal(int64(14991775432719844), resp.StartTimetoken) @@ -216,8 +219,9 @@ func TestHistoryResponseParsingStringWithTimetoken(t *testing.T) { assert := assert.New(t) jsonString := []byte(`[[{"timetoken":15232761410327866,"message":"hey-1"},{"timetoken":15232761410327866,"message":"hey-2"},{"timetoken":15232761410327866,"message":"hey-3"}],15232761410327866,15232761410327866]`) + pn := NewPubNubDemo() - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Nil(err) assert.Equal(int64(15232761410327866), resp.StartTimetoken) @@ -236,8 +240,9 @@ func TestHistoryResponseParsingInt(t *testing.T) { assert := assert.New(t) jsonString := []byte(`[[1,2,3,4,5,6,7],14991775432719844,14991868111600528]`) + pn := NewPubNubDemo() - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Nil(err) assert.Equal(int64(14991775432719844), resp.StartTimetoken) @@ -252,7 +257,8 @@ func TestHistoryResponseParsingInt1(t *testing.T) { int1 := int(1) jsonString := []byte(fmt.Sprintf("[[%d],14991775432719844,14991868111600528]", int1)) - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + pn := NewPubNubDemo() + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Nil(err) assert.Equal(int64(14991775432719844), resp.StartTimetoken) @@ -266,8 +272,9 @@ func TestHistoryResponseParsingSlice(t *testing.T) { assert := assert.New(t) jsonString := []byte(`[[[1,2,3],["one","two","three"]],14991775432719844,14991868111600528]`) + pn := NewPubNubDemo() - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Nil(err) assert.Equal(int64(14991775432719844), resp.StartTimetoken) @@ -275,10 +282,11 @@ func TestHistoryResponseParsingSlice(t *testing.T) { func TestHistoryResponseParsingMap(t *testing.T) { assert := assert.New(t) - pnconfig.CipherKey = "" + jsonString := []byte(`[[{"one":1,"two":2},{"three":3,"four":4}],14991775432719844,14991868111600528]`) + pn := NewPubNubDemo() - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Nil(err) assert.Equal(int64(14991775432719844), resp.StartTimetoken) @@ -291,13 +299,14 @@ func TestHistoryResponseParsingMap(t *testing.T) { func TestHistoryPNOther(t *testing.T) { assert := assert.New(t) - pnconfig.CipherKey = "testCipher" - pnconfig.UseRandomInitializationVector = false + pn := NewPubNubDemo() + pn.Config.CipherKey = "testCipher" + pn.Config.UseRandomInitializationVector = false int64Val := int64(14991775432719844) jsonString := []byte(`[[{"pn_other":"ven1bo79fk88nq5EIcnw/N9RmGzLeeWMnsabr1UL3iw="},1,"a",1.1,false,14991775432719844],14991775432719844,14991868111600528]`) - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Nil(err) assert.Equal(int64(14991775432719844), resp.StartTimetoken) @@ -331,11 +340,13 @@ func TestHistoryPNOther(t *testing.T) { func TestHistoryPNOtherYay(t *testing.T) { assert := assert.New(t) - pnconfig.CipherKey = "enigma" + pn := NewPubNubDemo() + pn.Config.UseRandomInitializationVector = false + pn.Config.CipherKey = "enigma" int64Val := int64(14991775432719844) jsonString := []byte(`[[{"pn_other":"Wi24KS4pcTzvyuGOHubiXg=="},1,"a",1.1,false,14991775432719844],14991775432719844,14991868111600528]`) - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Nil(err) assert.Equal(int64(14991775432719844), resp.StartTimetoken) @@ -369,7 +380,8 @@ func TestHistoryResponseParsingSliceInMapWithTimetoken(t *testing.T) { jsonString := []byte(`[[{"message":[1,2,3,["one","two","three"]],"timetoken":1111}],14991775432719844,14991868111600528]`) - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + pn := NewPubNubDemo() + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Nil(err) assert.Equal(int64(14991775432719844), resp.StartTimetoken) @@ -385,7 +397,8 @@ func TestHistoryResponseParsingMapInSlice(t *testing.T) { jsonString := []byte(`[[[{"one":"two","three":[5,6]}]],14991775432719844,14991868111600528]`) - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + pn := NewPubNubDemo() + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Nil(err) assert.Equal(int64(14991775432719844), resp.StartTimetoken) @@ -400,12 +413,14 @@ func TestHistoryResponseParsingMapInSlice(t *testing.T) { func TestHistoryEncrypt(t *testing.T) { assert := assert.New(t) + pnconfig := NewDemoConfig() pnconfig.CipherKey = "testCipher" - pubnub = NewPubNub(pnconfig) + pnconfig.UseRandomInitializationVector = false + pubnub := NewPubNub(pnconfig) jsonString := []byte(`[["MnwzPGdVgz2osQCIQJviGg=="],14991775432719844,14991868111600528]`) - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + resp, _, err := newHistoryResponse(jsonString, pubnub.initHistoryOpts(), fakeResponseState) assert.Nil(err) messages := resp.Messages @@ -415,11 +430,13 @@ func TestHistoryEncrypt(t *testing.T) { func TestHistoryEncryptSlice(t *testing.T) { assert := assert.New(t) - pnconfig.CipherKey = "testCipher" + pn := NewPubNubDemo() + pn.Config.CipherKey = "testCipher" + pn.Config.UseRandomInitializationVector = false jsonString := []byte(`[["gwkdY8qcv60GM/PslArWQsdXrQ6LwJD2HoaEfy0CjMc="],14991775432719844,14991868111600528]`) - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Nil(err) messages := resp.Messages @@ -436,11 +453,13 @@ func TestHistoryEncryptSlice(t *testing.T) { func TestHistoryEncryptMap(t *testing.T) { assert := assert.New(t) - pnconfig.CipherKey = "testCipher" + pn := NewPubNubDemo() + pn.Config.CipherKey = "testCipher" + pn.Config.UseRandomInitializationVector = false jsonString := []byte(`[["wIC13nvJcI4vBtWNFVUu0YDiqREr9kavB88xeyWTweDS363Yl84RCWqOHWTol4aY"],14991775432719844,14991868111600528]`) - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Nil(err) messages := resp.Messages @@ -464,7 +483,8 @@ func TestHistoryResponseMeta(t *testing.T) { jsonString := []byte(`[[{"message":"my-message","meta":{"m1":"n1","m2":"n2"}}],15699986472636251,15699986472636251]`) - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + pn := NewPubNubDemo() + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Nil(err) assert.Equal(int64(15699986472636251), resp.StartTimetoken) @@ -482,7 +502,8 @@ func TestHistoryResponseMetaAndTT(t *testing.T) { jsonString := []byte(`[[{"message":"my-message","meta":{"m1":"n1","m2":"n2"},"timetoken":15699986472636251}],15699986472636251,15699986472636251]`) - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + pn := NewPubNubDemo() + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Nil(err) assert.Equal(int64(15699986472636251), resp.StartTimetoken) @@ -501,7 +522,8 @@ func TestHistoryResponseError(t *testing.T) { jsonString := []byte(`s`) - _, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + pn := NewPubNubDemo() + _, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Equal("pubnub/parsing: Error unmarshalling response: {s}", err.Error()) } @@ -510,7 +532,8 @@ func TestHistoryResponseStartTTError(t *testing.T) { jsonString := []byte(`[[{"message":[1,2,3,["one","two","three"]],"timetoken":1111}],"s","a"]`) - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + pn := NewPubNubDemo() + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Equal(int64(0), resp.StartTimetoken) assert.Equal(int64(0), resp.EndTimetoken) assert.Nil(err) @@ -522,7 +545,8 @@ func TestHistoryResponseEndTTError(t *testing.T) { jsonString := []byte(`[[{"message":[1,2,3,["one","two","three"]],"timetoken":1111}],121324,"a"]`) - resp, _, err := newHistoryResponse(jsonString, initHistoryOpts(), fakeResponseState) + pn := NewPubNubDemo() + resp, _, err := newHistoryResponse(jsonString, pn.initHistoryOpts(), fakeResponseState) assert.Equal(int64(121324), resp.StartTimetoken) assert.Equal(int64(0), resp.EndTimetoken) assert.Nil(err) diff --git a/publish_file_message.go b/publish_file_message.go index eb4695ce..53cceb23 100644 --- a/publish_file_message.go +++ b/publish_file_message.go @@ -201,7 +201,7 @@ func (o *publishFileMessageOpts) buildPath() (string, error) { } } - if cipherKey := o.pubnub.Config.CipherKey; cipherKey != "" { + if o.pubnub.getCryptoModule() != nil { var msg string var p *publishBuilder if o.context() != nil { @@ -211,7 +211,7 @@ func (o *publishFileMessageOpts) buildPath() (string, error) { } p.opts.Message = o.Message - msg, errJSONMarshal := p.opts.encryptProcessing(cipherKey) + msg, errJSONMarshal := p.opts.encryptProcessing() if errJSONMarshal != nil { return "", errJSONMarshal } diff --git a/publish_request.go b/publish_request.go index 4b865d92..c625f3e5 100644 --- a/publish_request.go +++ b/publish_request.go @@ -198,13 +198,13 @@ func (o *publishOpts) validate() error { return nil } -func (o *publishOpts) encryptProcessing(cipherKey string) (string, error) { +func (o *publishOpts) encryptProcessing() (string, error) { var msg string var errJSONMarshal error o.pubnub.Config.Log.Println("EncryptString: encrypting", fmt.Sprintf("%s", o.Message)) if o.pubnub.Config.DisablePNOtherProcessing { - if msg, errJSONMarshal = utils.SerializeEncryptAndSerialize(o.Message, cipherKey, o.Serialize, o.pubnub.Config.UseRandomInitializationVector); errJSONMarshal != nil { + if msg, errJSONMarshal = serializeEncryptAndSerialize(o.pubnub.getCryptoModule(), o.Message, o.Serialize); errJSONMarshal != nil { o.pubnub.Config.Log.Printf("error in serializing: %v\n", errJSONMarshal) return "", errJSONMarshal } @@ -218,7 +218,7 @@ func (o *publishOpts) encryptProcessing(cipherKey string) (string, error) { if ok { o.pubnub.Config.Log.Println(ok, msgPart) - encMsg, errJSONMarshal := utils.SerializeAndEncrypt(msgPart, cipherKey, o.Serialize, o.pubnub.Config.UseRandomInitializationVector) + encMsg, errJSONMarshal := serializeAndEncrypt(o.pubnub.getCryptoModule(), msgPart, o.Serialize) if errJSONMarshal != nil { o.pubnub.Config.Log.Printf("error in serializing: %v\n", errJSONMarshal) return "", errJSONMarshal @@ -231,14 +231,14 @@ func (o *publishOpts) encryptProcessing(cipherKey string) (string, error) { } msg = string(jsonEncBytes) } else { - if msg, errJSONMarshal = utils.SerializeEncryptAndSerialize(o.Message, cipherKey, o.Serialize, o.pubnub.Config.UseRandomInitializationVector); errJSONMarshal != nil { + if msg, errJSONMarshal = serializeEncryptAndSerialize(o.pubnub.getCryptoModule(), o.Message, o.Serialize); errJSONMarshal != nil { o.pubnub.Config.Log.Printf("error in serializing: %v\n", errJSONMarshal) return "", errJSONMarshal } } break default: - if msg, errJSONMarshal = utils.SerializeEncryptAndSerialize(o.Message, cipherKey, o.Serialize, o.pubnub.Config.UseRandomInitializationVector); errJSONMarshal != nil { + if msg, errJSONMarshal = serializeEncryptAndSerialize(o.pubnub.getCryptoModule(), o.Message, o.Serialize); errJSONMarshal != nil { o.pubnub.Config.Log.Printf("error in serializing: %v\n", errJSONMarshal) return "", errJSONMarshal } @@ -261,8 +261,8 @@ func (o *publishOpts) buildPath() (string, error) { var msg string var errJSONMarshal error - if cipherKey := o.pubnub.Config.CipherKey; cipherKey != "" { - if msg, errJSONMarshal = o.encryptProcessing(cipherKey); errJSONMarshal != nil { + if o.pubnub.getCryptoModule() != nil { + if msg, errJSONMarshal = o.encryptProcessing(); errJSONMarshal != nil { return "", errJSONMarshal } @@ -336,8 +336,8 @@ func (o *publishOpts) buildQuery() (*url.Values, error) { func (o *publishOpts) buildBody() ([]byte, error) { if o.UsePost { - if cipherKey := o.pubnub.Config.CipherKey; cipherKey != "" { - msg, errJSONMarshal := o.encryptProcessing(cipherKey) + if o.pubnub.getCryptoModule() != nil { + msg, errJSONMarshal := o.encryptProcessing() if errJSONMarshal != nil { return []byte{}, errJSONMarshal } diff --git a/publish_request_test.go b/publish_request_test.go index 23989e6e..b14e2e0d 100644 --- a/publish_request_test.go +++ b/publish_request_test.go @@ -191,10 +191,10 @@ func AssertSuccessPublishGetMeta(t *testing.T, expectedString string, message in assert.Nil(err1) } -func AssertSuccessPublishPost(t *testing.T, expectedBody string, message interface{}) { +func AssertSuccessPublishPost(t *testing.T, pn *PubNub, expectedBody string, message interface{}) { assert := assert.New(t) - opts := newPublishOpts(pubnub, pubnub.ctx) + opts := newPublishOpts(pn, pn.ctx) opts.Channel = "ch" opts.Message = message opts.UsePost = true @@ -206,7 +206,7 @@ func AssertSuccessPublishPost(t *testing.T, expectedBody string, message interfa Path: path, } h.AssertPathsEqual(t, - "/publish/pub_key/sub_key/0/ch/0", + "/publish/demo/demo/0/ch/0", u.EscapedPath(), []int{}) body, err := opts.buildBody() @@ -270,6 +270,8 @@ func TestPublishMixedGet(t *testing.T) { } func TestPublishMixedPost(t *testing.T) { + pn := NewPubNub(NewDemoConfig()) + type msg struct { One string `json:"one"` Two string `json:"two"` @@ -282,16 +284,16 @@ func TestPublishMixedPost(t *testing.T) { msgMap["two"] = "hey2" msgMap["three"] = "hey3" - AssertSuccessPublishPost(t, "12", 12) - AssertSuccessPublishPost(t, "\"hey\"", "hey") - AssertSuccessPublishPost(t, "true", true) - AssertSuccessPublishPost(t, "[\"hey1\",\"hey2\",\"hey3\"]", + AssertSuccessPublishPost(t, pn, "12", 12) + AssertSuccessPublishPost(t, pn, "\"hey\"", "hey") + AssertSuccessPublishPost(t, pn, "true", true) + AssertSuccessPublishPost(t, pn, "[\"hey1\",\"hey2\",\"hey3\"]", []string{"hey1", "hey2", "hey3"}) - AssertSuccessPublishPost(t, "[1,2,3]", []int{1, 2, 3}) - AssertSuccessPublishPost(t, + AssertSuccessPublishPost(t, pn, "[1,2,3]", []int{1, 2, 3}) + AssertSuccessPublishPost(t, pn, "{\"one\":\"hey1\",\"two\":\"hey2\",\"three\":\"hey3\"}", msgStruct) - AssertSuccessPublishPost(t, + AssertSuccessPublishPost(t, pn, "{\"one\":\"hey1\",\"three\":\"hey3\",\"two\":\"hey2\"}", msgMap) } diff --git a/pubnub.go b/pubnub.go index 0f25bef9..e3e3f8ab 100644 --- a/pubnub.go +++ b/pubnub.go @@ -2,6 +2,7 @@ package pubnub import ( "fmt" + "github.com/pubnub/go/v7/crypto" "io/ioutil" "log" "net/http" @@ -14,7 +15,7 @@ import ( // Default constants const ( // Version :the version of the SDK - Version = "7.1.2" + Version = "7.2.0" // MaxSequence for publish messages MaxSequence = 65535 ) @@ -74,6 +75,26 @@ type PubNub struct { ctx Context cancel func() tokenManager *TokenManager + previousCipherKey string + previousIvFlag bool +} + +// TODO this needs to be tested +func (pn *PubNub) getCryptoModule() crypto.CryptoModule { + pn.Lock() + defer pn.Unlock() + if pn.previousCipherKey == pn.Config.CipherKey && pn.previousIvFlag == pn.Config.UseRandomInitializationVector { + return pn.Config.CryptoModule + } + + if pn.Config != nil && pn.Config.CipherKey != "" { + pn.Config.CryptoModule, _ = crypto.NewLegacyCryptoModule(pn.Config.CipherKey, pn.Config.UseRandomInitializationVector) + return pn.Config.CryptoModule + } else if pn.Config != nil && pn.Config.CipherKey == "" { + pn.Config.CryptoModule = nil + return pn.Config.CryptoModule + } + return nil } // Publish is used to send a message to all subscribers of a channel. @@ -752,7 +773,7 @@ func NewPubNub(pnconf *Config) *PubNub { if pnconf.Log == nil { pnconf.Log = log.New(ioutil.Discard, "", log.Ldate|log.Ltime|log.Lshortfile) } - pnconf.Log.Println(fmt.Sprintf("PubNub Go v4 SDK: %s\npnconf: %v\n%s\n%s\n%s", Version, pnconf, runtime.Version(), runtime.GOARCH, runtime.GOOS)) + pnconf.Log.Println(fmt.Sprintf("PubNub Go v7 SDK: %s\npnconf: %v\n%s\n%s\n%s", Version, pnconf, runtime.Version(), runtime.GOARCH, runtime.GOOS)) utils.CheckUUID(pnconf.UUID) pn := &PubNub{ @@ -760,8 +781,17 @@ func NewPubNub(pnconf *Config) *PubNub { nextPublishSequence: 0, ctx: ctx, cancel: cancel, + previousIvFlag: pnconf.UseRandomInitializationVector, + previousCipherKey: pnconf.CipherKey, } + if pnconf.CipherKey != "" { + var e error + pn.Config.CryptoModule, e = crypto.NewLegacyCryptoModule(pnconf.CipherKey, pnconf.UseRandomInitializationVector) + if e != nil { + panic(e) + } + } pn.subscriptionManager = newSubscriptionManager(pn, ctx) pn.heartbeatManager = newHeartbeatManager(pn, ctx) pn.telemetryManager = newTelemetryManager(pnconf.MaximumLatencyDataAge, ctx) diff --git a/pubnub_test.go b/pubnub_test.go index 6c52214d..948b0615 100644 --- a/pubnub_test.go +++ b/pubnub_test.go @@ -1,6 +1,7 @@ package pubnub import ( + "github.com/pubnub/go/v7/crypto" "testing" "github.com/stretchr/testify/assert" @@ -20,6 +21,32 @@ func TestInitializer(t *testing.T) { assert.Equal("my_secret_key", pubnub.Config.SecretKey) } +func TestCryptoModuleChangesWhenCipherKeyChanges(t *testing.T) { + pubnub := NewPubNubDemo() + + cryptoModule, _ := crypto.NewAesCbcCryptoModule("my_cipher_key", true) + pubnub.Config.CryptoModule = cryptoModule + a := assert.New(t) + + a.Equal(cryptoModule, pubnub.getCryptoModule()) + + pubnub.Config.CipherKey = "new_cipher_key" + a.NotEqual(cryptoModule, pubnub.getCryptoModule()) +} + +func TestCryptoModuleChangesIfIvFlagChanges(t *testing.T) { + pubnub := NewPubNubDemo() + + cryptoModule, _ := crypto.NewAesCbcCryptoModule("my_cipher_key", true) + pubnub.Config.CryptoModule = cryptoModule + a := assert.New(t) + + a.Equal(cryptoModule, pubnub.getCryptoModule()) + + pubnub.Config.UseRandomInitializationVector = false + a.NotEqual(cryptoModule, pubnub.getCryptoModule()) +} + func TestDemoInitializer(t *testing.T) { demo := NewPubNubDemo() diff --git a/subscription_manager.go b/subscription_manager.go index 7672bfe5..1873f313 100644 --- a/subscription_manager.go +++ b/subscription_manager.go @@ -3,14 +3,13 @@ package pubnub import ( "encoding/json" "errors" + "github.com/pubnub/go/v7/crypto" "net/http" "reflect" "strconv" "strings" "sync" "time" - - "github.com/pubnub/go/v7/utils" ) // SubscriptionManager Events: @@ -662,7 +661,7 @@ func processNonPresencePayload(m *SubscriptionManager, payload subscribeMessage, m.listenerManager.announceMessageActionsEvent(pnMessageActionsEvent) case PNMessageTypeFile: var err error - messagePayload, err = parseCipherInterface(payload.Payload, m.pubnub.Config) + messagePayload, err = parseCipherInterface(payload.Payload, m.pubnub.Config, m.pubnub.getCryptoModule()) if err != nil { pnStatus := &PNStatus{ Category: PNBadRequestCategory, @@ -681,7 +680,7 @@ func processNonPresencePayload(m *SubscriptionManager, payload subscribeMessage, m.listenerManager.announceFile(pnFilesEvent) default: var err error - messagePayload, err = parseCipherInterface(payload.Payload, m.pubnub.Config) + messagePayload, err = parseCipherInterface(payload.Payload, m.pubnub.Config, m.pubnub.getCryptoModule()) if err != nil { pnStatus := &PNStatus{ Category: PNBadRequestCategory, @@ -959,8 +958,8 @@ func createPNMessageResult(messagePayload interface{}, actualCh, subscribedCh, c // cipherKey: cipher key to use to decrypt. // // returns the decrypted data as interface and error. -func parseCipherInterface(data interface{}, pnConf *Config) (interface{}, error) { - if pnConf.CipherKey != "" { +func parseCipherInterface(data interface{}, pnConf *Config, module crypto.CryptoModule) (interface{}, error) { + if module != nil { pnConf.Log.Println("reflect.TypeOf(data).Kind()", reflect.TypeOf(data).Kind(), data) switch v := data.(type) { case map[string]interface{}: @@ -970,7 +969,7 @@ func parseCipherInterface(data interface{}, pnConf *Config) (interface{}, error) msg, ok := v["pn_other"].(string) if ok { pnConf.Log.Println("v[pn_other]", v["pn_other"], v, msg) - decrypted, errDecryption := utils.DecryptString(pnConf.CipherKey, msg, pnConf.UseRandomInitializationVector) + decrypted, errDecryption := decryptString(module, msg) if errDecryption != nil { pnConf.Log.Println(errDecryption, msg) return v, errDecryption @@ -993,7 +992,7 @@ func parseCipherInterface(data interface{}, pnConf *Config) (interface{}, error) return v, nil case string: var intf interface{} - decrypted, errDecryption := utils.DecryptString(pnConf.CipherKey, data.(string), pnConf.UseRandomInitializationVector) + decrypted, errDecryption := decryptString(module, data.(string)) if errDecryption != nil { pnConf.Log.Println(errDecryption, intf) intf = data diff --git a/subscription_manager_test.go b/subscription_manager_test.go index fa645598..af956a08 100644 --- a/subscription_manager_test.go +++ b/subscription_manager_test.go @@ -22,7 +22,7 @@ func TestParseCipherInterfaceCipherWithCipher(t *testing.T) { pn.Config.CipherKey = "enigma" pn.Config.UseRandomInitializationVector = false - intf, err := parseCipherInterface(s, pn.Config) + intf, err := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) assert.Nil(err) assert.Equal("yay!", intf.(string)) @@ -35,7 +35,7 @@ func TestParseCipherInterfacePlainWithCipher(t *testing.T) { pn := NewPubNub(NewDemoConfig()) pn.Config.CipherKey = "enigma" - intf, _ := parseCipherInterface(s, pn.Config) + intf, _ := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) assert.Equal("yay!", intf.(string)) } @@ -47,7 +47,7 @@ func TestParseCipherInterfaceCipherWithDiffCipher(t *testing.T) { pn := NewPubNub(NewDemoConfig()) pn.Config.CipherKey = "test" - intf, _ := parseCipherInterface(s, pn.Config) + intf, _ := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) assert.Equal("Wi24KS4pcTzvyuGOHubiXg==", intf.(string)) @@ -60,7 +60,7 @@ func TestParseCipherInterfacePlainWithDiffCipher(t *testing.T) { pn := NewPubNub(NewDemoConfig()) pn.Config.CipherKey = "test" - intf, _ := parseCipherInterface(s, pn.Config) + intf, _ := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) assert.Equal("yay!", intf.(string)) } @@ -72,7 +72,7 @@ func TestParseCipherInterfaceCipherWithoutCipher(t *testing.T) { pn := NewPubNub(NewDemoConfig()) pn.Config.CipherKey = "" - intf, _ := parseCipherInterface(s, pn.Config) + intf, _ := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) assert.Equal("Wi24KS4pcTzvyuGOHubiXg==", intf.(string)) } @@ -84,7 +84,7 @@ func TestParseCipherInterfacePlainWithoutCipher(t *testing.T) { pn := NewPubNub(NewDemoConfig()) pn.Config.CipherKey = "" - intf, _ := parseCipherInterface(s, pn.Config) + intf, _ := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) assert.Equal("yay!", intf.(string)) } @@ -99,7 +99,7 @@ func TestParseCipherInterfacePlainWithCipherStruct(t *testing.T) { pn := NewPubNub(NewDemoConfig()) pn.Config.CipherKey = "enigma" - intf, err := parseCipherInterface(s, pn.Config) + intf, err := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) assert.Nil(err) if msg, ok := intf.(customStruct); !ok { @@ -121,7 +121,7 @@ func TestParseCipherInterfacePlainWithoutCipherStruct(t *testing.T) { pn := NewPubNub(NewDemoConfig()) pn.Config.CipherKey = "" - intf, err := parseCipherInterface(s, pn.Config) + intf, err := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) assert.Nil(err) if msg, ok := intf.(customStruct); !ok { @@ -148,7 +148,7 @@ func TestParseCipherInterfacePlainWithCipherMapPNOther(t *testing.T) { pn := NewPubNub(NewDemoConfig()) pn.Config.CipherKey = "enigma" - intf, _ := parseCipherInterface(s, pn.Config) + intf, _ := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) msg := intf.(map[string]interface{}) assert.Equal("12345", msg["not_other"]) @@ -174,7 +174,7 @@ func TestParseCipherInterfacePlainWithoutCipherMapPNOther(t *testing.T) { pn := NewPubNub(NewDemoConfig()) pn.Config.CipherKey = "" - intf, _ := parseCipherInterface(s, pn.Config) + intf, _ := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) msg := intf.(map[string]interface{}) assert.Equal("12345", msg["not_other"]) @@ -196,7 +196,7 @@ func TestParseCipherInterfaceCipherWithoutCipherStringPNOther(t *testing.T) { pn := NewPubNub(NewDemoConfig()) pn.Config.CipherKey = "" - intf, _ := parseCipherInterface(s, pn.Config) + intf, _ := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) msg := intf.(map[string]interface{}) assert.Equal("1234", msg["not_other"]) @@ -219,7 +219,7 @@ func TestParseCipherInterfaceCipherWithCipherStringPNOther(t *testing.T) { pn.Config.CipherKey = "enigma" pn.Config.UseRandomInitializationVector = false - intf, _ := parseCipherInterface(s, pn.Config) + intf, _ := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) msg := intf.(map[string]interface{}) assert.Equal("1234", msg["not_other"]) @@ -238,7 +238,7 @@ func TestParseCipherInterfaceCipherWithoutCipherStruct(t *testing.T) { pn.Config.CipherKey = "enigma" pn.Config.UseRandomInitializationVector = false - intf, _ := parseCipherInterface(s, pn.Config) + intf, _ := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) msg := intf.(map[string]interface{}) assert.Equal("hi!", msg["Foo"]) @@ -256,7 +256,7 @@ func TestParseCipherInterfaceCipherWithCipherStructPNOther(t *testing.T) { pn.Config.CipherKey = "enigma" pn.Config.UseRandomInitializationVector = false - intf, _ := parseCipherInterface(s, pn.Config) + intf, _ := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) msg := intf.(map[string]interface{}) assert.Equal("1234", msg["not_other"]) @@ -278,7 +278,7 @@ func TestParseCipherInterfaceCipherWithOtherCipherStructPNOther(t *testing.T) { pn := NewPubNub(NewDemoConfig()) pn.Config.CipherKey = "test" - intf, _ := parseCipherInterface(s, pn.Config) + intf, _ := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) msg := intf.(map[string]interface{}) assert.Equal("1234", msg["not_other"]) @@ -298,7 +298,7 @@ func TestParseCipherInterfaceCipherWithCipherStructPNOtherDisable(t *testing.T) pn.Config.UseRandomInitializationVector = false pn.Config.CipherKey = "enigma" - intf, _ := parseCipherInterface(s, pn.Config) + intf, _ := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) msg := intf.(map[string]interface{}) assert.Equal("1234", msg["not_other"]) @@ -314,7 +314,7 @@ func TestParseCipherInterfaceCipherWithIntSlice(t *testing.T) { pn.Config.DisablePNOtherProcessing = true pn.Config.CipherKey = "" - intf, _ := parseCipherInterface(s, pn.Config) + intf, _ := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) msg := intf.([]int) assert.Equal(1, msg[0]) @@ -329,7 +329,7 @@ func TestParseCipherInterfaceCipherWithoutCipherStruct2(t *testing.T) { pn.Config.CipherKey = "enigma" pn.Config.UseRandomInitializationVector = false - intf, _ := parseCipherInterface(s, pn.Config) + intf, _ := parseCipherInterface(s, pn.Config, pn.getCryptoModule()) msg := intf.(map[string]interface{}) assert.Equal("12345", msg["not_other"]) if msgOther, ok := msg["pn_other"].(map[string]interface{}); !ok { diff --git a/tests/contract/contract_test.go b/tests/contract/contract_test.go index af36b554..fd29f9aa 100644 --- a/tests/contract/contract_test.go +++ b/tests/contract/contract_test.go @@ -13,8 +13,8 @@ var tagsFilter string var format string func TestMain(m *testing.M) { - flag.StringVar(&path, "path", "", "Path to feature files") - flag.StringVar(&tagsFilter, "tagsFilter", "~@skip && ~@na=go && ~@beta", "Tags filter") + flag.StringVar(&path, "path", "../../../sdk-specifications/features", "Path to feature files") + flag.StringVar(&tagsFilter, "tagsFilter", "~@skip && ~@na=go && @beta && @featureSet=cryptoModule", "Tags filter") flag.StringVar(&format, "format", "pretty", "Output formatter") flag.Parse() if path == "" { @@ -28,9 +28,9 @@ func TestFeatures(t *testing.T) { suite := godog.TestSuite{ ScenarioInitializer: InitializeScenario, Options: &godog.Options{ - Format: format, - Paths: []string{path}, - Tags: tagsFilter, + Format: format, + Paths: []string{path}, + Tags: tagsFilter, TestingT: t, // Testing instance that will run subtests. }, } diff --git a/tests/contract/crypto_state_test.go b/tests/contract/crypto_state_test.go new file mode 100644 index 00000000..0cf7ff94 --- /dev/null +++ b/tests/contract/crypto_state_test.go @@ -0,0 +1,52 @@ +package contract + +import ( + "context" + "errors" + "github.com/pubnub/go/v7/crypto" + "io" + "os" +) + +type cryptoStateKey struct{} + +type cryptoState struct { + cryptorNames []string + cryptorName string + cipherKey string + cryptoFeaturePath string + randomIv bool + result []byte + resultReader io.Reader + err error + cryptoModule crypto.CryptoModule + legacyCodeCipherKey string + legacyCodeRandomIv bool +} + +func (c *cryptoState) getCryptoModule() (crypto.CryptoModule, error) { + + if len(c.cryptorNames) > 0 && c.cryptorNames[0] == "legacy" { + return crypto.NewLegacyCryptoModule(c.cipherKey, c.randomIv) + } else if len(c.cryptorNames) > 0 && c.cryptorNames[0] == "acrh" { + return crypto.NewAesCbcCryptoModule(c.cipherKey, c.randomIv) + } else if c.cryptorName != "" { + cryptor, e := createCryptor(c.cryptorName, c.cipherKey, c.randomIv) + c.cryptoModule = crypto.NewCryptoModule(cryptor, []crypto.Cryptor{}) + return c.cryptoModule, e + } else { + return nil, errors.New("I don't know how to create this crypto module") + } +} + +func (c *cryptoState) openAssetFile(filename string) (io.ReadCloser, error) { + return os.Open(c.cryptoFeaturePath + "/assets/" + filename) +} + +func getCryptoState(ctx context.Context) *cryptoState { + return ctx.Value(cryptoStateKey{}).(*cryptoState) +} + +func newCryptoState(cryptoFeaturePath string) *cryptoState { + return &cryptoState{cryptoFeaturePath: cryptoFeaturePath, randomIv: false} +} diff --git a/tests/contract/crypto_steps_test.go b/tests/contract/crypto_steps_test.go new file mode 100644 index 00000000..cc46ef7a --- /dev/null +++ b/tests/contract/crypto_steps_test.go @@ -0,0 +1,243 @@ +package contract + +import ( + "bufio" + "bytes" + "context" + "encoding/base64" + "errors" + "fmt" + "github.com/pubnub/go/v7/crypto" + "github.com/pubnub/go/v7/utils" + "io" + "os" + "strings" +) + +func cryptor(ctx context.Context, cryptor string) error { + cryptoState := getCryptoState(ctx) + cryptoState.cryptorName = cryptor + return nil +} + +func cryptoModuleWithDefaultAndAdditionalLegacyCryptors(ctx context.Context, cryptor1 string, cryptor2 string) error { + cryptoState := getCryptoState(ctx) + cryptoState.cryptorNames = append(cryptoState.cryptorNames, cryptor1, cryptor2) + return nil +} + +func decryptedFileContentEqualToFileContent(ctx context.Context, filename string) error { + cryptoState := getCryptoState(ctx) + + file, e := os.Open(cryptoState.cryptoFeaturePath + "/assets/" + filename) + if e != nil { + return e + } + + fileContent, e := io.ReadAll(file) + if e != nil { + return e + } + + if cryptoState.result != nil { + if !bytes.Equal(cryptoState.result, fileContent) { + return errors.New("decrypted file content not equal to file content") + } + } else if cryptoState.resultReader != nil { + resultContent, e := io.ReadAll(cryptoState.resultReader) + if e != nil { + return e + } + if !bytes.Equal(resultContent, fileContent) { + return errors.New("decrypted file content not equal to file content") + } + } else { + return errors.New("no result") + } + return nil +} + +func legacyCodeWithCipherKeyAndConstantVector(ctx context.Context, cipherKey string) error { + cryptoState := getCryptoState(ctx) + cryptoState.legacyCodeCipherKey = cipherKey + cryptoState.legacyCodeRandomIv = false + return nil +} + +func legacyCodeWithCipherKeyAndRandomVector(ctx context.Context, cipherKey string) error { + cryptoState := getCryptoState(ctx) + cryptoState.legacyCodeCipherKey = cipherKey + cryptoState.legacyCodeRandomIv = true + return nil +} + +func iDecryptFileAs(ctx context.Context, filename string, decryptionType string) error { + + cryptoState := getCryptoState(ctx) + + module, e := cryptoState.getCryptoModule() + if e != nil { + return e + } + file, e := os.Open(cryptoState.cryptoFeaturePath + "/assets/" + filename) + if e != nil { + return e + } + + if decryptionType == "stream" { + cryptoState.resultReader, cryptoState.err = module.DecryptStream(bufio.NewReader(file)) + } else { + fileContent, e := io.ReadAll(file) + if e != nil { + return e + } + cryptoState.result, cryptoState.err = module.Decrypt(fileContent) + } + return nil +} + +func iDecryptFile(ctx context.Context, filename string) error { + + cryptoState := getCryptoState(ctx) + + module, e := cryptoState.getCryptoModule() + if e != nil { + return e + } + file, e := cryptoState.openAssetFile(filename) + if e != nil { + return e + } + + _, e = module.DecryptStream(file) + if e != nil { + cryptoState.err = e + } + return nil +} + +func createCryptor(name string, cipherKey string, randomIv bool) (crypto.Cryptor, error) { + if name == "acrh" { + return crypto.NewAesCbcCryptor(cipherKey) + } else if name == "legacy" { + return crypto.NewLegacyCryptor(cipherKey, randomIv) + } else { + return nil, fmt.Errorf("unknown crypto algorithm %s", name) + } +} + +func iEncryptFileAs(ctx context.Context, filename string, encryptionType string) error { + cryptoState := getCryptoState(ctx) + module, e := cryptoState.getCryptoModule() + if e != nil { + return e + } + file, e := cryptoState.openAssetFile(filename) + if e != nil { + return e + } + if encryptionType == "stream" { + cryptoState.resultReader, cryptoState.err = module.EncryptStream(bufio.NewReader(file)) + } else { + content, e := io.ReadAll(file) + if e != nil { + return e + } + cryptoState.result, cryptoState.err = module.Encrypt(content) + + } + return nil +} + +func iReceiveDecryptionError(ctx context.Context) error { + cryptoState := getCryptoState(ctx) + if cryptoState.err != nil { + if strings.HasPrefix(cryptoState.err.Error(), "decryption error") { + return nil + } else { + return cryptoState.err + } + } else { + return errors.New("expected error") + } +} + +func iReceiveSuccess(ctx context.Context) error { + cryptoState := getCryptoState(ctx) + if cryptoState.err != nil { + return cryptoState.err + } + return nil +} + +func iReceiveUnknownCryptoError(ctx context.Context) error { + cryptoState := getCryptoState(ctx) + if cryptoState.err != nil { + if strings.Contains(cryptoState.err.Error(), "unknown crypto error") { + return nil + } else { + return cryptoState.err + } + } else { + return errors.New("expected error") + } +} + +func withCipherKey(ctx context.Context, cipherKey string) error { + cryptoState := getCryptoState(ctx) + cryptoState.cipherKey = cipherKey + return nil +} + +func randomIv(iv string) bool { + if iv == "constant" { + return false + } else if iv == "random" { + return true + } else { + return false + } +} + +func withVector(ctx context.Context, iv string) error { + cryptoState := getCryptoState(ctx) + cryptoState.randomIv = randomIv(iv) + return nil +} + +func successfullyDecryptAnEncryptedFileWithLegacyCode(ctx context.Context) error { + cryptoState := getCryptoState(ctx) + if cryptoState.result != nil { + _, e := utils.DecryptString(cryptoState.legacyCodeCipherKey, base64.StdEncoding.EncodeToString(cryptoState.result), cryptoState.legacyCodeRandomIv) + return e + } else if cryptoState.resultReader != nil { + buffer := &closerBuffer{bytes.NewBuffer(nil)} + + utils.DecryptFile(cryptoState.legacyCodeCipherKey, 0, cryptoState.resultReader, buffer) + _, e := io.ReadAll(buffer) + return e + } else { + return errors.New("no result") + } +} + +func iReceiveEncryptionError(ctx context.Context) error { + cryptoState := getCryptoState(ctx) + if cryptoState.err != nil { + if strings.HasPrefix(cryptoState.err.Error(), "encryption error") { + return nil + } else { + return cryptoState.err + } + } else { + return errors.New("expected error") + } +} + +type closerBuffer struct { + *bytes.Buffer +} + +func (c *closerBuffer) Close() error { + return nil +} diff --git a/tests/contract/steps_mapping_test.go b/tests/contract/steps_mapping_test.go index 4e41059c..c69a3442 100644 --- a/tests/contract/steps_mapping_test.go +++ b/tests/contract/steps_mapping_test.go @@ -55,4 +55,22 @@ func MapSteps(ctx *godog.ScenarioContext) { ctx.Step(`^the error detail message is not empty$`, theErrorDetailMessageIsNotEmpty) ctx.Step(`^the result is successful$`, theResultIsSuccessful) ctx.Step(`^the token string \'(.*)\'$`, theTokenString) + + ctx.Step(`^Decrypted file content equal to the \'(.*)\' file content$`, decryptedFileContentEqualToFileContent) + ctx.Step(`^I decrypt \'(.*)\' file as \'(.*)\'$`, iDecryptFileAs) + ctx.Step(`^I decrypt \'(.*)\' file$`, iDecryptFile) + ctx.Step(`^I encrypt \'(.*)\' file as \'(.*)\'$`, iEncryptFileAs) + ctx.Step(`^I receive \'decryption error\'$`, iReceiveDecryptionError) + ctx.Step(`^I receive \'success\'$`, iReceiveSuccess) + ctx.Step(`^I receive \'unknown cryptor error\'$`, iReceiveUnknownCryptoError) + ctx.Step(`^I receive \'encryption error\'$`, iReceiveEncryptionError) + ctx.Step(`^with \'(.*)\' cipher key$`, withCipherKey) + ctx.Step(`^with \'(.*)\' vector$`, withVector) + + ctx.Step(`^Crypto module with \'(.*)\' cryptor$`, cryptor) + ctx.Step(`^Crypto module with default \'(.*)\' and additional \'(.*)\' cryptors$`, cryptoModuleWithDefaultAndAdditionalLegacyCryptors) + ctx.Step(`^Crypto module with \'(.*)\' cryptor$`, cryptor) + ctx.Step(`^Legacy code with \'(.*)\' cipher key and \'constant\' vector$`, legacyCodeWithCipherKeyAndConstantVector) + ctx.Step(`^Legacy code with \'(.*)\' cipher key and \'random\' vector$`, legacyCodeWithCipherKeyAndRandomVector) + ctx.Step(`^Successfully decrypt an encrypted file with legacy code$`, successfullyDecryptAnEncryptedFileWithLegacyCode) } diff --git a/tests/contract/utils_test.go b/tests/contract/utils_test.go index 6a212dfb..b26c8ba7 100644 --- a/tests/contract/utils_test.go +++ b/tests/contract/utils_test.go @@ -46,6 +46,7 @@ func before(ctx context.Context, sc *godog.Scenario) (context.Context, error) { newCtx = context.WithValue(newCtx, commonStateKey{}, commonState) newCtx = context.WithValue(newCtx, accessStateKey{}, accessState) + newCtx = context.WithValue(newCtx, cryptoStateKey{}, newCryptoState(contractTestConfig.featurePath+"/encryption")) if !contractTestConfig.serverMock { return newCtx, nil @@ -116,6 +117,7 @@ type contractTestConfig struct { serverMock bool hostPort string secure bool + featurePath string } func newContractTestConfig() (contractTestConfig, error) { @@ -135,6 +137,7 @@ func newContractTestConfig() (contractTestConfig, error) { hostPort: getenvWithDefault("HOST_PORT", "localhost:8090"), serverMock: serverMock, secure: secure, + featurePath: path, }, err } diff --git a/tests/e2e/file_upload_dec_output.txt b/tests/e2e/file_upload_dec_output.txt deleted file mode 100644 index 226f4a06..00000000 --- a/tests/e2e/file_upload_dec_output.txt +++ /dev/null @@ -1,352 +0,0 @@ -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== -File upload test - -Junk text start -============================== - - -Oh to talking improve produce in limited offices fifteen an. Wicket branch to answer do we. Place are decay men hours tiled. If or of ye throwing friendly required. Marianne interest in exertion as. Offering my branched confined oh dashwood. - -Their could can widen ten she any. As so we smart those money in. Am wrote up whole so tears sense oh. Absolute required of reserved in offering no. How sense found our those gay again taken the. Improved property reserved disposal do offering me. - -Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is. - -============================== -============================== - -END \ No newline at end of file diff --git a/tests/e2e/file_upload_sample_encrypted.txt b/tests/e2e/file_upload_sample_encrypted.txt deleted file mode 100644 index 6e61f95832057ab82f2a6a32b2455d776b5df647..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21904 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 0) - found := false - for i := range res6.Data { - if res6.Data[i].ID == id { - assert.Equal(name, res6.Data[i].Name) - assert.Equal(extid, res6.Data[i].ExternalID) - assert.Equal(purl, res6.Data[i].ProfileURL) - assert.Equal(email, res6.Data[i].Email) - // assert.Equal(res.Data.Created, res6.Data[i].Created) - if res2 != nil { - assert.Equal(res2.Data.Updated, res6.Data[i].Updated) - assert.Equal(res2.Data.ETag, res6.Data[i].ETag) - } - assert.Equal("b", res6.Data[i].Custom["a"]) - assert.Equal("d", res6.Data[i].Custom["c"]) - found = true - } - } - assert.True(found) - - res6F, st6F, err6F := pn.GetAllUUIDMetadata().Include(incl).Limit(100).Filter("name == '" + name + "'").Count(true).Execute() - assert.Nil(err6F) - assert.Equal(200, st6F.StatusCode) - assert.True(res6F.TotalCount > 0) - foundF := false - for i := range res6F.Data { - //fmt.Println(res6F.Data[i], id) - if res6F.Data[i].ID == id { - assert.Equal(name, res6F.Data[i].Name) - assert.Equal(extid, res6F.Data[i].ExternalID) - assert.Equal(purl, res6F.Data[i].ProfileURL) - assert.Equal(email, res6F.Data[i].Email) - // assert.Equal(res.Data.Created, res6F.Data[i].Created) - assert.Equal(res2.Data.Updated, res6F.Data[i].Updated) - assert.Equal(res2.Data.ETag, res6F.Data[i].ETag) - assert.Equal("b", res6F.Data[i].Custom["a"]) - assert.Equal("d", res6F.Data[i].Custom["c"]) - foundF = true - } - } - assert.True(foundF) - } - - //delete - res5, st5, err5 := pn.RemoveUUIDMetadata().UUID(id).Execute() - assert.Nil(err5) - assert.Equal(200, st5.StatusCode) - if res5 != nil { - assert.Nil(res5.Data) - } - - //getuser - res4, st4, err4 := pn.GetUUIDMetadata().Include(incl).UUID(id).Execute() - assert.NotNil(err4) - if res5 != nil { - assert.Nil(res4) - } - assert.Equal(404, st4.StatusCode) - -} - -func TestObjectsV2CreateUpdateGetDeleteChannel(t *testing.T) { - ObjectsCreateUpdateGetDeleteChannelCommon(t, false, false) -} - -func TestObjectsV2CreateUpdateGetDeleteChannelWithPAM(t *testing.T) { - ObjectsCreateUpdateGetDeleteChannelCommon(t, true, false) -} - -// func TestObjectsV2CreateUpdateGetDeleteChannelWithPAMWithoutSecKey(t *testing.T) { -// ObjectsCreateUpdateGetDeleteChannelCommon(t, true, true) -// } - -func ObjectsCreateUpdateGetDeleteChannelCommon(t *testing.T, withPAM, runWithoutSecretKey bool) { - assert := assert.New(t) - - pn := pubnub.NewPubNub(configCopy()) id := randomized("testchannel") - - if withPAM { - pn2 := ActivateWithPAMV2() - if runWithoutSecretKey { - tokens := RunGrantV2(pn2, []string{}, []string{id}, true, true, false, true, true, true) - SetPNV2(pn, pn2, tokens) - } else { - pn = pn2 - RunGrantV2(pn, []string{}, []string{id}, true, true, false, true, true, false) - } - - } if enableDebuggingInTests { pn.Config.Log = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) } name := randomized("name") desc := "desc" - - custom := make(map[string]interface{}) - custom["a"] = "b" - custom["c"] = "d" + custom := map[string]interface{}{"a": "b", "c": "d"} incl := []pubnub.PNChannelMetadataInclude{ pubnub.PNChannelMetadataIncludeCustom, } + defer removeChannelMetadata(a, pn, id) res, st, err := pn.SetChannelMetadata().Include(incl).Channel(id).Name(name).Description(desc).Custom(custom).Execute() - assert.Nil(err) - assert.Equal(200, st.StatusCode) + + a.Nil(err) + a.Equal(200, st.StatusCode) if res != nil { - assert.Equal(id, res.Data.ID) - assert.Equal(name, res.Data.Name) - assert.Equal(desc, res.Data.Description) - // assert.NotNil(res.Data.Created) - assert.NotNil(res.Data.Updated) - assert.NotNil(res.Data.ETag) - assert.Equal("b", res.Data.Custom["a"]) - assert.Equal("d", res.Data.Custom["c"]) + a.Equal(id, res.Data.ID) + a.Equal(name, res.Data.Name) + a.Equal(desc, res.Data.Description) + a.NotNil(res.Data.Updated) + a.NotNil(res.Data.ETag) + a.True(reflect.DeepEqual(custom, res.Data.Custom)) } desc = "desc2" - res2, st2, err2 := pn.SetChannelMetadata().Include(incl).Channel(id).Name(name).Description(desc).Custom(custom).Execute() - assert.Nil(err2) - assert.Equal(200, st2.StatusCode) - if res2 != nil { - assert.Equal(id, res2.Data.ID) - assert.Equal(name, res2.Data.Name) - assert.Equal(desc, res2.Data.Description) - // assert.Equal(res.Data.Created, res2.Data.Created) - assert.NotNil(res2.Data.Updated) - assert.NotNil(res2.Data.ETag) - assert.Equal("b", res2.Data.Custom["a"]) - assert.Equal("d", res2.Data.Custom["c"]) - } - - res3, st3, err3 := pn.GetChannelMetadata().Include(incl).Channel(id).Execute() - assert.Nil(err3) - assert.Equal(200, st3.StatusCode) - if res3 != nil { - assert.Equal(id, res3.Data.ID) - assert.Equal(name, res3.Data.Name) - assert.Equal(desc, res3.Data.Description) - // assert.Equal(res.Data.Created, res3.Data.Created) - assert.Equal(res2.Data.Updated, res3.Data.Updated) - assert.Equal(res2.Data.ETag, res3.Data.ETag) - assert.Equal("b", res3.Data.Custom["a"]) - assert.Equal("d", res3.Data.Custom["c"]) - } - - sort := []string{"updated:desc"} - //getusers - if withPAM { - res6, st6, err6 := pn.GetAllChannelMetadata().Include(incl).Sort(sort).Limit(100).Count(true).Execute() - assert.Nil(err6) - assert.Equal(200, st6.StatusCode) - found := false - if res6 != nil { - assert.True(res6.TotalCount > 0) - - for i := range res6.Data { - if res6.Data[i].ID == id { - assert.Equal(name, res6.Data[i].Name) - assert.Equal(desc, res6.Data[i].Description) - // assert.Equal(res.Data.Created, res6.Data[i].Created) - assert.Equal(res2.Data.Updated, res6.Data[i].Updated) - assert.Equal(res2.Data.ETag, res6.Data[i].ETag) - assert.Equal("b", res6.Data[i].Custom["a"]) - assert.Equal("d", res6.Data[i].Custom["c"]) - found = true - } - } - } - assert.True(found) - - res6F, st6F, err6F := pn.GetAllChannelMetadata().Include(incl).Limit(100).Filter("name like '" + name + "*'").Count(true).Execute() - assert.Nil(err6F) - assert.Equal(200, st6F.StatusCode) - foundF := false - if res6F != nil { - assert.True(res6F.TotalCount > 0) - - for i := range res6F.Data { - if res6F.Data[i].ID == id { - assert.Equal(name, res6F.Data[i].Name) - assert.Equal(desc, res6F.Data[i].Description) - // assert.Equal(res.Data.Created, res6F.Data[i].Created) - assert.Equal(res2.Data.Updated, res6F.Data[i].Updated) - assert.Equal(res2.Data.ETag, res6F.Data[i].ETag) - assert.Equal("b", res6F.Data[i].Custom["a"]) - assert.Equal("d", res6F.Data[i].Custom["c"]) - foundF = true - } - } - } - assert.True(foundF) - - } - - //delete - res5, st5, err5 := pn.RemoveChannelMetadata().Channel(id).Execute() - assert.Nil(err5) - assert.Equal(200, st5.StatusCode) - if res5 != nil { - assert.Nil(res5.Data) - } - - //getuser - res4, st4, err4 := pn.GetChannelMetadata().Include(incl).Channel(id).Execute() - assert.NotNil(err4) - if res4 != nil { - assert.Nil(res4) + res, st, err = pn.SetChannelMetadata().Include(incl).Channel(id).Name(name).Description(desc).Custom(custom).Execute() + a.Nil(err) + a.Equal(200, st.StatusCode) + if res != nil { + a.Equal(id, res.Data.ID) + a.Equal(desc, res.Data.Description) } - assert.Equal(404, st4.StatusCode) -} + _, st, err = pn.GetChannelMetadata().Include(incl).Channel(id).Execute() + a.Nil(err) + a.Equal(200, st.StatusCode) -func TestObjectsV2SetRemoveMembershipsV2(t *testing.T) { - ObjectsSetRemoveMembershipsCommonV2(t, false, false) } -func TestObjectsV2SetRemoveMembershipsV2WithPAM(t *testing.T) { - ObjectsSetRemoveMembershipsCommonV2(t, true, false) -} - -// PASSES after adding PAM checks for Update Members -// func TestObjectsV2SetRemoveMembershipsV2WithPAMWithoutSecKey(t *testing.T) { -// ObjectsSetRemoveMembershipsCommonV2(t, true, true) -// } - -func ObjectsSetRemoveMembershipsCommonV2(t *testing.T, withPAM, runWithoutSecretKey bool) { - assert := assert.New(t) - - limit := 100 - count := true - - pn := pubnub.NewPubNub(configCopy()) - - userid := randomized("testuser1") - channelid := randomized("testchannel") - - if withPAM { - pn2 := ActivateWithPAMV2() - if runWithoutSecretKey { - tokens := RunGrantV2(pn2, []string{userid}, []string{channelid}, true, true, true, true, true, true) - SetPNV2(pn, pn2, tokens) - } else { - pn = pn2 - RunGrantV2(pn, []string{userid}, []string{channelid}, true, true, true, true, true, false) - } - - } - if enableDebuggingInTests { - pn.Config.Log = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) - } - - name := randomized("name") - extid := "extid" - purl := "profileurl" - email := "email" - - custom := make(map[string]interface{}) - custom["a"] = "b" - custom["c"] = "d" - - inclUUID := []pubnub.PNUUIDMetadataInclude{ - pubnub.PNUUIDMetadataIncludeCustom, - } - - res, st, err := pn.SetUUIDMetadata().Include(inclUUID).UUID(userid).Name(name).ExternalID(extid).ProfileURL(purl).Email(email).Custom(custom).Execute() - assert.Nil(err) - assert.Equal(200, st.StatusCode) +func removeChannelMetadata(a *assert.Assertions, pn *pubnub.PubNub, id string) { + res, st, err := pn.RemoveChannelMetadata().Channel(id).Execute() + a.Nil(err) + a.Equal(200, st.StatusCode) if res != nil { - assert.Equal(userid, res.Data.ID) - assert.Equal(name, res.Data.Name) - assert.Equal(extid, res.Data.ExternalID) - assert.Equal(purl, res.Data.ProfileURL) - assert.Equal(email, res.Data.Email) - // assert.NotNil(res.Data.Created) - assert.NotNil(res.Data.Updated) - assert.NotNil(res.Data.ETag) - assert.Equal("b", res.Data.Custom["a"]) - assert.Equal("d", res.Data.Custom["c"]) + a.Nil(res.Data) } - - desc := "desc" - custom2 := make(map[string]interface{}) - custom2["a1"] = "b1" - custom2["c1"] = "d1" - - inclChannel := []pubnub.PNChannelMetadataInclude{ - pubnub.PNChannelMetadataIncludeCustom, - } - - res2, st2, err2 := pn.SetChannelMetadata().Include(inclChannel).Channel(channelid).Name(name).Description(desc).Custom(custom2).Execute() - assert.Nil(err2) - assert.Equal(200, st2.StatusCode) - //fmt.Println("res2-->", res2) - if res2 != nil { - assert.Equal(channelid, res2.Data.ID) - assert.Equal(name, res2.Data.Name) - assert.Equal(desc, res2.Data.Description) - // assert.NotNil(res2.Data.Created) - assert.NotNil(res2.Data.Updated) - assert.NotNil(res2.Data.ETag) - assert.Equal("b1", res2.Data.Custom["a1"]) - assert.Equal("d1", res2.Data.Custom["c1"]) - } - - userid2 := randomized("testuser2") - - _, st3, err3 := pn.SetUUIDMetadata().Include(inclUUID).UUID(userid2).Name(name).ExternalID(extid).ProfileURL(purl).Email(email).Custom(custom).Execute() - assert.Nil(err3) - assert.Equal(200, st3.StatusCode) - - channelid2 := randomized("testchannel") - - _, st4, err4 := pn.SetChannelMetadata().Include(inclChannel).Channel(channelid2).Name(name).Description(desc).Custom(custom2).Execute() - assert.Nil(err4) - assert.Equal(200, st4.StatusCode) - - userid3 := randomized("testuser3") - - _, stuser3, erruser3 := pn.SetUUIDMetadata().Include(inclUUID).UUID(userid3).Name(name).ExternalID(extid).ProfileURL(purl).Email(email).Custom(custom).Execute() - assert.Nil(erruser3) - assert.Equal(200, stuser3.StatusCode) - - channelid3 := randomized("testchannel") - - _, stchannel3, errchannel33 := pn.SetChannelMetadata().Include(inclChannel).Channel(channelid3).Name(name).Description(desc).Custom(custom2).Execute() - assert.Nil(errchannel33) - assert.Equal(200, stchannel3.StatusCode) - - inclSm := []pubnub.PNChannelMembersInclude{ - pubnub.PNChannelMembersIncludeUUIDCustom, - pubnub.PNChannelMembersIncludeUUID, - pubnub.PNChannelMembersIncludeCustom, - } - - custom3 := make(map[string]interface{}) - custom3["a3"] = "b3" - custom3["c3"] = "d3" - - uuid := pubnub.PNChannelMembersUUID{ - ID: userid, - } - - in := pubnub.PNChannelMembersSet{ - UUID: uuid, - Custom: custom3, - } - uuid3 := pubnub.PNChannelMembersUUID{ - ID: userid3, - } - - inUser3 := pubnub.PNChannelMembersSet{ - UUID: uuid3, - Custom: custom3, - } - - inArr := []pubnub.PNChannelMembersSet{ - in, - inUser3, - } - - //Add Channel Memberships - sortSetChannelMembers := []string{"uuid.id:desc"} - sort := []string{"updated:desc"} - - resAdd, stAdd, errAdd := pn.SetChannelMembers().Channel(channelid).Sort(sortSetChannelMembers).Set(inArr).Include(inclSm).Limit(limit).Count(count).Execute() - assert.Nil(errAdd) - assert.Equal(200, stAdd.StatusCode) - if errAdd == nil { - sortMembers1 := false - sortMembers2 := false - - found := false - assert.True(resAdd.TotalCount > 0) - //fmt.Println("resAdd-->", resAdd) - - for i := range resAdd.Data { - if resAdd.Data[i].UUID.ID == userid { - found = true - assert.Equal(custom3["a3"], resAdd.Data[i].Custom["a3"]) - assert.Equal(custom3["c3"], resAdd.Data[i].Custom["c3"]) - assert.Equal(userid, resAdd.Data[i].UUID.ID) - assert.Equal(name, resAdd.Data[i].UUID.Name) - assert.Equal(extid, resAdd.Data[i].UUID.ExternalID) - assert.Equal(purl, resAdd.Data[i].UUID.ProfileURL) - assert.Equal(email, resAdd.Data[i].UUID.Email) - assert.Equal(custom["a"], resAdd.Data[i].UUID.Custom["a"]) - assert.Equal(custom["c"], resAdd.Data[i].UUID.Custom["c"]) - } - } - if (resAdd.Data != nil) && (len(resAdd.Data) > 1) { - sortMembers1 = (resAdd.Data[1].UUID.ID == userid) - sortMembers2 = (resAdd.Data[0].UUID.ID == userid3) - assert.True(sortMembers1) - assert.True(sortMembers2) - } else { - assert.Fail("Sort ", "resAdd.Data null or ", len(resAdd.Data)) - } - - assert.True(found) - } else { - if enableDebuggingInTests { - - fmt.Println("ManageMembers->", errAdd.Error()) - } - } - - //Update Channel Memberships - if !withPAM { - - custom4 := make(map[string]interface{}) - custom4["a2"] = "b2" - custom4["c2"] = "d2" - - up := pubnub.PNChannelMembersSet{ - UUID: uuid, - Custom: custom4, - } - - upArr := []pubnub.PNChannelMembersSet{ - up, - } - - resUp, stUp, errUp := pn.SetChannelMembers().Channel(channelid).Sort(sort).Set(upArr).Include(inclSm).Limit(limit).Count(count).Execute() - assert.Nil(errUp) - assert.Equal(200, stUp.StatusCode) - if errUp == nil { - assert.True(resUp.TotalCount > 0) - foundUp := false - for i := range resUp.Data { - if resUp.Data[i].UUID.ID == userid { - foundUp = true - assert.Equal("b2", resUp.Data[i].Custom["a2"]) - assert.Equal("d2", resUp.Data[i].Custom["c2"]) - //assert.Equal(userid, resAdd.Data[i].UUID.ID) - assert.Equal(name, resAdd.Data[i].UUID.Name) - assert.Equal(extid, resAdd.Data[i].UUID.ExternalID) - assert.Equal(purl, resAdd.Data[i].UUID.ProfileURL) - assert.Equal(email, resAdd.Data[i].UUID.Email) - assert.Equal(custom["a"], resAdd.Data[i].UUID.Custom["a"]) - assert.Equal(custom["c"], resAdd.Data[i].UUID.Custom["c"]) - - } - } - assert.True(foundUp) - } else { - if enableDebuggingInTests { - - fmt.Println("ManageMembers->", errUp.Error()) - } - } - } - //Get Channel Memberships - - inclMemberships := []pubnub.PNMembershipsInclude{ - pubnub.PNMembershipsIncludeCustom, - pubnub.PNMembershipsIncludeChannel, - pubnub.PNMembershipsIncludeChannelCustom, - } - - //fmt.Println("GetMemberships ====>") - - resGetMem, stGetMem, errGetMem := pn.GetMemberships().UUID(userid).Include(inclMemberships).Sort(sort).Limit(limit).Count(count).Execute() - foundGetMem := false - assert.Nil(errGetMem) - if errGetMem == nil { - for i := range resGetMem.Data { - if resGetMem.Data[i].Channel.ID == channelid { - foundGetMem = true - assert.Equal(name, resGetMem.Data[i].Channel.Name) - assert.Equal(desc, resGetMem.Data[i].Channel.Description) - assert.Equal("b1", resGetMem.Data[i].Channel.Custom["a1"]) - assert.Equal("d1", resGetMem.Data[i].Channel.Custom["c1"]) - if withPAM { - assert.Equal("b3", resGetMem.Data[i].Custom["a3"]) - assert.Equal("d3", resGetMem.Data[i].Custom["c3"]) - } else { - assert.Equal("b2", resGetMem.Data[i].Custom["a2"]) - assert.Equal("d2", resGetMem.Data[i].Custom["c2"]) - } - } - } - assert.Equal(200, stGetMem.StatusCode) - assert.True(foundGetMem) - } else { - if enableDebuggingInTests { - fmt.Println("GetMemberships->", errGetMem.Error()) - } - } - - //filterExp := fmt.Sprintf("custom.c3 == '%s' || custom.c2 == '%s'", "d3", "d2") - filterExp := fmt.Sprintf("channel.name == '%s'", name) - - //fmt.Println("GetMemberships ====>", filterExp) - - resGetMemF, stGetMemF, errGetMemF := pn.GetMemberships().UUID(userid).Include(inclMemberships).Filter(filterExp).Limit(limit).Count(count).Execute() - foundGetMemF := false - assert.Nil(errGetMemF) - if errGetMemF == nil { - for i := range resGetMemF.Data { - if resGetMemF.Data[i].Channel.ID == channelid { - foundGetMemF = true - assert.Equal(name, resGetMemF.Data[i].Channel.Name) - assert.Equal(desc, resGetMemF.Data[i].Channel.Description) - assert.Equal("b1", resGetMemF.Data[i].Channel.Custom["a1"]) - assert.Equal("d1", resGetMemF.Data[i].Channel.Custom["c1"]) - if withPAM { - assert.Equal("b3", resGetMemF.Data[i].Custom["a3"]) - assert.Equal("d3", resGetMemF.Data[i].Custom["c3"]) - } else { - assert.Equal("b2", resGetMemF.Data[i].Custom["a2"]) - assert.Equal("d2", resGetMemF.Data[i].Custom["c2"]) - } - } - } - assert.Equal(200, stGetMemF.StatusCode) - assert.True(foundGetMemF) - } else { - if enableDebuggingInTests { - - fmt.Println("GetMemberships->", errGetMemF.Error()) - } - } - - //Remove Channel Memberships - re := pubnub.PNChannelMembersRemove{ - UUID: uuid, - } - - reArr := []pubnub.PNChannelMembersRemove{ - re, - } - resRem, stRem, errRem := pn.RemoveChannelMembers().Channel(channelid).Remove(reArr).Include(inclSm).Limit(limit).Count(count).Execute() - assert.Nil(errRem) - assert.Equal(200, stRem.StatusCode) - //fmt.Println("====>stRem.StatusCode", stRem.StatusCode) - if errRem == nil { - - foundRem := false - for i := range resRem.Data { - if resRem.Data[i].UUID.ID == userid { - foundRem = true - assert.Equal("b2", resRem.Data[i].Custom["a2"]) - assert.Equal("d2", resRem.Data[i].Custom["c2"]) - assert.Equal(userid, resRem.Data[i].UUID.ID) - assert.Equal(name, resRem.Data[i].UUID.Name) - assert.Equal(extid, resRem.Data[i].UUID.ExternalID) - assert.Equal(purl, resRem.Data[i].UUID.ProfileURL) - assert.Equal(email, resRem.Data[i].UUID.Email) - assert.Equal(custom["a"], resRem.Data[i].UUID.Custom["a"]) - assert.Equal(custom["c"], resRem.Data[i].UUID.Custom["c"]) - - } - } - assert.False(foundRem) - } else { - if enableDebuggingInTests { - - fmt.Println("ManageMembers->", errRem.Error()) - } - } - - channel2 := pubnub.PNMembershipsChannel{ - ID: channelid2, - } - - inMem := pubnub.PNMembershipsSet{ - Channel: channel2, - Custom: custom3, - } - - channel3 := pubnub.PNMembershipsChannel{ - ID: channelid3, - } - - inMemChannel3 := pubnub.PNMembershipsSet{ - Channel: channel3, - Custom: custom3, - } - - inArrMem := []pubnub.PNMembershipsSet{ - inMem, - inMemChannel3, - } - - //Add user memberships - resManageMemAdd, stManageMemAdd, errManageMemAdd := pn.SetMemberships().UUID(userid2).Set(inArrMem).Include(inclMemberships).Limit(limit).Count(count).Execute() - //fmt.Println("resManageMemAdd -->", resManageMemAdd) - assert.Nil(errManageMemAdd) - assert.Equal(200, stManageMemAdd.StatusCode) - if errManageMemAdd == nil { - foundManageMembers := false - for i := range resManageMemAdd.Data { - if resManageMemAdd.Data[i].Channel.ID == channelid2 { - assert.Equal(channelid2, resManageMemAdd.Data[i].Channel.ID) - assert.Equal(name, resManageMemAdd.Data[i].Channel.Name) - assert.Equal(desc, resManageMemAdd.Data[i].Channel.Description) - assert.Equal(custom2["a1"], resManageMemAdd.Data[i].Channel.Custom["a1"]) - assert.Equal(custom2["c1"], resManageMemAdd.Data[i].Channel.Custom["c1"]) - assert.Equal(custom3["a3"], resManageMemAdd.Data[i].Custom["a3"]) - assert.Equal(custom3["c3"], resManageMemAdd.Data[i].Custom["c3"]) - foundManageMembers = true - } - } - assert.True(foundManageMembers) - } else { - if enableDebuggingInTests { - - fmt.Println("ManageMemberships->", errManageMemAdd.Error()) - } - } - - // //Update user memberships - - custom5 := make(map[string]interface{}) - custom5["a5"] = "b5" - custom5["c5"] = "d5" - - upMem := pubnub.PNMembershipsSet{ - Channel: channel2, - Custom: custom5, - } - - upArrMem := []pubnub.PNMembershipsSet{ - upMem, - } - sortMemberships1 := false - sortMemberships2 := false - - resManageMemUp, stManageMemUp, errManageMemUp := pn.SetMemberships().UUID(userid2).Sort(sort).Set(upArrMem).Include(inclMemberships).Limit(limit).Count(count).Execute() - //fmt.Println("resManageMemUp -->", resManageMemUp) - assert.Nil(errManageMemUp) - assert.Equal(200, stManageMemUp.StatusCode) - if errManageMemUp == nil { - foundManageMembersUp := false - - for i := range resManageMemUp.Data { - //fmt.Println("resManageMemUp.Data[i].ID == channelid2-->", resRem.Data[i].UUID.ID, channelid2) - if resManageMemUp.Data[i].Channel.ID == channelid2 { - assert.Equal(channelid2, resManageMemUp.Data[i].Channel.ID) - assert.Equal(name, resManageMemUp.Data[i].Channel.Name) - assert.Equal(desc, resManageMemUp.Data[i].Channel.Description) - assert.Equal(custom2["a1"], resManageMemAdd.Data[i].Channel.Custom["a1"]) - assert.Equal(custom2["c1"], resManageMemAdd.Data[i].Channel.Custom["c1"]) - assert.Equal(custom5["a5"], resManageMemUp.Data[i].Custom["a5"]) - assert.Equal(custom5["c5"], resManageMemUp.Data[i].Custom["c5"]) - foundManageMembersUp = true - } - } - if (resManageMemUp.Data != nil) && (len(resManageMemUp.Data) > 1) { - sortMemberships1 = (resManageMemUp.Data[0].Channel.ID == channelid2) - sortMemberships2 = (resManageMemUp.Data[1].Channel.ID == channelid3) - assert.True(sortMemberships1) - assert.True(sortMemberships2) - } else { - assert.Fail("Sort ", "resManageMemUp.Data null or ", len(resManageMemUp.Data)) - } - - assert.True(foundManageMembersUp) - } else { - if enableDebuggingInTests { - - fmt.Println("ManageMemberships->", errManageMemUp.Error()) - } - } - - // //Get members - resGetMembers, stGetMembers, errGetMembers := pn.GetChannelMembers().Channel(channelid2).Include(inclSm).Limit(limit).Count(count).Execute() - //fmt.Println("resGetMembers -->", resGetMembers) - assert.Nil(errGetMembers) - assert.Equal(200, stGetMembers.StatusCode) - if errGetMembers == nil { - foundGetMembers := false - for i := range resGetMembers.Data { - if resGetMembers.Data[i].UUID.ID == userid2 { - foundGetMembers = true - assert.Equal(name, resGetMembers.Data[i].UUID.Name) - assert.Equal(extid, resGetMembers.Data[i].UUID.ExternalID) - assert.Equal(purl, resGetMembers.Data[i].UUID.ProfileURL) - assert.Equal(email, resGetMembers.Data[i].UUID.Email) - assert.Equal(custom["a"], resGetMembers.Data[i].UUID.Custom["a"]) - assert.Equal(custom["c"], resGetMembers.Data[i].UUID.Custom["c"]) - assert.Equal(custom5["a5"], resGetMembers.Data[i].Custom["a5"]) - assert.Equal(custom5["c5"], resGetMembers.Data[i].Custom["c5"]) - } - } - - assert.True(foundGetMembers) - } else { - if enableDebuggingInTests { - - fmt.Println("GetMembers->", errGetMembers.Error()) - } - } - - //filterExp2 := fmt.Sprintf("custom.a5 == '%s' || custom.c5 == '%s'", custom5["a5"], custom5["c5"]) - filterExp2 := fmt.Sprintf("uuid.name == '%s'", name) - //fmt.Println("GetMembers ====>", filterExp2) - - resGetMembersF, stGetMembersF, errGetMembersF := pn.GetChannelMembers().Channel(channelid2).Include(inclSm).Filter(filterExp2).Limit(limit).Count(count).Execute() - //fmt.Println("resGetMembers -->", resGetMembersF) - assert.Nil(errGetMembersF) - assert.Equal(200, stGetMembersF.StatusCode) - if errGetMembersF == nil { - foundGetMembersF := false - - for i := range resGetMembersF.Data { - if resGetMembersF.Data[i].UUID.ID == userid2 { - foundGetMembersF = true - assert.Equal(name, resGetMembersF.Data[i].UUID.Name) - assert.Equal(extid, resGetMembersF.Data[i].UUID.ExternalID) - assert.Equal(purl, resGetMembersF.Data[i].UUID.ProfileURL) - assert.Equal(email, resGetMembersF.Data[i].UUID.Email) - assert.Equal(custom["a"], resGetMembersF.Data[i].UUID.Custom["a"]) - assert.Equal(custom["c"], resGetMembersF.Data[i].UUID.Custom["c"]) - assert.Equal(custom5["a5"], resGetMembersF.Data[i].Custom["a5"]) - assert.Equal(custom5["c5"], resGetMembersF.Data[i].Custom["c5"]) - } - } - assert.True(foundGetMembersF) - } else { - if enableDebuggingInTests { - - fmt.Println("GetMembers->", errGetMembersF.Error()) - } - } - - // //Remove user memberships - - reMem := pubnub.PNMembershipsRemove{ - Channel: channel2, - } - - reArrMem := []pubnub.PNMembershipsRemove{ - reMem, - } - resManageMemRem, stManageMemRem, errManageMemRem := pn.RemoveMemberships().UUID(userid2).Sort(sort).Remove(reArrMem).Include(inclMemberships).Limit(limit).Count(count).Execute() - assert.Nil(errManageMemRem) - assert.Equal(200, stManageMemRem.StatusCode) - if errManageMemRem == nil { - - foundManageMemRem := false - for i := range resManageMemRem.Data { - if resManageMemRem.Data[i].Channel.ID == channelid2 { - foundManageMemRem = true - } - } - assert.False(foundManageMemRem) - } else { - if enableDebuggingInTests { - - fmt.Println("ManageMemberships->", errManageMemRem.Error()) - } - } - - //delete - res5, st5, err5 := pn.RemoveUUIDMetadata().UUID(userid).Execute() - assert.Nil(err5) - assert.Equal(200, st5.StatusCode) - - assert.Nil(res5.Data) - - //delete - res6, st6, err6 := pn.RemoveChannelMetadata().Channel(channelid).Execute() - assert.Nil(err6) - assert.Equal(200, st6.StatusCode) - assert.Nil(res6.Data) - - //delete - res52, st52, err52 := pn.RemoveUUIDMetadata().UUID(userid2).Execute() - assert.Nil(err52) - assert.Equal(200, st52.StatusCode) - if res52 != nil { - assert.Nil(res52.Data) - } - - //delete - res62, st62, err62 := pn.RemoveChannelMetadata().Channel(channelid2).Execute() - assert.Nil(err62) - assert.Equal(200, st62.StatusCode) - if res62 != nil { - assert.Nil(res62.Data) - } - -} - -func TestObjectsV2MembershipsV2(t *testing.T) { - ObjectsMembershipsCommonV2(t, false, false) } -func TestObjectsV2MembershipsV2WithPAM(t *testing.T) { - ObjectsMembershipsCommonV2(t, true, false) -} - -// PASSES after adding PAM checks for Update Members -// func TestObjectsV2MembershipsV2WithPAMWithoutSecKey(t *testing.T) { -// ObjectsMembershipsCommonV2(t, true, true) -// } - -func ObjectsMembershipsCommonV2(t *testing.T, withPAM, runWithoutSecretKey bool) { - assert := assert.New(t) - - limit := 100 - count := true +func TestObjectsV2UUIDMetadataSetUpdateGetRemove(t *testing.T) { + a := assert.New(t) pn := pubnub.NewPubNub(configCopy()) - userid := randomized("testuser1") - channelid := randomized("testchannel1") - - if withPAM { - pn2 := ActivateWithPAMV2() - if runWithoutSecretKey { - tokens := RunGrantV2(pn2, []string{userid}, []string{channelid}, true, true, true, true, true, true) - SetPNV2(pn, pn2, tokens) - } else { - pn = pn2 - RunGrantV2(pn, []string{userid}, []string{channelid}, true, true, true, true, true, false) - } - - } + id := randomized("testuuid") if enableDebuggingInTests { pn.Config.Log = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) } name := randomized("name") - extid := "extid" - purl := "profileurl" - email := "email" + email := "go@pubnub.com" + custom := map[string]interface{}{"a": "b", "c": "d"} - custom := make(map[string]interface{}) - custom["a"] = "b" - custom["c"] = "d" - - inclUUID := []pubnub.PNUUIDMetadataInclude{ + incl := []pubnub.PNUUIDMetadataInclude{ pubnub.PNUUIDMetadataIncludeCustom, } - res, st, err := pn.SetUUIDMetadata().Include(inclUUID).UUID(userid).Name(name).ExternalID(extid).ProfileURL(purl).Email(email).Custom(custom).Execute() - assert.Nil(err) - assert.Equal(200, st.StatusCode) - if res != nil { - assert.Equal(userid, res.Data.ID) - assert.Equal(name, res.Data.Name) - assert.Equal(extid, res.Data.ExternalID) - assert.Equal(purl, res.Data.ProfileURL) - assert.Equal(email, res.Data.Email) - // assert.NotNil(res.Data.Created) - assert.NotNil(res.Data.Updated) - assert.NotNil(res.Data.ETag) - assert.Equal("b", res.Data.Custom["a"]) - assert.Equal("d", res.Data.Custom["c"]) - } - - desc := "desc" - custom2 := make(map[string]interface{}) - custom2["a1"] = "b1" - custom2["c1"] = "d1" - - inclChannel := []pubnub.PNChannelMetadataInclude{ - pubnub.PNChannelMetadataIncludeCustom, - } - - res2, st2, err2 := pn.SetChannelMetadata().Include(inclChannel).Channel(channelid).Name(name).Description(desc).Custom(custom2).Execute() - assert.Nil(err2) - assert.Equal(200, st2.StatusCode) - //fmt.Println("res2-->", res2) - if res2 != nil { - assert.Equal(channelid, res2.Data.ID) - assert.Equal(name, res2.Data.Name) - assert.Equal(desc, res2.Data.Description) - // assert.NotNil(res2.Data.Created) - assert.NotNil(res2.Data.Updated) - assert.NotNil(res2.Data.ETag) - assert.Equal("b1", res2.Data.Custom["a1"]) - assert.Equal("d1", res2.Data.Custom["c1"]) - } - - userid2 := randomized("testuser2") - - _, st3, err3 := pn.SetUUIDMetadata().Include(inclUUID).UUID(userid2).Name(name).ExternalID(extid).ProfileURL(purl).Email(email).Custom(custom).Execute() - assert.Nil(err3) - assert.Equal(200, st3.StatusCode) - - channelid2 := randomized("testchannel2") - - _, st4, err4 := pn.SetChannelMetadata().Include(inclChannel).Channel(channelid2).Name(name).Description(desc).Custom(custom2).Execute() - assert.Nil(err4) - assert.Equal(200, st4.StatusCode) - - userid3 := randomized("testuser3") - - _, stuser3, erruser3 := pn.SetUUIDMetadata().Include(inclUUID).UUID(userid3).Name(name).ExternalID(extid).ProfileURL(purl).Email(email).Custom(custom).Execute() - assert.Nil(erruser3) - assert.Equal(200, stuser3.StatusCode) - - channelid3 := randomized("testchannel3") - - _, stchannel3, errchannel3 := pn.SetChannelMetadata().Include(inclChannel).Channel(channelid3).Name(name).Description(desc).Custom(custom2).Execute() - assert.Nil(errchannel3) - assert.Equal(200, stchannel3.StatusCode) - - inclSm := []pubnub.PNChannelMembersInclude{ - pubnub.PNChannelMembersIncludeUUIDCustom, - pubnub.PNChannelMembersIncludeUUID, - pubnub.PNChannelMembersIncludeCustom, - } - - custom3 := make(map[string]interface{}) - custom3["a3"] = "b3" - custom3["c3"] = "d3" - - uuid := pubnub.PNChannelMembersUUID{ - ID: userid, - } - - in := pubnub.PNChannelMembersSet{ - UUID: uuid, - Custom: custom3, - } - uuid3 := pubnub.PNChannelMembersUUID{ - ID: userid3, - } - - inUser3 := pubnub.PNChannelMembersSet{ - UUID: uuid3, - Custom: custom3, - } - - inArr := []pubnub.PNChannelMembersSet{ - in, - inUser3, - } - - //Add Channel Memberships - sortManageChannelMembers := []string{"uuid.id:desc"} - sort := []string{"updated:desc"} - - resAdd, stAdd, errAdd := pn.ManageChannelMembers().Channel(channelid).Sort(sortManageChannelMembers).Set(inArr).Remove([]pubnub.PNChannelMembersRemove{}).Include(inclSm).Limit(limit).Count(count).Execute() - assert.Nil(errAdd) - assert.Equal(200, stAdd.StatusCode) - if errAdd == nil { - sortMembers1 := false - sortMembers2 := false - - found := false - assert.True(resAdd.TotalCount > 0) - //fmt.Println("resAdd-->", resAdd) - - for i := range resAdd.Data { - if resAdd.Data[i].UUID.ID == userid { - found = true - assert.Equal(custom3["a3"], resAdd.Data[i].Custom["a3"]) - assert.Equal(custom3["c3"], resAdd.Data[i].Custom["c3"]) - assert.Equal(userid, resAdd.Data[i].UUID.ID) - assert.Equal(name, resAdd.Data[i].UUID.Name) - assert.Equal(extid, resAdd.Data[i].UUID.ExternalID) - assert.Equal(purl, resAdd.Data[i].UUID.ProfileURL) - assert.Equal(email, resAdd.Data[i].UUID.Email) - assert.Equal(custom["a"], resAdd.Data[i].UUID.Custom["a"]) - assert.Equal(custom["c"], resAdd.Data[i].UUID.Custom["c"]) - } - } - if (resAdd.Data != nil) && (len(resAdd.Data) > 1) { - sortMembers1 = (resAdd.Data[1].UUID.ID == userid) - sortMembers2 = (resAdd.Data[0].UUID.ID == userid3) - assert.True(sortMembers1) - assert.True(sortMembers2) - } else { - assert.Fail("Sort ", "resAdd.Data null or ", len(resAdd.Data)) - } - - assert.True(found) - } else { - if enableDebuggingInTests { - - fmt.Println("ManageMembers->", errAdd.Error()) - } - } - - //Update Channel Memberships - if !withPAM { - - custom4 := make(map[string]interface{}) - custom4["a2"] = "b2" - custom4["c2"] = "d2" - - up := pubnub.PNChannelMembersSet{ - UUID: uuid, - Custom: custom4, - } - - upArr := []pubnub.PNChannelMembersSet{ - up, - } - - resUp, stUp, errUp := pn.ManageChannelMembers().Channel(channelid).Sort(sort).Set(upArr).Remove([]pubnub.PNChannelMembersRemove{}).Include(inclSm).Limit(limit).Count(count).Execute() - assert.Nil(errUp) - assert.Equal(200, stUp.StatusCode) - if errUp == nil { - assert.True(resUp.TotalCount > 0) - foundUp := false - for i := range resUp.Data { - if resUp.Data[i].UUID.ID == userid { - foundUp = true - assert.Equal("b2", resUp.Data[i].Custom["a2"]) - assert.Equal("d2", resUp.Data[i].Custom["c2"]) - //assert.Equal(userid, resAdd.Data[i].UUID.ID) - assert.Equal(name, resAdd.Data[i].UUID.Name) - assert.Equal(extid, resAdd.Data[i].UUID.ExternalID) - assert.Equal(purl, resAdd.Data[i].UUID.ProfileURL) - assert.Equal(email, resAdd.Data[i].UUID.Email) - assert.Equal(custom["a"], resAdd.Data[i].UUID.Custom["a"]) - assert.Equal(custom["c"], resAdd.Data[i].UUID.Custom["c"]) - - } - } - assert.True(foundUp) - } else { - if enableDebuggingInTests { - - fmt.Println("ManageMembers->", errUp.Error()) - } - } - } - //Get Channel Memberships - - inclMemberships := []pubnub.PNMembershipsInclude{ - pubnub.PNMembershipsIncludeCustom, - pubnub.PNMembershipsIncludeChannel, - pubnub.PNMembershipsIncludeChannelCustom, - } - - //fmt.Println("GetMemberships ====>") - - resGetMem, stGetMem, errGetMem := pn.GetMemberships().UUID(userid).Include(inclMemberships).Sort(sort).Limit(limit).Count(count).Execute() - foundGetMem := false - assert.Nil(errGetMem) - if errGetMem == nil { - for i := range resGetMem.Data { - if resGetMem.Data[i].Channel.ID == channelid { - foundGetMem = true - assert.Equal(name, resGetMem.Data[i].Channel.Name) - assert.Equal(desc, resGetMem.Data[i].Channel.Description) - assert.Equal("b1", resGetMem.Data[i].Channel.Custom["a1"]) - assert.Equal("d1", resGetMem.Data[i].Channel.Custom["c1"]) - if withPAM { - assert.Equal("b3", resGetMem.Data[i].Custom["a3"]) - assert.Equal("d3", resGetMem.Data[i].Custom["c3"]) - } else { - assert.Equal("b2", resGetMem.Data[i].Custom["a2"]) - assert.Equal("d2", resGetMem.Data[i].Custom["c2"]) - } - } - } - assert.Equal(200, stGetMem.StatusCode) - assert.True(foundGetMem) - } else { - if enableDebuggingInTests { - fmt.Println("GetMemberships->", errGetMem.Error()) - } - } - - //filterExp := fmt.Sprintf("custom.c3 == '%s' || custom.c2 == '%s'", "d3", "d2") - filterExp := fmt.Sprintf("channel.name == '%s'", name) - - //fmt.Println("GetMemberships ====>", filterExp) - - resGetMemF, stGetMemF, errGetMemF := pn.GetMemberships().UUID(userid).Include(inclMemberships).Filter(filterExp).Limit(limit).Count(count).Execute() - foundGetMemF := false - assert.Nil(errGetMemF) - if errGetMemF == nil { - for i := range resGetMemF.Data { - if resGetMemF.Data[i].Channel.ID == channelid { - foundGetMemF = true - assert.Equal(name, resGetMemF.Data[i].Channel.Name) - assert.Equal(desc, resGetMemF.Data[i].Channel.Description) - assert.Equal("b1", resGetMemF.Data[i].Channel.Custom["a1"]) - assert.Equal("d1", resGetMemF.Data[i].Channel.Custom["c1"]) - if withPAM { - assert.Equal("b3", resGetMemF.Data[i].Custom["a3"]) - assert.Equal("d3", resGetMemF.Data[i].Custom["c3"]) - } else { - assert.Equal("b2", resGetMemF.Data[i].Custom["a2"]) - assert.Equal("d2", resGetMemF.Data[i].Custom["c2"]) - } - } - } - assert.Equal(200, stGetMemF.StatusCode) - assert.True(foundGetMemF) - } else { - if enableDebuggingInTests { - - fmt.Println("GetMemberships->", errGetMemF.Error()) - } - } - - //Remove Channel Memberships - re := pubnub.PNChannelMembersRemove{ - UUID: uuid, - } - - reArr := []pubnub.PNChannelMembersRemove{ - re, - } - resRem, stRem, errRem := pn.ManageChannelMembers().Channel(channelid).Set([]pubnub.PNChannelMembersSet{}).Remove(reArr).Include(inclSm).Limit(limit).Count(count).Execute() - assert.Nil(errRem) - assert.Equal(200, stRem.StatusCode) - if errRem == nil { + defer removeUUIDMetadata(a, pn, id) + res, st, err := pn.SetUUIDMetadata().Include(incl).UUID(id).Name(name).Email(email).Custom(custom).Execute() - foundRem := false - for i := range resRem.Data { - if resRem.Data[i].UUID.ID == userid { - foundRem = true - assert.Equal("b2", resRem.Data[i].Custom["a2"]) - assert.Equal("d2", resRem.Data[i].Custom["c2"]) - assert.Equal(userid, resRem.Data[i].UUID.ID) - assert.Equal(name, resRem.Data[i].UUID.Name) - assert.Equal(extid, resRem.Data[i].UUID.ExternalID) - assert.Equal(purl, resRem.Data[i].UUID.ProfileURL) - assert.Equal(email, resRem.Data[i].UUID.Email) - assert.Equal(custom["a"], resRem.Data[i].UUID.Custom["a"]) - assert.Equal(custom["c"], resRem.Data[i].UUID.Custom["c"]) - - } - } - assert.False(foundRem) - } else { - if enableDebuggingInTests { - - fmt.Println("ManageMembers->", errRem.Error()) - } - } - - channel2 := pubnub.PNMembershipsChannel{ - ID: channelid2, - } - - inMem := pubnub.PNMembershipsSet{ - Channel: channel2, - Custom: custom3, - } - - channel3 := pubnub.PNMembershipsChannel{ - ID: channelid3, - } - - inMemChannel3 := pubnub.PNMembershipsSet{ - Channel: channel3, - Custom: custom3, - } - - inArrMem := []pubnub.PNMembershipsSet{ - inMem, - inMemChannel3, - } - - //Add user memberships - resManageMemAdd, stManageMemAdd, errManageMemAdd := pn.ManageMemberships().UUID(userid2).Set(inArrMem).Remove([]pubnub.PNMembershipsRemove{}).Include(inclMemberships).Limit(limit).Count(count).Execute() - //fmt.Println("resManageMemAdd -->", resManageMemAdd) - assert.Nil(errManageMemAdd) - assert.Equal(200, stManageMemAdd.StatusCode) - if errManageMemAdd == nil { - foundManageMembers := false - for i := range resManageMemAdd.Data { - if resManageMemAdd.Data[i].Channel.ID == channelid2 { - assert.Equal(channelid2, resManageMemAdd.Data[i].Channel.ID) - assert.Equal(name, resManageMemAdd.Data[i].Channel.Name) - assert.Equal(desc, resManageMemAdd.Data[i].Channel.Description) - assert.Equal(custom2["a1"], resManageMemAdd.Data[i].Channel.Custom["a1"]) - assert.Equal(custom2["c1"], resManageMemAdd.Data[i].Channel.Custom["c1"]) - assert.Equal(custom3["a3"], resManageMemAdd.Data[i].Custom["a3"]) - assert.Equal(custom3["c3"], resManageMemAdd.Data[i].Custom["c3"]) - foundManageMembers = true - } - } - assert.True(foundManageMembers) - } else { - if enableDebuggingInTests { - - fmt.Println("ManageMemberships->", errManageMemAdd.Error()) - } - } - - // //Update user memberships - - custom5 := make(map[string]interface{}) - custom5["a5"] = "b5" - custom5["c5"] = "d5" - - upMem := pubnub.PNMembershipsSet{ - Channel: channel2, - Custom: custom5, - } - - upArrMem := []pubnub.PNMembershipsSet{ - upMem, - } - sortMemberships1 := false - sortMemberships2 := false - - resManageMemUp, stManageMemUp, errManageMemUp := pn.ManageMemberships().UUID(userid2).Sort(sort).Set(upArrMem).Remove([]pubnub.PNMembershipsRemove{}).Include(inclMemberships).Limit(limit).Count(count).Execute() - //fmt.Println("resManageMemUp -->", resManageMemUp) - assert.Nil(errManageMemUp) - assert.Equal(200, stManageMemUp.StatusCode) - if errManageMemUp == nil { - foundManageMembersUp := false - - for i := range resManageMemUp.Data { - //fmt.Println("resManageMemUp.Data[i].ID == channelid2-->", resRem.Data[i].UUID.ID, channelid2) - if resManageMemUp.Data[i].Channel.ID == channelid2 { - assert.Equal(channelid2, resManageMemUp.Data[i].Channel.ID) - assert.Equal(name, resManageMemUp.Data[i].Channel.Name) - assert.Equal(desc, resManageMemUp.Data[i].Channel.Description) - assert.Equal(custom2["a1"], resManageMemAdd.Data[i].Channel.Custom["a1"]) - assert.Equal(custom2["c1"], resManageMemAdd.Data[i].Channel.Custom["c1"]) - assert.Equal(custom5["a5"], resManageMemUp.Data[i].Custom["a5"]) - assert.Equal(custom5["c5"], resManageMemUp.Data[i].Custom["c5"]) - foundManageMembersUp = true - } - } - if (resManageMemUp.Data != nil) && (len(resManageMemUp.Data) > 1) { - sortMemberships1 = (resManageMemUp.Data[0].Channel.ID == channelid2) - sortMemberships2 = (resManageMemUp.Data[1].Channel.ID == channelid3) - assert.True(sortMemberships1) - assert.True(sortMemberships2) - } else { - assert.Fail("Sort ", "resManageMemUp.Data null or ", len(resManageMemUp.Data)) - } - - assert.True(foundManageMembersUp) - } else { - if enableDebuggingInTests { - - fmt.Println("ManageMemberships->", errManageMemUp.Error()) - } - } - - // //Get members - resGetMembers, stGetMembers, errGetMembers := pn.GetChannelMembers().Channel(channelid2).Include(inclSm).Limit(limit).Count(count).Execute() - //fmt.Println("resGetMembers -->", resGetMembers) - assert.Nil(errGetMembers) - assert.Equal(200, stGetMembers.StatusCode) - if errGetMembers == nil { - foundGetMembers := false - for i := range resGetMembers.Data { - if resGetMembers.Data[i].UUID.ID == userid2 { - foundGetMembers = true - assert.Equal(name, resGetMembers.Data[i].UUID.Name) - assert.Equal(extid, resGetMembers.Data[i].UUID.ExternalID) - assert.Equal(purl, resGetMembers.Data[i].UUID.ProfileURL) - assert.Equal(email, resGetMembers.Data[i].UUID.Email) - assert.Equal(custom["a"], resGetMembers.Data[i].UUID.Custom["a"]) - assert.Equal(custom["c"], resGetMembers.Data[i].UUID.Custom["c"]) - assert.Equal(custom5["a5"], resGetMembers.Data[i].Custom["a5"]) - assert.Equal(custom5["c5"], resGetMembers.Data[i].Custom["c5"]) - } - } - - assert.True(foundGetMembers) - } else { - if enableDebuggingInTests { - - fmt.Println("GetMembers->", errGetMembers.Error()) - } - } - - //filterExp2 := fmt.Sprintf("custom.a5 == '%s' || custom.c5 == '%s'", custom5["a5"], custom5["c5"]) - filterExp2 := fmt.Sprintf("uuid.name == '%s'", name) - //fmt.Println("GetMembers ====>", filterExp2) - - resGetMembersF, stGetMembersF, errGetMembersF := pn.GetChannelMembers().Channel(channelid2).Include(inclSm).Filter(filterExp2).Limit(limit).Count(count).Execute() - //fmt.Println("resGetMembers -->", resGetMembersF) - assert.Nil(errGetMembersF) - assert.Equal(200, stGetMembersF.StatusCode) - if errGetMembersF == nil { - foundGetMembersF := false - - for i := range resGetMembersF.Data { - if resGetMembersF.Data[i].UUID.ID == userid2 { - foundGetMembersF = true - assert.Equal(name, resGetMembersF.Data[i].UUID.Name) - assert.Equal(extid, resGetMembersF.Data[i].UUID.ExternalID) - assert.Equal(purl, resGetMembersF.Data[i].UUID.ProfileURL) - assert.Equal(email, resGetMembersF.Data[i].UUID.Email) - assert.Equal(custom["a"], resGetMembersF.Data[i].UUID.Custom["a"]) - assert.Equal(custom["c"], resGetMembersF.Data[i].UUID.Custom["c"]) - assert.Equal(custom5["a5"], resGetMembersF.Data[i].Custom["a5"]) - assert.Equal(custom5["c5"], resGetMembersF.Data[i].Custom["c5"]) - } - } - assert.True(foundGetMembersF) - } else { - if enableDebuggingInTests { - - fmt.Println("GetMembers->", errGetMembersF.Error()) - } + a.Nil(err) + a.Equal(200, st.StatusCode) + if res != nil { + a.Equal(id, res.Data.ID) + a.Equal(name, res.Data.Name) + a.Equal(email, res.Data.Email) + a.NotNil(res.Data.Updated) + a.NotNil(res.Data.ETag) + a.True(reflect.DeepEqual(custom, res.Data.Custom)) } - // //Remove user memberships + email = "gosdk@pubnub.com" - reMem := pubnub.PNMembershipsRemove{ - Channel: channel2, + res, st, err = pn.SetUUIDMetadata().Include(incl).UUID(id).Name(name).Email(email).Custom(custom).Execute() + a.Nil(err) + a.Equal(200, st.StatusCode) + if res != nil { + a.Equal(id, res.Data.ID) + a.Equal(email, res.Data.Email) } - reArrMem := []pubnub.PNMembershipsRemove{ - reMem, - } - resManageMemRem, stManageMemRem, errManageMemRem := pn.ManageMemberships().UUID(userid2).Sort(sort).Set([]pubnub.PNMembershipsSet{}).Remove(reArrMem).Include(inclMemberships).Limit(limit).Count(count).Execute() - assert.Nil(errManageMemRem) - assert.Equal(200, stManageMemRem.StatusCode) - if errManageMemRem == nil { + _, st, err = pn.GetUUIDMetadata().Include(incl).UUID(id).Execute() + a.Nil(err) + a.Equal(200, st.StatusCode) - foundManageMemRem := false - for i := range resManageMemRem.Data { - if resManageMemRem.Data[i].Channel.ID == channelid2 { - foundManageMemRem = true - } - } - assert.False(foundManageMemRem) - } else { - if enableDebuggingInTests { +} - fmt.Println("ManageMemberships->", errManageMemRem.Error()) - } +func removeUUIDMetadata(a *assert.Assertions, pn *pubnub.PubNub, id string) { + res, st, err := pn.RemoveUUIDMetadata().UUID(id).Execute() + a.Nil(err) + a.Equal(200, st.StatusCode) + if res != nil { + a.Nil(res.Data) } +} - //delete - res5, st5, err5 := pn.RemoveUUIDMetadata().UUID(userid).Execute() - assert.Nil(err5) - assert.Equal(200, st5.StatusCode) +func TestObjectsV2MembersAddRemove(t *testing.T) { + a := assert.New(t) - assert.Nil(res5.Data) + pn := pubnub.NewPubNub(configCopy()) + channelid := randomized("channel") + userid := randomized("uuid") + inc := []pubnub.PNChannelMembersInclude{pubnub.PNChannelMembersIncludeUUID} - //delete - res6, st6, err6 := pn.RemoveChannelMetadata().Channel(channelid).Execute() - assert.Nil(err6) - assert.Equal(200, st6.StatusCode) - assert.Nil(res6.Data) + defer removeChannelMembers(a, pn, channelid, userid) - //delete - res52, st52, err52 := pn.RemoveUUIDMetadata().UUID(userid2).Execute() - assert.Nil(err52) - assert.Equal(200, st52.StatusCode) - if res52 != nil { - assert.Nil(res52.Data) - } - - //delete - res62, st62, err62 := pn.RemoveChannelMetadata().Channel(channelid2).Execute() - assert.Nil(err62) - assert.Equal(200, st62.StatusCode) - if res62 != nil { - assert.Nil(res62.Data) + res, st, err := pn. + SetChannelMembers(). + Channel(channelid). + Set([]pubnub.PNChannelMembersSet{{UUID: pubnub.PNChannelMembersUUID{ID: userid}}}). + Include(inc). + Execute() + a.Nil(err) + a.Equal(200, st.StatusCode) + if err == nil { + a.True(len(res.Data) > 0) } } -func TestObjectsV2ListenersV2(t *testing.T) { - ObjectsListenersCommonV2(t, false, false) -} - -func TestObjectsV2ListenersV2WithPAM(t *testing.T) { - ObjectsListenersCommonV2(t, true, false) +func removeChannelMembers(a *assert.Assertions, pn *pubnub.PubNub, channelid string, userid string) { + _, st, err := pn. + RemoveChannelMembers(). + Channel(channelid). + Remove([]pubnub.PNChannelMembersRemove{{UUID: pubnub.PNChannelMembersUUID{ID: userid}}}). + Execute() + a.Nil(err) + a.Equal(200, st.StatusCode) } -// func TestObjectsV2ListenersV2WithPAMWithoutSecKey(t *testing.T) { -// ObjectsListenersCommonV2(t, true, true) -// } - -func ObjectsListenersCommonV2(t *testing.T, withPAM, runWithoutSecretKey bool) { - //Create channel names for Channel and User - eventWaitTime := 2 - assert := assert.New(t) - - limit := 100 - count := true +func TestObjectsV2MembershipAddRemove(t *testing.T) { + a := assert.New(t) pn := pubnub.NewPubNub(configCopy()) - pnSub := pubnub.NewPubNub(configCopy()) - - userid := randomized("testlistuser") - channelid := randomized("testlistchannel") - if withPAM { - pn2 := ActivateWithPAMV2() - if runWithoutSecretKey { - if enableDebuggingInTests { - pn2.Config.Log = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) - } - tokens := RunGrantV2(pn2, []string{userid}, []string{channelid}, true, true, true, true, true, true) - SetPNV2(pn, pn2, tokens) - SetPNV2(pnSub, pn2, tokens) - //You have to use Grant v2 to subscribe - pnSub.Config.AuthKey = "authKey" - pn2.Grant(). - Read(true).Write(true).Manage(true). - Channels([]string{userid, channelid}). - AuthKeys([]string{pnSub.Config.AuthKey}). - Execute() - } else { - pn = pn2 - pnSub = pn2 - if enableDebuggingInTests { - pn.Config.Log = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) - } - RunGrantV2(pn, []string{userid}, []string{channelid}, true, true, true, true, true, false) - } - } - if enableDebuggingInTests { - pn.Config.Log = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) - pnSub.Config.Log = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) - } - - //Subscribe to the channel names - - listener := pubnub.NewListener() - - var mut sync.RWMutex - - addUserToChannel := false - addUserToChannel2 := false - updateUserMem := false - updateUser := false - updateChannel := false - removeUserFromChannel := false - deleteUser := false - deleteChannel := false - - doneConnected := make(chan bool) - exitListener := make(chan bool) - - go func() { - ExitLabel: - for { - //fmt.Println("Running =--->") - select { - - case status := <-listener.Status: - switch status.Category { - case pubnub.PNConnectedCategory: - doneConnected <- true - default: - if enableDebuggingInTests { - - fmt.Println(" --- status: ", status) - } - } - - case userEvent := <-listener.UUIDEvent: - if enableDebuggingInTests { - - fmt.Println(" --- UserEvent: ") - fmt.Println(fmt.Sprintf("%s", userEvent)) - fmt.Println(fmt.Sprintf("userEvent.Channel: %s", userEvent.Channel)) - fmt.Println(fmt.Sprintf("userEvent.SubscribedChannel: %s", userEvent.SubscribedChannel)) - fmt.Println(fmt.Sprintf("userEvent.Event: %s", userEvent.Event)) - fmt.Println(fmt.Sprintf("userEvent.UUID: %s", userEvent.UUID)) - fmt.Println(fmt.Sprintf("userEvent.Description: %s", userEvent.Description)) - fmt.Println(fmt.Sprintf("userEvent.Timestamp: %s", userEvent.Timestamp)) - fmt.Println(fmt.Sprintf("userEvent.Name: %s", userEvent.Name)) - fmt.Println(fmt.Sprintf("userEvent.ExternalID: %s", userEvent.ExternalID)) - fmt.Println(fmt.Sprintf("userEvent.ProfileURL: %s", userEvent.ProfileURL)) - fmt.Println(fmt.Sprintf("userEvent.Email: %s", userEvent.Email)) - // fmt.Println(fmt.Sprintf("userEvent.Created: %s", userEvent.Created)) - fmt.Println(fmt.Sprintf("userEvent.Updated: %s", userEvent.Updated)) - fmt.Println(fmt.Sprintf("userEvent.ETag: %s", userEvent.ETag)) - fmt.Println(fmt.Sprintf("userEvent.Custom: %v", userEvent.Custom)) - } - - if (userEvent.Event == pubnub.PNObjectsEventRemove) && (userEvent.UUID == userid) { - mut.Lock() - deleteUser = true - mut.Unlock() - } - if (userEvent.Event == pubnub.PNObjectsEventSet) && (userEvent.UUID == userid) { - mut.Lock() - updateUser = true - mut.Unlock() - } - case channelEvent := <-listener.ChannelEvent: - - if enableDebuggingInTests { - - fmt.Println(" --- ChannelEvent: ") - fmt.Println(fmt.Sprintf("%s", channelEvent)) - fmt.Println(fmt.Sprintf("channelEvent.SubscribedChannel: %s", channelEvent.SubscribedChannel)) - fmt.Println(fmt.Sprintf("channelEvent.Event: %s", channelEvent.Event)) - fmt.Println(fmt.Sprintf("channelEvent.ChannelID: %s", channelEvent.ChannelID)) - fmt.Println(fmt.Sprintf("channelEvent.Channel: %s", channelEvent.Channel)) - fmt.Println(fmt.Sprintf("channelEvent.Description: %s", channelEvent.Description)) - fmt.Println(fmt.Sprintf("channelEvent.Timestamp: %s", channelEvent.Timestamp)) - // fmt.Println(fmt.Sprintf("channelEvent.Created: %s", channelEvent.Created)) - fmt.Println(fmt.Sprintf("channelEvent.Updated: %s", channelEvent.Updated)) - fmt.Println(fmt.Sprintf("channelEvent.ETag: %s", channelEvent.ETag)) - fmt.Println(fmt.Sprintf("channelEvent.Custom: %v", channelEvent.Custom)) - } - if (channelEvent.Event == pubnub.PNObjectsEventRemove) && (channelEvent.ChannelID == channelid) { - mut.Lock() - deleteChannel = true - mut.Unlock() - } - if (channelEvent.Event == pubnub.PNObjectsEventSet) && (channelEvent.ChannelID == channelid) { - mut.Lock() - updateChannel = true - mut.Unlock() - } - - case membershipEvent := <-listener.MembershipEvent: - if enableDebuggingInTests { - - fmt.Println(" --- MembershipEvent: ") - fmt.Println(fmt.Sprintf("%s", membershipEvent)) - fmt.Println(fmt.Sprintf("membershipEvent.SubscribedChannel: %s", membershipEvent.SubscribedChannel)) - fmt.Println(fmt.Sprintf("membershipEvent.Event: %s", membershipEvent.Event)) - fmt.Println(fmt.Sprintf("membershipEvent.Channel: %s", membershipEvent.Channel)) - fmt.Println(fmt.Sprintf("membershipEvent.UUID: %s", membershipEvent.UUID)) - fmt.Println(fmt.Sprintf("membershipEvent.ChannelID: %s", membershipEvent.ChannelID)) - fmt.Println(fmt.Sprintf("membershipEvent.Description: %s", membershipEvent.Description)) - fmt.Println(fmt.Sprintf("membershipEvent.Timestamp: %s", membershipEvent.Timestamp)) - fmt.Println(fmt.Sprintf("membershipEvent.Custom: %v", membershipEvent.Custom)) - } - if (membershipEvent.Event == pubnub.PNObjectsEventSet) && (membershipEvent.ChannelID == channelid) && (membershipEvent.UUID == userid) && ((membershipEvent.Channel == channelid) || (membershipEvent.Channel == userid)) { - mut.Lock() - addUserToChannel = true - mut.Unlock() - } - if (membershipEvent.Event == pubnub.PNObjectsEventSet) && (membershipEvent.ChannelID == channelid) && (membershipEvent.UUID == userid) && ((membershipEvent.Channel == channelid) || (membershipEvent.Channel == userid)) { - mut.Lock() - addUserToChannel2 = true - mut.Unlock() - } - if (membershipEvent.Event == pubnub.PNObjectsEventSet) && (membershipEvent.ChannelID == channelid) && (membershipEvent.UUID == userid) && ((membershipEvent.Channel == channelid) || (membershipEvent.Channel == userid)) { - mut.Lock() - updateUserMem = true - mut.Unlock() - } - if (membershipEvent.Event == pubnub.PNObjectsEventSet) && (membershipEvent.ChannelID == channelid) && (membershipEvent.UUID == userid) && ((membershipEvent.Channel == channelid) || (membershipEvent.Channel == userid)) { - mut.Lock() - updateUserMem = true - mut.Unlock() - } - if (membershipEvent.Event == pubnub.PNObjectsEventRemove) && (membershipEvent.ChannelID == channelid) && (membershipEvent.UUID == userid) && ((membershipEvent.Channel == channelid) || (membershipEvent.Channel == userid)) { - mut.Lock() - removeUserFromChannel = true - mut.Unlock() - } - case <-exitListener: - break ExitLabel - - } - - //fmt.Println("=>>>>>>>>>>>>> restart") - - } - - }() - - pnSub.AddListener(listener) - - pnSub.Subscribe().Channels([]string{userid, channelid}).Execute() - tic := time.NewTicker(time.Duration(eventWaitTime) * time.Second) - select { - case <-doneConnected: - case <-tic.C: - tic.Stop() - assert.Fail("timeout") - } - - name := "name" - extid := "extid" - purl := "profileurl" - email := "email" - desc := "desc" - - customUser := make(map[string]interface{}) - customUser["au"] = "bu" - customUser["cu"] = "du" - - inclUUID := []pubnub.PNUUIDMetadataInclude{ - pubnub.PNUUIDMetadataIncludeCustom, - } - - //Create User - _, st, err := pn.SetUUIDMetadata().Include(inclUUID).UUID(userid).Name(name).ExternalID(extid).ProfileURL(purl).Email(email).Custom(customUser).Execute() - assert.Nil(err) - assert.Equal(200, st.StatusCode) - - //Create Channel - customChannel := make(map[string]interface{}) - customChannel["as"] = "bs" - customChannel["cs"] = "ds" - - inclChannel := []pubnub.PNChannelMetadataInclude{ - pubnub.PNChannelMetadataIncludeCustom, - } - - _, st4, err4 := pn.SetChannelMetadata().Include(inclChannel).Channel(channelid).Name(name).Description(desc).Custom(customChannel).Execute() - assert.Nil(err4) - assert.Equal(200, st4.StatusCode) - - time.Sleep(1 * time.Second) - - //Update User - email = "email2" - //fmt.Println("SetUUIDMetadata, Update ===> ", userid) - - _, st2, err2 := pn.SetUUIDMetadata().Include(inclUUID).UUID(userid).Name(name).ExternalID(extid).ProfileURL(purl).Email(email).Custom(customUser).Execute() - assert.Nil(err2) - assert.Equal(200, st2.StatusCode) + channelid := randomized("channel") + userid := randomized("uuid") + inc := []pubnub.PNMembershipsInclude{pubnub.PNMembershipsIncludeChannel} - time.Sleep(1 * time.Second) - mut.Lock() - assert.True(updateUser) - mut.Unlock() + defer removeMemberships(a, pn, channelid, userid) - desc = "desc2" - - //fmt.Println("SetChannelMetadata, Update ===> ", channelid) - - //Update Channel - _, st3, err3 := pn.SetChannelMetadata().Include(inclChannel).Channel(channelid).Name(name).Description(desc).Custom(customChannel).Execute() - assert.Nil(err3) - assert.Equal(200, st3.StatusCode) - - time.Sleep(1 * time.Second) - mut.Lock() - assert.True(updateChannel) - mut.Unlock() - - //Add user to channel - inclSm := []pubnub.PNChannelMembersInclude{ - pubnub.PNChannelMembersIncludeCustom, - pubnub.PNChannelMembersIncludeUUID, - pubnub.PNChannelMembersIncludeUUIDCustom, - } - - if enableDebuggingInTests { - - fmt.Println("inclSm===>", inclSm) - for k, value := range inclSm { - fmt.Println("inclSm===>", k, value) - } - } - - custom3 := make(map[string]interface{}) - custom3["a3"] = "b3" - custom3["c3"] = "d3" - - uuid := pubnub.PNChannelMembersUUID{ - ID: userid, - } - - in := pubnub.PNChannelMembersSet{ - UUID: uuid, - Custom: custom3, - } - - inArr := []pubnub.PNChannelMembersSet{ - in, - } - - _, stAdd, errAdd := pn.ManageChannelMembers().Channel(channelid).Set(inArr).Remove([]pubnub.PNChannelMembersRemove{}).Include(inclSm).Limit(limit).Count(count).Execute() - assert.Nil(errAdd) - if enableDebuggingInTests { - - if errAdd != nil { - fmt.Println("ManageMembers-->", errAdd) - } - } - assert.Equal(200, stAdd.StatusCode) - - time.Sleep(1 * time.Second) - mut.Lock() - assert.True(addUserToChannel && addUserToChannel2) - mut.Unlock() - - //Update user membership - - //Read event - - custom5 := make(map[string]interface{}) - custom5["a5"] = "b5" - custom5["c5"] = "d5" - - channel := pubnub.PNMembershipsChannel{ - ID: channelid, - } - - upMem := pubnub.PNMembershipsSet{ - Channel: channel, - Custom: custom5, - } - - upArrMem := []pubnub.PNMembershipsSet{ - upMem, - } - - inclMemberships := []pubnub.PNMembershipsInclude{ - pubnub.PNMembershipsIncludeCustom, - pubnub.PNMembershipsIncludeChannel, - pubnub.PNMembershipsIncludeChannelCustom, - } - - resManageMemUp, stManageMemUp, errManageMemUp := pn.ManageMemberships().UUID(userid).Set(upArrMem).Remove([]pubnub.PNMembershipsRemove{}).Include(inclMemberships).Limit(limit).Count(count).Execute() - - assert.Nil(errManageMemUp) - if enableDebuggingInTests { - - fmt.Println("resManageMemUp -->", resManageMemUp) - if errManageMemUp != nil { - fmt.Println("ManageMemberships-->", errManageMemUp) - } - } - assert.Equal(200, stManageMemUp.StatusCode) - - time.Sleep(1 * time.Second) - mut.Lock() - assert.True(updateUserMem) - mut.Unlock() - - //Remove user from channel - reMem := pubnub.PNMembershipsRemove{ - Channel: channel, - } - - reArrMem := []pubnub.PNMembershipsRemove{ - reMem, - } - _, stManageMemRem, errManageMemRem := pn.ManageMemberships().UUID(userid).Set([]pubnub.PNMembershipsSet{}).Remove(reArrMem).Include(inclMemberships).Limit(limit).Count(count).Execute() - assert.Nil(errManageMemRem) - if enableDebuggingInTests { - - if errManageMemRem != nil { - fmt.Println("ManageMemberships-->", errManageMemRem) - } + res, st, err := pn. + SetMemberships(). + UUID(userid). + Set([]pubnub.PNMembershipsSet{{Channel: pubnub.PNMembershipsChannel{ID: channelid}}}). + Include(inc). + Execute() + a.Nil(err) + a.Equal(200, st.StatusCode) + if err == nil { + a.True(len(res.Data) > 0) } - assert.Equal(200, stManageMemRem.StatusCode) - time.Sleep(1 * time.Second) - mut.Lock() - assert.True(removeUserFromChannel) - mut.Unlock() - - //Delete user - res52, st52, err52 := pn.RemoveUUIDMetadata().UUID(userid).Execute() - assert.Nil(err52) - assert.Equal(200, st52.StatusCode) - assert.Nil(res52.Data) - - time.Sleep(1 * time.Second) - mut.Lock() - assert.True(deleteUser) - mut.Unlock() - - //Delete Channel - res62, st62, err62 := pn.RemoveChannelMetadata().Channel(channelid).Execute() - assert.Nil(err62) - assert.Equal(200, st62.StatusCode) - assert.Nil(res62.Data) - - time.Sleep(1 * time.Second) - mut.Lock() - assert.True(deleteChannel) - mut.Unlock() +} - exitListener <- true +func removeMemberships(a *assert.Assertions, pn *pubnub.PubNub, channelid string, userid string) { + _, st, err := pn. + RemoveMemberships(). + UUID(userid). + Remove([]pubnub.PNMembershipsRemove{{Channel: pubnub.PNMembershipsChannel{ID: channelid}}}). + Execute() + a.Nil(err) + a.Equal(200, st.StatusCode) } diff --git a/tests/e2e/publish_test.go b/tests/e2e/publish_test.go index 52a5f62b..527202be 100644 --- a/tests/e2e/publish_test.go +++ b/tests/e2e/publish_test.go @@ -2,6 +2,7 @@ package e2e import ( "fmt" + "os" "strings" "testing" "time" @@ -182,7 +183,10 @@ func TestPublishServerError(t *testing.T) { ResponseStatusCode: 403, }) - pn := pubnub.NewPubNub(configCopy()) + config := pubnub.NewConfigWithUserId(pubnub.UserId(pubnub.GenerateUUID())) + config.PublishKey = os.Getenv("PUBLISH_KEY") + config.SubscribeKey = os.Getenv("SUBSCRIBE_KEY") + pn := pubnub.NewPubNub(config) pn.SetClient(interceptor.GetClient()) _, _, err := pn.Publish().Channel("ch").Message("hey").Execute() diff --git a/utils.go b/utils.go new file mode 100644 index 00000000..01aeeadd --- /dev/null +++ b/utils.go @@ -0,0 +1,120 @@ +package pubnub + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "github.com/pubnub/go/v7/crypto" + "github.com/pubnub/go/v7/pnerr" + "io" + "strconv" +) + +// encodeNonAsciiChars creates unicode string of the non-ascii chars. +// It accepts the following parameters: +// message: to parse. +// +// returns the encoded string. +func encodeNonASCIIChars(message string) string { + runeOfMessage := []rune(message) + lenOfRune := len(runeOfMessage) + encodedString := bytes.NewBuffer(make([]byte, 0, lenOfRune)) + for i := 0; i < lenOfRune; i++ { + intOfRune := uint16(runeOfMessage[i]) + if intOfRune > 127 { + hexOfRune := strconv.FormatUint(uint64(intOfRune), 16) + dataLen := len(hexOfRune) + paddingNum := 4 - dataLen + encodedString.WriteString(`\u`) + for i := 0; i < paddingNum; i++ { + encodedString.WriteString("0") + } + encodedString.WriteString(hexOfRune) + } else { + encodedString.WriteString(string(runeOfMessage[i])) + } + } + return encodedString.String() +} + +func encryptString(module crypto.CryptoModule, message string) (string, error) { + encryptedData, e := module.Encrypt([]byte(encodeNonASCIIChars(message))) + if e != nil { + return "", e + } + return base64.StdEncoding.EncodeToString(encryptedData), nil +} + +func serializeEncryptAndSerialize(cryptoModule crypto.CryptoModule, msg interface{}, serialize bool) (string, error) { + var encrypted string + var err error + + if serialize { + jsonSerialized, errJSONMarshal := json.Marshal(msg) + if errJSONMarshal != nil { + return "", errJSONMarshal + } + encrypted, err = encryptString(cryptoModule, string(jsonSerialized)) + + } else { + if serializedMsg, ok := msg.(string); ok { + encrypted, err = encryptString(cryptoModule, string(serializedMsg)) + } else { + return "", pnerr.NewBuildRequestError("Message is not JSON serialized.") + } + } + if err != nil { + return "", err + } + jsonSerialized, errJSONMarshal := json.Marshal(encrypted) + if errJSONMarshal != nil { + return "", errJSONMarshal + } + return string(jsonSerialized), nil +} + +func serializeAndEncrypt(cryptoModule crypto.CryptoModule, msg interface{}, serialize bool) (string, error) { + var encrypted string + var err error + if serialize { + jsonSerialized, errJSONMarshal := json.Marshal(msg) + if errJSONMarshal != nil { + return "", errJSONMarshal + } + encrypted, err = encryptString(cryptoModule, string(jsonSerialized)) + } else { + if serializedMsg, ok := msg.(string); ok { + encrypted, err = encryptString(cryptoModule, serializedMsg) + } else { + return "", pnerr.NewBuildRequestError("Message is not JSON serialized.") + } + } + if err != nil { + return "", err + } + + return encrypted, nil +} + +func encryptStreamAndCopyTo(module crypto.CryptoModule, reader io.Reader, writer io.Writer) error { + encryptedStream, e := module.EncryptStream(reader) + if e != nil { + return e + } + _, e = io.Copy(writer, encryptedStream) + if e != nil { + return e + } + return nil +} + +func decryptString(cryptoModule crypto.CryptoModule, message string) (retVal interface{}, err error) { + value, decodeErr := base64.StdEncoding.DecodeString(message) + if decodeErr != nil { + return "***decrypt error***", fmt.Errorf("decrypt error on decode: %s", decodeErr) + } + + val, e := cryptoModule.Decrypt(value) + return fmt.Sprintf("%s", string(val)), e +} diff --git a/utils/crypto.go b/utils/crypto.go index 1fef1b13..2ed5327a 100644 --- a/utils/crypto.go +++ b/utils/crypto.go @@ -2,24 +2,17 @@ package utils import ( "bytes" - "crypto/aes" - "crypto/cipher" "crypto/hmac" - "crypto/rand" "crypto/sha256" "encoding/base64" - "encoding/hex" - "errors" "fmt" + "github.com/pubnub/go/v7/crypto" "io" "os" "strconv" "strings" ) -// 16 byte IV -var valIV = "0123456789012345" - // EncryptString creates the base64 encoded encrypted string using the // cipherKey. // It accepts the following parameters: @@ -29,33 +22,15 @@ var valIV = "0123456789012345" // // returns the base64 encoded encrypted string. func EncryptString(cipherKey string, message string, useRandomInitializationVector bool) string { - block, _ := aesCipher(cipherKey) - - message = encodeNonASCIIChars(message) - value := []byte(message) - value = padWithPKCS7(value) - iv := make([]byte, aes.BlockSize) - if useRandomInitializationVector { - iv = generateIV(aes.BlockSize) - } else { - iv = []byte(valIV) + cryptoModule, e := crypto.NewLegacyCryptoModule(cipherKey, useRandomInitializationVector) + if e != nil { + panic(e) } - blockmode := cipher.NewCBCEncrypter(block, iv) - - cipherBytes := make([]byte, len(value)) - blockmode.CryptBlocks(cipherBytes, value) - if useRandomInitializationVector { - return base64.StdEncoding.EncodeToString(append(iv, cipherBytes...)) + encryptedData, e := cryptoModule.Encrypt([]byte(encodeNonASCIIChars(message))) + if e != nil { + panic(e) } - return base64.StdEncoding.EncodeToString(cipherBytes) -} - -type A struct { - I string - Interface *B -} -type B struct { - Value string + return base64.StdEncoding.EncodeToString(encryptedData) } // DecryptString decodes encrypted string using the cipherKey @@ -67,74 +42,18 @@ type B struct { // // returns the unencoded encrypted string, // error if any. -func DecryptString(cipherKey string, message string, useRandomInitializationVector bool) ( - retVal interface{}, err error) { - if message == "" { - return "**decrypt error***", errors.New("message is empty") - } - - block, aesErr := aesCipher(cipherKey) - if aesErr != nil { - return "***decrypt error***", fmt.Errorf("decrypt error aes cipher: %s", aesErr) - } - +func DecryptString(cipherKey string, message string, useRandomInitializationVector bool) (retVal interface{}, err error) { value, decodeErr := base64.StdEncoding.DecodeString(message) if decodeErr != nil { return "***decrypt error***", fmt.Errorf("decrypt error on decode: %s", decodeErr) } - iv := make([]byte, aes.BlockSize) - if useRandomInitializationVector { - iv = value[:16] - value = value[16:] - } else { - iv = []byte(valIV) - } - - decrypter := cipher.NewCBCDecrypter(block, iv) - //to handle decryption errors - defer func() { - if r := recover(); r != nil { - retVal, err = "***decrypt error***", fmt.Errorf("decrypt error: %s", r) - } - }() - decrypted := make([]byte, len(value)) - decrypter.CryptBlocks(decrypted, value) - val, err := unpadPKCS7(decrypted) - if err != nil { - return "***decrypt error***", fmt.Errorf("decrypt error: %s", err) - } - return fmt.Sprintf("%s", string(val)), nil -} - -// aesCipher returns the cipher block -// -// It accepts the following parameters: -// cipherKey: cipher key. -// -// returns the cipher block, -// error if any. -func aesCipher(cipherKey string) (cipher.Block, error) { - key := EncryptCipherKey(cipherKey) - block, err := aes.NewCipher(key) - if err != nil { - return nil, err + cryptoModule, e := crypto.NewLegacyCryptoModule(cipherKey, useRandomInitializationVector) + if e != nil { + return nil, e } - return block, nil -} - -// EncryptCipherKey creates the 256 bit hex of the cipher key -// -// It accepts the following parameters: -// cipherKey: cipher key to use to decrypt. -// -// returns the 256 bit hex of the cipher key. -func EncryptCipherKey(cipherKey string) []byte { - hash := sha256.New() - hash.Write([]byte(cipherKey)) - - sha256String := hash.Sum(nil)[:16] - return []byte(hex.EncodeToString(sha256String)) + val, e := cryptoModule.Decrypt(value) + return fmt.Sprintf("%s", string(val)), e } // encodeNonAsciiChars creates unicode string of the non-ascii chars. @@ -178,214 +97,44 @@ func GetHmacSha256(secretKey string, input string) string { return signature } -// padWithPKCS7 pads the data as per the PKCS7 standard -// It accepts the following parameters: -// data: data to pad as byte array. -// returns the padded data as byte array. -func padWithPKCS7(data []byte) []byte { - blocklen := 16 - padlen := 1 - for ((len(data) + padlen) % blocklen) != 0 { - padlen = padlen + 1 - } - - pad := bytes.Repeat([]byte{byte(padlen)}, padlen) - return append(data, pad...) -} - -// unpadPKCS7 unpads the data as per the PKCS7 standard -// It accepts the following parameters: -// data: data to unpad as byte array. -// returns the unpadded data as byte array. -func unpadPKCS7(data []byte) ([]byte, error) { - blocklen := 16 - if len(data)%blocklen != 0 || len(data) == 0 { - return nil, fmt.Errorf("invalid data len %d", len(data)) - } - padlen := int(data[len(data)-1]) - if padlen > blocklen || padlen == 0 { - return nil, fmt.Errorf("padding is invalid") +func EncryptFile(cipherKey string, _ []byte, filePart io.Writer, file *os.File) { + cryptor, e := crypto.NewLegacyCryptoModule(cipherKey, true) + if e != nil { + panic(e) } - // check padding - pad := data[len(data)-padlen:] - for i := 0; i < padlen; i++ { - if pad[i] != byte(padlen) { - return nil, fmt.Errorf("padding is invalid") - } + r, e := cryptor.EncryptStream(file) + if e != nil { + panic(e) } - - return data[:len(data)-padlen], nil -} - -func generateIV(blocksize int) []byte { - iv := make([]byte, aes.BlockSize) - if _, err := rand.Read(iv); err != nil { - panic(err) + _, e = io.Copy(filePart, r) + if e != nil { + panic(e) } - return iv } -func EncryptFile(cipherKey string, iv []byte, filePart io.Writer, file *os.File) { - key := EncryptCipherKey(cipherKey) - block, err := aes.NewCipher(key) - if err != nil { - panic(err) - } - if bytes.Equal(iv, []byte{}) { - iv = generateIV(aes.BlockSize) +func DecryptFile(cipherKey string, _ int64, reader io.Reader, w io.WriteCloser) { + cryptoModule, e := crypto.NewLegacyCryptoModule(cipherKey, true) + if e != nil { + panic(e) } - _, e := filePart.Write(iv) + encryptedReader, e := cryptoModule.DecryptStream(reader) if e != nil { panic(e) } - blockSize := 16 - bufferSize := 16 - p := make([]byte, bufferSize) - - mode := cipher.NewCBCEncrypter(block, iv) - cryptoRan := false - fii, _ := file.Stat() - contentLenIn := fii.Size() - var contentRead int64 - - for { - n2, err2 := io.ReadFull(file, p) - contentRead += int64(n2) - if err2 != nil { - if err2 == io.EOF { - ciphertext := make([]byte, blockSize) - copy(ciphertext[:n2], p[:n2]) - break - } - - if err2 == io.ErrUnexpectedEOF { - if !cryptoRan { - text := make([]byte, blockSize) - ciphertext := make([]byte, blockSize) - copy(text[:n2], p[:n2]) - pad := bytes.Repeat([]byte{byte(blockSize - n2)}, blockSize-n2) - copy(text[n2:], pad) - mode.CryptBlocks(ciphertext, text) - filePart.Write(ciphertext) - } else { - text := make([]byte, blockSize) - ciphertext := make([]byte, blockSize) - copy(text[:n2], p[:n2]) - pad := bytes.Repeat([]byte{byte(blockSize - n2)}, blockSize-n2) - copy(text[n2:], pad) - mode.CryptBlocks(ciphertext, text) - filePart.Write(ciphertext) - - } - - } - break - } - - ciphertext := make([]byte, blockSize) - cryptoRan = true - if contentRead >= contentLenIn { - pad := bytes.Repeat([]byte{byte(blockSize - n2)}, blockSize-n2) - copy(p[n2:], pad) - } - - mode.CryptBlocks(ciphertext, p) - filePart.Write(ciphertext) - + _, e = io.Copy(w, encryptedReader) + if e != nil { + panic(e) } + e = w.Close() } -func DecryptFile(cipherKey string, contentLenEnc int64, reader io.Reader, w io.WriteCloser) { - key := EncryptCipherKey(cipherKey) - block, err := aes.NewCipher(key) - if err != nil { - panic(err) - } - blockSize := 16 - bufferSize := 16 - p := make([]byte, bufferSize) - ivBuff := make([]byte, blockSize) - emptyByteVar := make([]byte, blockSize) - - iv2 := make([]byte, blockSize) - count := 0 - - var mode cipher.BlockMode - - cryptoRan := false - var contentDownloaded int64 - - go func() { - ExitReadLabel: - for { - n2, err2 := io.ReadFull(reader, p) - if err2 != nil { - if err2 == io.EOF { - ciphertext := make([]byte, blockSize) - copy(ciphertext, p[:n2]) - ciphertext, _ = unpadPKCS7(ciphertext) - w.Write(ciphertext) - w.Close() - break ExitReadLabel - } - - if err2 == io.ErrUnexpectedEOF { - if bytes.Equal(iv2, emptyByteVar) { - copy(iv2, ivBuff[0:blockSize]) - mode = cipher.NewCBCDecrypter(block, iv2) - } - if !cryptoRan { - text := make([]byte, blockSize) - ciphertext := make([]byte, blockSize) - copy(text, p[:n2]) - mode.CryptBlocks(ciphertext, text) - ciphertext, _ = unpadPKCS7(ciphertext) - w.Write(ciphertext) - - w.Close() - break ExitReadLabel - } else { - ciphertext := make([]byte, blockSize) - copy(ciphertext, p[:n2]) - ciphertext, _ = unpadPKCS7(ciphertext) - w.Write(ciphertext) - w.Close() - break ExitReadLabel - } - - } - break ExitReadLabel - } else { - contentDownloaded += int64(n2) - if count < blockSize/bufferSize { - if err != nil { - panic(err) - } - copy(ivBuff[bufferSize*count:], p) - } else { - - if bytes.Equal(iv2, emptyByteVar) { - copy(iv2, ivBuff[0:blockSize]) - mode = cipher.NewCBCDecrypter(block, iv2) - } - - ciphertext := make([]byte, blockSize) - - text := make([]byte, blockSize) - copy(text, p[:n2]) - - mode.CryptBlocks(ciphertext, p) - cryptoRan = true - if contentDownloaded >= contentLenEnc { - ciphertext, _ = unpadPKCS7(ciphertext) - w.Write(ciphertext) - } else { - w.Write(ciphertext) - } - } - } - count++ +// EncryptCipherKey creates the 256 bit hex of the cipher key +// +// It accepts the following parameters: +// cipherKey: cipher key to use to decrypt. +// +// returns the 256 bit hex of the cipher key. - } - }() +func EncryptCipherKey(cipherKey string) []byte { + return crypto.EncryptCipherKey(cipherKey) } diff --git a/utils/crypto_test.go b/utils/crypto_test.go index 885ca613..b7b6236e 100644 --- a/utils/crypto_test.go +++ b/utils/crypto_test.go @@ -489,3 +489,22 @@ func CreateLoggerForTests() *log.Logger { } return infoLogger } + +func TestDecryptStuffFromPython(t *testing.T) { + cipherKey := "myCipherKey" + s1 := "KGc+SNJD7mIveY+KNIL/L9ZzAjC0dCJCju+HXRwSW2k=" + s2 := "PXjHv0L05kgj0mqIE9s7n4LDPrLtjnfamMoHyiMoL0R1uzSMsYp7dDfqEWrnoaqS" + s3 := "UE5FRAFBQ1JIEHvl3cY3RYsHnbKm6VR51XG/Y7HodnkumKHxo+mrsxbIjZvFpVuILQ0oZysVwjNsDNMKiMfZteoJ8P1/mvPmbuQKLErBzS2l7vEohCwbmAJODPR2yNhJGB8989reTZ7Y7Q==" + + d1, _ := DecryptString(cipherKey, s1, false) + d2, _ := DecryptString(cipherKey, s2, true) + d3, _ := DecryptString(cipherKey, s3, true) + + r1 := d1.(string) + r2 := d2.(string) + r3 := d3.(string) + + fmt.Printf("%s: => %d\n", r1, len(r1)) + fmt.Printf("%s: => %d\n", r2, len(r2)) + fmt.Printf("%s: => %d\n", r3, len(r3)) +}