fix(app): only an otpauth URI is shown as a QR code or opened
The instance sends the URI an authenticator is set up from; one that isn't otpauth: (a web page, an Android intent) was drawn as a QR code and handed to the system to open. Now it is neither; the key stays to type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
c9904de31d
commit
5270ce6775
3 changed files with 64 additions and 3 deletions
|
|
@ -18,6 +18,12 @@ final class const AuthenticatorSetup({
|
||||||
required final String id,
|
required final String id,
|
||||||
required final String uri,
|
required final String uri,
|
||||||
}) with Equatable {
|
}) with Equatable {
|
||||||
|
/// Whether [uri] is what an authenticator app opens (`otpauth:`): only
|
||||||
|
/// then is it shown as a QR code or handed to the system to open, as the
|
||||||
|
/// instance could send any address (a web page, an intent).
|
||||||
|
bool get isAuthenticatorUri =>
|
||||||
|
Uri.tryParse(uri)?.isScheme('otpauth') ?? false;
|
||||||
|
|
||||||
/// The secret the URI holds, Base32, in groups of four to type by hand.
|
/// The secret the URI holds, Base32, in groups of four to type by hand.
|
||||||
String get secret {
|
String get secret {
|
||||||
final raw = Uri.tryParse(uri)?.queryParameters['secret'] ?? '';
|
final raw = Uri.tryParse(uri)?.queryParameters['secret'] ?? '';
|
||||||
|
|
|
||||||
|
|
@ -550,13 +550,17 @@ class const AuthenticatorSetupView({
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = HalkyonAppLocalizations.of(context);
|
final l10n = HalkyonAppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final phone = Platform.isAndroid || Platform.isIOS;
|
// Only an otpauth URI is shown as a QR code or opened ([AuthenticatorSetup.isAuthenticatorUri]).
|
||||||
|
final opens = setup.isAuthenticatorUri;
|
||||||
|
final phone = opens && (Platform.isAndroid || Platform.isIOS);
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
SheetIntro(l10n.passwordAuthenticatorIntro),
|
SheetIntro(l10n.passwordAuthenticatorIntro),
|
||||||
|
if (opens) ...[
|
||||||
Center(child: QrCodeView(data: setup.uri)),
|
Center(child: QrCodeView(data: setup.uri)),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
Text(l10n.passwordAuthenticatorKey, style: theme.textTheme.labelLarge),
|
Text(l10n.passwordAuthenticatorKey, style: theme.textTheme.labelLarge),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
|
|
|
||||||
|
|
@ -57,4 +57,55 @@ void main() {
|
||||||
);
|
);
|
||||||
expectNothingLearnt(tester, "the phrase's words");
|
expectNothingLearnt(tester, "the phrase's words");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("only an otpauth URI is an authenticator's to open", () {
|
||||||
|
expect(
|
||||||
|
const AuthenticatorSetup(
|
||||||
|
id: 'a',
|
||||||
|
uri: 'otpauth://totp/x:anna?secret=JBSWY3DPEHPK3PXP',
|
||||||
|
).isAuthenticatorUri,
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
for (final uri in [
|
||||||
|
'https://evil.example/otpauth',
|
||||||
|
'intent://scan#Intent;scheme=otpauth;end',
|
||||||
|
'tel:+4930123',
|
||||||
|
'',
|
||||||
|
]) {
|
||||||
|
expect(
|
||||||
|
AuthenticatorSetup(id: 'a', uri: uri).isAuthenticatorUri,
|
||||||
|
isFalse,
|
||||||
|
reason: uri,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('what an instance sends that is no otpauth URI is never shown '
|
||||||
|
'as a QR code', (tester) async {
|
||||||
|
final code = TextEditingController();
|
||||||
|
addTearDown(code.dispose);
|
||||||
|
await pump(
|
||||||
|
tester,
|
||||||
|
AuthenticatorSetupView(
|
||||||
|
setup: const AuthenticatorSetup(
|
||||||
|
id: 'a',
|
||||||
|
uri: 'https://evil.example/?secret=JBSWY3DPEHPK3PXP',
|
||||||
|
),
|
||||||
|
code: code,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(find.byType(QrCodeView), findsNothing);
|
||||||
|
|
||||||
|
await pump(
|
||||||
|
tester,
|
||||||
|
AuthenticatorSetupView(
|
||||||
|
setup: const AuthenticatorSetup(
|
||||||
|
id: 'a',
|
||||||
|
uri: 'otpauth://totp/x:anna?secret=JBSWY3DPEHPK3PXP',
|
||||||
|
),
|
||||||
|
code: code,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(find.byType(QrCodeView), findsOneWidget);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue