diff --git a/auth/lib/l10n/arb/app_en.arb b/auth/lib/l10n/arb/app_en.arb index e16a39c79..0c42d78f7 100644 --- a/auth/lib/l10n/arb/app_en.arb +++ b/auth/lib/l10n/arb/app_en.arb @@ -20,6 +20,7 @@ "codeIssuerHint": "Issuer", "codeSecretKeyHint": "Secret Key", "codeAccountHint": "Account (you@domain.com)", + "digitsAccountHint": "Digits (default=6)", "accountKeyType": "Type of key", "sessionExpired": "Session expired", "@sessionExpired": { diff --git a/auth/lib/models/code.dart b/auth/lib/models/code.dart index 09591a62f..524e896ad 100644 --- a/auth/lib/models/code.dart +++ b/auth/lib/models/code.dart @@ -58,18 +58,20 @@ class Code { updatedAlgo, updatedType, updatedCounter, - "otpauth://${updatedType.name}/$updateIssuer:$updateAccount?algorithm=${updatedAlgo.name}&digits=$updatedDigits&issuer=$updateIssuer&period=$updatePeriod&secret=$updatedSecret${updatedType == Type.hotp ? "&counter=$updatedCounter" : ""}", + "otpauth://${updatedType.name}/$updateIssuer:$updateAccount?algorithm=${updatedAlgo.name}" + "${updatedType == Type.steam ? "" : "&digits=$updatedDigits"}&issuer=$updateIssuer" + "&period=$updatePeriod&secret=$updatedSecret${updatedType == Type.hotp ? "&counter=$updatedCounter" : ""}", generatedID: generatedID, ); } static Code fromAccountAndSecret( + Type type, String account, String issuer, String secret, + int digits, ) { - final digits = - issuer.toLowerCase() == "steam" ? steamDigits : defaultDigits; return Code( account, issuer, @@ -77,23 +79,21 @@ class Code { defaultPeriod, secret, Algorithm.sha1, - Type.totp, + type, 0, - "otpauth://totp/$issuer:$account?algorithm=SHA1&digits=6&issuer=$issuer&period=30&secret=$secret", + "otpauth://${type.name}/$issuer:$account?algorithm=SHA1${type == Type.steam ? "" : "&digits=$digits"}&issuer=$issuer&period=30&secret=$secret", ); } static Code fromRawData(String rawData) { Uri uri = Uri.parse(rawData); final issuer = _getIssuer(uri); - final digits = - issuer.toLowerCase() == "steam" ? steamDigits : _getDigits(uri); try { return Code( _getAccount(uri), issuer, - digits, + _getDigits(uri, issuer), _getPeriod(uri), getSanitizedSecret(uri.queryParameters['secret']!), _getAlgorithm(uri), @@ -147,8 +147,9 @@ class Code { } } - static int _getDigits(Uri uri) { + static int _getDigits(Uri uri, String issuer) { try { + if (issuer.toLowerCase() == "steam") return steamDigits; return int.parse(uri.queryParameters['digits']!); } catch (e) { return defaultDigits; @@ -191,8 +192,10 @@ class Code { } static Type _getType(Uri uri) { - if (uri.host == "totp" || uri.host == "steam") { + if (uri.host == "totp") { return Type.totp; + } else if (uri.host == "steam") { + return Type.steam; } else if (uri.host == "hotp") { return Type.hotp; } @@ -230,6 +233,9 @@ class Code { enum Type { totp, hotp, + steam; + + bool get isTOTPCompatible => this == totp || this == steam; } enum Algorithm { diff --git a/auth/lib/onboarding/view/setup_enter_secret_key_page.dart b/auth/lib/onboarding/view/setup_enter_secret_key_page.dart index 3937142d6..64837f8f5 100644 --- a/auth/lib/onboarding/view/setup_enter_secret_key_page.dart +++ b/auth/lib/onboarding/view/setup_enter_secret_key_page.dart @@ -20,6 +20,7 @@ class _SetupEnterSecretKeyPageState extends State { late TextEditingController _issuerController; late TextEditingController _accountController; late TextEditingController _secretController; + late TextEditingController _digitsController; late bool _secretKeyObscured; @override @@ -34,6 +35,9 @@ class _SetupEnterSecretKeyPageState extends State { _secretController = TextEditingController( text: widget.code?.secret, ); + _digitsController = TextEditingController( + text: widget.code?.digits.toString(), + ); _secretKeyObscured = widget.code != null; super.initState(); } @@ -61,6 +65,8 @@ class _SetupEnterSecretKeyPageState extends State { }, decoration: InputDecoration( hintText: l10n.codeIssuerHint, + floatingLabelBehavior: FloatingLabelBehavior.auto, + labelText: l10n.codeIssuerHint, ), controller: _issuerController, autofocus: true, @@ -78,6 +84,8 @@ class _SetupEnterSecretKeyPageState extends State { }, decoration: InputDecoration( hintText: l10n.codeSecretKeyHint, + floatingLabelBehavior: FloatingLabelBehavior.auto, + labelText: l10n.codeSecretKeyHint, suffixIcon: IconButton( onPressed: () { setState(() { @@ -105,9 +113,33 @@ class _SetupEnterSecretKeyPageState extends State { }, decoration: InputDecoration( hintText: l10n.codeAccountHint, + floatingLabelBehavior: FloatingLabelBehavior.auto, + labelText: l10n.codeAccountHint, ), controller: _accountController, ), + const SizedBox( + height: 20, + ), + TextFormField( + validator: (value) { + if (value == null || value.isEmpty) { + return "Please enter some number"; + } + if (int.tryParse(value) == null) { + return "Please enter a valid number"; + } + return null; + }, + readOnly: widget.code?.type == Type.steam, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: l10n.digitsAccountHint, + labelText: l10n.digitsAccountHint, + floatingLabelBehavior: FloatingLabelBehavior.auto, + ), + controller: _digitsController, + ), const SizedBox( height: 40, ), @@ -152,6 +184,10 @@ class _SetupEnterSecretKeyPageState extends State { final account = _accountController.text.trim(); final issuer = _issuerController.text.trim(); final secret = _secretController.text.trim().replaceAll(' ', ''); + final digits = int.tryParse(_digitsController.text.trim()) ?? + (widget.code?.type == Type.steam + ? Code.steamDigits + : Code.defaultDigits); if (widget.code != null && widget.code!.secret != secret) { ButtonResult? result = await showChoiceActionSheet( context, @@ -168,14 +204,17 @@ class _SetupEnterSecretKeyPageState extends State { } final Code newCode = widget.code == null ? Code.fromAccountAndSecret( + Type.totp, account, issuer, secret, + digits, ) : widget.code!.copyWith( account: account, issuer: issuer, secret: secret, + digits: digits, ); // Verify the validity of the code getOTP(newCode); diff --git a/auth/lib/ui/code_widget.dart b/auth/lib/ui/code_widget.dart index f97e865ec..d989edf18 100644 --- a/auth/lib/ui/code_widget.dart +++ b/auth/lib/ui/code_widget.dart @@ -53,7 +53,7 @@ class _CodeWidgetState extends State { String newCode = _getCurrentOTP(); if (newCode != _currentCode.value) { _currentCode.value = newCode; - if (widget.code.type == Type.totp) { + if (widget.code.type.isTOTPCompatible) { _nextCode.value = _getNextTotp(); } } @@ -78,7 +78,7 @@ class _CodeWidgetState extends State { _shouldShowLargeIcon = PreferenceService.instance.shouldShowLargeIcons(); if (!_isInitialized) { _currentCode.value = _getCurrentOTP(); - if (widget.code.type == Type.totp) { + if (widget.code.type.isTOTPCompatible) { _nextCode.value = _getNextTotp(); } _isInitialized = true; @@ -213,7 +213,7 @@ class _CodeWidgetState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - if (widget.code.type == Type.totp) + if (widget.code.type.isTOTPCompatible) CodeTimerProgress( period: widget.code.period, ), @@ -263,7 +263,7 @@ class _CodeWidgetState extends State { }, ), ), - widget.code.type == Type.totp + widget.code.type.isTOTPCompatible ? GestureDetector( onTap: () { _copyNextToClipboard(); @@ -481,7 +481,7 @@ class _CodeWidgetState extends State { String _getNextTotp() { try { - assert(widget.code.type == Type.totp); + assert(widget.code.type.isTOTPCompatible); return getNextTotp(widget.code); } catch (e) { return context.l10n.error; diff --git a/auth/lib/ui/settings/data/import/bitwarden_import.dart b/auth/lib/ui/settings/data/import/bitwarden_import.dart index 90e527dde..7a562d82b 100644 --- a/auth/lib/ui/settings/data/import/bitwarden_import.dart +++ b/auth/lib/ui/settings/data/import/bitwarden_import.dart @@ -92,9 +92,11 @@ Future _processBitwardenExportFile( var account = item['login']['username']; code = Code.fromAccountAndSecret( + Type.totp, account, issuer, totp, + Code.defaultDigits, ); }