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:
@@ -6,9 +6,14 @@ import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../push/push_secure_storage.dart';
|
||||
|
||||
class AccountData {
|
||||
static const _usernameField = 'username';
|
||||
static const _passwordField = 'password';
|
||||
// App password lives in the push-shared (group-scoped) keystore so the iOS
|
||||
// Notification Service Extension can authenticate Nextcloud calls too.
|
||||
static const _appPasswordField = 'nextcloud_app_password';
|
||||
|
||||
static const FlutterSecureStorage _secureStorage = FlutterSecureStorage();
|
||||
|
||||
@@ -23,6 +28,7 @@ class AccountData {
|
||||
|
||||
String? _username;
|
||||
String? _password;
|
||||
String? _appPassword;
|
||||
|
||||
String getUsername() {
|
||||
if (_username == null) throw Exception('Username not initialized');
|
||||
@@ -58,14 +64,49 @@ class AccountData {
|
||||
_populated = Completer();
|
||||
_username = null;
|
||||
_password = null;
|
||||
_appPassword = null;
|
||||
await _secureStorage.delete(key: _usernameField);
|
||||
await _secureStorage.delete(key: _passwordField);
|
||||
await _clearAppPasswordStorage();
|
||||
}
|
||||
|
||||
/// Persists a freshly minted Nextcloud app password. After this every
|
||||
/// [getBasicAuthHeader] call authenticates with the app password instead of
|
||||
/// the real password.
|
||||
Future<void> setAppPassword(String appPassword) async {
|
||||
_appPassword = appPassword;
|
||||
try {
|
||||
await pushSecureStorage.write(key: _appPasswordField, value: appPassword);
|
||||
} on Object {
|
||||
// Group-scoped keystore may be unavailable (e.g. iOS entitlement not yet
|
||||
// provisioned). Keeping it in memory still lets this session use it.
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearAppPassword() async {
|
||||
_appPassword = null;
|
||||
await _clearAppPasswordStorage();
|
||||
}
|
||||
|
||||
bool hasAppPassword() => _appPassword != null && _appPassword!.isNotEmpty;
|
||||
|
||||
Future<void> _clearAppPasswordStorage() async {
|
||||
try {
|
||||
await pushSecureStorage.delete(key: _appPasswordField);
|
||||
} on Object {
|
||||
// ignore — nothing stored or keystore unavailable
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _migrateAndLoad() async {
|
||||
await _migrateFromLegacyStorage();
|
||||
_username = await _secureStorage.read(key: _usernameField);
|
||||
_password = await _secureStorage.read(key: _passwordField);
|
||||
try {
|
||||
_appPassword = await pushSecureStorage.read(key: _appPasswordField);
|
||||
} on Object {
|
||||
_appPassword = null;
|
||||
}
|
||||
if (!_populated.isCompleted) _populated.complete();
|
||||
}
|
||||
|
||||
@@ -97,6 +138,21 @@ class AccountData {
|
||||
/// Prefer this over embedding credentials in URLs — error logs and crash
|
||||
/// reports often capture the URL but not headers.
|
||||
String getBasicAuthHeader() {
|
||||
if (!isPopulated()) {
|
||||
throw Exception(
|
||||
'AccountData (e.g. username or password) is not initialized!',
|
||||
);
|
||||
}
|
||||
// Prefer the scoped app password once available; it survives real-password
|
||||
// rotation and is what the push-v2 registration is bound to.
|
||||
final secret = _appPassword ?? _password;
|
||||
return 'Basic ${base64Encode(utf8.encode('$_username:$secret'))}';
|
||||
}
|
||||
|
||||
/// Basic-auth header that always uses the real password. Needed exactly once,
|
||||
/// to mint the app password via `core/getapppassword` (an app password cannot
|
||||
/// mint another).
|
||||
String getRealPasswordBasicAuthHeader() {
|
||||
if (!isPopulated()) {
|
||||
throw Exception(
|
||||
'AccountData (e.g. username or password) is not initialized!',
|
||||
|
||||
Reference in New Issue
Block a user