Files
Client/lib/push/push_decryptor.dart
T

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