diff --git a/packages/at_client/lib/src/decryption_service/shared_key_decryption.dart b/packages/at_client/lib/src/decryption_service/shared_key_decryption.dart index 0e372ea94..db222dc3c 100644 --- a/packages/at_client/lib/src/decryption_service/shared_key_decryption.dart +++ b/packages/at_client/lib/src/decryption_service/shared_key_decryption.dart @@ -1,3 +1,4 @@ +import 'package:at_chops/at_chops.dart'; import 'package:at_client/src/client/at_client_spec.dart'; import 'package:at_client/src/decryption_service/decryption.dart'; import 'package:at_client/src/response/default_response_parser.dart'; @@ -5,7 +6,6 @@ import 'package:at_client/src/util/encryption_util.dart'; import 'package:at_commons/at_builders.dart'; import 'package:at_commons/at_commons.dart'; import 'package:at_utils/at_logger.dart'; -import 'package:at_chops/at_chops.dart'; /// Class responsible for decrypting the value of shared key's that are not owned /// by currentAtSign @@ -50,14 +50,26 @@ class SharedKeyDecryption implements AtKeyDecryption { intent: Intent.fetchEncryptionPublicKey, exceptionScenario: ExceptionScenario.localVerbExecutionFailed); } - if (currentAtSignPublicKey != null && - (atKey.metadata.pubKeyCS != null && - atKey.metadata.pubKeyCS != - EncryptionUtil.md5CheckSum(currentAtSignPublicKey))) { + if (currentAtSignPublicKey.isNullOrEmpty) { + throw AtPublicKeyNotFoundException('Public key cannot be null or empty'); + } + + final isPubKeyHashMismatch = atKey.metadata.pubKeyHash != null && + atKey.metadata.pubKeyHash?.hash != + AtChops.hashWith(HashingAlgoType.fromString( + atKey.metadata.pubKeyHash!.hashingAlgo)) + .hash(currentAtSignPublicKey!.codeUnits); + + final isPubKeyCSMismatch = atKey.metadata.pubKeyCS != null && + atKey.metadata.pubKeyCS != + EncryptionUtil.md5CheckSum(currentAtSignPublicKey!); + + if (isPubKeyHashMismatch || isPubKeyCSMismatch) { throw AtPublicKeyChangeException( - 'Public key has changed. Cannot decrypt shared key ${atKey.toString()}', - intent: Intent.fetchEncryptionPublicKey, - exceptionScenario: ExceptionScenario.decryptionFailed); + 'Public key has changed. Cannot decrypt shared key ${atKey.toString()}', + intent: Intent.fetchEncryptionPublicKey, + exceptionScenario: ExceptionScenario.decryptionFailed, + ); } AtEncryptionResult decryptionResultFromAtChops; diff --git a/packages/at_client/lib/src/encryption_service/abstract_atkey_encryption.dart b/packages/at_client/lib/src/encryption_service/abstract_atkey_encryption.dart index 6158b2827..bb7b52eb1 100644 --- a/packages/at_client/lib/src/encryption_service/abstract_atkey_encryption.dart +++ b/packages/at_client/lib/src/encryption_service/abstract_atkey_encryption.dart @@ -1,3 +1,4 @@ +import 'package:at_chops/at_chops.dart'; import 'package:at_client/at_client.dart'; import 'package:at_client/src/client/secondary.dart'; import 'package:at_client/src/encryption_service/encryption.dart'; @@ -9,7 +10,6 @@ import 'package:at_commons/at_builders.dart'; import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart'; import 'package:at_utils/at_logger.dart'; import 'package:meta/meta.dart'; -import 'package:at_chops/at_chops.dart'; /// Contains the common code for [SharedKeyEncryption] and [StreamEncryption] abstract class AbstractAtKeyEncryption implements AtKeyEncryption { @@ -49,8 +49,15 @@ abstract class AbstractAtKeyEncryption implements AtKeyEncryption { if (storeSharedKeyEncryptedWithData) { atKey.metadata.sharedKeyEnc = theirEncryptedSymmetricKeyCopy; + // This is a legacy checksum with MD5 algo. atKey.metadata.pubKeyCS = EncryptionUtil.md5CheckSum(await _getSharedWithPublicKey(atKey)); + // Hashed the encryption public key with sha512. This is to ensure the encryption + // public key of the receiver are same during encryption and decryption process. + String hash = await AtChops.hashWith(HashingAlgoType.sha512) + .hash((await _getSharedWithPublicKey(atKey)).codeUnits); + atKey.metadata.pubKeyHash = + PublicKeyHash(hash, HashingAlgoType.sha512.name); } } diff --git a/packages/at_client/lib/src/response/at_notification.dart b/packages/at_client/lib/src/response/at_notification.dart index 05f7bab0b..0e1948088 100644 --- a/packages/at_client/lib/src/response/at_notification.dart +++ b/packages/at_client/lib/src/response/at_notification.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:at_client/at_client.dart'; class AtNotification { @@ -34,6 +36,15 @@ class AtNotification { metadata.skeEncAlgo = json['metadata'][AtConstants.sharedKeyEncryptedEncryptingAlgo]; metadata.sharedKeyEnc = json['metadata'][AtConstants.sharedKeyEncrypted]; + // AtContants.sharedWithPublicKeyHash will be sent by the server starting v3.0.52 + // Notifications received from Secondary server before 3.0.52 does not contain + // AtConstants.sharedWithPublicKeyHash. Therefore, check for null. + if (json['metadata'][AtConstants.sharedWithPublicKeyHash] != null) { + var publicKeyHash = + jsonDecode(json['metadata'][AtConstants.sharedWithPublicKeyHash]); + metadata.pubKeyHash = + PublicKeyHash(publicKeyHash['hash'], publicKeyHash['hashingAlgo']); + } } return AtNotification(json['id'], json['key'], json['from'], json['to'], diff --git a/packages/at_client/lib/src/service/sync_service_impl.dart b/packages/at_client/lib/src/service/sync_service_impl.dart index 3ed450851..e1ce67810 100644 --- a/packages/at_client/lib/src/service/sync_service_impl.dart +++ b/packages/at_client/lib/src/service/sync_service_impl.dart @@ -741,6 +741,13 @@ class SyncServiceImpl implements SyncService, AtSignChangeListener { if (metadata.pubKeyCS != null) { metadataStr += ':pubKeyCS:${metadata.pubKeyCS}'; } + if (metadata.pubKeyHash != null) { + metadataStr += + ':${AtConstants.sharedWithPublicKeyHash}:${metadata.pubKeyHash?.hash}'; + metadataStr += + ':${AtConstants.sharedWithPublicKeyHashingAlgo}:${metadata.pubKeyHash?.hashingAlgo}'; + } + if (metadata.encoding != null) { metadataStr += ':encoding:${metadata.encoding}'; } @@ -963,6 +970,12 @@ class SyncServiceImpl implements SyncService, AtSignChangeListener { builder.atKey.metadata.pubKeyCS = metaData[AtConstants.sharedWithPublicKeyCheckSum]; } + if (metaData[AtConstants.sharedWithPublicKeyHash] != null) { + Map pubKeyHash = + jsonDecode(metaData[AtConstants.sharedWithPublicKeyHash]); + builder.atKey.metadata.pubKeyHash = + PublicKeyHash(pubKeyHash['hash'], pubKeyHash['hashingAlgo']); + } if (metaData[AtConstants.encoding] != null) { builder.atKey.metadata.encoding = metaData[AtConstants.encoding]; } @@ -984,6 +997,13 @@ class SyncServiceImpl implements SyncService, AtSignChangeListener { builder.atKey.metadata.skeEncAlgo = metaData[AtConstants.sharedKeyEncryptedEncryptingAlgo]; } + + if (metaData[AtConstants.sharedWithPublicKeyHash] != null && + metaData[AtConstants.sharedWithPublicKeyHashingAlgo] != null) { + builder.atKey.metadata.pubKeyHash = PublicKeyHash( + metaData[AtConstants.sharedWithPublicKeyHash], + metaData[AtConstants.sharedWithPublicKeyHashingAlgo]); + } } } diff --git a/packages/at_client/lib/src/transformer/request_transformer/notify_request_transformer.dart b/packages/at_client/lib/src/transformer/request_transformer/notify_request_transformer.dart index 5fd9df181..6ad1a359d 100644 --- a/packages/at_client/lib/src/transformer/request_transformer/notify_request_transformer.dart +++ b/packages/at_client/lib/src/transformer/request_transformer/notify_request_transformer.dart @@ -2,13 +2,13 @@ import 'dart:async'; +import 'package:at_client/src/encryption_service/encryption.dart'; import 'package:at_client/src/preference/at_client_preference.dart'; import 'package:at_client/src/service/notification_service.dart'; import 'package:at_client/src/transformer/at_transformer.dart'; import 'package:at_client/src/util/at_client_util.dart'; import 'package:at_commons/at_builders.dart'; import 'package:at_commons/at_commons.dart'; -import 'package:at_client/src/encryption_service/encryption.dart'; /// Class is responsible for taking the [NotificationParams] and converting into [NotifyVerbBuilder] class NotificationRequestTransformer @@ -96,6 +96,8 @@ class NotificationRequestTransformer notificationParams.atKey.metadata.skeEncKeyName; builder.atKey.metadata.skeEncAlgo = notificationParams.atKey.metadata.skeEncAlgo; + builder.atKey.metadata.pubKeyHash = + notificationParams.atKey.metadata.pubKeyHash; } Future _encryptNotificationValue(AtKey atKey, String value) async { diff --git a/packages/at_client/lib/src/util/at_client_util.dart b/packages/at_client/lib/src/util/at_client_util.dart index a571c5329..20f7c658d 100644 --- a/packages/at_client/lib/src/util/at_client_util.dart +++ b/packages/at_client/lib/src/util/at_client_util.dart @@ -105,6 +105,8 @@ class AtClientUtil { metadataMap[AtConstants.sharedKeyEncryptedEncryptingAlgo]; metadata.isPublic = isPublic; metadata.isCached = isCached; + metadata.pubKeyHash = PublicKeyHash.fromJson( + metadataMap[AtConstants.sharedWithPublicKeyHash]); return metadata; } diff --git a/packages/at_client/pubspec.yaml b/packages/at_client/pubspec.yaml index 620b39adc..99ceb8ec2 100644 --- a/packages/at_client/pubspec.yaml +++ b/packages/at_client/pubspec.yaml @@ -31,19 +31,19 @@ dependencies: async: ^2.9.0 at_utf7: ^1.0.0 at_base2e15: ^1.0.0 - at_commons: ^5.0.0 + at_commons: ^5.0.2 at_utils: ^3.0.19 - at_chops: ^2.0.1 + at_chops: ^2.2.0 at_lookup: ^3.0.49 - at_auth: ^2.0.7 + at_auth: ^2.0.10 at_persistence_spec: ^2.0.14 - at_persistence_secondary_server: ^3.0.64 + at_persistence_secondary_server: ^3.1.0 meta: ^1.8.0 version: ^3.0.2 dev_dependencies: - lints: ^4.0.0 - test: ^1.21.4 + lints: ^5.0.0 + test: ^1.25.8 at_demo_data: ^1.0.1 coverage: ^1.5.0 mocktail: ^1.0.3 diff --git a/packages/at_client/test/encryption_decryption_test.dart b/packages/at_client/test/encryption_decryption_test.dart new file mode 100644 index 000000000..58f6a4494 --- /dev/null +++ b/packages/at_client/test/encryption_decryption_test.dart @@ -0,0 +1,170 @@ +import 'dart:io'; + +import 'package:at_chops/at_chops.dart'; +import 'package:at_client/at_client.dart'; +import 'package:at_client/src/decryption_service/shared_key_decryption.dart'; +import 'package:at_client/src/encryption_service/shared_key_encryption.dart'; +import 'package:at_commons/at_builders.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockRemoteSecondary extends Mock implements RemoteSecondary {} + +void main() { + String currentAtSign = '@alice'; + String namespace = 'unit.test'; + + MockRemoteSecondary mockRemoteSecondary = MockRemoteSecondary(); + + late AtChops atChops; + late AtClient atClient; + + setUp(() async { + AtClientPreference atClientPreference = AtClientPreference() + ..isLocalStoreRequired = true + ..hiveStoragePath = 'test/unit_test_storage/hive' + ..commitLogPath = 'test/unit_test_storage/commit'; + + AtEncryptionKeyPair atEncryptionKeyPair = + AtChopsUtil.generateAtEncryptionKeyPair(); + AtPkamKeyPair atPkamKeyPair = AtChopsUtil.generateAtPkamKeyPair(); + AtChopsKeys atChopsKeys = + AtChopsKeys.create(atEncryptionKeyPair, atPkamKeyPair); + atChopsKeys.selfEncryptionKey = + AtChopsUtil.generateSymmetricKey(EncryptionKeyType.aes256); + atChops = AtChopsImpl(atChopsKeys); + + atClient = await AtClientImpl.create( + currentAtSign, namespace, atClientPreference, + remoteSecondary: mockRemoteSecondary, atChops: atChops); + + // During decryption, fetches the encryption public key from local keystore. + // So, store the encryption public key into local secondary keystore. + await atClient.getLocalSecondary()?.putValue( + 'public:publickey$currentAtSign', + atEncryptionKeyPair.atPublicKey.publicKey); + }); + + tearDownAll(() { + Directory('test/unit_test_storage').deleteSync(recursive: true); + }); + + group('A group of tests related to encryption and decryption of shared keys', + () { + test( + 'A test to verify encryption and decryption of shared key is successful - with publicKeyHash', + () async { + registerFallbackValue(LLookupVerbBuilder()); + AtKey sharedKey = + (AtKey.shared('email', namespace: namespace, sharedBy: currentAtSign) + ..sharedWith('@bob')) + .build(); + String value = 'alice@atsign.com'; + + when(() => mockRemoteSecondary + .executeVerb(any(that: EncryptedSharedKeyMatcher()))) + .thenAnswer((_) => Future.value('data:null')); + + // Returns encryption public key of sharedWith atSign. + // For unit test, reusing the current AtSign encryptionPublicKey. + when(() => mockRemoteSecondary + .executeVerb(any(that: EncryptionPublicKeyMatcher()))) + .thenAnswer((_) => Future.value( + atChops.atChopsKeys.atEncryptionKeyPair?.atPublicKey.publicKey)); + + // Encryption + SharedKeyEncryption sharedKeyEncryption = SharedKeyEncryption(atClient); + String encryptedValue = + await sharedKeyEncryption.encrypt(sharedKey, value); + expect(sharedKey.metadata.pubKeyHash?.hash.isNotEmpty, true); + expect(sharedKey.metadata.pubKeyHash?.hashingAlgo, 'sha512'); + expect(sharedKey.metadata.sharedKeyEnc?.isNotEmpty, true); + expect(sharedKey.metadata.pubKeyCS?.isNotEmpty, true); + + // Explicitly setting pubKeyCS to null, so that pubKeyHash will only be used + // during decryption. + sharedKey.metadata.pubKeyCS = null; + expect(sharedKey.metadata.pubKeyCS, null); + + // Decryption + SharedKeyDecryption sharedKeyDecryption = SharedKeyDecryption(atClient); + String decryptedValue = + await sharedKeyDecryption.decrypt(sharedKey, encryptedValue); + expect(decryptedValue, value); + }); + + test('A test to verify exception is thrown when publicKeyHash mismatch', + () async { + registerFallbackValue(LLookupVerbBuilder()); + AtKey sharedKey = + (AtKey.shared('email', namespace: namespace, sharedBy: currentAtSign) + ..sharedWith('@bob')) + .build(); + String value = 'alice@atsign.com'; + + when(() => mockRemoteSecondary + .executeVerb(any(that: EncryptedSharedKeyMatcher()))) + .thenAnswer((_) => Future.value('data:null')); + + // Returns encryption public key of sharedWith atSign. + // For unit test, reusing the current AtSign encryptionPublicKey. + when(() => mockRemoteSecondary + .executeVerb(any(that: EncryptionPublicKeyMatcher()))) + .thenAnswer((_) => Future.value( + atChops.atChopsKeys.atEncryptionKeyPair?.atPublicKey.publicKey)); + + // Encryption + SharedKeyEncryption sharedKeyEncryption = SharedKeyEncryption(atClient); + String encryptedValue = + await sharedKeyEncryption.encrypt(sharedKey, value); + expect(sharedKey.metadata.pubKeyHash?.hash.isNotEmpty, true); + expect(sharedKey.metadata.pubKeyHash?.hashingAlgo, 'sha512'); + expect(sharedKey.metadata.sharedKeyEnc?.isNotEmpty, true); + expect(sharedKey.metadata.pubKeyCS?.isNotEmpty, true); + + // Explicitly setting pubKeyCS to null, so that pubKeyHash will only be used + // during decryption. + sharedKey.metadata.pubKeyCS = null; + expect(sharedKey.metadata.pubKeyCS, null); + // Explicity changing the publicKeyHash value to mimic change in publicKeyHash + // value. + sharedKey.metadata.pubKeyHash = + PublicKeyHash('dummy_hash_value', HashingAlgoType.sha512.name); + + // Decryption + SharedKeyDecryption sharedKeyDecryption = SharedKeyDecryption(atClient); + expect( + () async => + await sharedKeyDecryption.decrypt(sharedKey, encryptedValue), + throwsA(predicate((dynamic e) => + e is AtPublicKeyChangeException && + e.message == + 'Public key has changed. Cannot decrypt shared key ${sharedKey.toString()}'))); + }); + }); +} + +class EncryptedSharedKeyMatcher extends Matcher { + @override + Description describe(Description description) { + return description; + } + + @override + bool matches(item, Map matchState) { + return item.atKey.key.startsWith(AtConstants.atEncryptionSharedKey); + } +} + +class EncryptionPublicKeyMatcher extends Matcher { + @override + Description describe(Description description) { + // TODO: implement describe + throw UnimplementedError(); + } + + @override + bool matches(item, Map matchState) { + return item.atKey.key.startsWith('publickey'); + } +} diff --git a/tests/at_end2end_test/pubspec.yaml b/tests/at_end2end_test/pubspec.yaml index fd1a4f012..33fb13150 100644 --- a/tests/at_end2end_test/pubspec.yaml +++ b/tests/at_end2end_test/pubspec.yaml @@ -15,6 +15,6 @@ dependencies: path: ../../packages/at_client dev_dependencies: - test: ^1.24.3 + test: ^1.25.8 lints: ^2.0.0 coverage: ^1.5.0 diff --git a/tests/at_functional_test/pubspec.yaml b/tests/at_functional_test/pubspec.yaml index bd4a441a4..d7cfd2647 100644 --- a/tests/at_functional_test/pubspec.yaml +++ b/tests/at_functional_test/pubspec.yaml @@ -15,6 +15,6 @@ dependencies: at_lookup: ^3.0.49 dev_dependencies: - test: ^1.24.3 - lints: ^2.0.0 + test: ^1.25.8 + lints: ^5.0.0 coverage: ^1.5.0 \ No newline at end of file diff --git a/tests/at_functional_test/test/atclient_sharedkey_test.dart b/tests/at_functional_test/test/atclient_sharedkey_test.dart index dbcbf3f3f..e8411d96a 100644 --- a/tests/at_functional_test/test/atclient_sharedkey_test.dart +++ b/tests/at_functional_test/test/atclient_sharedkey_test.dart @@ -3,6 +3,7 @@ import 'package:at_client/src/encryption_service/encryption_manager.dart'; import 'package:at_functional_test/src/config_util.dart'; import 'package:at_functional_test/src/sync_service.dart'; import 'package:test/test.dart'; + import 'test_utils.dart'; void main() { @@ -29,6 +30,8 @@ void main() { var metadata = await atClient.getMeta(phoneKey); expect(metadata!.sharedKeyEnc, isNotEmpty); expect(metadata.pubKeyCS, isNotEmpty); + expect(metadata.pubKeyHash?.hash, isNotEmpty); + expect(metadata.pubKeyHash?.hashingAlgo, isNotEmpty); }); test('sharedKey and checksum metadata sync to local storage', () async { @@ -42,7 +45,7 @@ void main() { AtKeyEncryptionManager(atClient).get(phoneKey, currentAtSign); var encryptedValue = await encryptionService.encrypt(phoneKey, value); var result = await atClient.getRemoteSecondary()!.executeCommand( - 'update:sharedKeyEnc:${phoneKey.metadata.sharedKeyEnc}:pubKeyCS:${phoneKey.metadata.pubKeyCS}:${phoneKey.sharedWith}:${phoneKey.key}.$namespace$currentAtSign $encryptedValue\n', + 'update:sharedKeyEnc:${phoneKey.metadata.sharedKeyEnc}:pubKeyCS:${phoneKey.metadata.pubKeyCS}:pubKeyHash:${phoneKey.metadata.pubKeyHash?.hash}:hashingAlgo:${phoneKey.metadata.pubKeyHash?.hashingAlgo}:${phoneKey.sharedWith}:${phoneKey.key}.$namespace$currentAtSign $encryptedValue\n', auth: true); expect(result != null, true); await FunctionalTestSyncService.getInstance() @@ -50,5 +53,7 @@ void main() { var metadata = await atClient.getMeta(phoneKey); expect(metadata?.sharedKeyEnc, isNotEmpty); expect(metadata?.pubKeyCS, isNotEmpty); + expect(metadata?.pubKeyHash?.hash, isNotEmpty); + expect(metadata?.pubKeyHash?.hashingAlgo, isNotEmpty); }); }