fix(app): no sheet is left busy by an error nothing expected
An instance's salt of another length made the device's cryptography throw an ArgumentError, a wrapping's nonce a RangeError, and libsodium or the keyring may throw their own; none was a refusal, so the step's error escaped and the sheet stayed busy, which can't be closed. halkyon_keys now checks the lengths of what comes from elsewhere before libsodium sees it (KeyRefusal.malformed: a salt, a wrapping, a sealed key's box, a sealed payload), the device's cryptography turns anything else into the general failure, and so does every password cubit, the error logged by its type alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
4914df348e
commit
cf69e62ad4
6 changed files with 318 additions and 32 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/password/bloc/unexpected_failure.dart';
|
||||
import 'package:halkyon_app/src/password/data/password_repository.dart';
|
||||
import 'package:halkyon_app/src/password/model/mail_code_purpose.dart';
|
||||
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
||||
|
|
@ -162,7 +163,8 @@ class AccountRecoveryCubit(
|
|||
if (!isClosed) {
|
||||
emit(state.copyWith(busy: false));
|
||||
}
|
||||
} on PasswordFailed catch (failed) {
|
||||
} on Object catch (error) {
|
||||
final failed = asPasswordFailed(error);
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(busy: false, problem: failed));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/password/bloc/unexpected_failure.dart';
|
||||
import 'package:halkyon_app/src/password/data/password_repository.dart';
|
||||
import 'package:halkyon_app/src/password/model/account_authenticator.dart';
|
||||
import 'package:halkyon_app/src/password/model/account_security.dart';
|
||||
|
|
@ -43,7 +44,10 @@ class AccountSecurityCubit(final PasswordRepository _passwords)
|
|||
if (!isClosed) {
|
||||
emit(AccountSecurityState(security: security));
|
||||
}
|
||||
} on PasswordFailed {
|
||||
} on Object catch (error) {
|
||||
if (error is! PasswordFailed) {
|
||||
logUnexpected(error);
|
||||
}
|
||||
if (!isClosed) {
|
||||
emit(AccountSecurityState(security: state.security, failed: true));
|
||||
}
|
||||
|
|
@ -79,7 +83,8 @@ class SecurityStepCubit<T>() extends Cubit<SecurityStepState<T>> {
|
|||
emit(SecurityStepState<T>(result: result));
|
||||
}
|
||||
return true;
|
||||
} on PasswordFailed catch (failed) {
|
||||
} on Object catch (error) {
|
||||
final failed = asPasswordFailed(error);
|
||||
if (!isClosed) {
|
||||
emit(SecurityStepState<T>(problem: failed, result: state.result));
|
||||
}
|
||||
|
|
@ -140,7 +145,8 @@ class AddAuthenticatorCubit(
|
|||
AddAuthenticatorState(step: AddAuthenticatorStep.code, setup: setup),
|
||||
);
|
||||
}
|
||||
} on PasswordFailed catch (failed) {
|
||||
} on Object catch (error) {
|
||||
final failed = asPasswordFailed(error);
|
||||
if (!isClosed) {
|
||||
emit(AddAuthenticatorState(problem: failed));
|
||||
}
|
||||
|
|
@ -180,7 +186,8 @@ class AddAuthenticatorCubit(
|
|||
),
|
||||
);
|
||||
}
|
||||
} on PasswordFailed catch (failed) {
|
||||
} on Object catch (error) {
|
||||
final failed = asPasswordFailed(error);
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
AddAuthenticatorState(
|
||||
|
|
@ -250,7 +257,8 @@ class NewPhraseCubit(final PasswordRepository _passwords)
|
|||
if (!isClosed) {
|
||||
emit(NewPhraseState(step: NewPhraseStep.shown, phrase: made.phrase));
|
||||
}
|
||||
} on PasswordFailed catch (failed) {
|
||||
} on Object catch (error) {
|
||||
final failed = asPasswordFailed(error);
|
||||
if (!isClosed) {
|
||||
emit(NewPhraseState(problem: failed));
|
||||
}
|
||||
|
|
@ -287,7 +295,8 @@ class NewPhraseCubit(final PasswordRepository _passwords)
|
|||
if (!isClosed) {
|
||||
emit(NewPhraseState(step: NewPhraseStep.done, phrase: state.phrase));
|
||||
}
|
||||
} on PasswordFailed catch (failed) {
|
||||
} on Object catch (error) {
|
||||
final failed = asPasswordFailed(error);
|
||||
if (!isClosed) {
|
||||
emit(_again(problem: failed));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/password/bloc/unexpected_failure.dart';
|
||||
import 'package:halkyon_app/src/password/data/account_crypto.dart';
|
||||
import 'package:halkyon_app/src/password/data/password_repository.dart';
|
||||
import 'package:halkyon_app/src/password/model/account_authenticator.dart';
|
||||
|
|
@ -338,7 +339,8 @@ class AccountSetupCubit(
|
|||
if (!isClosed) {
|
||||
emit(state.copyWith(busy: false));
|
||||
}
|
||||
} on PasswordFailed catch (failed) {
|
||||
} on Object catch (error) {
|
||||
final failed = asPasswordFailed(error);
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/password/bloc/unexpected_failure.dart';
|
||||
import 'package:halkyon_app/src/password/data/password_repository.dart';
|
||||
import 'package:halkyon_app/src/password/model/account_authenticator.dart';
|
||||
import 'package:halkyon_app/src/password/model/mail_code_purpose.dart';
|
||||
|
|
@ -351,7 +352,8 @@ class PasswordSignInCubit(
|
|||
if (!isClosed) {
|
||||
emit(state.copyWith(busy: false));
|
||||
}
|
||||
} on PasswordFailed catch (failed) {
|
||||
} on Object catch (error) {
|
||||
final failed = asPasswordFailed(error);
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(busy: false, problem: failed));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
||||
|
||||
/// [error] as the refusal a sheet shows: a [PasswordFailed] as it is,
|
||||
/// anything else (an instance's malformed answer, libsodium's or the
|
||||
/// keyring's own errors) as [PasswordProblem.failed], logged by its type
|
||||
/// alone, so a sheet is never left busy and never closes on an error it
|
||||
/// didn't expect (ADR 0017's full reading).
|
||||
PasswordFailed asPasswordFailed(Object error) {
|
||||
if (error is PasswordFailed) {
|
||||
return error;
|
||||
}
|
||||
logUnexpected(error);
|
||||
return const PasswordFailed(PasswordProblem.failed);
|
||||
}
|
||||
|
||||
/// Logs [error] by its type alone: what it says may hold a secret.
|
||||
void logUnexpected(Object error) =>
|
||||
log('Failed unexpectedly: ${error.runtimeType}', name: 'Passwords');
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data';
|
||||
|
||||
|
|
@ -301,8 +302,20 @@ Future<T> _guarded<T>(Future<T> Function() step) async {
|
|||
PasswordRefusal.empty ||
|
||||
PasswordRefusal.controlCharacter => PasswordProblem.passwordUnusable,
|
||||
});
|
||||
} on KeyRefused {
|
||||
throw const PasswordFailed(PasswordProblem.keysMissing);
|
||||
} on KeyRefused catch (refused) {
|
||||
throw PasswordFailed(switch (refused.reason) {
|
||||
// What the instance sent isn't of the lengths it must have.
|
||||
KeyRefusal.malformed => PasswordProblem.failed,
|
||||
_ => PasswordProblem.keysMissing,
|
||||
});
|
||||
} on PasswordFailed {
|
||||
rethrow;
|
||||
} on Object catch (error) {
|
||||
// Anything else (libsodium's own errors, Argon2id without the memory
|
||||
// it asks for) fails the step rather than escaping it, which left the
|
||||
// sheet busy for good; logged by its type alone.
|
||||
log('The keys failed: ${error.runtimeType}', name: 'Keys');
|
||||
throw const PasswordFailed(PasswordProblem.failed);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue