Initial commit
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2026-09-11 15:59:56 -03:00
commit 0037b0b9cf
651 changed files with 199705 additions and 0 deletions

View File

@@ -0,0 +1,302 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'package:file_picker/file_picker.dart';
import 'package:tatchat/utils/error_reporter.dart';
import 'package:tatchat/utils/localized_exception_extension.dart';
import 'package:tatchat/utils/platform_infos.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:go_router/go_router.dart';
import 'package:material_ui/material_ui.dart';
import 'package:matrix/encryption.dart';
import 'package:matrix/matrix.dart';
import 'bootstrap_state.dart';
class BootstrapViewModel extends ValueNotifier<BootstrapViewModelState> {
final Client client;
final bool reset;
final TextEditingController enterPassphraseOrRecovController =
TextEditingController();
final TextEditingController newPassphraseController = TextEditingController();
final TextEditingController repeatPassphraseController =
TextEditingController();
final ScrollController devicesScrollController = ScrollController();
BootstrapViewModel({required this.client, required this.reset})
: super(BootstrapViewModelState()..reset = reset) {
_init();
}
@override
void dispose() {
_cancelKeyVerification();
enterPassphraseOrRecovController.dispose();
newPassphraseController.dispose();
repeatPassphraseController.dispose();
devicesScrollController.dispose();
super.dispose();
}
void _checkCanCreatePassphrase([_]) {
final passphrase = newPassphraseController.text;
value.newPassphraseEqualsRepeatPassphrase =
passphrase.isNotEmpty && passphrase == repeatPassphraseController.text;
value.newPassphraseLongEnough = passphrase.length >= 12;
value.newPassphraseUpperAndLowerCase =
passphrase.contains(RegExp(r'[A-Z]')) &&
passphrase.contains(RegExp(r'[a-z]'));
value.newPassphraseSpecialCharacters = passphrase.contains(
RegExp(r'[!@#$%^&*(),.?":{}|<>]'),
);
value.newPassphraseNumbers = passphrase.contains(RegExp(r'\d'));
notifyListeners();
}
Future<void> retryKeyVerification() async {
value.noSecretsreceived = false;
value.keyVerification = await client.userDeviceKeys[client.userID!]!
.startVerification();
value.keyVerification?.onUpdate = _onKeyVerificationUpdate;
notifyListeners();
}
Future<void> _init() async {
final state = value.cryptoIdentityState = await client
.getCryptoIdentityState();
newPassphraseController.addListener(_checkCanCreatePassphrase);
repeatPassphraseController.addListener(_checkCanCreatePassphrase);
enterPassphraseOrRecovController.addListener(
_passphraseOrRecoveryKeyEntered,
);
if (state.initialized) {
if (state.connected) return notifyListeners();
await client.updateUserDeviceKeys();
final devices = value.connectedDevices =
client.userDeviceKeys[client.userID!]?.deviceKeys.values
.where(
(device) => device.hasValidSignatureChain(
verifiedByTheirMasterKey: true,
),
)
.toList() ??
[];
if (devices.isNotEmpty) {
value.keyVerification = await client.userDeviceKeys[client.userID!]!
.startVerification();
value.keyVerification?.onUpdate = _onKeyVerificationUpdate;
}
if (supportsSecureStorage) {
try {
final keyFromSecureStorage = await FlutterSecureStorage().read(
key: _secureStorageKey,
);
if (keyFromSecureStorage != null) {
enterPassphraseOrRecovController.text = keyFromSecureStorage;
}
} catch (e, s) {
Logs().e('Unable to read key from secure storage', e, s);
}
}
}
notifyListeners();
}
void _passphraseOrRecoveryKeyEntered() {
final passphraseOrRecoveryKeyEntered =
enterPassphraseOrRecovController.text.isNotEmpty;
if (value.passphraseOrRecoveryKeyEntered !=
passphraseOrRecoveryKeyEntered) {
value.passphraseOrRecoveryKeyEntered = passphraseOrRecoveryKeyEntered;
notifyListeners();
}
}
Future<void> _onKeyVerificationUpdate() async {
if (value.keyVerification?.state == KeyVerificationState.done) {
value.waitingForSecrets = true;
value.noSecretsreceived = false;
notifyListeners();
value.cryptoIdentityState = await client.getCryptoIdentityState();
var tries = 0;
const max = 10;
while (value.cryptoIdentityState?.connected != true) {
Logs().d('Waiting for secrets... [$tries/$max]');
if (tries >= max) return;
await Future.delayed(const Duration(seconds: 1));
value.cryptoIdentityState = await client.getCryptoIdentityState();
tries++;
}
if (value.cryptoIdentityState?.connected != true) {
value.waitingForSecrets = false;
value.noSecretsreceived = true;
}
}
notifyListeners();
}
Future<void> setOrSkipPassphrase(
String? passphrase,
BuildContext context,
) async {
value.isLoading = true;
notifyListeners();
try {
value.recoveryKey = await client.initCryptoIdentity(
passphrase: passphrase,
wipeCrossSigning: !reset,
wipeKeyBackup: !reset,
wipeSecureStorage: !reset,
setupOnlineKeyBackup: !reset,
setupMasterKey: !reset,
setupSelfSigningKey: !reset,
setupUserSigningKey: !reset,
);
} catch (e, s) {
if (!context.mounted) return;
ErrorReporter(
context,
'Unable to init crypto identity',
).onErrorCallback(e, s);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
value.isLoading = false;
}
notifyListeners();
}
void _cancelKeyVerification() {
final keyVerification = value.keyVerification;
if (keyVerification != null &&
keyVerification.state != KeyVerificationState.done &&
keyVerification.state != KeyVerificationState.error) {
keyVerification.cancel();
}
}
bool get supportsSecureStorage =>
PlatformInfos.isMobile || PlatformInfos.isDesktop;
Future<void> unlock(BuildContext context) async {
final key = enterPassphraseOrRecovController.text.trim();
if (key.isEmpty) return;
_cancelKeyVerification();
value.unlockWithError = null;
value.isLoading = true;
notifyListeners();
try {
await client.restoreCryptoIdentity(key);
value.isLoading = false;
value.cryptoIdentityState = await client.getCryptoIdentityState();
notifyListeners();
return;
} catch (e, s) {
if (e is! InvalidPassphraseException) {
const errorMessage = 'Unexpected error on unlock passphrase';
if (context.mounted) {
ErrorReporter(context, errorMessage).onErrorCallback(e, s);
} else {
Logs().wtf(errorMessage, e, s);
}
}
value.isLoading = false;
value.unlockWithError = e;
notifyListeners();
if (supportsSecureStorage) {
await FlutterSecureStorage().delete(key: _secureStorageKey);
}
return;
}
}
void goToRoomsPageAfterSuccess(BuildContext context) {
for (final room in client.rooms) {
final lastEvent = room.lastEvent;
if (lastEvent == null ||
lastEvent.messageType != MessageTypes.BadEncrypted ||
lastEvent.content['can_request_session'] != true) {
continue;
}
final sessionId = lastEvent.content.tryGet<String>('session_id');
final senderKey = lastEvent.content.tryGet<String>('sender_key');
if (sessionId != null && senderKey != null) {
client.encryption?.keyManager.maybeAutoRequest(
room.id,
sessionId,
senderKey,
);
}
}
context.go('/rooms');
}
void toggleObscureText() {
value.obscureText = !value.obscureText;
notifyListeners();
}
void startResetAccount() {
value.reset = true;
notifyListeners();
}
String get _secureStorageKey => 'ssss_recovery_key_${client.userID}';
Future<void> openRecoveryKeyFile(BuildContext context) async {
final result = await FilePicker.pickFile(
allowedExtensions: ['txt'],
type: FileType.custom,
);
final file = result?.xFile;
if (file == null) return;
try {
final key = await file.readAsString();
enterPassphraseOrRecovController.text = key;
} catch (e, s) {
Logs().d('Unable to read recovery key file', e, s);
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
}
}
if (context.mounted) await unlock(context);
}
Future<void> toggleRecoveryKeyDownloaded(
bool? downloaded,
BuildContext context,
) async {
final path = await FilePicker.saveFile(
fileName:
'FluffyChat-Recovery-Key-${DateTime.now().toIso8601String()}.txt',
bytes: Uint8List.fromList(value.recoveryKey!.codeUnits),
);
if (path == null) return;
value.recoveryKeyDownloaded = downloaded == true;
notifyListeners();
}
Future<void> toggleRecoveryKeyStoredInSecureStorage(bool? stored) async {
if (stored == true) {
await FlutterSecureStorage().write(
key: _secureStorageKey,
value: value.recoveryKey,
);
} else {
await FlutterSecureStorage().delete(key: _secureStorageKey);
}
value.recoveryKeyStoredInSecureStorage = stored == true;
notifyListeners();
}
}