Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CryptoModule #117

Merged
merged 20 commits into from
Oct 16, 2023
Merged

CryptoModule #117

merged 20 commits into from
Oct 16, 2023

Conversation

mohitpubnub
Copy link
Contributor

@mohitpubnub mohitpubnub commented Sep 27, 2023

fix: Improve security of crypto implementation.

Improved security of crypto implementation by adding enhanced AES-CBC cryptor.

feat: Add crypto module

Add crypto module that allows configure SDK to encrypt and decrypt messages.

jguz-pubnub
jguz-pubnub previously approved these changes Sep 27, 2023
@mohitpubnub mohitpubnub marked this pull request as ready for review October 2, 2023 06:54
@mohitpubnub mohitpubnub self-assigned this Oct 2, 2023
@mohitpubnub mohitpubnub added priority: medium This PR should be reviewed after all high priority PRs. type: feature This PR contains new feature. type: fix This PR contains fixes to existing features. type: test This PR contains new tests for existing functionality or fixes to existing tests. type: refactor This PR contains refactored existing features. labels Oct 2, 2023
@@ -21,4 +21,15 @@ class CipherKey {
factory CipherKey.fromList(List<int> key) {
return CipherKey._(key);
}

@override
bool operator ==(dynamic other) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jguz-pubnub for comparison and to distinguish explicitly provided keyset/using keyset v/s default. Till the time we support cipherKey at configuration level.


AesCbcCryptor(this.cipherKey);
@override
List<int> decrypt(EncryptedData encryptedData) {
Copy link

@jguz-pubnub jguz-pubnub Oct 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a nitpick from developer to developer, so you don't have to fix it 🙂 You can enable code formatting on save. I know it from Android Studio through:

Preferences -> Languages & Frameworks -> Flutter -> Format code on save

I'm almost sure a similar option may also be available for Visual Studio you use. Moreover, the whole code structure doesn't wrap if you add some trailing commas at the end and save a source file. For example, something like this:

@override
  List<int> decrypt(EncryptedData encryptedData) {
    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())),
    );
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setting it up. this will look elegant.
Thanks!

if (encrypted.metadata == null) return encrypted.data;

var header =
CryptorHeader.from(defaultCryptor.identifier, encrypted.metadata!);
Copy link

@jguz-pubnub jguz-pubnub Oct 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it safe to access .metadata using an exclamation mark here? The same goes for header! below

EDIT: Regarding metadata, perhaps you could declare it as non-optional List<int> in the EncryptedData class. The empty list might indicate there's no metadata and you could avoid handling such an optional variable every time you want to access it. Let's leave it as it is now if this would require significant effort or may break your unit/contract tests

} else {
return crypto.Key(Uint8List.fromList(cipherKey.data));
}
factory CryptoModule.aescbcCryptoModule(CipherKey cipherKey,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: typo in method's name (could be camel-cased aesCbcCryptoModule)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah! naming!

/// Decrypts [input] based on the [key] and [configuration].
///
/// If [configuration] is `null`, then [CryptoModule.defaultConfiguration] is used.
List<ICryptor> _getAllCryptor() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: _getAllCryptors

var iv = crypto.IV.fromUtf8('0123456789012345');
return encrypter.encrypt(input, iv: iv).base64;
}
var allCryptors = _getAllCryptor();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps you could simplify instantiating local cryptor variable in this way:

var cryptor = allCryptors.firstWhere(
  (element) => element.identifier == (header?.identifier ?? ''),
);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch

if (encryptedData.length >= 5) {
version = encryptedData[4];
} else {
throw CryptoException('decryption error');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you want to provide more detailed messages that clearly indicate what's missing? For example, missing sentinel bytes, version byte, etc. Let's leave it as it is if you think it brings no value

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be helpful.
WIP: update along with tests in next commit.


/// Decrypts [input] with [key] based on [configuration].
///
/// If [configuration] is `null`, then [CryptoModule.defaultConfiguration] is used.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like those documentation comments! 👏

* Added more information in exception string for reason fo failure.
* naming convention fix for CryptoModule factory.
* code formatting.
@@ -12,7 +12,7 @@ class CryptoException extends PubNubException {
}

/// @nodoc
abstract class ICryptoModule {
abstract class ILegacyCryptor {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android Studio shows me this class is never used. Is it true? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah! We do not need it. Earlier I was thinking to have a different interface for LegacyCryptor. But then I was able to figure out that we can unify the signature and have only one interface that's ICryptor.

removing it.


var header =
CryptorHeader.from(defaultCryptor.identifier, encrypted.metadata);
var headerData = List<int>.filled(header!.length, 0);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure that header is not null here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, This is encryption code. Before attempting CryptoHeader.from(...), Here is what we did:

    var encrypted = defaultCryptor.encrypt(data);
    if (encrypted.metadata.isEmpty) return encrypted.data;

Which means If the default cryptor is not giving metadata(that means legacy one) we return from there only.

If we haven't return that means It's not legacy cryptor and If it's not legacy cryptor then we must have header.[going forward we won't ever support cryptor which does not conforms our header requirement] So header! will never throw.

I think I know the root of confusion:
from(..) has this if (id == LEGACY_IDENTIFIER) return null;
This was there for testing purpose(If CryptoHeader.from() called with legacy id then it should work as expected), while consuming this method in encrypt method. We are sure that execution will reach to Header.from(...) only when we have non-legacy cryptor.

I think I can remove if (id == LEGACY_IDENTIFIER) return null; condition. Because from method will be called only when we deal with non legacy cryptor.

var cryptor = _getCryptor(header);
var headerLength = header != null ? header.length : 0;
var metadata = headerLength > 0
? data.sublist((headerLength - header!.metadataLength), headerLength)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same, are you sure that header is not null here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, Because we already performed check :
var headerLength = header != null ? header.length : 0;

we go with sublist only when we have headerLength>0 that means header is not null.

@@ -104,10 +106,14 @@ 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));
message['message'] = _keyset.cipherKey ==
Copy link

@jguz-pubnub jguz-pubnub Oct 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain to me what's the logic here? Perhaps the idea is the same for other changes below

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibility 1: User has provided a custom cipherKey(or a keyset or one of the keyset from KeysetStore via using param) while calling history.
In this case _keyset(current one from param).cipherKey == _core.keysets.defaultKeyset.cipherKey => false
That means we need to use custom cipherKey - and so we perform encryptWithKey

Possibility 2: User hasn't provided key which means _keyset.cipherKey is already defaulted to defaultKeyset's cipherkey
In this case _keyset.cipherKey == _core.keysets.defaultKeyset.cipherKey => true
That means we can use our decrypt() methods of cryptoModule because that cryptoModule is already initialised with defaultKeyset's cipherKey.

@mohitpubnub
Copy link
Contributor Author

@pubnub-release-bot release dart as v4.3.0

Copy link
Contributor

@parfeon parfeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@mohitpubnub mohitpubnub merged commit ba5935d into master Oct 16, 2023
5 checks passed
@mohitpubnub mohitpubnub deleted the clen-1585 branch October 16, 2023 11:15
@pubnub-release-bot
Copy link
Contributor

🚀 Release successfully completed 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority: medium This PR should be reviewed after all high priority PRs. type: feature This PR contains new feature. type: fix This PR contains fixes to existing features. type: refactor This PR contains refactored existing features. type: test This PR contains new tests for existing functionality or fixes to existing tests.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants