import 'dart:convert'; import 'package:crypton/crypton.dart'; import 'package:flutter/foundation.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}); /// [verify] + [decrypt] on a background isolate: pure-Dart RSA-2048 (with /// the OAEP → PKCS#1 fallback) takes tens of ms up to >100 ms on older /// phones, which would otherwise land on the UI thread for every push. Future<({bool verified, PushSubject? subject})> verifyAndDecryptInBackground( String subjectBase64, String signatureBase64, ) => compute(_verifyAndDecrypt, (this, subjectBase64, signatureBase64)); /// 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; 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( devicePrivateKey.asPointyCastle, ), ); final out = cipher.process(data); return utf8.decode(out); } on Object { return null; } } } ({bool verified, PushSubject? subject}) _verifyAndDecrypt( (PushDecryptor, String, String) args, ) { final (decryptor, subject, signature) = args; if (!decryptor.verify(subject, signature)) { return (verified: false, subject: null); } return (verified: true, subject: decryptor.decrypt(subject)); }