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; 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; } } }