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:
2026-07-04 22:50:18 +02:00
parent 32f7c311bc
commit 74a2ddd17f
56 changed files with 2987 additions and 285 deletions
@@ -0,0 +1,20 @@
import 'package:http/http.dart' as http;
import '../nextcloud_ocs.dart';
/// Revokes the current app password server-side via
/// `DELETE /ocs/v2.php/core/apppassword`. Best-effort: the shared OCS headers
/// authenticate with the app password itself (it revokes the credential it was
/// made with) and the result is ignored — logout clears local state regardless.
class DeleteAppPassword {
final http.Client _client;
DeleteAppPassword({http.Client? client}) : _client = client ?? http.Client();
Future<void> run() async {
await _client.delete(
NextcloudOcs.uri('core/apppassword'),
headers: NextcloudOcs.headers(),
);
}
}
@@ -0,0 +1,45 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../../model/account_data.dart';
import '../nextcloud_ocs.dart';
/// Exchanges the user's real Nextcloud password for a scoped app password via
/// `GET /ocs/v2.php/core/getapppassword`. All subsequent Nextcloud calls then
/// authenticate with the app password (see [AccountData.getBasicAuthHeader]),
/// which is what the push-v2 registration binds to.
///
/// Must authenticate with the *real* password — an app password cannot mint
/// another one.
class GetAppPassword {
final http.Client _client;
GetAppPassword({http.Client? client}) : _client = client ?? http.Client();
/// Returns the freshly minted app password. Throws on any transport or
/// protocol error — callers treat push registration as best-effort and swallow
/// failures.
Future<String> run() async {
final response = await _client.get(
NextcloudOcs.uri('core/getapppassword'),
headers: {
...NextcloudOcs.headers(),
// Deliberately NOT the shared Authorization value: that one prefers
// the app password, but an app password cannot mint another one —
// this endpoint requires the real password.
'Authorization': AccountData().getRealPasswordBasicAuthHeader(),
},
);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw Exception('getapppassword HTTP ${response.statusCode}');
}
final json = jsonDecode(utf8.decode(response.bodyBytes));
final data = (json as Map)['ocs']?['data'];
final appPassword = data is Map ? data['apppassword'] as String? : null;
if (appPassword == null || appPassword.isEmpty) {
throw Exception('getapppassword: no apppassword in response');
}
return appPassword;
}
}