Skip to content

Commit

Permalink
Merge pull request #1440 from atsign-foundation/1417-uptake-pubkeyhas…
Browse files Browse the repository at this point in the history
…h-changes

feat: Uptake public key hash changes
  • Loading branch information
sitaram-kalluri authored Dec 18, 2024
2 parents d754774 + e9ee174 commit fb92d52
Show file tree
Hide file tree
Showing 11 changed files with 249 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
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';
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
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}
}

Expand Down
11 changes: 11 additions & 0 deletions packages/at_client/lib/src/response/at_notification.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:convert';

import 'package:at_client/at_client.dart';

class AtNotification {
Expand Down Expand Up @@ -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'],
Expand Down
20 changes: 20 additions & 0 deletions packages/at_client/lib/src/service/sync_service_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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}';
}
Expand Down Expand Up @@ -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];
}
Expand All @@ -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]);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String> _encryptNotificationValue(AtKey atKey, String value) async {
Expand Down
2 changes: 2 additions & 0 deletions packages/at_client/lib/src/util/at_client_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ class AtClientUtil {
metadataMap[AtConstants.sharedKeyEncryptedEncryptingAlgo];
metadata.isPublic = isPublic;
metadata.isCached = isCached;
metadata.pubKeyHash = PublicKeyHash.fromJson(
metadataMap[AtConstants.sharedWithPublicKeyHash]);

return metadata;
}
Expand Down
12 changes: 6 additions & 6 deletions packages/at_client/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
170 changes: 170 additions & 0 deletions packages/at_client/test/encryption_decryption_test.dart
Original file line number Diff line number Diff line change
@@ -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 = '[email protected]';

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 = '[email protected]';

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');
}
}
2 changes: 1 addition & 1 deletion tests/at_end2end_test/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions tests/at_functional_test/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit fb92d52

Please sign in to comment.