implemented an E2E-encrypted Nextcloud push-v2 notification system with support for RSA decryption and signature verification; introduced an iOS Notification Service Extension and native AppDelegate handlers for Talk actions (inline reply and mark-as-read); replaced the legacy notification registration with a new lifecycle managing app passwords and secure keypair storage; added background message handling with tray synchronization and a test notification utility in the settings.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypton/crypton.dart';
|
||||
import 'package:pointycastle/export.dart' as pc;
|
||||
|
||||
import 'push_subject.dart';
|
||||
|
||||
/// Verifies and decrypts the encrypted `subject` of a Nextcloud push-v2
|
||||
/// notification. Pure crypto, no plugin/platform access, so it runs safely
|
||||
/// inside the FCM background isolate.
|
||||
///
|
||||
/// - Signature: `SHA512withRSA` over the *encrypted* subject bytes, verified
|
||||
/// with the per-user server public key returned at registration.
|
||||
/// - Encryption: the subject is encrypted with the device public key, so the
|
||||
/// device decrypts with its private key. Nextcloud 32 defaults to
|
||||
/// OAEP (SHA-1/MGF1-SHA-1); older instances use PKCS#1 v1.5. We try OAEP
|
||||
/// first and fall back to PKCS#1.
|
||||
class PushDecryptor {
|
||||
final RSAPrivateKey devicePrivateKey;
|
||||
|
||||
/// Per-user server public key (the `publicKey` from the NC registration
|
||||
/// response). When null, signature verification is skipped.
|
||||
final RSAPublicKey? serverPublicKey;
|
||||
|
||||
const PushDecryptor({required this.devicePrivateKey, this.serverPublicKey});
|
||||
|
||||
/// Returns true when [signatureBase64] is a valid server signature over the
|
||||
/// encrypted subject. Returns true when no server key is configured (the
|
||||
/// proxy already verified the signature before forwarding).
|
||||
bool verify(String subjectBase64, String signatureBase64) {
|
||||
final key = serverPublicKey;
|
||||
if (key == null) return true;
|
||||
try {
|
||||
final signed = Uint8List.fromList(base64.decode(subjectBase64));
|
||||
final signature = Uint8List.fromList(base64.decode(signatureBase64));
|
||||
return key.verifySHA512Signature(signed, signature);
|
||||
} on Object {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypts the base64 subject into a [PushSubject], or returns null when
|
||||
/// neither padding scheme yields valid JSON.
|
||||
PushSubject? decrypt(String subjectBase64) {
|
||||
final encrypted = Uint8List.fromList(base64.decode(subjectBase64));
|
||||
final plain = _decryptOaep(encrypted) ?? _decryptPkcs1(encrypted);
|
||||
if (plain == null) return null;
|
||||
try {
|
||||
final json = jsonDecode(plain) as Map<String, dynamic>;
|
||||
return PushSubject.fromJson(json);
|
||||
} on Object {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String? _decryptOaep(Uint8List data) =>
|
||||
_tryDecrypt(pc.OAEPEncoding(pc.RSAEngine()), data);
|
||||
|
||||
String? _decryptPkcs1(Uint8List data) =>
|
||||
_tryDecrypt(pc.PKCS1Encoding(pc.RSAEngine()), data);
|
||||
|
||||
String? _tryDecrypt(pc.AsymmetricBlockCipher cipher, Uint8List data) {
|
||||
try {
|
||||
cipher.init(
|
||||
false,
|
||||
pc.PrivateKeyParameter<pc.RSAPrivateKey>(
|
||||
devicePrivateKey.asPointyCastle,
|
||||
),
|
||||
);
|
||||
final out = cipher.process(data);
|
||||
return utf8.decode(out);
|
||||
} on Object {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user