Skip to content

Commit

Permalink
feat(notifications_push_repository): Verify push notification signature
Browse files Browse the repository at this point in the history
Signed-off-by: provokateurin <[email protected]>
  • Loading branch information
provokateurin committed Nov 17, 2024
1 parent 64bf100 commit a10e9de
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 28 deletions.
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!');
}

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

0 comments on commit a10e9de

Please sign in to comment.