From ba5935daf57f584bc5ea49e1bc32554f81218119 Mon Sep 17 00:00:00 2001 From: Mohit Tejani <60129002+mohitpubnub@users.noreply.github.com> Date: Mon, 16 Oct 2023 16:45:16 +0530 Subject: [PATCH] CryptoModule (#117) * cryptorHeader for new cryptoModule * format! * added AesCbcCryptor, ICryptor * format! * * LegacyCryptor, * added: newCryptoModule * new crytoModule * test: crypto Module * dart fix * format!! again * * expose CryptoModule for users, * deprecation warning for `cipherKey` * code cleanup * show CryptoConfiguration for legacy cryptor configuration support * Update CODEOWNERS * * Added empty content handling while encry and decrypt, * Added more information in exception string for reason fo failure. * naming convention fix for CryptoModule factory. * code formatting. * removed unused class ILegacyCryptor * fix: encrypt/decrypt File methods * fix: explicit cipherKey at method params defaulting to legacy cryptor * added condition to apply user specified cryptoModule for encryption/decryption, * refactoring to fix broken format issue with decrypted hostory messages, * files encryption/ decryption conditions refined * update LICENSE * PubNub SDK v4.3.0 release. --------- Co-authored-by: PubNub Release Bot <120067856+pubnub-release-bot@users.noreply.github.com> --- .github/CODEOWNERS | 4 +- .pubnub.yml | 9 +- .../lib/src/steps/crypto_module/_utils.dart | 25 +++ .../crypto_module/crypto_module_steps.dart | 30 +++ .../crypto_module/step_given_cipher_key.dart | 13 ++ .../step_given_crypto_module.dart | 13 ++ .../step_given_legacy_crypto.dart | 17 ++ .../step_given_multiple_cryptors.dart | 17 ++ .../crypto_module/step_given_vector.dart | 14 ++ .../step_then_decrypt_success.dart | 21 ++ .../crypto_module/step_then_decrypted.dart | 22 ++ .../crypto_module/step_then_outcome.dart | 20 ++ .../crypto_module/step_when_decrypt.dart | 28 +++ .../crypto_module/step_when_decrypt_as.dart | 57 ++++++ .../crypto_module/step_when_encrypt.dart | 35 ++++ acceptance_tests/lib/src/steps/steps.dart | 2 + pubnub/CHANGELOG.md | 9 + pubnub/LICENSE | 48 ++--- pubnub/README.md | 2 +- pubnub/lib/crypto.dart | 5 +- pubnub/lib/pubnub.dart | 2 + pubnub/lib/src/core/core.dart | 2 +- pubnub/lib/src/core/crypto/cipher_key.dart | 11 + pubnub/lib/src/core/crypto/crypto.dart | 31 ++- pubnub/lib/src/core/keyset/keyset.dart | 2 +- pubnub/lib/src/crypto/aesCbcCryptor.dart | 59 ++++++ pubnub/lib/src/crypto/crypto.dart | 192 ++++++++---------- .../lib/src/crypto/cryptoConfiguration.dart | 18 ++ pubnub/lib/src/crypto/cryptorHeader.dart | 93 +++++++++ pubnub/lib/src/crypto/legacyCryptor.dart | 182 +++++++++++++++++ pubnub/lib/src/default.dart | 7 +- pubnub/lib/src/dx/_endpoints/files.dart | 11 +- pubnub/lib/src/dx/_endpoints/history.dart | 13 +- pubnub/lib/src/dx/batch/batch.dart | 19 +- .../lib/src/dx/channel/channel_history.dart | 26 ++- pubnub/lib/src/dx/files/files.dart | 47 ++++- pubnub/lib/src/dx/publish/publish.dart | 13 +- .../subscribe_loop/subscribe_loop.dart | 15 +- pubnub/pubspec.yaml | 2 +- pubnub/test/unit/crypto/crypto_test.dart | 29 ++- pubnub/test/unit/dx/file_test.dart | 7 +- 41 files changed, 983 insertions(+), 189 deletions(-) create mode 100644 acceptance_tests/lib/src/steps/crypto_module/_utils.dart create mode 100644 acceptance_tests/lib/src/steps/crypto_module/crypto_module_steps.dart create mode 100644 acceptance_tests/lib/src/steps/crypto_module/step_given_cipher_key.dart create mode 100644 acceptance_tests/lib/src/steps/crypto_module/step_given_crypto_module.dart create mode 100644 acceptance_tests/lib/src/steps/crypto_module/step_given_legacy_crypto.dart create mode 100644 acceptance_tests/lib/src/steps/crypto_module/step_given_multiple_cryptors.dart create mode 100644 acceptance_tests/lib/src/steps/crypto_module/step_given_vector.dart create mode 100644 acceptance_tests/lib/src/steps/crypto_module/step_then_decrypt_success.dart create mode 100644 acceptance_tests/lib/src/steps/crypto_module/step_then_decrypted.dart create mode 100644 acceptance_tests/lib/src/steps/crypto_module/step_then_outcome.dart create mode 100644 acceptance_tests/lib/src/steps/crypto_module/step_when_decrypt.dart create mode 100644 acceptance_tests/lib/src/steps/crypto_module/step_when_decrypt_as.dart create mode 100644 acceptance_tests/lib/src/steps/crypto_module/step_when_encrypt.dart create mode 100644 pubnub/lib/src/crypto/aesCbcCryptor.dart create mode 100644 pubnub/lib/src/crypto/cryptoConfiguration.dart create mode 100644 pubnub/lib/src/crypto/cryptorHeader.dart create mode 100644 pubnub/lib/src/crypto/legacyCryptor.dart diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4a0adc12..6b85982b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,3 @@ -* @are @mohitpubnub @parfeon -.github/* @parfeon @are @mohitpubnub +* @jguz-pubnub @mohitpubnub @parfeon +.github/* @jguz-pubnub @parfeon @mohitpubnub README.md @techwritermat @kazydek diff --git a/.pubnub.yml b/.pubnub.yml index 00eba502..e5ed3bb3 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,5 +1,12 @@ --- changelog: + - date: 2023-10-16 + version: v4.3.0 + changes: + - type: feature + text: "Add crypto module that allows configure SDK to encrypt and decrypt messages." + - type: bug + text: "Improved security of crypto implementation by adding enhanced AES-CBC cryptor." - date: 2023-07-27 version: v4.2.4 changes: @@ -425,7 +432,7 @@ supported-platforms: platforms: - "Dart SDK >=2.6.0 <3.0.0" version: "PubNub Dart SDK" -version: "4.2.4" +version: "4.3.0" sdks: - full-name: PubNub Dart SDK diff --git a/acceptance_tests/lib/src/steps/crypto_module/_utils.dart b/acceptance_tests/lib/src/steps/crypto_module/_utils.dart new file mode 100644 index 00000000..e2879a1a --- /dev/null +++ b/acceptance_tests/lib/src/steps/crypto_module/_utils.dart @@ -0,0 +1,25 @@ +import 'dart:io'; + +String getCryptoFilePath(String filename) { + var assets = Directory( + '../../service-contract-mock/contract/features/encryption/assets'); + return '${assets.path}/$filename'; +} + +bool listEquals(List list1, List list2) { + if (identical(list1, list2)) { + return true; + } + + if (list1.length != list2.length) { + return false; + } + + for (var i = 0; i < list1.length; i += 1) { + if (list1[i] != list2[i]) { + return false; + } + } + + return true; +} diff --git a/acceptance_tests/lib/src/steps/crypto_module/crypto_module_steps.dart b/acceptance_tests/lib/src/steps/crypto_module/crypto_module_steps.dart new file mode 100644 index 00000000..5cf6b15f --- /dev/null +++ b/acceptance_tests/lib/src/steps/crypto_module/crypto_module_steps.dart @@ -0,0 +1,30 @@ +import 'package:acceptance_tests/src/steps/crypto_module/step_given_cipher_key.dart'; +import 'package:acceptance_tests/src/steps/crypto_module/step_given_legacy_crypto.dart'; +import 'package:acceptance_tests/src/steps/crypto_module/step_given_multiple_cryptors.dart'; +import 'package:acceptance_tests/src/steps/crypto_module/step_given_vector.dart'; +import 'package:acceptance_tests/src/steps/crypto_module/step_then_decrypt_success.dart'; +import 'package:acceptance_tests/src/steps/crypto_module/step_then_decrypted.dart'; +import 'package:acceptance_tests/src/steps/crypto_module/step_then_outcome.dart'; +import 'package:acceptance_tests/src/steps/crypto_module/step_when_decrypt.dart'; +import 'package:acceptance_tests/src/steps/crypto_module/step_when_decrypt_as.dart'; +import 'package:acceptance_tests/src/steps/crypto_module/step_when_encrypt.dart'; + +import '../../world.dart'; + +import 'package:gherkin/gherkin.dart'; + +import 'step_given_crypto_module.dart'; + +final List> cryptoSteps = [ + StepGivenCryptoModule(), + StepGivenCipherKey(), + StepGivenVector(), + StepWhenDecryptFileAs(), + StepThenDecryptedContentEquals(), + StepGivenLegacyCryptoModule(), + StepWhenDecryptFile(), + StepThenOutcome(), + StepWhenEncrypt(), + ThenDecryptSuccessWithLegacy(), + StepGivenCryptoModuleWithMultipleCryptors() +]; diff --git a/acceptance_tests/lib/src/steps/crypto_module/step_given_cipher_key.dart b/acceptance_tests/lib/src/steps/crypto_module/step_given_cipher_key.dart new file mode 100644 index 00000000..fa9b3824 --- /dev/null +++ b/acceptance_tests/lib/src/steps/crypto_module/step_given_cipher_key.dart @@ -0,0 +1,13 @@ +import 'package:gherkin/gherkin.dart'; + +import '../../world.dart'; + +class StepGivenCipherKey extends Given1WithWorld { + @override + RegExp get pattern => RegExp(r'with {string} cipher key'); + + @override + Future executeStep(String cipherKey) async { + world.scenarioContext['cipherKey'] = cipherKey; + } +} diff --git a/acceptance_tests/lib/src/steps/crypto_module/step_given_crypto_module.dart b/acceptance_tests/lib/src/steps/crypto_module/step_given_crypto_module.dart new file mode 100644 index 00000000..41b0f1cf --- /dev/null +++ b/acceptance_tests/lib/src/steps/crypto_module/step_given_crypto_module.dart @@ -0,0 +1,13 @@ +import 'package:gherkin/gherkin.dart'; + +import '../../world.dart'; + +class StepGivenCryptoModule extends Given1WithWorld { + @override + RegExp get pattern => RegExp(r'Crypto module with {string} cryptor'); + + @override + Future executeStep(String id) async { + world.scenarioContext['cryptorId'] = id; + } +} diff --git a/acceptance_tests/lib/src/steps/crypto_module/step_given_legacy_crypto.dart b/acceptance_tests/lib/src/steps/crypto_module/step_given_legacy_crypto.dart new file mode 100644 index 00000000..2b2c3527 --- /dev/null +++ b/acceptance_tests/lib/src/steps/crypto_module/step_given_legacy_crypto.dart @@ -0,0 +1,17 @@ +import 'package:gherkin/gherkin.dart'; + +import '../../world.dart'; + +class StepGivenLegacyCryptoModule + extends Given2WithWorld { + @override + RegExp get pattern => + RegExp(r'Legacy code with {string} cipher key and {vector} vector'); + + @override + Future executeStep(String cipherKey, String vector) async { + world.scenarioContext['useRandomIntializationVector'] = + vector == 'constant' ? false : true; + world.scenarioContext['cipherKey'] = cipherKey; + } +} diff --git a/acceptance_tests/lib/src/steps/crypto_module/step_given_multiple_cryptors.dart b/acceptance_tests/lib/src/steps/crypto_module/step_given_multiple_cryptors.dart new file mode 100644 index 00000000..bdebfc28 --- /dev/null +++ b/acceptance_tests/lib/src/steps/crypto_module/step_given_multiple_cryptors.dart @@ -0,0 +1,17 @@ +import 'package:gherkin/gherkin.dart'; + +import '../../world.dart'; + +class StepGivenCryptoModuleWithMultipleCryptors + extends Given2WithWorld { + @override + RegExp get pattern => RegExp( + r'Crypto module with default {string} and additional {string} cryptors'); + + @override + Future executeStep( + String defaultCryptorId, String additionalCryptorId) async { + world.scenarioContext['defaultCryptorId'] = defaultCryptorId; + world.scenarioContext['additionalCryptorId'] = additionalCryptorId; + } +} diff --git a/acceptance_tests/lib/src/steps/crypto_module/step_given_vector.dart b/acceptance_tests/lib/src/steps/crypto_module/step_given_vector.dart new file mode 100644 index 00000000..c9e68044 --- /dev/null +++ b/acceptance_tests/lib/src/steps/crypto_module/step_given_vector.dart @@ -0,0 +1,14 @@ +import 'package:gherkin/gherkin.dart'; + +import '../../world.dart'; + +class StepGivenVector extends Given1WithWorld { + @override + RegExp get pattern => RegExp(r'with {string} vector'); + + @override + Future executeStep(String vector) async { + world.scenarioContext['useRandomIntializationVector'] = + vector == 'constant' ? false : true; + } +} diff --git a/acceptance_tests/lib/src/steps/crypto_module/step_then_decrypt_success.dart b/acceptance_tests/lib/src/steps/crypto_module/step_then_decrypt_success.dart new file mode 100644 index 00000000..35db0761 --- /dev/null +++ b/acceptance_tests/lib/src/steps/crypto_module/step_then_decrypt_success.dart @@ -0,0 +1,21 @@ +import 'package:gherkin/gherkin.dart'; +import 'package:pubnub/core.dart'; +import 'package:test/test.dart'; + +import '../../world.dart'; +import '_utils.dart'; + +class ThenDecryptSuccessWithLegacy extends ThenWithWorld { + @override + RegExp get pattern => + RegExp(r'Successfully decrypt an encrypted file with legacy code'); + + @override + Future executeStep() async { + ICryptoModule cryptoModule = world.scenarioContext['cryptoModule']; + var decryptedData = + cryptoModule.decrypt(world.scenarioContext['encryptedData']); + this.expect( + listEquals(decryptedData, world.scenarioContext['fileData']), isTrue); + } +} diff --git a/acceptance_tests/lib/src/steps/crypto_module/step_then_decrypted.dart b/acceptance_tests/lib/src/steps/crypto_module/step_then_decrypted.dart new file mode 100644 index 00000000..27cdf081 --- /dev/null +++ b/acceptance_tests/lib/src/steps/crypto_module/step_then_decrypted.dart @@ -0,0 +1,22 @@ +import 'dart:io'; + +import 'package:gherkin/gherkin.dart'; +import 'package:test/test.dart'; + +import '../../world.dart'; +import '_utils.dart'; + +class StepThenDecryptedContentEquals + extends Then1WithWorld { + @override + RegExp get pattern => + RegExp(r'Decrypted file content equal to the {string} file content'); + + @override + Future executeStep(String file) async { + var sourceContent = + File(getCryptoFilePath(file)).readAsBytesSync().toList(); + var ec = world.scenarioContext['decryptedContent']; + this.expect(listEquals(ec, sourceContent), isTrue); + } +} diff --git a/acceptance_tests/lib/src/steps/crypto_module/step_then_outcome.dart b/acceptance_tests/lib/src/steps/crypto_module/step_then_outcome.dart new file mode 100644 index 00000000..05a68932 --- /dev/null +++ b/acceptance_tests/lib/src/steps/crypto_module/step_then_outcome.dart @@ -0,0 +1,20 @@ +import 'package:gherkin/gherkin.dart'; +import 'package:pubnub/core.dart'; +import 'package:test/expect.dart'; + +import '../../world.dart'; + +class StepThenOutcome extends Then1WithWorld { + @override + RegExp get pattern => RegExp(r'I receive {string}'); + + @override + Future executeStep(String expected) async { + if (expected == 'success') { + this.expect(world.latestException, isNull); + } else { + var outcome = world.latestException; + this.expect((outcome as CryptoException).message, contains(expected)); + } + } +} diff --git a/acceptance_tests/lib/src/steps/crypto_module/step_when_decrypt.dart b/acceptance_tests/lib/src/steps/crypto_module/step_when_decrypt.dart new file mode 100644 index 00000000..a39304e4 --- /dev/null +++ b/acceptance_tests/lib/src/steps/crypto_module/step_when_decrypt.dart @@ -0,0 +1,28 @@ +import 'dart:io'; + +import 'package:gherkin/gherkin.dart'; +import 'package:pubnub/core.dart'; +import 'package:pubnub/crypto.dart'; + +import '../../world.dart'; +import '_utils.dart'; + +class StepWhenDecryptFile extends When1WithWorld { + @override + RegExp get pattern => RegExp(r'I decrypt {string} file'); + + @override + Future executeStep(String file) async { + late ICryptoModule cryptoModule; + var cipherKey = CipherKey.fromUtf8(world.scenarioContext['cipherKey']); + if (world.scenarioContext['cryptorId'] == 'acrh') { + cryptoModule = CryptoModule(defaultCryptor: AesCbcCryptor(cipherKey)); + } + var data = File(getCryptoFilePath(file)).readAsBytesSync().toList(); + try { + cryptoModule.decrypt(data); + } catch (e) { + world.latestException = e; + } + } +} diff --git a/acceptance_tests/lib/src/steps/crypto_module/step_when_decrypt_as.dart b/acceptance_tests/lib/src/steps/crypto_module/step_when_decrypt_as.dart new file mode 100644 index 00000000..f7ca88d8 --- /dev/null +++ b/acceptance_tests/lib/src/steps/crypto_module/step_when_decrypt_as.dart @@ -0,0 +1,57 @@ +import 'dart:io'; + +import 'package:gherkin/gherkin.dart'; +import 'package:pubnub/core.dart'; +import 'package:pubnub/crypto.dart'; + +import '../../world.dart'; +import '_utils.dart'; + +class StepWhenDecryptFileAs + extends When2WithWorld { + @override + RegExp get pattern => RegExp(r'I decrypt {string} file as {string}'); + + @override + Future executeStep(String file, String format) async { + var cryptorId = world.scenarioContext['cryptorId']; + var cipherKey = CipherKey.fromUtf8(world.scenarioContext['cipherKey']); + late ICryptor cryptor; + late ICryptoModule cryptoModule; + if (cryptorId == 'legacy') { + cryptor = LegacyCryptor(cipherKey, + cryptoConfiguration: CryptoConfiguration( + useRandomInitializationVector: + world.scenarioContext['useRandomIntializationVector'])); + cryptoModule = CryptoModule(defaultCryptor: cryptor); + } else if (cryptorId == 'acrh') { + cryptor = AesCbcCryptor(cipherKey); + cryptoModule = CryptoModule(defaultCryptor: cryptor); + } + var defaultCryptorId = world.scenarioContext['defaultCryptorId']; + if (defaultCryptorId == 'legacy') { + var defaultCryptor = cryptor = LegacyCryptor(cipherKey, + cryptoConfiguration: CryptoConfiguration( + useRandomInitializationVector: + world.scenarioContext['useRandomIntializationVector'])); + var additionalCryptor = AesCbcCryptor(cipherKey); + cryptoModule = CryptoModule( + defaultCryptor: defaultCryptor, cryptors: [additionalCryptor]); + } else if (defaultCryptorId == 'acrh') { + var additionalCryptor = cryptor = LegacyCryptor(cipherKey, + cryptoConfiguration: CryptoConfiguration( + useRandomInitializationVector: + world.scenarioContext['useRandomIntializationVector'])); + var defaultCryptor = AesCbcCryptor(cipherKey); + cryptoModule = CryptoModule( + defaultCryptor: defaultCryptor, cryptors: [additionalCryptor]); + } + var fileData = File(getCryptoFilePath(file)).readAsBytesSync().toList(); + try { + world.scenarioContext['decryptedContent'] = + cryptoModule.decrypt(fileData); + } catch (e) { + world.latestException = e; + } + } +} diff --git a/acceptance_tests/lib/src/steps/crypto_module/step_when_encrypt.dart b/acceptance_tests/lib/src/steps/crypto_module/step_when_encrypt.dart new file mode 100644 index 00000000..c3086dd6 --- /dev/null +++ b/acceptance_tests/lib/src/steps/crypto_module/step_when_encrypt.dart @@ -0,0 +1,35 @@ +import 'dart:io'; + +import 'package:gherkin/gherkin.dart'; +import 'package:pubnub/core.dart'; +import 'package:pubnub/crypto.dart'; + +import '../../world.dart'; +import '_utils.dart'; + +class StepWhenEncrypt extends When2WithWorld { + @override + RegExp get pattern => RegExp(r'I encrypt {string} file as {string}'); + + @override + Future executeStep(String file, String format) async { + if (format == 'binary') { + var fileData = File(getCryptoFilePath(file)).readAsBytesSync().toList(); + world.scenarioContext['fileData'] = fileData; + + var cryptor = LegacyCryptor( + CipherKey.fromUtf8(world.scenarioContext['cipherKey']), + cryptoConfiguration: CryptoConfiguration( + useRandomInitializationVector: + world.scenarioContext['useRandomIntializationVector'])); + var cryptoModule = CryptoModule(defaultCryptor: cryptor); + world.scenarioContext['cryptoModule'] = cryptoModule; + try { + var encryptedData = cryptoModule.encrypt(fileData); + world.scenarioContext['encryptedData'] = encryptedData; + } catch (e) { + world.latestException = e; + } + } + } +} diff --git a/acceptance_tests/lib/src/steps/steps.dart b/acceptance_tests/lib/src/steps/steps.dart index b6e7824d..24d70351 100644 --- a/acceptance_tests/lib/src/steps/steps.dart +++ b/acceptance_tests/lib/src/steps/steps.dart @@ -1,6 +1,7 @@ import 'package:gherkin/gherkin.dart'; import '../world.dart'; +import 'crypto_module/crypto_module_steps.dart'; import 'step_given_demo_keyset.dart'; import 'pam_v3/pamv3_steps.dart' show pamv3Steps; import 'step_given_channel.dart'; @@ -23,6 +24,7 @@ import 'steps_files.dart'; import 'steps_push.dart'; final List> steps = [ + ...cryptoSteps, ...pamv3Steps, StepGivenChannel(), StepGivenDemoKeyset(), diff --git a/pubnub/CHANGELOG.md b/pubnub/CHANGELOG.md index 3d60b202..f297c4e0 100644 --- a/pubnub/CHANGELOG.md +++ b/pubnub/CHANGELOG.md @@ -1,3 +1,12 @@ +## v4.3.0 +October 16 2023 + +#### Added +- Add crypto module that allows configure SDK to encrypt and decrypt messages. + +#### Fixed +- Improved security of crypto implementation by adding enhanced AES-CBC cryptor. + ## v4.2.4 July 27 2023 diff --git a/pubnub/LICENSE b/pubnub/LICENSE index 6574c99f..5e1ef188 100644 --- a/pubnub/LICENSE +++ b/pubnub/LICENSE @@ -1,27 +1,29 @@ -PubNub Real-time Cloud-Hosted Push API and Push Notification Client Frameworks -Copyright (c) 2020 PubNub Inc. -http://www.pubnub.com/ -http://www.pubnub.com/terms +PubNub Software Development Kit License Agreement +Copyright © 2023 PubNub Inc. All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Subject to the terms and conditions of the license, you are hereby granted +a non-exclusive, worldwide, royalty-free license to (a) copy and modify +the software in source code or binary form for use with the software services +and interfaces provided by PubNub, and (b) redistribute unmodified copies +of the software to third parties. The software may not be incorporated in +or used to provide any product or service competitive with the products +and services of PubNub. -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this license shall be included +in or with all copies or substantial portions of the software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +This license does not grant you permission to use the trade names, trademarks, +service marks, or product names of PubNub, except as required for reasonable +and customary use in describing the origin of the software and reproducing +the content of this license. -PubNub Real-time Cloud-Hosted Push API and Push Notification Client Frameworks -Copyright (c) 2020 PubNub Inc. -http://www.pubnub.com/ -http://www.pubnub.com/terms \ No newline at end of file +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL PUBNUB OR THE AUTHORS OR COPYRIGHT HOLDERS OF THE SOFTWARE BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +https://www.pubnub.com/ +https://www.pubnub.com/terms diff --git a/pubnub/README.md b/pubnub/README.md index 0ff36f5d..5034dd8c 100644 --- a/pubnub/README.md +++ b/pubnub/README.md @@ -14,7 +14,7 @@ To add the package to your Dart or Flutter project, add `pubnub` as a dependency ```yaml dependencies: - pubnub: ^4.2.4 + pubnub: ^4.3.0 ``` After adding the dependency to `pubspec.yaml`, run the `dart pub get` command in the root directory of your project (the same that the `pubspec.yaml` is in). diff --git a/pubnub/lib/crypto.dart b/pubnub/lib/crypto.dart index 59783dd5..2aff4a02 100644 --- a/pubnub/lib/crypto.dart +++ b/pubnub/lib/crypto.dart @@ -5,6 +5,9 @@ /// {@category Modules} library pubnub.crypto; -export 'src/crypto/crypto.dart' show CryptoConfiguration, CryptoModule; +export 'src/crypto/crypto.dart' show CryptoModule; export 'src/crypto/encryption_mode.dart' show EncryptionMode, EncryptionModeExtension; +export 'src/crypto/cryptoConfiguration.dart' show CryptoConfiguration; +export 'src/crypto/aesCbcCryptor.dart' show AesCbcCryptor; +export 'src/crypto/legacyCryptor.dart' show LegacyCryptor; diff --git a/pubnub/lib/pubnub.dart b/pubnub/lib/pubnub.dart index 3d4380db..ca74c5d9 100644 --- a/pubnub/lib/pubnub.dart +++ b/pubnub/lib/pubnub.dart @@ -28,6 +28,8 @@ export 'src/core/exceptions.dart' export 'src/core/policies/retry_policy.dart' show RetryPolicy, LinearRetryPolicy, ExponentialRetryPolicy; export 'src/core/crypto/crypto.dart' show CipherKey; +export 'src/crypto/crypto.dart' show CryptoModule; +export 'src/crypto/cryptoConfiguration.dart' show CryptoConfiguration; // DX export 'src/dx/_utils/utils.dart' show InvariantException; diff --git a/pubnub/lib/src/core/core.dart b/pubnub/lib/src/core/core.dart index 22ebb8e7..ef58886b 100644 --- a/pubnub/lib/src/core/core.dart +++ b/pubnub/lib/src/core/core.dart @@ -21,7 +21,7 @@ class Core { /// Internal module responsible for supervising. SupervisorModule supervisor = SupervisorModule(); - static String version = '4.2.4'; + static String version = '4.3.0'; Core( {Keyset? defaultKeyset, diff --git a/pubnub/lib/src/core/crypto/cipher_key.dart b/pubnub/lib/src/core/crypto/cipher_key.dart index 0ebd6da5..7588c95b 100644 --- a/pubnub/lib/src/core/crypto/cipher_key.dart +++ b/pubnub/lib/src/core/crypto/cipher_key.dart @@ -21,4 +21,15 @@ class CipherKey { factory CipherKey.fromList(List key) { return CipherKey._(key); } + + @override + bool operator ==(dynamic other) { + if (other == null) { + return false; + } + if (runtimeType == other.runtimeType) { + return utf8.decode(data) == utf8.decode(other!.data); + } + return false; + } } diff --git a/pubnub/lib/src/core/crypto/crypto.dart b/pubnub/lib/src/core/crypto/crypto.dart index f932ab56..f37cd26a 100644 --- a/pubnub/lib/src/core/crypto/crypto.dart +++ b/pubnub/lib/src/core/crypto/crypto.dart @@ -11,13 +11,38 @@ class CryptoException extends PubNubException { CryptoException(String message) : super(message); } -/// @nodoc abstract class ICryptoModule { void register(Core core); - String encrypt(CipherKey key, String input); - dynamic decrypt(CipherKey key, String input); + List encrypt(List input); + List decrypt(List input); List encryptFileData(CipherKey key, List input); List decryptFileData(CipherKey key, List input); + + List encryptWithKey(CipherKey key, List input); + List decryptWithKey(CipherKey key, List input); +} + +/// @nodoc +abstract class ICryptor { + String get identifier; + + EncryptedData encrypt(List input); + List decrypt(EncryptedData input); + + EncryptedData encryptFileData(List input); + List decryptFileData(EncryptedData input); +} + +class EncryptedData { + List _data; + List _metadata; + + List get data => _data; + List get metadata => _metadata; + + EncryptedData._(this._data, this._metadata); + + factory EncryptedData.from(data, metadata) => EncryptedData._(data, metadata); } diff --git a/pubnub/lib/src/core/keyset/keyset.dart b/pubnub/lib/src/core/keyset/keyset.dart index 1ce7e1c0..959e23cd 100644 --- a/pubnub/lib/src/core/keyset/keyset.dart +++ b/pubnub/lib/src/core/keyset/keyset.dart @@ -37,7 +37,7 @@ class Keyset { this.publishKey, this.secretKey, this.authKey, - this.cipherKey, + @Deprecated('Use `cipherKey` at CryptoModule') this.cipherKey, }) : assert((uuid == null) ^ (userId == null)), uuid = userId != null ? UUID(userId.value) : uuid!; } diff --git a/pubnub/lib/src/crypto/aesCbcCryptor.dart b/pubnub/lib/src/crypto/aesCbcCryptor.dart new file mode 100644 index 00000000..5fd57d52 --- /dev/null +++ b/pubnub/lib/src/crypto/aesCbcCryptor.dart @@ -0,0 +1,59 @@ +import 'package:encrypt/encrypt.dart' as crypto; +import 'package:crypto/crypto.dart' show sha256; +import 'dart:typed_data' show Uint8List; + +import 'package:pubnub/core.dart'; + +/// AesCbcCryptor is new and enhanced cryptor to encrypt/decrypt +/// PubNub messages. +/// It's always preferred to use this cryptor instead old cryptor. +class AesCbcCryptor implements ICryptor { + CipherKey cipherKey; + + AesCbcCryptor(this.cipherKey); + @override + List decrypt(EncryptedData encryptedData) { + if (encryptedData.data.isEmpty) { + throw CryptoException('decryption error: empty content'); + } + var encrypter = crypto.Encrypter( + crypto.AES(_getKey(), mode: crypto.AESMode.cbc), + ); + return encrypter.decryptBytes( + crypto.Encrypted(Uint8List.fromList(encryptedData.data.toList())), + iv: crypto.IV(Uint8List.fromList(encryptedData.metadata.toList()))); + } + + @override + EncryptedData encrypt(List input) { + var encrypter = crypto.Encrypter( + crypto.AES(_getKey(), mode: crypto.AESMode.cbc), + ); + var iv = _getIv(); + var data = encrypter.encryptBytes(input, iv: iv).bytes.toList(); + return EncryptedData.from(data, iv.bytes.toList()); + } + + @override + String get identifier => 'ACRH'; + + crypto.IV _getIv() { + return crypto.IV.fromSecureRandom(16); + } + + crypto.Key _getKey() { + return crypto.Key.fromBase16( + sha256.convert(cipherKey.data).toString(), + ); + } + + @override + List decryptFileData(EncryptedData input) { + return decrypt(input); + } + + @override + EncryptedData encryptFileData(List input) { + return encrypt(input); + } +} diff --git a/pubnub/lib/src/crypto/crypto.dart b/pubnub/lib/src/crypto/crypto.dart index d451f62f..5ea0b08c 100644 --- a/pubnub/lib/src/crypto/crypto.dart +++ b/pubnub/lib/src/crypto/crypto.dart @@ -1,132 +1,112 @@ -import 'package:encrypt/encrypt.dart' as crypto; -import 'package:crypto/crypto.dart' show sha256; -import 'dart:convert' show base64, utf8; -import 'dart:typed_data' show Uint8List; - import 'package:pubnub/core.dart'; -import 'encryption_mode.dart'; - -/// Configuration used in cryptography. -class CryptoConfiguration { - /// Encryption mode used. - final EncryptionMode encryptionMode; - - /// Whether key should be encrypted. - final bool encryptKey; - - /// Whether a random IV should be used. - final bool useRandomInitializationVector; +import 'package:pubnub/src/crypto/aesCbcCryptor.dart'; +import 'package:pubnub/src/crypto/legacyCryptor.dart'; +import 'cryptoConfiguration.dart'; +import 'cryptorHeader.dart'; - const CryptoConfiguration( - {this.encryptionMode = EncryptionMode.CBC, - this.encryptKey = true, - this.useRandomInitializationVector = true}); -} - -/// Default cryptography module used in PubNub SDK. +/// CryptoModule is responsible for encryption and decryption +/// of PubNub messages. class CryptoModule implements ICryptoModule { - final CryptoConfiguration defaultConfiguration; + final ICryptor defaultCryptor; + List? cryptors; + + late CryptoConfiguration defaultConfiguration; + late LegacyCryptoModule legacyCryptoModule; + + CryptoModule( + {required this.defaultCryptor, + this.cryptors, + this.defaultConfiguration = const CryptoConfiguration()}) { + legacyCryptoModule = + LegacyCryptoModule(defaultConfiguration: defaultConfiguration); + } - /// Default configuration is: - /// * `encryptionMode` set to [EncryptionMode.CBC]. - /// * `encryptKey` set to `true`. - /// * `useRandomInitializationVector` set to `true`. - CryptoModule({this.defaultConfiguration = const CryptoConfiguration()}); + factory CryptoModule.legacyCryptoModule(CipherKey cipherKey, + {defaultCryptoConfiguration = const CryptoConfiguration()}) { + return CryptoModule( + defaultCryptor: LegacyCryptor(cipherKey, + cryptoConfiguration: defaultCryptoConfiguration), + cryptors: [AesCbcCryptor(cipherKey)]); + } - crypto.Key _getKey(CipherKey cipherKey, CryptoConfiguration configuration) { - if (configuration.encryptKey) { - return crypto.Key.fromUtf8( - sha256.convert(cipherKey.data).toString().substring(0, 32)); - } else { - return crypto.Key(Uint8List.fromList(cipherKey.data)); - } + factory CryptoModule.aesCbcCryptoModule(CipherKey cipherKey, + {defaultCryptoConfiguration = const CryptoConfiguration()}) { + return CryptoModule( + defaultCryptor: AesCbcCryptor(cipherKey), + cryptors: [ + LegacyCryptor(cipherKey, + cryptoConfiguration: defaultCryptoConfiguration), + ]); } - /// Decrypts [input] with [key] based on [configuration]. - /// - /// If [configuration] is `null`, then [CryptoModule.defaultConfiguration] is used. @override - dynamic decrypt(CipherKey key, String input, - {CryptoConfiguration? configuration}) { - var config = configuration ?? defaultConfiguration; + List encrypt(List data) { + _emptyContentValidation(data); + var encrypted = defaultCryptor.encrypt(data); + if (encrypted.metadata.isEmpty) return encrypted.data; + var header = + CryptorHeader.from(defaultCryptor.identifier, encrypted.metadata); + var headerData = List.filled(header!.length, 0); + var pos = 0; + headerData.setAll(pos, header.data); + pos = header.length - encrypted.metadata.length; + headerData.setAll(pos, encrypted.metadata); + return [...headerData, ...encrypted.data]; + } - var encrypter = crypto.Encrypter( - crypto.AES(_getKey(key, config), mode: config.encryptionMode.value())); + @override + List decrypt(List data) { + var header = CryptorHeader.tryParse(data); + var cryptor = _getCryptor(header); + var headerLength = header != null ? header.length : 0; + var metadata = headerLength > 0 + ? data.sublist((headerLength - header!.metadataLength), headerLength) + : List.empty(); + return cryptor! + .decrypt(EncryptedData.from(data.sublist(headerLength), metadata)); + } + + ICryptor? _getCryptor(CryptorHeaderV1? header) { try { - if (config.useRandomInitializationVector) { - var data = base64.decode(input); - return utf8.decode(encrypter.decryptBytes( - crypto.Encrypted(Uint8List.fromList(data.sublist(16))), - iv: crypto.IV.fromBase64(base64.encode(data.sublist(0, 16))))); - } else { - var iv = crypto.IV.fromUtf8('0123456789012345'); - return encrypter.decrypt64(input, iv: iv); - } + return _getAllCryptors().firstWhere( + (element) => element.identifier == (header?.identifier ?? ''), + ); } catch (e) { - throw CryptoException('Error while decrypting message:\n$e'); + throw CryptoException( + 'unknown cryptor error: none of the registered cryptor can decrypt.'); } } - /// Encrypts [input] with [key] based on [configuration]. - /// - /// If [configuration] is `null`, then [CryptoModule.defaultConfiguration] is used. - @override - String encrypt(CipherKey key, dynamic input, - {CryptoConfiguration? configuration}) { - var config = configuration ?? defaultConfiguration; - - var encrypter = crypto.Encrypter( - crypto.AES(_getKey(key, config), mode: config.encryptionMode.value())); - try { - if (config.useRandomInitializationVector) { - var iv = crypto.IV.fromSecureRandom(16); + List _getAllCryptors() { + return [defaultCryptor, ...cryptors ?? []]; + } - var encrypted = encrypter.encrypt(input, iv: iv).bytes; - return base64.encode([...iv.bytes, ...encrypted]); - } else { - var iv = crypto.IV.fromUtf8('0123456789012345'); - return encrypter.encrypt(input, iv: iv).base64; - } - } catch (e) { - throw CryptoException('Error while encrypting message:\n$e'); + void _emptyContentValidation(List content) { + if (content.isEmpty) { + throw CryptoException('encryption error: empty content'); } } - /// Decrypts [input] based on the [key] and [configuration]. - /// - /// If [configuration] is `null`, then [CryptoModule.defaultConfiguration] is used. @override - List decryptFileData(CipherKey key, List input, - {CryptoConfiguration? configuration}) { - var config = configuration ?? defaultConfiguration; - var encrypter = crypto.Encrypter( - crypto.AES(_getKey(key, config), mode: config.encryptionMode.value())); - try { - return encrypter.decryptBytes( - crypto.Encrypted(Uint8List.fromList(input.sublist(16))), - iv: crypto.IV.fromBase64(base64.encode(input.sublist(0, 16)))); - } catch (e) { - throw CryptoException('Error while decrypting file data: \n$e}'); - } + List decryptFileData(CipherKey key, List input) { + return legacyCryptoModule.decryptFileData(key, input); + } + + @override + List decryptWithKey(CipherKey key, List input) { + return legacyCryptoModule.decryptWithKey(key, input); } - /// Encrypts [input] based on the [key] and [configuration]. - /// - /// If [configuration] is `null`, then [CryptoModule.defaultConfiguration] is used. @override - List encryptFileData(CipherKey key, List input, - {CryptoConfiguration? configuration}) { - var iv = crypto.IV.fromSecureRandom(16); - var config = configuration ?? defaultConfiguration; - var encrypter = crypto.Encrypter( - crypto.AES(_getKey(key, config), mode: config.encryptionMode.value())); + List encryptFileData(CipherKey key, List input) { + _emptyContentValidation(input); + return legacyCryptoModule.encryptFileData(key, input); + } - try { - var encrypted = encrypter.encryptBytes(input, iv: iv).bytes; - return [...iv.bytes, ...encrypted]; - } catch (e) { - throw CryptoException('Error while encrypting file data:\n$e'); - } + @override + List encryptWithKey(CipherKey key, List input) { + _emptyContentValidation(input); + return legacyCryptoModule.encryptWithKey(key, input); } /// @nodoc diff --git a/pubnub/lib/src/crypto/cryptoConfiguration.dart b/pubnub/lib/src/crypto/cryptoConfiguration.dart new file mode 100644 index 00000000..86dd9de3 --- /dev/null +++ b/pubnub/lib/src/crypto/cryptoConfiguration.dart @@ -0,0 +1,18 @@ +import 'encryption_mode.dart'; + +/// Configuration used in cryptography. +class CryptoConfiguration { + /// Encryption mode used. + final EncryptionMode encryptionMode; + + /// Whether key should be encrypted. + final bool encryptKey; + + /// Whether a random IV should be used. + final bool useRandomInitializationVector; + + const CryptoConfiguration( + {this.encryptionMode = EncryptionMode.CBC, + this.encryptKey = true, + this.useRandomInitializationVector = true}); +} diff --git a/pubnub/lib/src/crypto/cryptorHeader.dart b/pubnub/lib/src/crypto/cryptorHeader.dart new file mode 100644 index 00000000..46f7e14e --- /dev/null +++ b/pubnub/lib/src/crypto/cryptorHeader.dart @@ -0,0 +1,93 @@ +import 'dart:convert' show utf8; + +import 'package:pubnub/core.dart'; + +/// @nodoc +class CryptorHeader { + static const SENTINEL = 'PNED'; + static const LEGACY_IDENTIFIER = ''; + static const IDENTIFIER_LENGTH = 4; + static const MAX_VERSION = 1; + + static CryptorHeaderV1? from(String id, List metadata) { + if (id == LEGACY_IDENTIFIER) return null; + return CryptorHeaderV1(id, metadata.length); + } + + static CryptorHeaderV1? tryParse(List encryptedData) { + List sentinel; + var version; + if (encryptedData.length >= 4) { + sentinel = encryptedData.sublist(0, 4).toList(); + if (utf8.decode(sentinel, allowMalformed: true) != SENTINEL) return null; + } + + if (encryptedData.length >= 5) { + version = encryptedData[4]; + } else { + throw CryptoException('decryption error: invalid or no header version.'); + } + if (version > MAX_VERSION) { + throw CryptoException( + 'unknown cryptor error: header version is higher than supported versions.'); + } + + var identifier; + var pos = 5 + IDENTIFIER_LENGTH; + if (encryptedData.length >= pos) { + identifier = encryptedData.sublist(5, pos).toList(); + } else { + throw CryptoException( + 'decryption error: invalid or No identifier found.'); + } + var metadataLength; + if (encryptedData.length > pos + 1) { + metadataLength = encryptedData[pos]; + } + pos += 1; + if (metadataLength == 255 && encryptedData.length >= pos + 2) { + metadataLength = encryptedData + .sublist(pos, pos + 2) + .fold(0, (acc, el) => (acc << 8) + el); + } + return CryptorHeaderV1(utf8.decode(identifier), metadataLength); + } +} + +/// @nodoc +class CryptorHeaderV1 { + static const VERSION = 1; + final String _identifier; + final int _metadataLength; + + CryptorHeaderV1(this._identifier, this._metadataLength); + + String get identifier => _identifier; + int get metadataLength => _metadataLength; + + int get length { + return (CryptorHeader.SENTINEL.length + + 1 + + CryptorHeader.IDENTIFIER_LENGTH + + (_metadataLength < 225 ? 1 : 3) + + _metadataLength); + } + + List get data { + var pos = 0; + var header = List.filled(length, 0); + header.setAll(pos, CryptorHeader.SENTINEL.codeUnits); + pos += CryptorHeader.SENTINEL.length; + header[pos] = VERSION; + pos++; + header.setAll(pos, _identifier.codeUnits); + pos += CryptorHeader.IDENTIFIER_LENGTH; + var metadataLength = this.metadataLength; + if (metadataLength < 255) { + header[pos] = metadataLength; + } else { + header.setAll(pos, [255, metadataLength >> 8, metadataLength & 0xff]); + } + return header; + } +} diff --git a/pubnub/lib/src/crypto/legacyCryptor.dart b/pubnub/lib/src/crypto/legacyCryptor.dart new file mode 100644 index 00000000..f554a6c7 --- /dev/null +++ b/pubnub/lib/src/crypto/legacyCryptor.dart @@ -0,0 +1,182 @@ +import 'package:pubnub/core.dart'; + +import 'package:encrypt/encrypt.dart' as crypto; +import 'package:crypto/crypto.dart' show sha256; +import 'dart:convert' show base64; +import 'dart:typed_data' show Uint8List; + +import 'cryptoConfiguration.dart'; +import 'encryption_mode.dart'; +import 'crypto.dart'; + +/// Legacy cryptor exists so that SDK will be able to decrypt old contents +/// Which encrypted in past +class LegacyCryptor implements ICryptor { + final CryptoConfiguration cryptoConfiguration; + final CipherKey cipherKey; + + late LegacyCryptoModule cryptor; + + LegacyCryptor(this.cipherKey, + {this.cryptoConfiguration = const CryptoConfiguration()}) { + cryptor = LegacyCryptoModule(defaultConfiguration: cryptoConfiguration); + } + + @override + List decrypt(EncryptedData input) { + if (input.data.length <= 16) { + throw CryptoException('decryption error: empty content'); + } + return cryptor.decryptWithKey(cipherKey, input.data); + } + + @override + EncryptedData encrypt(List input) { + return EncryptedData.from( + cryptor.encryptWithKey(cipherKey, input), List.empty()); + } + + @override + String get identifier => ''; + + @override + List decryptFileData(EncryptedData input) { + if (input.data.length <= 16) { + throw CryptoException('decryption error: empty content'); + } + return cryptor.decryptFileData(cipherKey, input.data); + } + + @override + EncryptedData encryptFileData(List input) { + return EncryptedData.from( + cryptor.encryptFileData(cipherKey, input), List.empty()); + } +} + +/// Legacy CryptoModule module used in PubNub SDK when CipherKey is not provided. +class LegacyCryptoModule implements ICryptoModule { + final CryptoConfiguration defaultConfiguration; + + /// Default configuration is: + /// * `encryptionMode` set to [EncryptionMode.CBC]. + /// * `encryptKey` set to `true`. + /// * `useRandomInitializationVector` set to `true`. + LegacyCryptoModule({this.defaultConfiguration = const CryptoConfiguration()}); + + crypto.Key _getKey(CipherKey cipherKey, CryptoConfiguration configuration) { + if (configuration.encryptKey) { + return crypto.Key.fromUtf8( + sha256.convert(cipherKey.data).toString().substring(0, 32)); + } else { + return crypto.Key(Uint8List.fromList(cipherKey.data)); + } + } + + /// Decrypts [input] with [key] based on [configuration]. + /// + /// If [configuration] is `null`, then [CryptoModule.defaultConfiguration] is used. + @override + List decryptWithKey(CipherKey key, List input, + {CryptoConfiguration? configuration}) { + var config = configuration ?? defaultConfiguration; + if (Uint8List.fromList(input.sublist(16)).isEmpty) { + throw CryptoException('decryption error: empty content'); + } + var encrypter = crypto.Encrypter( + crypto.AES(_getKey(key, config), mode: config.encryptionMode.value())); + try { + if (config.useRandomInitializationVector) { + return encrypter.decryptBytes( + crypto.Encrypted(Uint8List.fromList(input.sublist(16))), + iv: crypto.IV.fromBase64(base64.encode(input.sublist(0, 16)))); + } else { + var iv = crypto.IV.fromUtf8('0123456789012345'); + return encrypter + .decryptBytes(crypto.Encrypted(Uint8List.fromList(input)), iv: iv); + } + } catch (e) { + throw CryptoException('Error while decrypting message:\n$e'); + } + } + + /// Encrypts [input] with [key] based on [configuration]. + /// + /// If [configuration] is `null`, then [CryptoModule.defaultConfiguration] is used. + @override + List encryptWithKey(CipherKey key, List input, + {CryptoConfiguration? configuration}) { + var config = configuration ?? defaultConfiguration; + + var encrypter = crypto.Encrypter( + crypto.AES(_getKey(key, config), mode: config.encryptionMode.value())); + try { + if (config.useRandomInitializationVector) { + var iv = crypto.IV.fromSecureRandom(16); + var encrypted = []; + if (input.isNotEmpty) { + encrypted = encrypter.encryptBytes(input, iv: iv).bytes; + } + return [...iv.bytes, ...encrypted]; + } else { + var iv = crypto.IV.fromUtf8('0123456789012345'); + if (input.isEmpty) return [...iv.bytes]; + return encrypter.encryptBytes(input, iv: iv).bytes; + } + } catch (e) { + throw CryptoException('Error while encrypting message:\n$e'); + } + } + + /// Decrypts [input] based on the [key] and [configuration]. + /// + /// If [configuration] is `null`, then [CryptoModule.defaultConfiguration] is used. + @override + List decryptFileData(CipherKey key, List input, + {CryptoConfiguration? configuration}) { + var config = configuration ?? defaultConfiguration; + var encrypter = crypto.Encrypter( + crypto.AES(_getKey(key, config), mode: config.encryptionMode.value())); + try { + return encrypter.decryptBytes( + crypto.Encrypted(Uint8List.fromList(input.sublist(16))), + iv: crypto.IV.fromBase64(base64.encode(input.sublist(0, 16)))); + } catch (e) { + throw CryptoException('Error while decrypting file data: \n$e}'); + } + } + + /// Encrypts [input] based on the [key] and [configuration]. + /// + /// If [configuration] is `null`, then [CryptoModule.defaultConfiguration] is used. + @override + List encryptFileData(CipherKey key, List input, + {CryptoConfiguration? configuration}) { + var iv = crypto.IV.fromSecureRandom(16); + var config = configuration ?? defaultConfiguration; + var encrypter = crypto.Encrypter( + crypto.AES(_getKey(key, config), mode: config.encryptionMode.value())); + + try { + var encrypted = encrypter.encryptBytes(input, iv: iv).bytes; + return [...iv.bytes, ...encrypted]; + } catch (e) { + throw CryptoException('Error while encrypting file data:\n$e'); + } + } + + @override + List decrypt(List input) { + // Note: Unreachable code. Till the time legacy encryption supported. + return List.empty(); + } + + @override + List encrypt(List input) { + // Note: Unreachable code. Till the time legacy encryption supported. + return List.empty(); + } + + @override + void register(Core core) {} +} diff --git a/pubnub/lib/src/default.dart b/pubnub/lib/src/default.dart index 8db1c4de..ac0f2b73 100644 --- a/pubnub/lib/src/default.dart +++ b/pubnub/lib/src/default.dart @@ -1,3 +1,5 @@ +import 'package:pubnub/src/crypto/legacyCryptor.dart'; + import '../core.dart'; import 'networking/networking.dart'; @@ -75,7 +77,10 @@ class PubNub extends Core defaultKeyset: defaultKeyset, networking: networking ?? NetworkingModule(), parser: parser ?? ParserModule(), - crypto: crypto ?? CryptoModule()) { + crypto: crypto ?? + (defaultKeyset?.cipherKey != null + ? CryptoModule.legacyCryptoModule(defaultKeyset!.cipherKey!) + : LegacyCryptoModule())) { batch = BatchDx(this); channelGroups = ChannelGroupDx(this); objects = ObjectsDx(this); diff --git a/pubnub/lib/src/dx/_endpoints/files.dart b/pubnub/lib/src/dx/_endpoints/files.dart index b13eaf06..90b11857 100644 --- a/pubnub/lib/src/dx/_endpoints/files.dart +++ b/pubnub/lib/src/dx/_endpoints/files.dart @@ -1,7 +1,8 @@ import 'package:pubnub/core.dart'; import 'package:pubnub/pubnub.dart'; -typedef decryptFunction = List Function(CipherKey key, List data); +typedef decryptFileDataFunctionType = List Function( + CipherKey key, List data); class GenerateFileUploadUrlParams extends Parameters { Keyset keyset; @@ -150,9 +151,11 @@ class DownloadFileResult extends Result { /// @nodoc factory DownloadFileResult.fromJson(dynamic object, {CipherKey? cipherKey, Function? decryptFunction}) { - if (cipherKey != null) { - return DownloadFileResult._( - decryptFunction!(cipherKey, object.byteList as List)); + if (cipherKey != null || + !(decryptFunction is decryptFileDataFunctionType)) { + return DownloadFileResult._(decryptFunction is decryptFileDataFunctionType + ? decryptFunction(cipherKey!, object.byteList as List) + : decryptFunction!(object.byteList as List)); } return DownloadFileResult._(object.byteList); } diff --git a/pubnub/lib/src/dx/_endpoints/history.dart b/pubnub/lib/src/dx/_endpoints/history.dart index 0c244bae..e0589e6f 100644 --- a/pubnub/lib/src/dx/_endpoints/history.dart +++ b/pubnub/lib/src/dx/_endpoints/history.dart @@ -1,7 +1,10 @@ +import 'dart:convert'; + import 'package:pubnub/core.dart'; import 'package:pubnub/src/dx/_utils/utils.dart'; -typedef decryptFunction = List Function(CipherKey key, String data); +typedef decryptWithKey = List Function(CipherKey key, List data); +typedef decrypt = List Function(List data); class FetchHistoryParams extends Parameters { Keyset keyset; @@ -158,9 +161,13 @@ class BatchHistoryResultEntry { factory BatchHistoryResultEntry.fromJson(Map object, {CipherKey? cipherKey, Function? decryptFunction}) { return BatchHistoryResultEntry._( - cipherKey == null + (cipherKey == null && decryptFunction is decryptWithKey) ? object['message'] - : decryptFunction!(cipherKey, object['message']), + : (decryptFunction is decryptWithKey + ? json.decode(utf8.decode(decryptFunction(cipherKey!, + base64.decode(object['message'] as String).toList()))) + : json.decode(utf8.decode(decryptFunction!( + base64.decode(object['message'] as String).toList())))), Timetoken(BigInt.parse('${object['timetoken']}')), object['uuid'], MessageTypeExtension.fromInt(object['message_type']), diff --git a/pubnub/lib/src/dx/batch/batch.dart b/pubnub/lib/src/dx/batch/batch.dart index 6028791d..59986850 100644 --- a/pubnub/lib/src/dx/batch/batch.dart +++ b/pubnub/lib/src/dx/batch/batch.dart @@ -1,8 +1,8 @@ import 'package:pubnub/core.dart'; import 'package:pubnub/src/default.dart'; - import 'package:pubnub/src/dx/_utils/utils.dart'; import 'package:pubnub/src/dx/_endpoints/history.dart'; +import '../../../crypto.dart'; export 'package:pubnub/src/dx/_endpoints/history.dart' show BatchHistoryResult, BatchHistoryResultEntry, CountMessagesResult; @@ -56,12 +56,17 @@ class BatchDx { includeUUID: includeUUID); return defaultFlow( - keyset: keyset, - core: _core, - params: params, - serialize: (object, [_]) => BatchHistoryResult.fromJson(object, - cipherKey: keyset?.cipherKey, - decryptFunction: _core.crypto.decrypt)); + keyset: keyset, + core: _core, + params: params, + serialize: (object, [_]) => BatchHistoryResult.fromJson(object, + cipherKey: keyset?.cipherKey, + decryptFunction: + (keyset?.cipherKey == _core.keysets.defaultKeyset.cipherKey && + _core.crypto is CryptoModule) + ? _core.crypto.decrypt + : _core.crypto.decryptWithKey), + ); } /// Get multiple channels' message count using one call. diff --git a/pubnub/lib/src/dx/channel/channel_history.dart b/pubnub/lib/src/dx/channel/channel_history.dart index 73c6c6c6..a9d260c5 100644 --- a/pubnub/lib/src/dx/channel/channel_history.dart +++ b/pubnub/lib/src/dx/channel/channel_history.dart @@ -1,8 +1,11 @@ +import 'dart:convert'; + import 'package:pubnub/core.dart'; import 'package:pubnub/src/default.dart'; import 'package:pubnub/src/dx/_utils/utils.dart'; import 'package:pubnub/src/dx/_endpoints/history.dart'; +import '../../../crypto.dart'; import 'channel.dart'; /// Order of messages based on timetoken. @@ -103,11 +106,15 @@ class ChannelHistory { _cursor = result.endTimetoken; _messages.addAll(await Future.wait(result.messages.map((message) async { - if (_keyset.cipherKey != null) { - message['message'] = await _core.parser.decode(_core.crypto - .decrypt(_keyset.cipherKey!, message['message'] as String)); + if (_keyset.cipherKey != null || _core.crypto is CryptoModule) { + message['message'] = _keyset.cipherKey == + _core.keysets.defaultKeyset.cipherKey + ? await _core.parser.decode(utf8.decode(_core.crypto.decrypt( + base64.decode(message['message'] as String).toList()))) + : await _core.parser.decode(utf8.decode(_core.crypto + .decryptWithKey(_keyset.cipherKey!, + base64.decode(message['message'] as String).toList()))); } - print(message); return BaseMessage( publishedAt: Timetoken(BigInt.from(message['timetoken'])), content: message['message'], @@ -202,9 +209,14 @@ class PaginatedChannelHistory { } _messages.addAll(await Future.wait(result.messages.map((message) async { - if (_keyset.cipherKey != null) { - message['message'] = await _core.parser.decode(_core.crypto - .decrypt(_keyset.cipherKey!, message['message'] as String)); + if (_keyset.cipherKey != null || _core.crypto is CryptoModule) { + message['message'] = _keyset.cipherKey == + _core.keysets.defaultKeyset.cipherKey + ? await _core.parser.decode(utf8.decode(_core.crypto + .decrypt(base64.decode(message['message'] as String)))) + : await _core.parser.decode(utf8.decode(_core.crypto.encryptWithKey( + _keyset.cipherKey!, + base64.decode(message['message'] as String).toList()))); } return BaseMessage( originalMessage: message, diff --git a/pubnub/lib/src/dx/files/files.dart b/pubnub/lib/src/dx/files/files.dart index 5a9f7310..56da781c 100644 --- a/pubnub/lib/src/dx/files/files.dart +++ b/pubnub/lib/src/dx/files/files.dart @@ -1,7 +1,10 @@ +import 'dart:convert'; + import 'package:pubnub/core.dart'; import 'package:pubnub/src/dx/_utils/utils.dart'; import 'package:pubnub/src/dx/_endpoints/files.dart'; +import '../../../crypto.dart'; import 'schema.dart'; import 'extensions/keyset.dart'; @@ -72,8 +75,13 @@ class FileDx { serialize: (object, [_]) => GenerateFileUploadUrlResult.fromJson(object)); - if (keyset.cipherKey != null || cipherKey != null) { - file = _core.crypto.encryptFileData(cipherKey ?? keyset.cipherKey!, file); + if (keyset.cipherKey != null || + cipherKey != null || + _core.crypto is CryptoModule) { + file = (cipherKey != null || + !(keyset.cipherKey == _core.keysets.defaultKeyset.cipherKey)) + ? _core.crypto.encryptFileData(cipherKey ?? keyset.cipherKey!, file) + : _core.crypto.encrypt(file); } var fileInfo = FileInfo( @@ -156,9 +164,15 @@ class FileDx { Ensure(keyset.publishKey).isNotNull('publish key'); var messagePayload = await _core.parser.encode(message); - if (cipherKey != null || keyset.cipherKey != null) { - messagePayload = await _core.parser.encode( - _core.crypto.encrypt(cipherKey ?? keyset.cipherKey!, messagePayload)); + if (cipherKey != null || + keyset.cipherKey != null || + _core.crypto is CryptoModule) { + messagePayload = (cipherKey != null || + !(keyset.cipherKey == _core.keysets.defaultKeyset.cipherKey)) + ? await _core.parser.encode(base64.encode(_core.crypto.encryptWithKey( + cipherKey ?? keyset.cipherKey!, utf8.encode(messagePayload)))) + : await _core.parser.encode( + base64.encode(_core.crypto.encrypt(utf8.encode(messagePayload)))); } if (meta != null) meta = await _core.parser.encode(meta); return defaultFlow( @@ -190,7 +204,12 @@ class FileDx { deserialize: false, serialize: (object, [_]) => DownloadFileResult.fromJson(object, cipherKey: cipherKey ?? keyset!.cipherKey, - decryptFunction: _core.crypto.decryptFileData)); + decryptFunction: cipherKey != null || + !(keyset?.cipherKey == + _core.keysets.defaultKeyset.cipherKey) || + !(_core.crypto is CryptoModule) + ? _core.crypto.decryptFileData + : _core.crypto.decrypt)); } /// Lists all files in a [channel]. @@ -280,9 +299,13 @@ class FileDx { /// If that fails as well, then it will throw [InvariantException]. List encryptFile(List bytes, {CipherKey? cipherKey, Keyset? keyset, String? using}) { + if (cipherKey != null) { + return _core.crypto.encryptFileData(cipherKey, bytes); + } keyset ??= _core.keysets[using]; - return _core.crypto - .encryptFileData((cipherKey ?? keyset.cipherKey)!, bytes); + return keyset.cipherKey == _core.keysets.defaultKeyset.cipherKey + ? _core.crypto.encrypt(bytes) + : _core.crypto.encryptFileData(keyset.cipherKey!, bytes); } /// Decrypts file content in bytes format. @@ -294,8 +317,12 @@ class FileDx { /// If that fails as well, then it will throw [InvariantException]. List decryptFile(List bytes, {CipherKey? cipherKey, Keyset? keyset, String? using}) { + if (cipherKey != null) { + return _core.crypto.decryptFileData(cipherKey, bytes); + } keyset ??= _core.keysets[using]; - return _core.crypto - .decryptFileData((cipherKey ?? keyset.cipherKey)!, bytes); + return keyset.cipherKey == _core.keysets.defaultKeyset.cipherKey + ? _core.crypto.decrypt(bytes) + : _core.crypto.decryptFileData(keyset.cipherKey!, bytes); } } diff --git a/pubnub/lib/src/dx/publish/publish.dart b/pubnub/lib/src/dx/publish/publish.dart index 1c50f675..afa72049 100644 --- a/pubnub/lib/src/dx/publish/publish.dart +++ b/pubnub/lib/src/dx/publish/publish.dart @@ -1,5 +1,8 @@ +import 'dart:convert'; + import 'package:pubnub/core.dart'; +import '../../../crypto.dart'; import '../_utils/utils.dart'; import '../_endpoints/publish.dart'; @@ -48,9 +51,13 @@ mixin PublishDx on Core { var payload = await super.parser.encode(message); - if (keyset.cipherKey != null) { - payload = - await super.parser.encode(crypto.encrypt(keyset.cipherKey!, payload)); + if (keyset.cipherKey != null || crypto is CryptoModule) { + payload = keyset.cipherKey == keysets.defaultKeyset.cipherKey + ? await super + .parser + .encode(base64.encode(crypto.encrypt(utf8.encode(payload)))) + : await super.parser.encode(base64.encode( + crypto.encryptWithKey(keyset.cipherKey!, utf8.encode(payload)))); } var params = PublishParams(keyset, channel, payload, diff --git a/pubnub/lib/src/subscribe/subscribe_loop/subscribe_loop.dart b/pubnub/lib/src/subscribe/subscribe_loop/subscribe_loop.dart index 6ff47416..68a6214d 100644 --- a/pubnub/lib/src/subscribe/subscribe_loop/subscribe_loop.dart +++ b/pubnub/lib/src/subscribe/subscribe_loop/subscribe_loop.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'dart:convert'; import 'package:async/async.dart'; import 'package:pubnub/core.dart'; import 'subscribe_loop_state.dart'; import 'subscribe_fiber.dart'; +import '../../../crypto.dart'; import '../envelope.dart'; import '../_endpoints/subscribe.dart'; @@ -126,16 +128,21 @@ class SubscribeLoop { 'Result: timetoken ${result.timetoken}, new messages: ${result.messages.length}'); yield* Stream.fromIterable(result.messages).asyncMap((object) async { - if (state.keyset.cipherKey != null && + if ((state.keyset.cipherKey != null || core.crypto is CryptoModule) && (object['e'] == null || object['e'] == 4 || object['e'] == 0) && !object['c'].endsWith('-pnpres')) { try { _logger.info('Decrypting message...'); - object['d'] = await core.parser.decode( - core.crypto.decrypt(state.keyset.cipherKey!, object['d'])); + object['d'] = state.keyset.cipherKey == + core.keysets.defaultKeyset.cipherKey + ? await core.parser.decode(utf8.decode(core.crypto + .decrypt(base64.decode(object['d'] as String).toList()))) + : await core.parser.decode(utf8.decode(core.crypto + .decryptWithKey(state.keyset.cipherKey!, + base64.decode(object['d'] as String).toList()))); } catch (e) { throw PubNubException( - 'Can not decrypt the message payload. Please check keyset configuration.'); + 'Can not decrypt the message payload. Please check keyset or crypto configuration'); } } return Envelope.fromJson(object); diff --git a/pubnub/pubspec.yaml b/pubnub/pubspec.yaml index 5864cf51..11e219ec 100644 --- a/pubnub/pubspec.yaml +++ b/pubnub/pubspec.yaml @@ -1,6 +1,6 @@ name: pubnub description: PubNub SDK v5 for Dart lang (with Flutter support) that allows you to create real-time applications -version: 4.2.4 +version: 4.3.0 homepage: https://www.pubnub.com/docs/sdks/dart environment: diff --git a/pubnub/test/unit/crypto/crypto_test.dart b/pubnub/test/unit/crypto/crypto_test.dart index a3285026..42d2ffca 100644 --- a/pubnub/test/unit/crypto/crypto_test.dart +++ b/pubnub/test/unit/crypto/crypto_test.dart @@ -1,5 +1,9 @@ +import 'dart:convert'; + import 'package:pubnub/core.dart'; +import 'package:pubnub/src/crypto/aesCbcCryptor.dart'; import 'package:pubnub/src/crypto/crypto.dart'; +import 'package:pubnub/src/crypto/cryptorHeader.dart'; import 'package:test/test.dart'; @@ -10,17 +14,34 @@ void main() { group('Crypto [PubNubCryptoModule]', () { setUp(() { key = CipherKey.fromUtf8('thecustomsecretkey'); - crypto = CryptoModule(); + crypto = CryptoModule.legacyCryptoModule(CipherKey.fromUtf8('')); }); test('should work in two ways', () async { var plaintext = 'hello world'; - var ciphertext = crypto.encrypt(key, plaintext); + var ciphertext = crypto.encryptWithKey(key, plaintext.codeUnits); + + var result = crypto.decryptWithKey(key, ciphertext); + + expect(utf8.decode(result), equals(plaintext)); + }); + + test('cryptoHeader tryParse/data from encrypted text', () async { + var encryptedDataWithHeader = 'PNEDACRH�_�ƿ'; + var headerData = [80, 78, 69, 68, 1, 65, 67, 82, 72, 16]; + var expectedBytes = [...headerData, ...List.filled(16, 0)]; - var result = crypto.decrypt(key, ciphertext); + var header = CryptorHeader.tryParse(encryptedDataWithHeader.codeUnits); + expect(header!.data, equals(expectedBytes)); + }); - expect(result, equals(plaintext)); + test('AesCbcCryptor should work as expected', () async { + var plainText = 'Hello there!'; + var cryptor = AesCbcCryptor(CipherKey.fromUtf8('pubnubenigma')); + var encryptedData = cryptor.encrypt(plainText.codeUnits); + var decrypted = cryptor.decrypt(encryptedData); + expect(utf8.decode(decrypted), equals(plainText)); }); }); } diff --git a/pubnub/test/unit/dx/file_test.dart b/pubnub/test/unit/dx/file_test.dart index 6523cf3d..81641d5e 100644 --- a/pubnub/test/unit/dx/file_test.dart +++ b/pubnub/test/unit/dx/file_test.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'package:pubnub/crypto.dart'; import 'package:test/test.dart'; import 'package:pubnub/pubnub.dart'; @@ -10,16 +9,12 @@ part 'fixtures/files.dart'; void main() { late PubNub pubnub; var keyset = - Keyset(subscribeKey: 'test', publishKey: 'test', uuid: UUID('test')); + Keyset(subscribeKey: 'test', publishKey: 'test', userId: UserId('test')); group('DX [file]', () { setUp(() { pubnub = PubNub( defaultKeyset: keyset, networking: FakeNetworkingModule(), - crypto: CryptoModule( - defaultConfiguration: - CryptoConfiguration(useRandomInitializationVector: false), - ), ); });