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

feat(notifications_push_repository): Verify push notification signature #2659

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:convert';

import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:built_value/standard_json_plugin.dart';
Expand All @@ -19,16 +21,28 @@ abstract class PushNotification implements Built<PushNotification, PushNotificat
factory PushNotification.fromEncrypted(
Map<String, dynamic> json,
String accountID,
RSAPrivateKey privateKey,
RSAPrivateKey devicePrivateKey,
RSAPublicKey userPublicKey,
) {
final subject = notifications.DecryptedSubject.fromEncrypted(privateKey, json['subject'] as String);
final subject = json['subject'] as String;
final signature = json['signature'] as String;

final valid = userPublicKey.verifySHA512Signature(
base64.decode(subject),
base64.decode(signature),
);
if (!valid) {
throw Exception('Failed to verify push notification signature!');
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should add a custom PushNotificationException instead of throwing a generic one

}

final decryptedSubject = notifications.DecryptedSubject.fromEncrypted(devicePrivateKey, subject);

return PushNotification(
(b) => b
..accountID = accountID
..priority = json['priority'] as String
..type = json['type'] as String
..subject.replace(subject),
..subject.replace(decryptedSubject),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,20 @@ Future<BuiltList<PushNotification>> parseEncryptedPushNotifications(
Uint8List notifications,
String accountID,
) async {
final privateKey = await getDevicePrivateKey(storage);
final subscriptions = await storage.readSubscriptions();

final subscription = subscriptions[accountID];
if (subscription == null) {
throw Exception('Subscription for account $accountID not found.');
}

final pushDevice = subscription.pushDevice;
if (pushDevice == null) {
throw Exception('Push device for account $accountID not found.');
}

final userPublicKey = RSAPublicKey.fromPEM(pushDevice.publicKey);
final devicePrivateKey = await getDevicePrivateKey(storage);

final builder = ListBuilder<PushNotification>();

Expand All @@ -37,7 +50,8 @@ Future<BuiltList<PushNotification>> parseEncryptedPushNotifications(
PushNotification.fromEncrypted(
data,
accountID,
privateKey,
devicePrivateKey,
userPublicKey,
),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ PushNotification {
);
});

test('fromEncrypted', () {
const privateKeyPEM = '''
group('fromEncrypted', () {
const devicePrivateKeyPEM = '''
-----BEGIN RSA PRIVATE KEY-----
MIICHwIBADANBgkqhkiG9w0BAQEFAASCAgkwggIFAgEAAm4BELTz808T8iAkvBkg
tnWs4a1aNcCFAAX54ePLK40YAL/tQjUGoIe0+zO7yzMT0bydk6BFOdyrIP2iwALN
Expand All @@ -140,21 +140,65 @@ VQI3BH+FwRTRntc3caGF4qVixb+Wu6OLwHg77MjdvKEo8KqTiQjxgAjmUkXPaS8N
4FkEfiY9QA36EQI3AKxizo9goAHnTmY1OVi+4GLp0HroWP64RjW8R/cUemggMqEa
UJYvEQEss8/UoYhOACOm5PEqNg==
-----END RSA PRIVATE KEY-----''';
final privateKey = RSAPrivateKey.fromPEM(privateKeyPEM);
final devicePrivateKey = RSAPrivateKey.fromPEM(devicePrivateKeyPEM);

const userPrivateKeyPEM = '''
-----BEGIN RSA PRIVATE KEY-----
MIIB1wIBADANBgkqhkiG9w0BAQEFAASCAcEwggG9AgEAAl4BimSPD4wH/LwlJk3H
dj6FCPqwZDBgLQQGVZsC6iZuCRMOH9paXuCOSBMw6l9IDrHy23jEasfu6tOnA/vy
QP01oPa0Kkp7qUFdN/eVHOdfBp0KKEPhr6bGGr1Lh+BJAgMBAAECXgFFGmWPVEgV
PuaEr6LXRuwVHckfnXz6PpIWKQR7DZiw5ENFYJIUGZlPsnolCMv2JzvKc66MYlnK
G+I+Lpm9mc2bPbj3aq8vh25mjiyn2mgB9AdlGoDNcW4QN8pisQ0CLxnR3uq23oPB
1hpR/eU+vU5iQ1/ZwKZUf24CihDFGpS9je1X43TYCRDMH3sMFttXAi8PRlpU/AdO
xpqZYcoilhhu/numcU1qEBgiDiTuTHizVGw/OwmDeq3SHjJLhMV9XwIvAVwonbxc
JByFpoVDFlwjpIlQezABEcHJpIXFt/Rp3gPOAf5rILBwac4WqmiMm6kCLwSYBgbV
HWWFuW0zydUJCyQmiQ2PudaSLI/hbR32Bb75PuztVnkiZjBxQHMR5UtFAi8TXKJV
4gkQHdyTMlUgtwTItoS5AWmZU5FUJbDva9S3JerKrTkbeslJiHEhSYrbZw==
-----END RSA PRIVATE KEY-----
''';
final userPrivateKey = RSAPrivateKey.fromPEM(userPrivateKeyPEM);

const subject =
'AOXrekPv+79XU82vEXx5WiA9WREus8uYYkfijtKdl4ggWRvvykaY5hQP7OT5P7iKSCzjmO7yNQTuXDJXYtWo/1Pq0AYSVrA3y37pNYr8d/WZklfvQtxIB6o/HTG6pUd1kER7QxVkP7RSHvw/9PU=';

expect(
PushNotification.fromEncrypted(
json.decode(
'{"priority":"priority","type":"type","subject":"$subject"}',
) as Map<String, dynamic>,
'accountID',
privateKey,
),
equalsBuilt(createPushNotification()),
);
test('Valid signature', () {
const signature =
'AFhc8Bp2PFwbrsqr9Ygk9T4JRwaqnsojvJ0MnkMIKpX8TYe0/SWj1bVQhWamMQ1uQ3xeFIOFHP3AkoqJ+id7f9CpqETOSqTrUHDFDedCbb8Lwoa95q4lnchDvI6Hbw==';

expect(
PushNotification.fromEncrypted(
json.decode(
'{"priority":"priority","type":"type","subject":"$subject","signature":"$signature"}',
) as Map<String, dynamic>,
'accountID',
devicePrivateKey,
userPrivateKey.publicKey,
),
equalsBuilt(createPushNotification()),
);
});

test('Invalid signature', () {
const signature = 'abcd';

expect(
() => PushNotification.fromEncrypted(
json.decode(
'{"priority":"priority","type":"type","subject":"$subject","signature":"$signature"}',
) as Map<String, dynamic>,
'accountID',
devicePrivateKey,
userPrivateKey.publicKey,
),
throwsA(
isA<Exception>().having(
(e) => e.toString(),
'toString',
'Exception: Failed to verify push notification signature!',
),
),
);
});
});
});
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import 'dart:convert';

import 'package:built_collection/built_collection.dart';
import 'package:built_value_test/matcher.dart';
import 'package:crypton/crypton.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:notifications_push_repository/notifications_push_repository.dart';
import 'package:notifications_push_repository/src/models/models.dart';
import 'package:notifications_push_repository/src/utils/encryption.dart';
import 'package:notifications_push_repository/testing.dart';

class _StorageMock extends Mock implements NotificationsPushStorage {}

void main() {
const privateKeyPEM = '''
const devicePrivateKeyPEM = '''
-----BEGIN RSA PRIVATE KEY-----
MIICHwIBADANBgkqhkiG9w0BAQEFAASCAgkwggIFAgEAAm4BELTz808T8iAkvBkg
tnWs4a1aNcCFAAX54ePLK40YAL/tQjUGoIe0+zO7yzMT0bydk6BFOdyrIP2iwALN
Expand Down Expand Up @@ -41,34 +43,68 @@ UJYvEQEss8/UoYhOACOm5PEqNg==
when(() => storage.readDevicePrivateKey()).thenAnswer((_) => null);
when(() => storage.saveDevicePrivateKey(any())).thenAnswer((_) async {});

final privateKey = await getDevicePrivateKey(storage);
expect(privateKey.toFormattedPEM(), isNot(privateKeyPEM));
final devicePrivateKey = await getDevicePrivateKey(storage);
expect(devicePrivateKey.toFormattedPEM(), isNot(devicePrivateKeyPEM));

verify(() => storage.saveDevicePrivateKey(privateKey)).called(1);
verify(() => storage.saveDevicePrivateKey(devicePrivateKey)).called(1);
});

test('Existing', () async {
when(() => storage.readDevicePrivateKey()).thenAnswer((_) => RSAPrivateKey.fromPEM(privateKeyPEM));
when(() => storage.readDevicePrivateKey()).thenAnswer((_) => RSAPrivateKey.fromPEM(devicePrivateKeyPEM));

final privateKey = await getDevicePrivateKey(storage);
expect(privateKey.toFormattedPEM(), equals(privateKeyPEM));
final devicePrivateKey = await getDevicePrivateKey(storage);
expect(devicePrivateKey.toFormattedPEM(), equals(devicePrivateKeyPEM));

verifyNever(() => storage.saveDevicePrivateKey(any()));
});
});

test('parseEncryptedPushNotifications', () async {
when(() => storage.readDevicePrivateKey()).thenAnswer((_) => RSAPrivateKey.fromPEM(privateKeyPEM));
const userPrivateKeyPEM = '''
-----BEGIN RSA PRIVATE KEY-----
MIIB1wIBADANBgkqhkiG9w0BAQEFAASCAcEwggG9AgEAAl4BuCI4NDeaY4S8d2zj
U7znZjsA9WYlmn9HsYx8ITp1hZi83MWfQ3ckDhDlDe8eWi6C1OxXobFzESATcGkg
8IlnEar5PMqFB0DFa0CS4ruPDHJAb57G+WW2hIvEWKc/AgMBAAECXgGYrUWMzthv
cc/iAFxw46XlmgIA2xEtjOPQK8cSv4piO3macXG5nkX/PZbCQnbnUeB4NXBgRxw0
mawTnRW5lIANlDkIGbyXYJ86JHS8Q1PGRtVHb1xDwIkbDHtSS1ECLx8kWzsn2oYA
gwe3yMMpJtxCJTwV7giist9gixv+GFhPH+ZOWu9RsPPqY5VZK7oXAi8OIhO+VbZi
8lB7ZJ9pHUFoJRZZYUBQ8aARwXLP6htbWm/zFknBpUl3tntlG6Q9GQIvHf5WH9Ny
lD1J9dT8d3rbAqhyVDyK1aZdwOarFHrV17fdcWNmEbkMROAqqV0I0GECLwQQQjL4
e+8pEoDX5omXcsXZ2/oo3xAm2MoiH7ut6N20O/ndj6lQt7XmzsW8U9WJAi8Tutz8
e/rwA4zvN4TD8v1bsHu3g6/6k60RTPp5xy0AvYPjYV+ur2H2W/eIDMkXQA==
-----END RSA PRIVATE KEY-----
''';
final userPrivateKey = RSAPrivateKey.fromPEM(userPrivateKeyPEM);

when(() => storage.readDevicePrivateKey()).thenAnswer((_) => RSAPrivateKey.fromPEM(devicePrivateKeyPEM));
when(() => storage.readSubscriptions()).thenAnswer(
(_) async => BuiltMap<String, PushSubscription>({
'accountID': PushSubscription(
(b) => b.pushDevice.update(
(b) => b
..publicKey = userPrivateKey.publicKey.toPEM()
..deviceIdentifier = ''
..signature = '',
),
),
}),
);

const subject1 =
'AOXrekPv+79XU82vEXx5WiA9WREus8uYYkfijtKdl4ggWRvvykaY5hQP7OT5P7iKSCzjmO7yNQTuXDJXYtWo/1Pq0AYSVrA3y37pNYr8d/WZklfvQtxIB6o/HTG6pUd1kER7QxVkP7RSHvw/9PU=';
const signature1 =
'AH1A1S9v0uxwcukZXWMX2+3PwTx6ngtGinVX2DHeeQ8j5N6UjN2gl5c9XwhmqO934zSMgQi5lSDlh2NyZb3B44f5IEbehghokXoTd7Bc46auocR1ZIuxDfJGey03qw==';

const subject2 =
'AGcV+V73rhvcT2OMu5AAQNd01zd4BWCJqgZc782MOXlj62yKv4AxfbXLZpKjH2tFn8WiZRg6DJmX25v3652mzaJefC4d/urfbIGYN1a30NNSpPJIxjZ1XWUe2MV+aKuaj+liKYukVvzOpK+scCM=';
const signature2 =
'AI6nF0DSVgYAGe2UhdgsC9UhGWdvRlTQjBS/2Gj0rMxzYH3GAq5gy/oodJjfjP9iJnhBTltrl8j0Daowcp6DRP8WgKRXzSj3ECAeepHE4whLSGP5gR+nBPe3eCHN8w==';

final messages = utf8.encode(
Uri(
queryParameters: {
'message1': '{"priority":"priority","type":"type","subject":"$subject1"}',
'message2': '{"priority":"priority","type":"type","subject":"$subject2"}',
'message1': '{"priority":"priority","type":"type","subject":"$subject1","signature":"$signature1"}',
'message2': '{"priority":"priority","type":"type","subject":"$subject2","signature":"$signature2"}',
},
).query,
);
Expand Down
Loading