diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index c7504b68a..dc34b7bdc 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -78,6 +78,7 @@ "passwordForDecryptingExport" : "Password to decrypt export", "passwordEmptyError": "Password can not be empty", "importFromApp": "Import codes from {appName}", + "importGoogleAuthGuide": "Export your accounts from Google Authenticator to a QR code using the \"Transfer Accounts\" option. Then using another device, scan the QR code.", "importSelectJsonFile": "Select JSON file", "importEnteEncGuide": "Select the encrypted JSON file exported from ente", "importRaivoGuide": "Use the \"Export OTPs to Zip archive\" option in Raivo's Settings.\n\nExtract the zip file and import the JSON file.", diff --git a/lib/models/protos/googleauth.pb.dart b/lib/models/protos/googleauth.pb.dart new file mode 100644 index 000000000..6bd352850 --- /dev/null +++ b/lib/models/protos/googleauth.pb.dart @@ -0,0 +1,201 @@ +// +// Generated code. Do not modify. +// source: googleauth.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'googleauth.pbenum.dart'; + +export 'googleauth.pbenum.dart'; + +class MigrationPayload_OtpParameters extends $pb.GeneratedMessage { + factory MigrationPayload_OtpParameters() => create(); + MigrationPayload_OtpParameters._() : super(); + factory MigrationPayload_OtpParameters.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory MigrationPayload_OtpParameters.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'MigrationPayload.OtpParameters', package: const $pb.PackageName(_omitMessageNames ? '' : 'googleauth'), createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'secret', $pb.PbFieldType.OY) + ..aOS(2, _omitFieldNames ? '' : 'name') + ..aOS(3, _omitFieldNames ? '' : 'issuer') + ..e(4, _omitFieldNames ? '' : 'algorithm', $pb.PbFieldType.OE, defaultOrMaker: MigrationPayload_Algorithm.ALGORITHM_UNSPECIFIED, valueOf: MigrationPayload_Algorithm.valueOf, enumValues: MigrationPayload_Algorithm.values) + ..e(5, _omitFieldNames ? '' : 'digits', $pb.PbFieldType.OE, defaultOrMaker: MigrationPayload_DigitCount.DIGIT_COUNT_UNSPECIFIED, valueOf: MigrationPayload_DigitCount.valueOf, enumValues: MigrationPayload_DigitCount.values) + ..e(6, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: MigrationPayload_OtpType.OTP_TYPE_UNSPECIFIED, valueOf: MigrationPayload_OtpType.valueOf, enumValues: MigrationPayload_OtpType.values) + ..aInt64(7, _omitFieldNames ? '' : 'counter') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + MigrationPayload_OtpParameters clone() => MigrationPayload_OtpParameters()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + MigrationPayload_OtpParameters copyWith(void Function(MigrationPayload_OtpParameters) updates) => super.copyWith((message) => updates(message as MigrationPayload_OtpParameters)) as MigrationPayload_OtpParameters; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static MigrationPayload_OtpParameters create() => MigrationPayload_OtpParameters._(); + MigrationPayload_OtpParameters createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static MigrationPayload_OtpParameters getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static MigrationPayload_OtpParameters? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get secret => $_getN(0); + @$pb.TagNumber(1) + set secret($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasSecret() => $_has(0); + @$pb.TagNumber(1) + void clearSecret() => clearField(1); + + @$pb.TagNumber(2) + $core.String get name => $_getSZ(1); + @$pb.TagNumber(2) + set name($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasName() => $_has(1); + @$pb.TagNumber(2) + void clearName() => clearField(2); + + @$pb.TagNumber(3) + $core.String get issuer => $_getSZ(2); + @$pb.TagNumber(3) + set issuer($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasIssuer() => $_has(2); + @$pb.TagNumber(3) + void clearIssuer() => clearField(3); + + @$pb.TagNumber(4) + MigrationPayload_Algorithm get algorithm => $_getN(3); + @$pb.TagNumber(4) + set algorithm(MigrationPayload_Algorithm v) { setField(4, v); } + @$pb.TagNumber(4) + $core.bool hasAlgorithm() => $_has(3); + @$pb.TagNumber(4) + void clearAlgorithm() => clearField(4); + + @$pb.TagNumber(5) + MigrationPayload_DigitCount get digits => $_getN(4); + @$pb.TagNumber(5) + set digits(MigrationPayload_DigitCount v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasDigits() => $_has(4); + @$pb.TagNumber(5) + void clearDigits() => clearField(5); + + @$pb.TagNumber(6) + MigrationPayload_OtpType get type => $_getN(5); + @$pb.TagNumber(6) + set type(MigrationPayload_OtpType v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasType() => $_has(5); + @$pb.TagNumber(6) + void clearType() => clearField(6); + + @$pb.TagNumber(7) + $fixnum.Int64 get counter => $_getI64(6); + @$pb.TagNumber(7) + set counter($fixnum.Int64 v) { $_setInt64(6, v); } + @$pb.TagNumber(7) + $core.bool hasCounter() => $_has(6); + @$pb.TagNumber(7) + void clearCounter() => clearField(7); +} + +class MigrationPayload extends $pb.GeneratedMessage { + factory MigrationPayload() => create(); + MigrationPayload._() : super(); + factory MigrationPayload.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory MigrationPayload.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'MigrationPayload', package: const $pb.PackageName(_omitMessageNames ? '' : 'googleauth'), createEmptyInstance: create) + ..pc(1, _omitFieldNames ? '' : 'otpParameters', $pb.PbFieldType.PM, subBuilder: MigrationPayload_OtpParameters.create) + ..a<$core.int>(2, _omitFieldNames ? '' : 'version', $pb.PbFieldType.O3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'batchSize', $pb.PbFieldType.O3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'batchIndex', $pb.PbFieldType.O3) + ..a<$core.int>(5, _omitFieldNames ? '' : 'batchId', $pb.PbFieldType.O3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + MigrationPayload clone() => MigrationPayload()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + MigrationPayload copyWith(void Function(MigrationPayload) updates) => super.copyWith((message) => updates(message as MigrationPayload)) as MigrationPayload; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static MigrationPayload create() => MigrationPayload._(); + MigrationPayload createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static MigrationPayload getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static MigrationPayload? _defaultInstance; + + @$pb.TagNumber(1) + $core.List get otpParameters => $_getList(0); + + @$pb.TagNumber(2) + $core.int get version => $_getIZ(1); + @$pb.TagNumber(2) + set version($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasVersion() => $_has(1); + @$pb.TagNumber(2) + void clearVersion() => clearField(2); + + @$pb.TagNumber(3) + $core.int get batchSize => $_getIZ(2); + @$pb.TagNumber(3) + set batchSize($core.int v) { $_setSignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasBatchSize() => $_has(2); + @$pb.TagNumber(3) + void clearBatchSize() => clearField(3); + + @$pb.TagNumber(4) + $core.int get batchIndex => $_getIZ(3); + @$pb.TagNumber(4) + set batchIndex($core.int v) { $_setSignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasBatchIndex() => $_has(3); + @$pb.TagNumber(4) + void clearBatchIndex() => clearField(4); + + @$pb.TagNumber(5) + $core.int get batchId => $_getIZ(4); + @$pb.TagNumber(5) + set batchId($core.int v) { $_setSignedInt32(4, v); } + @$pb.TagNumber(5) + $core.bool hasBatchId() => $_has(4); + @$pb.TagNumber(5) + void clearBatchId() => clearField(5); +} + + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/models/protos/googleauth.pbenum.dart b/lib/models/protos/googleauth.pbenum.dart new file mode 100644 index 000000000..ae0b0afd6 --- /dev/null +++ b/lib/models/protos/googleauth.pbenum.dart @@ -0,0 +1,72 @@ +// +// Generated code. Do not modify. +// source: googleauth.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class MigrationPayload_Algorithm extends $pb.ProtobufEnum { + static const MigrationPayload_Algorithm ALGORITHM_UNSPECIFIED = MigrationPayload_Algorithm._(0, _omitEnumNames ? '' : 'ALGORITHM_UNSPECIFIED'); + static const MigrationPayload_Algorithm ALGORITHM_SHA1 = MigrationPayload_Algorithm._(1, _omitEnumNames ? '' : 'ALGORITHM_SHA1'); + static const MigrationPayload_Algorithm ALGORITHM_SHA256 = MigrationPayload_Algorithm._(2, _omitEnumNames ? '' : 'ALGORITHM_SHA256'); + static const MigrationPayload_Algorithm ALGORITHM_SHA512 = MigrationPayload_Algorithm._(3, _omitEnumNames ? '' : 'ALGORITHM_SHA512'); + static const MigrationPayload_Algorithm ALGORITHM_MD5 = MigrationPayload_Algorithm._(4, _omitEnumNames ? '' : 'ALGORITHM_MD5'); + + static const $core.List values = [ + ALGORITHM_UNSPECIFIED, + ALGORITHM_SHA1, + ALGORITHM_SHA256, + ALGORITHM_SHA512, + ALGORITHM_MD5, + ]; + + static final $core.Map<$core.int, MigrationPayload_Algorithm> _byValue = $pb.ProtobufEnum.initByValue(values); + static MigrationPayload_Algorithm? valueOf($core.int value) => _byValue[value]; + + const MigrationPayload_Algorithm._($core.int v, $core.String n) : super(v, n); +} + +class MigrationPayload_DigitCount extends $pb.ProtobufEnum { + static const MigrationPayload_DigitCount DIGIT_COUNT_UNSPECIFIED = MigrationPayload_DigitCount._(0, _omitEnumNames ? '' : 'DIGIT_COUNT_UNSPECIFIED'); + static const MigrationPayload_DigitCount DIGIT_COUNT_SIX = MigrationPayload_DigitCount._(1, _omitEnumNames ? '' : 'DIGIT_COUNT_SIX'); + static const MigrationPayload_DigitCount DIGIT_COUNT_EIGHT = MigrationPayload_DigitCount._(2, _omitEnumNames ? '' : 'DIGIT_COUNT_EIGHT'); + + static const $core.List values = [ + DIGIT_COUNT_UNSPECIFIED, + DIGIT_COUNT_SIX, + DIGIT_COUNT_EIGHT, + ]; + + static final $core.Map<$core.int, MigrationPayload_DigitCount> _byValue = $pb.ProtobufEnum.initByValue(values); + static MigrationPayload_DigitCount? valueOf($core.int value) => _byValue[value]; + + const MigrationPayload_DigitCount._($core.int v, $core.String n) : super(v, n); +} + +class MigrationPayload_OtpType extends $pb.ProtobufEnum { + static const MigrationPayload_OtpType OTP_TYPE_UNSPECIFIED = MigrationPayload_OtpType._(0, _omitEnumNames ? '' : 'OTP_TYPE_UNSPECIFIED'); + static const MigrationPayload_OtpType OTP_TYPE_HOTP = MigrationPayload_OtpType._(1, _omitEnumNames ? '' : 'OTP_TYPE_HOTP'); + static const MigrationPayload_OtpType OTP_TYPE_TOTP = MigrationPayload_OtpType._(2, _omitEnumNames ? '' : 'OTP_TYPE_TOTP'); + + static const $core.List values = [ + OTP_TYPE_UNSPECIFIED, + OTP_TYPE_HOTP, + OTP_TYPE_TOTP, + ]; + + static final $core.Map<$core.int, MigrationPayload_OtpType> _byValue = $pb.ProtobufEnum.initByValue(values); + static MigrationPayload_OtpType? valueOf($core.int value) => _byValue[value]; + + const MigrationPayload_OtpType._($core.int v, $core.String n) : super(v, n); +} + + +const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/lib/models/protos/googleauth.pbjson.dart b/lib/models/protos/googleauth.pbjson.dart new file mode 100644 index 000000000..7eff3302b --- /dev/null +++ b/lib/models/protos/googleauth.pbjson.dart @@ -0,0 +1,93 @@ +// +// Generated code. Do not modify. +// source: googleauth.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use migrationPayloadDescriptor instead') +const MigrationPayload$json = { + '1': 'MigrationPayload', + '2': [ + {'1': 'otp_parameters', '3': 1, '4': 3, '5': 11, '6': '.googleauth.MigrationPayload.OtpParameters', '10': 'otpParameters'}, + {'1': 'version', '3': 2, '4': 1, '5': 5, '10': 'version'}, + {'1': 'batch_size', '3': 3, '4': 1, '5': 5, '10': 'batchSize'}, + {'1': 'batch_index', '3': 4, '4': 1, '5': 5, '10': 'batchIndex'}, + {'1': 'batch_id', '3': 5, '4': 1, '5': 5, '10': 'batchId'}, + ], + '3': [MigrationPayload_OtpParameters$json], + '4': [MigrationPayload_Algorithm$json, MigrationPayload_DigitCount$json, MigrationPayload_OtpType$json], +}; + +@$core.Deprecated('Use migrationPayloadDescriptor instead') +const MigrationPayload_OtpParameters$json = { + '1': 'OtpParameters', + '2': [ + {'1': 'secret', '3': 1, '4': 1, '5': 12, '10': 'secret'}, + {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'}, + {'1': 'issuer', '3': 3, '4': 1, '5': 9, '10': 'issuer'}, + {'1': 'algorithm', '3': 4, '4': 1, '5': 14, '6': '.googleauth.MigrationPayload.Algorithm', '10': 'algorithm'}, + {'1': 'digits', '3': 5, '4': 1, '5': 14, '6': '.googleauth.MigrationPayload.DigitCount', '10': 'digits'}, + {'1': 'type', '3': 6, '4': 1, '5': 14, '6': '.googleauth.MigrationPayload.OtpType', '10': 'type'}, + {'1': 'counter', '3': 7, '4': 1, '5': 3, '10': 'counter'}, + ], +}; + +@$core.Deprecated('Use migrationPayloadDescriptor instead') +const MigrationPayload_Algorithm$json = { + '1': 'Algorithm', + '2': [ + {'1': 'ALGORITHM_UNSPECIFIED', '2': 0}, + {'1': 'ALGORITHM_SHA1', '2': 1}, + {'1': 'ALGORITHM_SHA256', '2': 2}, + {'1': 'ALGORITHM_SHA512', '2': 3}, + {'1': 'ALGORITHM_MD5', '2': 4}, + ], +}; + +@$core.Deprecated('Use migrationPayloadDescriptor instead') +const MigrationPayload_DigitCount$json = { + '1': 'DigitCount', + '2': [ + {'1': 'DIGIT_COUNT_UNSPECIFIED', '2': 0}, + {'1': 'DIGIT_COUNT_SIX', '2': 1}, + {'1': 'DIGIT_COUNT_EIGHT', '2': 2}, + ], +}; + +@$core.Deprecated('Use migrationPayloadDescriptor instead') +const MigrationPayload_OtpType$json = { + '1': 'OtpType', + '2': [ + {'1': 'OTP_TYPE_UNSPECIFIED', '2': 0}, + {'1': 'OTP_TYPE_HOTP', '2': 1}, + {'1': 'OTP_TYPE_TOTP', '2': 2}, + ], +}; + +/// Descriptor for `MigrationPayload`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List migrationPayloadDescriptor = $convert.base64Decode( + 'ChBNaWdyYXRpb25QYXlsb2FkElEKDm90cF9wYXJhbWV0ZXJzGAEgAygLMiouZ29vZ2xlYXV0aC' + '5NaWdyYXRpb25QYXlsb2FkLk90cFBhcmFtZXRlcnNSDW90cFBhcmFtZXRlcnMSGAoHdmVyc2lv' + 'bhgCIAEoBVIHdmVyc2lvbhIdCgpiYXRjaF9zaXplGAMgASgFUgliYXRjaFNpemUSHwoLYmF0Y2' + 'hfaW5kZXgYBCABKAVSCmJhdGNoSW5kZXgSGQoIYmF0Y2hfaWQYBSABKAVSB2JhdGNoSWQargIK' + 'DU90cFBhcmFtZXRlcnMSFgoGc2VjcmV0GAEgASgMUgZzZWNyZXQSEgoEbmFtZRgCIAEoCVIEbm' + 'FtZRIWCgZpc3N1ZXIYAyABKAlSBmlzc3VlchJECglhbGdvcml0aG0YBCABKA4yJi5nb29nbGVh' + 'dXRoLk1pZ3JhdGlvblBheWxvYWQuQWxnb3JpdGhtUglhbGdvcml0aG0SPwoGZGlnaXRzGAUgAS' + 'gOMicuZ29vZ2xlYXV0aC5NaWdyYXRpb25QYXlsb2FkLkRpZ2l0Q291bnRSBmRpZ2l0cxI4CgR0' + 'eXBlGAYgASgOMiQuZ29vZ2xlYXV0aC5NaWdyYXRpb25QYXlsb2FkLk90cFR5cGVSBHR5cGUSGA' + 'oHY291bnRlchgHIAEoA1IHY291bnRlciJ5CglBbGdvcml0aG0SGQoVQUxHT1JJVEhNX1VOU1BF' + 'Q0lGSUVEEAASEgoOQUxHT1JJVEhNX1NIQTEQARIUChBBTEdPUklUSE1fU0hBMjU2EAISFAoQQU' + 'xHT1JJVEhNX1NIQTUxMhADEhEKDUFMR09SSVRITV9NRDUQBCJVCgpEaWdpdENvdW50EhsKF0RJ' + 'R0lUX0NPVU5UX1VOU1BFQ0lGSUVEEAASEwoPRElHSVRfQ09VTlRfU0lYEAESFQoRRElHSVRfQ0' + '9VTlRfRUlHSFQQAiJJCgdPdHBUeXBlEhgKFE9UUF9UWVBFX1VOU1BFQ0lGSUVEEAASEQoNT1RQ' + 'X1RZUEVfSE9UUBABEhEKDU9UUF9UWVBFX1RPVFAQAg=='); + diff --git a/lib/models/protos/googleauth.pbserver.dart b/lib/models/protos/googleauth.pbserver.dart new file mode 100644 index 000000000..0afd43e5e --- /dev/null +++ b/lib/models/protos/googleauth.pbserver.dart @@ -0,0 +1,14 @@ +// +// Generated code. Do not modify. +// source: googleauth.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: constant_identifier_names +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +export 'googleauth.pb.dart'; + diff --git a/lib/ui/code_widget.dart b/lib/ui/code_widget.dart index 73163205b..daa983236 100644 --- a/lib/ui/code_widget.dart +++ b/lib/ui/code_widget.dart @@ -27,12 +27,12 @@ class _CodeWidgetState extends State { final ValueNotifier _currentCode = ValueNotifier(""); final ValueNotifier _nextCode = ValueNotifier(""); final Logger logger = Logger("_CodeWidgetState"); + bool _isInitialized = false; @override void initState() { super.initState(); - _currentCode.value = _getTotp(); - _nextCode.value = _getNextTotp(); + _everySecondTimer = Timer.periodic(const Duration(milliseconds: 500), (Timer t) { String newCode = _getTotp(); @@ -53,6 +53,11 @@ class _CodeWidgetState extends State { @override Widget build(BuildContext context) { + if (!_isInitialized) { + _currentCode.value = _getTotp(); + _nextCode.value = _getNextTotp(); + _isInitialized = true; + } final l10n = context.l10n; return Container( margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8), @@ -125,8 +130,7 @@ class _CodeWidgetState extends State { children: [ Text( safeDecode(widget.code.issuer).trim(), - style: - Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.headline6, ), const SizedBox(height: 2), Text( diff --git a/lib/ui/home_page.dart b/lib/ui/home_page.dart index 900f047a0..6664c0fb7 100644 --- a/lib/ui/home_page.dart +++ b/lib/ui/home_page.dart @@ -54,6 +54,7 @@ class _HomePageState extends State { @override void initState() { + super.initState(); _textController.addListener(_applyFilteringAndRefresh); _loadCodes(); _streamSubscription = Bus.instance.on().listen((event) { @@ -64,7 +65,7 @@ class _HomePageState extends State { await autoLogoutAlert(context); }); _initDeepLinks(); - super.initState(); + } void _loadCodes() { @@ -222,7 +223,11 @@ class _HomePageState extends State { } else { final list = ListView.builder( itemBuilder: ((context, index) { - return CodeWidget(_filteredCodes[index]); + try { + return CodeWidget(_filteredCodes[index]); + } catch(e) { + return const Text("Failed"); + } }), itemCount: _filteredCodes.length, ); diff --git a/lib/ui/scanner_gauth_page.dart b/lib/ui/scanner_gauth_page.dart new file mode 100644 index 000000000..ee84c89f6 --- /dev/null +++ b/lib/ui/scanner_gauth_page.dart @@ -0,0 +1,97 @@ +import 'dart:io'; + +import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/models/code.dart'; +import 'package:ente_auth/theme/ente_theme.dart'; +import 'package:ente_auth/ui/settings/data/import/google_auth_import.dart'; +import 'package:ente_auth/utils/toast_util.dart'; +import 'package:flutter/material.dart'; +import 'package:qr_code_scanner/qr_code_scanner.dart'; + +class ScannerGoogleAuthPage extends StatefulWidget { + const ScannerGoogleAuthPage({Key? key}) : super(key: key); + + @override + State createState() => ScannerGoogleAuthPageState(); +} + +class ScannerGoogleAuthPageState extends State { + final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); + QRViewController? controller; + String? totp; + + // In order to get hot reload to work we need to pause the camera if the platform + // is android, or resume the camera if the platform is iOS. + @override + void reassemble() { + super.reassemble(); + if (Platform.isAndroid) { + controller!.pauseCamera(); + } else if (Platform.isIOS) { + controller!.resumeCamera(); + } + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Scaffold( + appBar: AppBar( + title: Text(l10n.scan), + ), + body: Column( + children: [ + Expanded( + flex: 5, + child: QRView( + key: qrKey, + overlay: QrScannerOverlayShape( + borderColor: getEnteColorScheme(context).primary700), + onQRViewCreated: _onQRViewCreated, + formatsAllowed: const [BarcodeFormat.qrcode], + ), + ), + Expanded( + flex: 1, + child: Center( + child: (totp != null) ? Text(totp!) : Text(l10n.scanACode), + ), + ) + ], + ), + ); + } + + void _onQRViewCreated(QRViewController controller) { + this.controller = controller; + // h4ck to remove black screen on Android scanners: https://github.com/juliuscanute/qr_code_scanner/issues/560#issuecomment-1159611301 + if (Platform.isAndroid) { + controller.pauseCamera(); + controller.resumeCamera(); + } + controller.scannedDataStream.listen((scanData) { + try { + if (scanData.code == null) { + return; + } + if (scanData.code!.startsWith(kGoogleAuthExportPrefix)) { + List codes = parseGoogleAuth(scanData.code!); + controller.dispose(); + Navigator.of(context).pop(codes); + } else { + showToast(context, "Invalid QR code"); + } + } catch (e) { + controller.dispose(); + Navigator.of(context).pop(); + showToast(context, "Error " + e.toString()); + } + }); + } + + @override + void dispose() { + controller?.dispose(); + super.dispose(); + } +} diff --git a/lib/ui/settings/data/import/google_auth_import.dart b/lib/ui/settings/data/import/google_auth_import.dart new file mode 100644 index 000000000..f1ea5a35a --- /dev/null +++ b/lib/ui/settings/data/import/google_auth_import.dart @@ -0,0 +1,145 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:base32/base32.dart'; +import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/models/code.dart'; +import 'package:ente_auth/models/protos/googleauth.pb.dart'; +import 'package:ente_auth/services/authenticator_service.dart'; +import 'package:ente_auth/store/code_store.dart'; +import 'package:ente_auth/ui/components/buttons/button_widget.dart'; +import 'package:ente_auth/ui/components/dialog_widget.dart'; +import 'package:ente_auth/ui/components/models/button_type.dart'; +import 'package:ente_auth/ui/scanner_gauth_page.dart'; +import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; + +const kGoogleAuthExportPrefix = 'otpauth-migration://offline?data='; + +Future showGoogleAuthInstruction(BuildContext context) async { + final l10n = context.l10n; + final result = await showDialogWidget( + context: context, + title: l10n.importFromApp("Google Authenticator"), + body: l10n.importGoogleAuthGuide, + buttons: [ + ButtonWidget( + buttonType: ButtonType.primary, + labelText: l10n.scanAQrCode, + isInAlert: true, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.first, + ), + ButtonWidget( + buttonType: ButtonType.secondary, + labelText: context.l10n.cancel, + buttonSize: ButtonSize.large, + isInAlert: true, + buttonAction: ButtonAction.second, + ), + ], + ); + if (result?.action != null && result!.action != ButtonAction.cancel) { + if (result.action == ButtonAction.first) { + final List? codes = await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return const ScannerGoogleAuthPage(); + }, + ), + ); + if (codes == null || codes.isEmpty) { + return; + } + for (final code in codes) { + await CodeStore.instance.addCode(code, shouldSync: false); + } + unawaited(AuthenticatorService.instance.sync()); + final DialogWidget dialog = choiceDialog( + title: context.l10n.importSuccessTitle, + body: context.l10n.importSuccessDesc(codes.length ?? 0), + firstButtonLabel: l10n.ok, + firstButtonType: ButtonType.primary, + ); + await showConfettiDialog( + context: context, + dialogBuilder: (BuildContext context) { + return dialog; + }, + ); + } + } +} + +List parseGoogleAuth(String qrCodeData) { + try { + List codes = []; + final String payload = qrCodeData.substring(kGoogleAuthExportPrefix.length); + debugPrint("GoogleAuthImport: payload: $payload"); + final Uint8List base64Decoded = base64Decode(Uri.decodeComponent(payload)); + final MigrationPayload mPayload = + MigrationPayload.fromBuffer(base64Decoded); + for (var otpParameter in mPayload.otpParameters) { + // Build the OTP URL + String otpUrl; + String issuer = otpParameter.issuer; + String account = otpParameter.name; + var counter = otpParameter.counter; + // Create a list of bytes from the list of integers. + Uint8List bytes = Uint8List.fromList(otpParameter.secret); + + // Encode the bytes to base 32. + String base32String = base32.encode(bytes); + String secret = base32String; + // identify digit count + int digits = 6; + int timer = 30; // default timer, no field in Google Auth + Algorithm algorithm = Algorithm.sha1; + switch (otpParameter.algorithm) { + case MigrationPayload_Algorithm.ALGORITHM_MD5: + throw Exception('GoogleAuthImport: MD5 is not supported'); + case MigrationPayload_Algorithm.ALGORITHM_SHA1: + algorithm = Algorithm.sha1; + break; + case MigrationPayload_Algorithm.ALGORITHM_SHA256: + algorithm = Algorithm.sha256; + break; + case MigrationPayload_Algorithm.ALGORITHM_SHA512: + algorithm = Algorithm.sha512; + break; + case MigrationPayload_Algorithm.ALGORITHM_UNSPECIFIED: + algorithm = Algorithm.sha1; + break; + } + switch (otpParameter.digits) { + case MigrationPayload_DigitCount.DIGIT_COUNT_EIGHT: + digits = 8; + break; + case MigrationPayload_DigitCount.DIGIT_COUNT_SIX: + digits = 6; + break; + case MigrationPayload_DigitCount.DIGIT_COUNT_UNSPECIFIED: + digits = 6; + } + + if (otpParameter.type == MigrationPayload_OtpType.OTP_TYPE_TOTP || + otpParameter.type == MigrationPayload_OtpType.OTP_TYPE_UNSPECIFIED) { + otpUrl = + 'otpauth://totp/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&period=$timer'; + } else if (otpParameter.type == MigrationPayload_OtpType.OTP_TYPE_HOTP) { + otpUrl = + 'otpauth://hotp/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&counter=$counter'; + } else { + throw Exception('Invalid OTP type'); + } + codes.add(Code.fromRawData(otpUrl)); + } + return codes; + } catch (e, s) { + Logger("GoogleAuthImport") + .severe("Error while parsing Google Auth QR code", e, s); + throw Exception('Failed to parse Google Auth QR code \n ${e.toString()}'); + } +} diff --git a/lib/ui/settings/data/import/import_service.dart b/lib/ui/settings/data/import/import_service.dart index dd2616579..0d4782e82 100644 --- a/lib/ui/settings/data/import/import_service.dart +++ b/lib/ui/settings/data/import/import_service.dart @@ -1,5 +1,6 @@ import 'package:ente_auth/ui/settings/data/import/encrypted_ente_import.dart'; +import 'package:ente_auth/ui/settings/data/import/google_auth_import.dart'; import 'package:ente_auth/ui/settings/data/import/plain_text_import.dart'; import 'package:ente_auth/ui/settings/data/import/raivo_plain_text_import.dart'; import 'package:ente_auth/ui/settings/data/import_page.dart'; @@ -12,12 +13,22 @@ class ImportService { ImportService._internal(); Future initiateImport(BuildContext context,ImportType type) async { - if(type == ImportType.plainText) { - showImportInstructionDialog(context); - } else if(type == ImportType.ravio) { - showRaivoImportInstruction(context); - } else { - showEncryptedImportInstruction(context); + switch(type) { + + case ImportType.plainText: + showImportInstructionDialog(context); + break; + case ImportType.encrypted: + showEncryptedImportInstruction(context); + break; + case ImportType.ravio: + showRaivoImportInstruction(context); + break; + case ImportType.googleAuthenticator: + showGoogleAuthInstruction(context); + + // showToast(context, 'coming soon'); + break; } } } \ No newline at end of file diff --git a/lib/ui/settings/data/import/raivo_plain_text_import.dart b/lib/ui/settings/data/import/raivo_plain_text_import.dart index 5664d0e43..e118a85ec 100644 --- a/lib/ui/settings/data/import/raivo_plain_text_import.dart +++ b/lib/ui/settings/data/import/raivo_plain_text_import.dart @@ -53,52 +53,23 @@ Future _pickRaivoJsonFile(BuildContext context) async { final progressDialog = createProgressDialog(context, l10n.pleaseWait); await progressDialog.show(); try { - File file = File(result.files.single.path!); - final jsonString = await file.readAsString(); - List jsonArray = jsonDecode(jsonString); - final parsedCodes = []; - for (var item in jsonArray) { - var kind = item['kind']; - var algorithm = item['algorithm']; - var timer = item['timer']; - var digits = item['digits']; - var issuer = item['issuer']; - var secret = item['secret']; - var account = item['account']; - var counter = item['counter']; - - // Build the OTP URL - String otpUrl; - - if (kind.toLowerCase() == 'totp') { - otpUrl = - 'otpauth://$kind/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&period=$timer'; - } else if (kind.toLowerCase() == 'hotp') { - otpUrl = - 'otpauth://$kind/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&counter=$counter'; - } else { - throw Exception('Invalid OTP type'); - } - parsedCodes.add(Code.fromRawData(otpUrl)); - } - - for (final code in parsedCodes) { - await CodeStore.instance.addCode(code, shouldSync: false); - } - unawaited(AuthenticatorService.instance.sync()); + String path = result.files.single.path!; + int? count = await _processRaivoExportFile(context, path); await progressDialog.hide(); - final DialogWidget dialog = choiceDialog( - title: context.l10n.importSuccessTitle, - body: context.l10n.importSuccessDesc(parsedCodes.length), - firstButtonLabel: l10n.ok, - firstButtonType: ButtonType.primary, - ); - await showConfettiDialog( - context: context, - dialogBuilder: (BuildContext context) { - return dialog; - }, - ); + if(count != null) { + final DialogWidget dialog = choiceDialog( + title: context.l10n.importSuccessTitle, + body: context.l10n.importSuccessDesc(count ?? 0), + firstButtonLabel: l10n.ok, + firstButtonType: ButtonType.primary, + ); + await showConfettiDialog( + context: context, + dialogBuilder: (BuildContext context) { + return dialog; + }, + ); + } } catch (e) { await progressDialog.hide(); await showErrorDialog( @@ -108,3 +79,49 @@ Future _pickRaivoJsonFile(BuildContext context) async { ); } } + +Future _processRaivoExportFile(BuildContext context,String path) async { + File file = File(path); + if(path.endsWith('.zip')) { + await showErrorDialog( + context, + context.l10n.sorry, + "We don't support zip files yet. Please unzip the file and try again.", + ); + return null; + } + final jsonString = await file.readAsString(); + List jsonArray = jsonDecode(jsonString); + final parsedCodes = []; + for (var item in jsonArray) { + var kind = item['kind']; + var algorithm = item['algorithm']; + var timer = item['timer']; + var digits = item['digits']; + var issuer = item['issuer']; + var secret = item['secret']; + var account = item['account']; + var counter = item['counter']; + + // Build the OTP URL + String otpUrl; + + if (kind.toLowerCase() == 'totp') { + otpUrl = + 'otpauth://$kind/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&period=$timer'; + } else if (kind.toLowerCase() == 'hotp') { + otpUrl = + 'otpauth://$kind/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&counter=$counter'; + } else { + throw Exception('Invalid OTP type'); + } + parsedCodes.add(Code.fromRawData(otpUrl)); + } + + for (final code in parsedCodes) { + await CodeStore.instance.addCode(code, shouldSync: false); + } + unawaited(AuthenticatorService.instance.sync()); + int count = parsedCodes.length; + return count; +} diff --git a/lib/ui/settings/data/import_page.dart b/lib/ui/settings/data/import_page.dart index e08c8776f..7e17ae81c 100644 --- a/lib/ui/settings/data/import_page.dart +++ b/lib/ui/settings/data/import_page.dart @@ -14,6 +14,7 @@ enum ImportType { plainText, encrypted, ravio, + googleAuthenticator, } class ImportCodePage extends StatelessWidget { @@ -21,6 +22,7 @@ class ImportCodePage extends StatelessWidget { ImportType.plainText, ImportType.encrypted, ImportType.ravio, + ImportType.googleAuthenticator, ]; ImportCodePage({super.key}); @@ -32,7 +34,9 @@ class ImportCodePage extends StatelessWidget { case ImportType.encrypted: return context.l10n.importTypeEnteEncrypted; case ImportType.ravio: - return 'Ravio OTP'; + return 'Raivo OTP'; + case ImportType.googleAuthenticator: + return 'Google Authenticator'; } } diff --git a/protos/googleauth.proto b/protos/googleauth.proto new file mode 100644 index 000000000..5a449c63e --- /dev/null +++ b/protos/googleauth.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; +package googleauth; + +message MigrationPayload { + enum Algorithm { + ALGORITHM_UNSPECIFIED = 0; + ALGORITHM_SHA1 = 1; + ALGORITHM_SHA256 = 2; + ALGORITHM_SHA512 = 3; + ALGORITHM_MD5 = 4; + } + + enum DigitCount { + DIGIT_COUNT_UNSPECIFIED = 0; + DIGIT_COUNT_SIX = 1; + DIGIT_COUNT_EIGHT = 2; + } + + enum OtpType { + OTP_TYPE_UNSPECIFIED = 0; + OTP_TYPE_HOTP = 1; + OTP_TYPE_TOTP = 2; + } + + message OtpParameters { + bytes secret = 1; + string name = 2; + string issuer = 3; + Algorithm algorithm = 4; + DigitCount digits = 5; + OtpType type = 6; + int64 counter = 7; + } + + repeated OtpParameters otp_parameters = 1; + int32 version = 2; + int32 batch_size = 3; + int32 batch_index = 4; + int32 batch_id = 5; +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 7f17a478f..e09d0238a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -26,7 +26,7 @@ packages: source: hosted version: "5.13.0" archive: - dependency: transitive + dependency: "direct main" description: name: archive sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" @@ -50,7 +50,7 @@ packages: source: hosted version: "2.10.0" base32: - dependency: transitive + dependency: "direct main" description: name: base32 sha256: ddad4ebfedf93d4500818ed8e61443b734ffe7cf8a45c668c9b34ef6adde02e2 @@ -1063,6 +1063,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.4" + protobuf: + dependency: "direct main" + description: + name: protobuf + sha256: "4034a02b7e231e7e60bff30a8ac13a7347abfdac0798595fae0b90a3f0afe759" + url: "https://pub.dev" + source: hosted + version: "3.0.0" provider: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c40330e77..d44a55bd0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,8 @@ environment: dependencies: adaptive_theme: ^3.1.0 # done + archive: ^3.3.7 + base32: ^2.1.3 bip39: ^1.0.6 #done bloc: ^8.0.3 #done clipboard: ^0.1.3 @@ -58,6 +60,7 @@ dependencies: path_provider: ^2.0.11 pinput: ^1.2.2 pointycastle: ^3.7.3 + protobuf: ^3.0.0 qr_code_scanner: ^1.0.1 sentry: ^6.12.1 sentry_flutter: ^6.12.1