78 lines
2.7 KiB
Dart
78 lines
2.7 KiB
Dart
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;
|
|
}
|
|
}
|
|
}
|