Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f8673b65b | ||
|
|
a5caa06944 | ||
|
|
457c6ffd2f | ||
|
|
1d960fadce | ||
|
|
cba39f4ef5 | ||
|
|
9f270cf471 | ||
|
|
5b8ccea31a | ||
|
|
92201eb4ea | ||
|
|
9d519d90c7 | ||
|
|
4d34ab7a01 | ||
|
|
3c25216116 | ||
|
|
5d0007d749 | ||
|
|
1afd2ce27e | ||
|
|
438193ac39 | ||
|
|
286940a5d1 | ||
|
|
50ebcdd1f0 | ||
|
|
53c406f30c | ||
|
|
acbbd8d049 | ||
|
|
5c9e879628 | ||
|
|
4b633d1a02 | ||
|
|
b95ed5fbb5 | ||
|
|
ffcd4022ea | ||
|
|
aa525bfeb1 | ||
|
|
18d098b310 | ||
|
|
57930b2dd8 | ||
|
|
0d0c89900a | ||
|
|
4893753b19 | ||
|
|
a594d1c962 | ||
|
|
2e3b6b27de | ||
|
|
326653054d | ||
|
|
a7818fc6d4 | ||
|
|
5e327a7d65 | ||
|
|
9c0aea66ec | ||
|
|
523f216b61 | ||
|
|
fb0fa73c03 | ||
|
|
535109d08f | ||
|
|
a02cfef105 | ||
|
|
2ff8963c52 | ||
|
|
117d397d77 | ||
|
|
c5b3808dee | ||
|
|
7cd5b313de | ||
|
|
8cbba78bcb | ||
|
|
9d95bd16cd | ||
|
|
f9eb7c11fd | ||
|
|
e5c8e90889 | ||
|
|
a4941d9975 | ||
|
|
69f09c896c | ||
|
|
7f7954d4ea | ||
|
|
f736357b04 | ||
|
|
c413564a4b | ||
|
|
445f3f20f0 | ||
|
|
05a36b051d | ||
|
|
75843814bd | ||
|
|
c3a38b5002 | ||
|
|
615809e383 | ||
|
|
10ca447f54 | ||
|
|
7fefadc808 | ||
|
|
991f7ce302 | ||
|
|
864fbd015d | ||
|
|
427cf73d9d | ||
|
|
8428af3b40 | ||
|
|
c117473ce8 | ||
|
|
a90670e404 | ||
|
|
c758704a2e | ||
|
|
0a010994ee | ||
|
|
d965a0239f | ||
|
|
a773f999fa | ||
|
|
3b710e9274 | ||
|
|
08090f6873 | ||
|
|
a59c5539b2 | ||
|
|
75fb88ba1d | ||
|
|
e8416728b4 | ||
|
|
d0481775ad | ||
|
|
2960cb7bac | ||
|
|
2b43825eef | ||
|
|
814fc98274 | ||
|
|
1ba113147f | ||
|
|
630c3f8ec8 | ||
|
|
f975895e4d | ||
|
|
456e46052d | ||
|
|
0d9223e6ea | ||
|
|
cdd8d7906b | ||
|
|
945d6d6728 | ||
|
|
7d8a85b861 | ||
|
|
54817cc100 | ||
|
|
34a39a2a86 | ||
|
|
52e9567a12 | ||
|
|
0a05fc917b |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Flutter CI
|
||||
name: release
|
||||
|
||||
# This workflow is triggered on pushes to the repository.
|
||||
on:
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.7.3"
|
||||
flutter-version: "3.10.6"
|
||||
|
||||
# Fetch sub modules
|
||||
- run: git submodule update --init --recursive
|
||||
|
||||
56
CONTRIBUTING.md
Normal file
56
CONTRIBUTING.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for showing interest in contributing to ente Authenticator. There are a couple of ways to help
|
||||
out. This document contains some general guidelines for each type of
|
||||
contribution.
|
||||
|
||||
|
||||
## Translations
|
||||
[](https://crowdin.com/project/ente-authenticator-app)
|
||||
|
||||
We use [Crowdin](https://crowdin.com/project/ente-authenticator-app) to crowdsource
|
||||
translations of ente Authenticator.
|
||||
If your language is not listed for translation, feel free to [create a GitHub issue](https://github.com/ente-io/auth/issues/new?title=Request+for+New+Language+Translation&body=Language+name%3A) to have it added.
|
||||
|
||||
## Icons
|
||||
|
||||
ente Auth supports the icon pack provided by
|
||||
[simple-icons](https://github.com/simple-icons/simple-icons).
|
||||
|
||||
If you would like to add your own custom icon, please open a pull-request
|
||||
with the relevant SVG and color
|
||||
code ([example PR](https://github.com/ente-io/auth/pull/213/files)).
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
If you're planning on adding a new feature or making other changes, please
|
||||
discuss it with us by creating [an
|
||||
issue](https://github.com/ente-io/auth/issues/new)
|
||||
on GitHub. Discussing your idea with us first ensures that everyone is on the
|
||||
same page before you start working on your change.
|
||||
|
||||
### 💻 Setup
|
||||
|
||||
1. [Install Flutter v3.10.6](https://flutter.dev/docs/get-started/install)
|
||||
2. Clone this repository with `git clone git@github.com:ente-io/auth.git`
|
||||
3. Pull in all submodules with `git submodule update --init --recursive`
|
||||
4. For Android, run
|
||||
```bash
|
||||
flutter run -t lib/main.dart --flavor independent
|
||||
```
|
||||
5. For iOS, run `flutter run`
|
||||
|
||||
|
||||
#### Localization
|
||||
If the feature requires adding new strings, you can do that by following these steps:
|
||||
|
||||
1. Add a new entry inside [app_en.arb](https://github.com/ente-io/auth/blob/main/lib/l10n/arb/app_en.arb) (Remember to save)
|
||||
2. In your dart file, add follwing import
|
||||
```dart
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
```
|
||||
3. Refer to the string using `context.l10n.<keyName>`. For example
|
||||
```dart
|
||||
context.l10n.account
|
||||
```
|
||||
56
README.md
56
README.md
@@ -1,5 +1,9 @@
|
||||
# ente Authenticator
|
||||
|
||||

|
||||
[](https://crowdin.com/project/ente-authenticator-app)
|
||||
[](https://matrix.to/#/#ente:matrix.org)
|
||||
|
||||
ente's Authenticator app helps you generate and store 2 step verification (2FA)
|
||||
tokens on your mobile devices.
|
||||
|
||||
@@ -25,22 +29,23 @@ your devices. Every new device you sign into will have access to these tokens.
|
||||
### Offline Mode
|
||||
|
||||
ente generates 2FA tokens offline, so your network connectivity will not get in
|
||||
the way of your workflow.
|
||||
the way of your workflow. If you wish to use the app without an account for e2ee
|
||||
backups, you can do that as well.
|
||||
|
||||
### Import and Export Tokens
|
||||
|
||||
You can add tokens to ente by one of the following methods:
|
||||
1. Scanning a QR code
|
||||
2. Manually entering (copy-pasting) a 2FA secret
|
||||
3. Bulk importing from a file that contains a list of codes in the following
|
||||
format:
|
||||
```
|
||||
otpauth://totp/provider.com:you@email.com?secret=YOUR_SECRET
|
||||
```
|
||||
The codes maybe separated by new lines or commas.
|
||||
3. Bulk importing from other popular 2FA apps and from a file that contains a list of codes in the following format:.
|
||||
```
|
||||
otpauth://totp/provider.com:you@email.com?secret=YOUR_SECRET
|
||||
```
|
||||
|
||||
You can also export the codes you have added to ente, to an **unencrypted** text
|
||||
file, that adheres to the above format.
|
||||
You can export the codes you have added to ente, to either an **encrypted** or
|
||||
plain text file. The encrypted backups can be accessed through the app, as well
|
||||
as a standalone CLI tool. [Read
|
||||
more](migration-guides/encrypted_export.md#how-to-use-the-exported-data).
|
||||
|
||||
|
||||
## 📲 Download
|
||||
@@ -71,16 +76,10 @@ src="https://ente.io/static/ed265c3abdcd3efa5e29f64b927bcb44/e230a/play-store-ba
|
||||
src="https://user-images.githubusercontent.com/1161789/154795157-c4468ff9-97fd-46f3-87fe-dca789d8733a.png">
|
||||
</a>
|
||||
|
||||
### Web
|
||||
### Web / Desktop
|
||||
|
||||
You can view your 2FA codes at [auth.ente.io](https://auth.ente.io). For adding or managing your secrets, please use our mobile app.
|
||||
|
||||
### Desktop
|
||||
|
||||
Support for desktop platforms (Linux, Windows and older Macs) is a [work in
|
||||
progress](https://github.com/ente-io/auth/tree/desktop).
|
||||
|
||||
Please ⭐ this repo to be notified of updates.
|
||||
|
||||
|
||||
## 🔩 Architecture
|
||||
@@ -89,7 +88,7 @@ The architecture that powers end-to-end encrypted storage and sync of your
|
||||
tokens has been documented [here](architecture/README.md).
|
||||
|
||||
|
||||
## 🧑💻 Building from source
|
||||
## 🧑💻 Build from source
|
||||
|
||||
1. [Install Flutter](https://flutter.dev/docs/get-started/install)
|
||||
2. Clone this repository with `git clone git@github.com:ente-io/auth.git`
|
||||
@@ -101,28 +100,27 @@ For maintainers, there is [additional documentation](RELEASES.md) on
|
||||
automatically publishing the main branch to App store, Play store and GitHub
|
||||
releases.
|
||||
|
||||
## 🙂 Icons
|
||||
|
||||
ente Auth supports the icon pack provided by
|
||||
[simple-icons](https://github.com/simple-icons/simple-icons).
|
||||
## 🧑🔧 Contribute
|
||||
|
||||
If you would like to add your own custom icon, please create a pull-request with
|
||||
an SVG for your service that matches the contents within `assets/custom-icons`.
|
||||
Please refer to our [contribution guide](./CONTRIBUTING.md) if you wish to add
|
||||
an icon, assist with translation, or develop new features.
|
||||
|
||||
## 🙋♂️ Support
|
||||
You can also support us by giving this project a ⭐ star on GitHub or by leaving
|
||||
a review on
|
||||
[PlayStore](https://play.google.com/store/apps/details?id=io.ente.auth),
|
||||
[AppStore](https://apps.apple.com/us/app/ente-authenticator/id6444121398) or
|
||||
[AlternativeTo](https://alternativeto.net/software/ente-authenticator/).
|
||||
|
||||
|
||||
## 🙋♂️ Help
|
||||
|
||||
If you need help, please reach out to support@ente.io, and a human will get in
|
||||
touch with you.
|
||||
|
||||
If you have feature requests, please create a [GitHub issue](https://github.com/ente-io/auth/issues/).
|
||||
|
||||
If you wish to support us, please ⭐
|
||||
[star](https://github.com/ente-io/auth/stargazers) this project.
|
||||
|
||||
## 🙌 Translation
|
||||
[](https://crowdin.com/project/ente-authenticator-app)
|
||||
|
||||
If you're interested in helping out with translation, please visit our [Crowdin project](https://crowdin.com/project/ente-authenticator-app) to get started. Thank you for your support.
|
||||
|
||||
## 💜 Community
|
||||
|
||||
|
||||
@@ -24,6 +24,6 @@ subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
{
|
||||
"title": "ente",
|
||||
"hex": "1DB954"
|
||||
},
|
||||
{
|
||||
"title": "Github",
|
||||
"hex": "858585"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
1
assets/custom-icons/icons/github.svg
Normal file
1
assets/custom-icons/icons/github.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||
|
After Width: | Height: | Size: 822 B |
2
flutter
2
flutter
Submodule flutter updated: b06b8b2710...f468f3366c
@@ -81,14 +81,16 @@ PODS:
|
||||
- Flutter
|
||||
- MTBBarcodeScanner
|
||||
- Reachability (3.2)
|
||||
- SDWebImage (5.15.0):
|
||||
- SDWebImage/Core (= 5.15.0)
|
||||
- SDWebImage/Core (5.15.0)
|
||||
- Sentry/HybridSDK (7.31.5)
|
||||
- SDWebImage (5.17.0):
|
||||
- SDWebImage/Core (= 5.17.0)
|
||||
- SDWebImage/Core (5.17.0)
|
||||
- Sentry/HybridSDK (8.9.1):
|
||||
- SentryPrivate (= 8.9.1)
|
||||
- sentry_flutter (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 7.31.5)
|
||||
- Sentry/HybridSDK (= 8.9.1)
|
||||
- SentryPrivate (8.9.1)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@@ -121,11 +123,11 @@ DEPENDENCIES:
|
||||
- move_to_background (from `.symlinks/plugins/move_to_background/ios`)
|
||||
- open_filex (from `.symlinks/plugins/open_filex/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
|
||||
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- uni_links (from `.symlinks/plugins/uni_links/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
@@ -140,6 +142,7 @@ SPEC REPOS:
|
||||
- Reachability
|
||||
- SDWebImage
|
||||
- Sentry
|
||||
- SentryPrivate
|
||||
- SwiftyGif
|
||||
- Toast
|
||||
|
||||
@@ -177,7 +180,7 @@ EXTERNAL SOURCES:
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/ios"
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
qr_code_scanner:
|
||||
:path: ".symlinks/plugins/qr_code_scanner/ios"
|
||||
sentry_flutter:
|
||||
@@ -185,7 +188,7 @@ EXTERNAL SOURCES:
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/ios"
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
uni_links:
|
||||
@@ -214,13 +217,14 @@ SPEC CHECKSUMS:
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
|
||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
|
||||
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
|
||||
SDWebImage: 9bec4c5cdd9579e1f57104735ee0c37df274d593
|
||||
Sentry: 4c9babff9034785067c896fd580b1f7de44da020
|
||||
sentry_flutter: 1346a880b24c0240807b53b10cf50ddad40f504e
|
||||
SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9
|
||||
Sentry: e3203780941722a1fcfee99e351de14244c7f806
|
||||
sentry_flutter: 8f0ffd53088e6a4d50c095852c5cad9e4405025c
|
||||
SentryPrivate: 5e3683390f66611fc7c6215e27645873adb55d13
|
||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
@@ -129,7 +128,8 @@ class _AppState extends State<App> {
|
||||
|
||||
Map<String, WidgetBuilder> get _getRoutes {
|
||||
return {
|
||||
"/": (context) => Configuration.instance.hasConfiguredAccount()
|
||||
"/": (context) => Configuration.instance.hasConfiguredAccount() ||
|
||||
Configuration.instance.hasOptedForOfflineMode()
|
||||
? const HomePage()
|
||||
: const OnboardingPage(),
|
||||
};
|
||||
|
||||
@@ -38,9 +38,7 @@ final darkTheme = ThemeData(
|
||||
fontFamily: "Inter",
|
||||
brightness: Brightness.dark,
|
||||
scaffoldBackgroundColor: Colors.black,
|
||||
backgroundColor: Colors.black,
|
||||
appBarTheme: const AppBarTheme(color: Colors.orange),
|
||||
colorScheme: const ColorScheme.dark(primary: Colors.white),
|
||||
textTheme: _buildTextTheme(Colors.white),
|
||||
outlinedButtonTheme: buildOutlinedButtonThemeData(
|
||||
bgDisabled: Colors.grey.shade500,
|
||||
@@ -59,25 +57,25 @@ final darkTheme = ThemeData(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
), colorScheme: const ColorScheme.dark(primary: Colors.white).copyWith(background: Colors.black),
|
||||
);
|
||||
|
||||
TextTheme _buildTextTheme(Color textColor) {
|
||||
return const TextTheme().copyWith(
|
||||
headline4: TextStyle(
|
||||
headlineMedium: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
headline5: TextStyle(
|
||||
headlineSmall: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
// AG: Body
|
||||
headline6: TextStyle(
|
||||
titleLarge: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 18,
|
||||
fontFamily: "Inter",
|
||||
@@ -92,31 +90,31 @@ TextTheme _buildTextTheme(Color textColor) {
|
||||
height: 28,
|
||||
),
|
||||
|
||||
subtitle1: TextStyle(
|
||||
titleMedium: TextStyle(
|
||||
color: textColor,
|
||||
fontFamily: "Inter",
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
subtitle2: TextStyle(
|
||||
titleSmall: TextStyle(
|
||||
color: textColor,
|
||||
fontFamily: "Inter",
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
bodyText1: TextStyle(
|
||||
bodyLarge: TextStyle(
|
||||
fontFamily: "Inter",
|
||||
color: textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
bodyText2: TextStyle(
|
||||
bodyMedium: TextStyle(
|
||||
fontFamily: "Inter",
|
||||
color: textColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
caption: TextStyle(
|
||||
bodySmall: TextStyle(
|
||||
color: textColor.withOpacity(0.6),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io' as io;
|
||||
import 'dart:typed_data';
|
||||
@@ -29,15 +30,23 @@ class Configuration {
|
||||
);
|
||||
static const emailKey = "email";
|
||||
static const keyAttributesKey = "key_attributes";
|
||||
static const keyKey = "key";
|
||||
|
||||
static const keyShouldShowLockScreen = "should_show_lock_screen";
|
||||
static const lastTempFolderClearTimeKey = "last_temp_folder_clear_time";
|
||||
static const keyKey = "key";
|
||||
static const secretKeyKey = "secret_key";
|
||||
static const authSecretKeyKey = "auth_secret_key";
|
||||
static const offlineAuthSecretKey = "offline_auth_secret_key";
|
||||
static const tokenKey = "token";
|
||||
static const encryptedTokenKey = "encrypted_token";
|
||||
static const userIDKey = "user_id";
|
||||
static const hasMigratedSecureStorageKey = "has_migrated_secure_storage";
|
||||
static const hasOptedForOfflineModeKey = "has_opted_for_offline_mode";
|
||||
final List<String> onlineSecureKeys = [
|
||||
keyKey,
|
||||
secretKeyKey,
|
||||
authSecretKeyKey
|
||||
];
|
||||
|
||||
final kTempFolderDeletionTimeBuffer = const Duration(days: 1).inMicroseconds;
|
||||
|
||||
@@ -45,28 +54,20 @@ class Configuration {
|
||||
|
||||
String? _cachedToken;
|
||||
late String _documentsDirectory;
|
||||
String? _key;
|
||||
late SharedPreferences _preferences;
|
||||
String? _key;
|
||||
String? _secretKey;
|
||||
String? _authSecretKey;
|
||||
String? _offlineAuthKey;
|
||||
late FlutterSecureStorage _secureStorage;
|
||||
late String _tempDirectory;
|
||||
late String _thumbnailCacheDirectory;
|
||||
|
||||
// 6th July 22: Remove this after 3 months. Hopefully, active users
|
||||
// will migrate to newer version of the app, where shared media is stored
|
||||
// on appSupport directory which OS won't clean up automatically
|
||||
late String _sharedTempMediaDirectory;
|
||||
|
||||
late String _sharedDocumentsMediaDirectory;
|
||||
String? _volatilePassword;
|
||||
|
||||
final _secureStorageOptionsIOS = const IOSOptions(
|
||||
accessibility: KeychainAccessibility.first_unlock_this_device,
|
||||
);
|
||||
|
||||
// const IOSOptions(accessibility: IOSAccessibility.first_unlock);
|
||||
|
||||
Future<void> init() async {
|
||||
_preferences = await SharedPreferences.getInstance();
|
||||
_secureStorage = const FlutterSecureStorage();
|
||||
@@ -88,15 +89,27 @@ class Configuration {
|
||||
_logger.warning(e);
|
||||
}
|
||||
tempDirectory.createSync(recursive: true);
|
||||
final tempDirectoryPath = (await getTemporaryDirectory()).path;
|
||||
_thumbnailCacheDirectory = tempDirectoryPath + "/thumbnail-cache";
|
||||
io.Directory(_thumbnailCacheDirectory).createSync(recursive: true);
|
||||
_sharedTempMediaDirectory = tempDirectoryPath + "/ente-shared-media";
|
||||
io.Directory(_sharedTempMediaDirectory).createSync(recursive: true);
|
||||
_sharedDocumentsMediaDirectory = _documentsDirectory + "/ente-shared-media";
|
||||
io.Directory(_sharedDocumentsMediaDirectory).createSync(recursive: true);
|
||||
await _initOnlineAccount();
|
||||
await _initOfflineAccount();
|
||||
}
|
||||
|
||||
Future<void> _initOfflineAccount() async {
|
||||
_offlineAuthKey = await _secureStorage.read(
|
||||
key: offlineAuthSecretKey,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _initOnlineAccount() async {
|
||||
if (!_preferences.containsKey(tokenKey)) {
|
||||
await _secureStorage.deleteAll(iOptions: _secureStorageOptionsIOS);
|
||||
for (final key in onlineSecureKeys) {
|
||||
unawaited(
|
||||
_secureStorage.delete(
|
||||
key: key,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_key = await _secureStorage.read(
|
||||
key: keyKey,
|
||||
@@ -113,13 +126,17 @@ class Configuration {
|
||||
if (_key == null) {
|
||||
await logout(autoLogout: true);
|
||||
}
|
||||
await _migrateSecurityStorageToFirstUnlock();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout({bool autoLogout = false}) async {
|
||||
await _preferences.clear();
|
||||
await _secureStorage.deleteAll(iOptions: _secureStorageOptionsIOS);
|
||||
for (String key in onlineSecureKeys) {
|
||||
await _secureStorage.delete(
|
||||
key: key,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
}
|
||||
await AuthenticatorDB.instance.clearTable();
|
||||
_key = null;
|
||||
_cachedToken = null;
|
||||
@@ -179,8 +196,9 @@ class Configuration {
|
||||
return KeyGenResult(attributes, privateAttributes, loginKey);
|
||||
}
|
||||
|
||||
|
||||
Future<Tuple2<KeyAttributes, Uint8List>> getAttributesForNewPassword(String password) async {
|
||||
Future<Tuple2<KeyAttributes, Uint8List>> getAttributesForNewPassword(
|
||||
String password,
|
||||
) async {
|
||||
// Get master key
|
||||
final masterKey = getKey();
|
||||
|
||||
@@ -215,18 +233,16 @@ class Configuration {
|
||||
// SRP setup for existing users.
|
||||
Future<Uint8List> decryptSecretsAndGetKeyEncKey(
|
||||
String password,
|
||||
KeyAttributes attributes,
|
||||
{
|
||||
KeyAttributes attributes, {
|
||||
Uint8List? keyEncryptionKey,
|
||||
}
|
||||
) async {
|
||||
}) async {
|
||||
_logger.info('Start decryptAndSaveSecrets');
|
||||
keyEncryptionKey ??= await CryptoUtil.deriveKey(
|
||||
utf8.encode(password) as Uint8List,
|
||||
Sodium.base642bin(attributes.kekSalt),
|
||||
attributes.memLimit,
|
||||
attributes.opsLimit,
|
||||
);
|
||||
utf8.encode(password) as Uint8List,
|
||||
Sodium.base642bin(attributes.kekSalt),
|
||||
attributes.memLimit,
|
||||
attributes.opsLimit,
|
||||
);
|
||||
|
||||
_logger.info('user-key done');
|
||||
Uint8List key;
|
||||
@@ -356,52 +372,31 @@ class Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setKey(String? key) async {
|
||||
Future<void> setKey(String key) async {
|
||||
_key = key;
|
||||
if (key == null) {
|
||||
await _secureStorage.delete(
|
||||
key: keyKey,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
} else {
|
||||
await _secureStorage.write(
|
||||
key: keyKey,
|
||||
value: key,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
}
|
||||
await _secureStorage.write(
|
||||
key: keyKey,
|
||||
value: key,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setSecretKey(String? secretKey) async {
|
||||
_secretKey = secretKey;
|
||||
if (secretKey == null) {
|
||||
await _secureStorage.delete(
|
||||
key: secretKeyKey,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
} else {
|
||||
await _secureStorage.write(
|
||||
key: secretKeyKey,
|
||||
value: secretKey,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
}
|
||||
await _secureStorage.write(
|
||||
key: secretKeyKey,
|
||||
value: secretKey,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setAuthSecretKey(String? authSecretKey) async {
|
||||
_authSecretKey = authSecretKey;
|
||||
if (authSecretKey == null) {
|
||||
await _secureStorage.delete(
|
||||
key: authSecretKeyKey,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
} else {
|
||||
await _secureStorage.write(
|
||||
key: authSecretKeyKey,
|
||||
value: authSecretKey,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
}
|
||||
await _secureStorage.write(
|
||||
key: authSecretKeyKey,
|
||||
value: authSecretKey,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List? getKey() {
|
||||
@@ -416,6 +411,10 @@ class Configuration {
|
||||
return _authSecretKey == null ? null : Sodium.base642bin(_authSecretKey!);
|
||||
}
|
||||
|
||||
Uint8List? getOfflineSecretKey() {
|
||||
return _offlineAuthKey == null ? null : Sodium.base642bin(_offlineAuthKey!);
|
||||
}
|
||||
|
||||
Uint8List getRecoveryKey() {
|
||||
final keyAttributes = getKeyAttributes()!;
|
||||
return CryptoUtil.decryptSync(
|
||||
@@ -430,22 +429,34 @@ class Configuration {
|
||||
return _tempDirectory;
|
||||
}
|
||||
|
||||
String getThumbnailCacheDirectory() {
|
||||
return _thumbnailCacheDirectory;
|
||||
}
|
||||
|
||||
String getOldSharedMediaCacheDirectory() {
|
||||
return _sharedTempMediaDirectory;
|
||||
}
|
||||
|
||||
String getSharedMediaDirectory() {
|
||||
return _sharedDocumentsMediaDirectory;
|
||||
}
|
||||
|
||||
bool hasConfiguredAccount() {
|
||||
return getToken() != null && _key != null;
|
||||
}
|
||||
|
||||
bool hasOptedForOfflineMode() {
|
||||
return _preferences.getBool(hasOptedForOfflineModeKey) ?? false;
|
||||
}
|
||||
|
||||
Future<void> optForOfflineMode() async {
|
||||
if ((await _secureStorage.containsKey(
|
||||
key: offlineAuthSecretKey,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
))) {
|
||||
_offlineAuthKey = await _secureStorage.read(
|
||||
key: offlineAuthSecretKey,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
} else {
|
||||
_offlineAuthKey = Sodium.bin2base64(CryptoUtil.generateKey());
|
||||
await _secureStorage.write(
|
||||
key: offlineAuthSecretKey,
|
||||
value: _offlineAuthKey,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
}
|
||||
await _preferences.setBool(hasOptedForOfflineModeKey, true);
|
||||
}
|
||||
|
||||
bool shouldShowLockScreen() {
|
||||
if (_preferences.containsKey(keyShouldShowLockScreen)) {
|
||||
return _preferences.getBool(keyShouldShowLockScreen)!;
|
||||
@@ -465,27 +476,4 @@ class Configuration {
|
||||
String? getVolatilePassword() {
|
||||
return _volatilePassword;
|
||||
}
|
||||
|
||||
Future<void> _migrateSecurityStorageToFirstUnlock() async {
|
||||
final hasMigratedSecureStorageToFirstUnlock =
|
||||
_preferences.getBool(hasMigratedSecureStorageKey) ?? false;
|
||||
if (!hasMigratedSecureStorageToFirstUnlock &&
|
||||
_key != null &&
|
||||
_secretKey != null) {
|
||||
await _secureStorage.write(
|
||||
key: keyKey,
|
||||
value: _key,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
await _secureStorage.write(
|
||||
key: secretKeyKey,
|
||||
value: _secretKey,
|
||||
iOptions: _secureStorageOptionsIOS,
|
||||
);
|
||||
await _preferences.setBool(
|
||||
hasMigratedSecureStorageKey,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ class SuperLogging {
|
||||
|
||||
static void setUserID(String userID) async {
|
||||
if (config.sentryDsn != null) {
|
||||
Sentry.configureScope((scope) => scope.user = SentryUser(id: userID));
|
||||
Sentry.configureScope((scope) => scope.setUser(SentryUser(id: userID)));
|
||||
$.info("setting sentry user ID to: $userID");
|
||||
}
|
||||
}
|
||||
@@ -310,7 +310,7 @@ class SuperLogging {
|
||||
if (_preferences.containsKey(keyShouldReportErrors)) {
|
||||
return _preferences.getBool(keyShouldReportErrors)!;
|
||||
} else {
|
||||
return false;
|
||||
return kDebugMode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ class TunneledTransport implements Transport {
|
||||
: _dsn = _options.dsn != null ? Dsn.parse(_options.dsn!) : null,
|
||||
_headers = _buildHeaders(
|
||||
_options.platformChecker.isWeb,
|
||||
_options.sdk.identifier,
|
||||
_options.sentryClientName,
|
||||
) {
|
||||
_credentialBuilder = _CredentialBuilder(
|
||||
_dsn,
|
||||
_options.sdk.identifier,
|
||||
_options.sentryClientName,
|
||||
_options.clock,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,11 +11,6 @@ final lightThemeData = ThemeData(
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
primaryIconTheme:
|
||||
const IconThemeData(color: Colors.red, opacity: 1.0, size: 50.0),
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: Colors.black,
|
||||
secondary: Color.fromARGB(255, 163, 163, 163),
|
||||
),
|
||||
accentColor: const Color.fromRGBO(0, 0, 0, 0.6),
|
||||
outlinedButtonTheme: buildOutlinedButtonThemeData(
|
||||
bgDisabled: const Color.fromRGBO(158, 158, 158, 1),
|
||||
bgEnabled: const Color.fromRGBO(0, 0, 0, 1),
|
||||
@@ -26,9 +21,7 @@ final lightThemeData = ThemeData(
|
||||
onPrimary: const Color.fromRGBO(255, 255, 255, 1),
|
||||
primary: const Color.fromRGBO(0, 0, 0, 1),
|
||||
),
|
||||
toggleableActiveColor: const Color.fromRGBO(102, 187, 106, 1),
|
||||
scaffoldBackgroundColor: const Color.fromRGBO(255, 255, 255, 1),
|
||||
backgroundColor: const Color.fromRGBO(255, 255, 255, 1),
|
||||
appBarTheme: const AppBarTheme().copyWith(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
@@ -38,8 +31,8 @@ final lightThemeData = ThemeData(
|
||||
//https://api.flutter.dev/flutter/material/TextTheme-class.html
|
||||
textTheme: _buildTextTheme(const Color.fromRGBO(0, 0, 0, 1)),
|
||||
primaryTextTheme: const TextTheme().copyWith(
|
||||
bodyText2: const TextStyle(color: Colors.yellow),
|
||||
bodyText1: const TextStyle(color: Colors.orange),
|
||||
bodyMedium: const TextStyle(color: Colors.yellow),
|
||||
bodyLarge: const TextStyle(color: Colors.orange),
|
||||
),
|
||||
cardColor: const Color.fromRGBO(250, 250, 250, 1.0),
|
||||
dialogTheme: const DialogTheme().copyWith(
|
||||
@@ -79,7 +72,27 @@ final lightThemeData = ThemeData(
|
||||
? const Color.fromRGBO(255, 255, 255, 1)
|
||||
: const Color.fromRGBO(0, 0, 0, 1);
|
||||
}),
|
||||
),
|
||||
), radioTheme: RadioThemeData(
|
||||
fillColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.disabled)) { return null; }
|
||||
if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
|
||||
return null;
|
||||
}),
|
||||
), switchTheme: SwitchThemeData(
|
||||
thumbColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.disabled)) { return null; }
|
||||
if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
|
||||
return null;
|
||||
}),
|
||||
trackColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.disabled)) { return null; }
|
||||
if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
|
||||
return null;
|
||||
}),
|
||||
), colorScheme: const ColorScheme.light(
|
||||
primary: Colors.black,
|
||||
secondary: Color.fromARGB(255, 163, 163, 163),
|
||||
).copyWith(background: const Color.fromRGBO(255, 255, 255, 1)),
|
||||
);
|
||||
|
||||
final darkThemeData = ThemeData(
|
||||
@@ -90,13 +103,10 @@ final darkThemeData = ThemeData(
|
||||
primaryIconTheme:
|
||||
const IconThemeData(color: Colors.red, opacity: 1.0, size: 50.0),
|
||||
hintColor: const Color.fromRGBO(158, 158, 158, 1),
|
||||
colorScheme: const ColorScheme.dark(primary: Colors.white),
|
||||
accentColor: const Color.fromRGBO(45, 194, 98, 0.2),
|
||||
buttonTheme: const ButtonThemeData().copyWith(
|
||||
buttonColor: const Color.fromRGBO(45, 194, 98, 1.0),
|
||||
),
|
||||
textTheme: _buildTextTheme(const Color.fromRGBO(255, 255, 255, 1)),
|
||||
toggleableActiveColor: const Color.fromRGBO(102, 187, 106, 1),
|
||||
outlinedButtonTheme: buildOutlinedButtonThemeData(
|
||||
bgDisabled: const Color.fromRGBO(158, 158, 158, 1),
|
||||
bgEnabled: const Color.fromRGBO(255, 255, 255, 1),
|
||||
@@ -108,7 +118,6 @@ final darkThemeData = ThemeData(
|
||||
primary: const Color.fromRGBO(255, 255, 255, 1),
|
||||
),
|
||||
scaffoldBackgroundColor: const Color.fromRGBO(0, 0, 0, 1),
|
||||
backgroundColor: const Color.fromRGBO(0, 0, 0, 1),
|
||||
appBarTheme: const AppBarTheme().copyWith(
|
||||
color: Colors.black,
|
||||
elevation: 0,
|
||||
@@ -155,59 +164,76 @@ final darkThemeData = ThemeData(
|
||||
return const Color.fromRGBO(158, 158, 158, 1);
|
||||
}
|
||||
}),
|
||||
),
|
||||
), radioTheme: RadioThemeData(
|
||||
fillColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.disabled)) { return null; }
|
||||
if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
|
||||
return null;
|
||||
}),
|
||||
), switchTheme: SwitchThemeData(
|
||||
thumbColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.disabled)) { return null; }
|
||||
if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
|
||||
return null;
|
||||
}),
|
||||
trackColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.disabled)) { return null; }
|
||||
if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
|
||||
return null;
|
||||
}),
|
||||
), colorScheme: const ColorScheme.dark(primary: Colors.white).copyWith(background: const Color.fromRGBO(0, 0, 0, 1)),
|
||||
);
|
||||
|
||||
TextTheme _buildTextTheme(Color textColor) {
|
||||
return const TextTheme().copyWith(
|
||||
headline4: TextStyle(
|
||||
headlineMedium: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'Inter',
|
||||
),
|
||||
headline5: TextStyle(
|
||||
headlineSmall: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'Inter',
|
||||
),
|
||||
headline6: TextStyle(
|
||||
titleLarge: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 18,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
subtitle1: TextStyle(
|
||||
titleMedium: TextStyle(
|
||||
color: textColor,
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
subtitle2: TextStyle(
|
||||
titleSmall: TextStyle(
|
||||
color: textColor,
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
bodyText1: TextStyle(
|
||||
bodyLarge: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
color: textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
bodyText2: TextStyle(
|
||||
bodyMedium: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
color: textColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
caption: TextStyle(
|
||||
bodySmall: TextStyle(
|
||||
color: textColor.withOpacity(0.4),
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overline: TextStyle(
|
||||
labelSmall: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
color: textColor,
|
||||
fontSize: 14,
|
||||
@@ -344,7 +370,7 @@ extension CustomColorScheme on ColorScheme {
|
||||
? const Color.fromRGBO(245, 245, 245, 1.0)
|
||||
: const Color.fromRGBO(30, 30, 30, 1.0);
|
||||
|
||||
Color get searchResultsCountTextColor => brightness == Brightness.light
|
||||
Color get mutedTextColor => brightness == Brightness.light
|
||||
? const Color.fromRGBO(80, 80, 80, 1)
|
||||
: const Color.fromRGBO(150, 150, 150, 1);
|
||||
|
||||
@@ -410,9 +436,7 @@ ElevatedButtonThemeData buildElevatedButtonThemeData({
|
||||
}) {
|
||||
return ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
elevation: elevation,
|
||||
onPrimary: onPrimary,
|
||||
primary: primary,
|
||||
foregroundColor: onPrimary, backgroundColor: primary, elevation: elevation,
|
||||
alignment: Alignment.center,
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"@counterAppBarTitle": {
|
||||
"description": "Text shown in the AppBar of the Counter Page"
|
||||
},
|
||||
"onBoardingBody": "Secure your 2FA codes",
|
||||
"onBoardingBody": "Securely backup your 2FA codes",
|
||||
"onBoardingGetStarted": "Get Started",
|
||||
"setupFirstAccount": "Setup your first account",
|
||||
"importScanQrCode": "Scan a QR Code",
|
||||
@@ -70,7 +70,7 @@
|
||||
"incorrectPasswordTitle": "Incorrect password",
|
||||
"welcomeBack": "Welcome back!",
|
||||
"madeWithLoveAtPrefix": "made with ❤️ at ",
|
||||
"supportDevs": "Subscribe to <bold-green>ente</bold-green> to support this project.",
|
||||
"supportDevs": "Subscribe to <bold-green>ente</bold-green> to support this project",
|
||||
"supportDiscount": "Use coupon code \"AUTH\" to get 10% off first year",
|
||||
"changeEmail": "Change email",
|
||||
"changePassword": "Change password",
|
||||
@@ -253,6 +253,10 @@
|
||||
"privacy": "Privacy",
|
||||
"terms": "Terms",
|
||||
"checkForUpdates": "Check for updates",
|
||||
"downloadUpdate": "Download",
|
||||
"criticalUpdateAvailable": "Critical update available",
|
||||
"updateAvailable": "Update available",
|
||||
"update": "Update",
|
||||
"checking": "Checking...",
|
||||
"youAreOnTheLatestVersion": "You are on the latest version",
|
||||
"warning": "Warning",
|
||||
@@ -316,5 +320,10 @@
|
||||
"encrypted": "Encrypted",
|
||||
"plainText": "Plain text",
|
||||
"passwordToEncryptExport": "Password to encrypt export",
|
||||
"export": "Export"
|
||||
"export": "Export",
|
||||
"useOffline": "Use without backups",
|
||||
"signInToBackup": "Sign in to backup your codes",
|
||||
"singIn": "Sign in",
|
||||
"sigInBackupReminder": "Please export your codes to ensure that you have a backup you can restore from.",
|
||||
"offlineModeWarning": "You have chosen to proceed without backups. Please take manual backups to make sure your codes are safe."
|
||||
}
|
||||
|
||||
@@ -68,10 +68,14 @@
|
||||
"changePassword": "Modifier le mot de passe",
|
||||
"data": "Données",
|
||||
"importCodes": "Importer les codes",
|
||||
"importSelectJsonFile": "Sélectionnez un fichier JSON",
|
||||
"importEnteEncGuide": "Sélectionnez le fichier JSON chiffré exporté à partir de ente",
|
||||
"exportCodes": "Exporter les codes",
|
||||
"importInstruction": "Veuillez sélectionner un fichier qui contient une liste de vos codes dans le format suivant",
|
||||
"importCodeDelimiterInfo": "Les codes peuvent être séparés par une virgule ou un retour à la ligne",
|
||||
"selectFile": "Sélectionner le fichier",
|
||||
"emailVerificationToggle": "Vérification de l'adresse e-mail",
|
||||
"authToChangeEmailVerificationSetting": "Veuillez vous authentifier pour modifier votre adresse e-mail",
|
||||
"authToViewYourRecoveryKey": "Veuillez vous authentifier pour afficher votre clé de récupération",
|
||||
"authToChangeYourEmail": "Veuillez vous authentifier pour modifier votre adresse e-mail",
|
||||
"authToChangeYourPassword": "Veuillez vous authentifier pour modifier votre mot de passe",
|
||||
@@ -90,6 +94,14 @@
|
||||
"enterYourPasswordHint": "Saisir votre mot de passe",
|
||||
"forgotPassword": "Mot de passe oublié",
|
||||
"oops": "Oups",
|
||||
"suggestFeatures": "Suggérer des fonctionnalités",
|
||||
"faq": "FAQ",
|
||||
"faq_q_2": "Puis-je accéder à mes codes sur mon ordinateur ?",
|
||||
"faq_a_2": "Vous pouvez accéder à vos codes sur le web via auth.ente.io.",
|
||||
"faq_q_3": "Comment puis-je supprimer des codes ?",
|
||||
"faq_a_3": "Vous pouvez supprimer un code en glissant vers la gauche.",
|
||||
"faq_q_4": "Comment puis-je soutenir le projet?",
|
||||
"faq_a_4": "Vous pouvez soutenir le développement de ce projet en vous abonnant à notre application Photos, ente.io.",
|
||||
"somethingWentWrongMessage": "Quelque chose s'est mal passé, veuillez recommencer",
|
||||
"leaveFamily": "Quitter le plan familial",
|
||||
"leaveFamilyMessage": "Êtes-vous certains de vouloir quitter le plan familial?",
|
||||
@@ -156,6 +168,7 @@
|
||||
"recoveryKeySaveDescription": "Nous ne stockons pas cette clé, veuillez enregistrer cette clé de 24 mots dans un endroit sûr.",
|
||||
"doThisLater": "Plus tard",
|
||||
"saveKey": "Enregistrer la clé",
|
||||
"back": "Retour",
|
||||
"createAccount": "Créer un compte",
|
||||
"passwordStrength": "Force du mot de passe : {passwordStrengthValue}",
|
||||
"@passwordStrength": {
|
||||
@@ -186,6 +199,7 @@
|
||||
"continueLabel": "Continuer",
|
||||
"insecureDevice": "Appareil non sécurisé",
|
||||
"sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Désolé, nous n'avons pas pu générer de clés sécurisées sur cet appareil.\n\nVeuillez vous inscrire depuis un autre appareil.",
|
||||
"howItWorks": "Comment ça fonctionne",
|
||||
"ackPasswordLostWarning": "Je comprends que si je perds mon mot de passe, je risque de perdre mes données puisque celles-ci sont <underline>chiffrées de bout en bout</underline>.",
|
||||
"loginTerms": "En cliquant sur \"Connexion\", j'accepte les <u-terms>conditions d'utilisation</u-terms> et la <u-policy>politique de confidentialité</u-policy>",
|
||||
"logInLabel": "Connexion",
|
||||
@@ -262,5 +276,19 @@
|
||||
"thisWillLogYouOutOfTheFollowingDevice": "Cela vous déconnectera de l'appareil suivant :",
|
||||
"terminateSession": "Quitter la session ?",
|
||||
"terminate": "Quitter",
|
||||
"thisDevice": "Cet appareil"
|
||||
"thisDevice": "Cet appareil",
|
||||
"toResetVerifyEmail": "Pour réinitialiser votre mot de passe, veuillez d'abord vérifier votre e-mail.",
|
||||
"thisEmailIsAlreadyInUse": "Cette adresse mail est déjà utilisé",
|
||||
"verificationFailedPleaseTryAgain": "La vérification a échouée, veuillez réessayer",
|
||||
"yourVerificationCodeHasExpired": "Votre code de vérification a expiré",
|
||||
"incorrectCode": "Code non valide",
|
||||
"sorryTheCodeYouveEnteredIsIncorrect": "Le code que vous avez saisi est incorrect",
|
||||
"emailChangedTo": "L'e-mail a été changé en {newEmail}",
|
||||
"authenticationFailedPleaseTryAgain": "L'authentification a échouée, veuillez réessayer",
|
||||
"authenticationSuccessful": "Authentification réussie!",
|
||||
"twofactorAuthenticationSuccessfullyReset": "L'authentification à deux facteurs a été réinitialisée avec succès ",
|
||||
"incorrectRecoveryKey": "Clé de récupération non valide",
|
||||
"theRecoveryKeyYouEnteredIsIncorrect": "La clé de récupération que vous avez entrée est incorrecte",
|
||||
"enterPassword": "Saisissez le mot de passe",
|
||||
"export": "Exporter"
|
||||
}
|
||||
@@ -85,6 +85,7 @@
|
||||
"importSelectJsonFile": "Seleziona file JSON",
|
||||
"importEnteEncGuide": "Seleziona il file JSON criptato esportato da ente",
|
||||
"importRaivoGuide": "Utilizza l'opzione \"Esporta i codici OTP in archivio Zip\" nelle impostazioni di Raivo.\n\nEstrai il file zip e importa il file JSON.",
|
||||
"importAegisGuide": "Usa l'opzione \"Esporta la cassaforte\" nelle impostazioni di Aegis.\n\nSe la tua cassaforte è criptata, dovrai inserire la password della cassaforte per decriptarla.",
|
||||
"exportCodes": "Esporta codici",
|
||||
"importLabel": "Importa",
|
||||
"importInstruction": "Per favore seleziona un file contenente una lista dei tuoi codici nel seguente formato",
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"importSelectJsonFile": "Selecteer JSON bestand",
|
||||
"importEnteEncGuide": "Selecteer het versleutelde JSON-bestand dat vanuit ente geëxporteerd is",
|
||||
"importRaivoGuide": "Gebruik de optie \"Export OTPs to Zip archive\" in Raivo's instellingen.\n\nPak het zip-bestand uit en importeer het JSON-bestand.",
|
||||
"importAegisGuide": "Gebruik de optie \"Exporteer de kluis\" in de instellingen van Aegis.\n\nAls uw kluis is versleuteld, moet u het wachtwoord invoeren om de kluis te ontsleutelen.",
|
||||
"exportCodes": "Codes exporteren",
|
||||
"importLabel": "Importeren",
|
||||
"importInstruction": "Selecteer een bestand dat een lijst van uw codes in de volgende indeling bevat",
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"importSelectJsonFile": "Выбрать JSON-файл",
|
||||
"importEnteEncGuide": "Выберите зашифрованный JSON-файл, экспортированный из ente",
|
||||
"importRaivoGuide": "Используйте опцию «Export OTPs to Zip archive» в настройках Raivo.\n\nРаспакуйте zip-архив и импортируйте JSON-файл.",
|
||||
"importAegisGuide": "Используйте опцию «Экспортировать хранилище» в настройках Aegis.\n\nЕсли ваше хранилище зашифровано, то для его расшифровки потребуется ввести пароль хранилища.",
|
||||
"exportCodes": "Экспортировать коды",
|
||||
"importLabel": "Импорт",
|
||||
"importInstruction": "Пожалуйста, выберите файл, содержащий список ваших кодов в следующем формате",
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"importSelectJsonFile": "JSON dosyasını seçin",
|
||||
"importEnteEncGuide": "Ente'den dışa aktarılan şifrelenmiş JSON dosyasını seçin",
|
||||
"importRaivoGuide": "Raivo'nun ayarlarında \"OTP'leri Zip arşivine aktar\" seçeneğini kullanın.\n\nZip dosyasını çıkarın ve JSON dosyasını içe aktarın.",
|
||||
"importAegisGuide": "Aegis'in Ayarlarında \"Kasayı dışa aktar\" seçeneğini kullanın.\n\nKasanız şifrelenmişse, kasanın şifresini çözmek için kasa parolasını girmeniz gerekecektir.",
|
||||
"exportCodes": "Kodu dışa aktar",
|
||||
"importLabel": "İçe aktar",
|
||||
"importInstruction": "Lütfen aşağıdaki şekilde kodlarınızın listesini içeren dosyayı seçiniz",
|
||||
@@ -163,7 +164,7 @@
|
||||
"close": "Kapat",
|
||||
"oopsSomethingWentWrong": "Hay aksi, bir sorun oluştu.",
|
||||
"selectLanguage": "Dil Seçin",
|
||||
"language": "Language",
|
||||
"language": "Dil",
|
||||
"social": "Sosyal Medya",
|
||||
"security": "Güvenlik",
|
||||
"lockscreen": "Kilit ekranı",
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"importSelectJsonFile": "选择 JSON 文件",
|
||||
"importEnteEncGuide": "选择从ente导出的加密JSON文件",
|
||||
"importRaivoGuide": "使用 Raivo 设置中的“将 OTP 导出到 Zip 存档”选项。\n\n解压 zip 文件并导入 JSON 文件。",
|
||||
"importAegisGuide": "在Aegis的设置中使用\"导出密码库\"选项。\n\n如果您的密码库已加密,您需要输入密码才能解密密码库。",
|
||||
"exportCodes": "导出代码",
|
||||
"importLabel": "导入",
|
||||
"importInstruction": "请以以下格式选择包含代码列表的文件",
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'dart:core' as $core;
|
||||
import 'package:fixnum/fixnum.dart' as $fixnum;
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
// ignore: always_use_package_imports
|
||||
import 'googleauth.pbenum.dart';
|
||||
|
||||
export 'googleauth.pbenum.dart';
|
||||
|
||||
@@ -7,17 +7,23 @@ import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/events/trigger_logout_event.dart';
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/locale.dart';
|
||||
import 'package:ente_auth/theme/text_style.dart';
|
||||
import 'package:ente_auth/ui/account/email_entry_page.dart';
|
||||
import 'package:ente_auth/ui/account/login_page.dart';
|
||||
import 'package:ente_auth/ui/account/logout_dialog.dart';
|
||||
import 'package:ente_auth/ui/account/password_entry_page.dart';
|
||||
import 'package:ente_auth/ui/account/password_reentry_page.dart';
|
||||
import 'package:ente_auth/ui/common/gradient_button.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_result.dart';
|
||||
import 'package:ente_auth/ui/home_page.dart';
|
||||
import 'package:ente_auth/ui/settings/language_picker.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
|
||||
class OnboardingPage extends StatefulWidget {
|
||||
const OnboardingPage({Key? key}) : super(key: key);
|
||||
@@ -103,15 +109,18 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Authenticator",
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
l10n.onBoardingBody,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
Theme.of(context).textTheme.headline6!.copyWith(
|
||||
Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Colors.white38,
|
||||
// color: Theme.of(context)
|
||||
// .colorScheme
|
||||
// .mutedTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -128,7 +137,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 28),
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
|
||||
child: Hero(
|
||||
tag: "log_in",
|
||||
child: ElevatedButton(
|
||||
@@ -145,6 +154,23 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 20),
|
||||
child: GestureDetector(
|
||||
onTap: _optForOfflineMode,
|
||||
child: Center(
|
||||
child: Text(
|
||||
l10n.useOffline,
|
||||
style: body.copyWith(
|
||||
color:
|
||||
Theme.of(context).colorScheme.mutedTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -155,6 +181,36 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _optForOfflineMode() async {
|
||||
bool canCheckBio = await LocalAuthentication().canCheckBiometrics;
|
||||
if(!canCheckBio) {
|
||||
showToast(context, "Sorry, biometric authentication is not supported on this device.");
|
||||
return;
|
||||
}
|
||||
final bool hasOptedBefore = Configuration.instance.hasOptedForOfflineMode();
|
||||
ButtonResult? result;
|
||||
if(!hasOptedBefore) {
|
||||
result = await showChoiceActionSheet(
|
||||
context,
|
||||
title: context.l10n.warning,
|
||||
body: context.l10n.offlineModeWarning,
|
||||
secondButtonLabel: context.l10n.cancel,
|
||||
firstButtonLabel: context.l10n.ok,
|
||||
);
|
||||
}
|
||||
if (hasOptedBefore || result?.action == ButtonAction.first) {
|
||||
await Configuration.instance.optForOfflineMode();
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const HomePage();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void _navigateToSignUpPage() {
|
||||
Widget page;
|
||||
if (Configuration.instance.getEncryptedToken() == null) {
|
||||
@@ -163,7 +219,9 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
// No key
|
||||
if (Configuration.instance.getKeyAttributes() == null) {
|
||||
// Never had a key
|
||||
page = const PasswordEntryPage(mode: PasswordEntryMode.set,);
|
||||
page = const PasswordEntryPage(
|
||||
mode: PasswordEntryMode.set,
|
||||
);
|
||||
} else if (Configuration.instance.getKey() == null) {
|
||||
// Yet to decrypt the key
|
||||
page = const PasswordReentryPage();
|
||||
@@ -189,7 +247,9 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
// No key
|
||||
if (Configuration.instance.getKeyAttributes() == null) {
|
||||
// Never had a key
|
||||
page = const PasswordEntryPage(mode: PasswordEntryMode.set,);
|
||||
page = const PasswordEntryPage(
|
||||
mode: PasswordEntryMode.set,
|
||||
);
|
||||
} else if (Configuration.instance.getKey() == null) {
|
||||
// Yet to decrypt the key
|
||||
page = const PasswordReentryPage();
|
||||
|
||||
@@ -10,30 +10,27 @@ class SetupEnterSecretKeyPage extends StatefulWidget {
|
||||
SetupEnterSecretKeyPage({this.code, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SetupEnterSecretKeyPage> createState() =>
|
||||
_SetupEnterSecretKeyPageState();
|
||||
State<SetupEnterSecretKeyPage> createState() => _SetupEnterSecretKeyPageState();
|
||||
}
|
||||
|
||||
class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
late TextEditingController _issuerController;
|
||||
late TextEditingController _accountController;
|
||||
late TextEditingController _secretController;
|
||||
late bool _secretKeyObscured;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_issuerController = TextEditingController(
|
||||
text: widget.code != null
|
||||
? safeDecode(widget.code!.issuer).trim()
|
||||
: null,
|
||||
text: widget.code != null ? safeDecode(widget.code!.issuer).trim() : null,
|
||||
);
|
||||
_accountController = TextEditingController(
|
||||
text: widget.code != null
|
||||
? safeDecode(widget.code!.account).trim()
|
||||
: null,
|
||||
text: widget.code != null ? safeDecode(widget.code!.account).trim() : null,
|
||||
);
|
||||
_secretController = TextEditingController(
|
||||
text: widget.code != null ? widget.code!.secret : null,
|
||||
);
|
||||
_secretKeyObscured = widget.code != null;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -77,7 +74,18 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.codeSecretKeyHint,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_secretKeyObscured = !_secretKeyObscured;
|
||||
});
|
||||
},
|
||||
icon: _secretKeyObscured
|
||||
? const Icon(Icons.visibility_off_rounded)
|
||||
: const Icon(Icons.visibility_rounded),
|
||||
),
|
||||
),
|
||||
obscureText: _secretKeyObscured,
|
||||
controller: _secretController,
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -118,8 +126,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
try {
|
||||
final account = _accountController.text.trim();
|
||||
final issuer = _issuerController.text.trim();
|
||||
final secret =
|
||||
_secretController.text.trim().replaceAll(' ', '');
|
||||
final secret = _secretController.text.trim().replaceAll(' ', '');
|
||||
final Code newCode = widget.code == null
|
||||
? Code.fromAccountAndSecret(
|
||||
account,
|
||||
|
||||
@@ -15,18 +15,29 @@ import 'package:ente_auth/models/authenticator/auth_key.dart';
|
||||
import 'package:ente_auth/models/authenticator/entity_result.dart';
|
||||
import 'package:ente_auth/models/authenticator/local_auth_entity.dart';
|
||||
import 'package:ente_auth/store/authenticator_db.dart';
|
||||
import 'package:ente_auth/store/offline_authenticator_db.dart';
|
||||
import 'package:ente_auth/utils/crypto_util.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
enum AccountMode {
|
||||
online,
|
||||
offline,
|
||||
}
|
||||
extension on AccountMode {
|
||||
bool get isOnline => this == AccountMode.online;
|
||||
bool get isOffline => this == AccountMode.offline;
|
||||
}
|
||||
|
||||
class AuthenticatorService {
|
||||
final _logger = Logger((AuthenticatorService).toString());
|
||||
final _config = Configuration.instance;
|
||||
late SharedPreferences _prefs;
|
||||
late AuthenticatorGateway _gateway;
|
||||
late AuthenticatorDB _db;
|
||||
late OfflineAuthenticatorDB _offlineDb;
|
||||
final String _lastEntitySyncTime = "lastEntitySyncTime";
|
||||
|
||||
AuthenticatorService._privateConstructor();
|
||||
@@ -34,25 +45,34 @@ class AuthenticatorService {
|
||||
static final AuthenticatorService instance =
|
||||
AuthenticatorService._privateConstructor();
|
||||
|
||||
AccountMode getAccountMode() {
|
||||
return Configuration.instance.hasOptedForOfflineMode() &&
|
||||
!Configuration.instance.hasConfiguredAccount()
|
||||
? AccountMode.offline
|
||||
: AccountMode.online;
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
_db = AuthenticatorDB.instance;
|
||||
_offlineDb = OfflineAuthenticatorDB.instance;
|
||||
_gateway = AuthenticatorGateway(Network.instance.getDio(), _config);
|
||||
if (Configuration.instance.hasConfiguredAccount()) {
|
||||
unawaited(sync());
|
||||
unawaited(onlineSync());
|
||||
}
|
||||
Bus.instance.on<SignedInEvent>().listen((event) {
|
||||
unawaited(sync());
|
||||
unawaited(onlineSync());
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<EntityResult>> getEntities() async {
|
||||
final List<LocalAuthEntity> result = await _db.getAll();
|
||||
Future<List<EntityResult>> getEntities(AccountMode mode) async {
|
||||
final List<LocalAuthEntity> result =
|
||||
mode.isOnline ? await _db.getAll() : await _offlineDb.getAll();
|
||||
final List<EntityResult> entities = [];
|
||||
if (result.isEmpty) {
|
||||
return entities;
|
||||
}
|
||||
final key = await getOrCreateAuthDataKey();
|
||||
final key = await getOrCreateAuthDataKey(mode);
|
||||
for (LocalAuthEntity e in result) {
|
||||
try {
|
||||
final decryptedValue = await CryptoUtil.decryptChaCha(
|
||||
@@ -75,17 +95,23 @@ class AuthenticatorService {
|
||||
return entities;
|
||||
}
|
||||
|
||||
Future<int> addEntry(String plainText, bool shouldSync) async {
|
||||
var key = await getOrCreateAuthDataKey();
|
||||
Future<int> addEntry(
|
||||
String plainText,
|
||||
bool shouldSync,
|
||||
AccountMode accountMode,
|
||||
) async {
|
||||
var key = await getOrCreateAuthDataKey(accountMode);
|
||||
final encryptedKeyData = await CryptoUtil.encryptChaCha(
|
||||
utf8.encode(plainText) as Uint8List,
|
||||
key,
|
||||
);
|
||||
String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
|
||||
String header = Sodium.bin2base64(encryptedKeyData.header!);
|
||||
final insertedID = await _db.insert(encryptedData, header);
|
||||
final insertedID = accountMode.isOnline
|
||||
? await _db.insert(encryptedData, header)
|
||||
: await _offlineDb.insert(encryptedData, header);
|
||||
if (shouldSync) {
|
||||
unawaited(sync());
|
||||
unawaited(onlineSync());
|
||||
}
|
||||
return insertedID;
|
||||
}
|
||||
@@ -94,45 +120,60 @@ class AuthenticatorService {
|
||||
int generatedID,
|
||||
String plainText,
|
||||
bool shouldSync,
|
||||
AccountMode accountMode,
|
||||
) async {
|
||||
var key = await getOrCreateAuthDataKey();
|
||||
var key = await getOrCreateAuthDataKey(accountMode);
|
||||
final encryptedKeyData = await CryptoUtil.encryptChaCha(
|
||||
utf8.encode(plainText) as Uint8List,
|
||||
key,
|
||||
);
|
||||
String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
|
||||
String header = Sodium.bin2base64(encryptedKeyData.header!);
|
||||
final int affectedRows =
|
||||
await _db.updateEntry(generatedID, encryptedData, header);
|
||||
final int affectedRows = accountMode.isOnline
|
||||
? await _db.updateEntry(generatedID, encryptedData, header)
|
||||
: await _offlineDb.updateEntry(generatedID, encryptedData, header);
|
||||
assert(
|
||||
affectedRows == 1,
|
||||
"updateEntry should have updated exactly one row",
|
||||
);
|
||||
if (shouldSync) {
|
||||
unawaited(sync());
|
||||
unawaited(onlineSync());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteEntry(int genID) async {
|
||||
LocalAuthEntity? result = await _db.getEntryByID(genID);
|
||||
Future<void> deleteEntry(int genID, AccountMode accountMode) async {
|
||||
LocalAuthEntity? result = accountMode.isOnline
|
||||
? await _db.getEntryByID(genID)
|
||||
: await _offlineDb.getEntryByID(genID);
|
||||
if (result == null) {
|
||||
_logger.info("No entry found for given id");
|
||||
return;
|
||||
}
|
||||
if (result.id != null) {
|
||||
if (result.id != null && accountMode.isOnline) {
|
||||
await _gateway.deleteEntity(result.id!);
|
||||
} else {
|
||||
debugPrint("Skipping delete since account mode is offline");
|
||||
}
|
||||
if(accountMode.isOnline) {
|
||||
await _db.deleteByIDs(generatedIDs: [genID]);
|
||||
} else {
|
||||
await _offlineDb.deleteByIDs(generatedIDs: [genID]);
|
||||
}
|
||||
await _db.deleteByIDs(generatedIDs: [genID]);
|
||||
}
|
||||
|
||||
Future<void> sync() async {
|
||||
Future<bool> onlineSync() async {
|
||||
try {
|
||||
if(getAccountMode().isOffline) {
|
||||
debugPrint("Skipping sync since account mode is offline");
|
||||
return false;
|
||||
}
|
||||
_logger.info("Sync");
|
||||
await _remoteToLocalSync();
|
||||
_logger.info("remote fetch completed");
|
||||
await _localToRemoteSync();
|
||||
_logger.info("local push completed");
|
||||
Bus.instance.fire(CodesUpdatedEvent());
|
||||
return true;
|
||||
} on UnauthorizedError {
|
||||
if ((await _db.removeSyncedData()) > 0) {
|
||||
Bus.instance.fire(CodesUpdatedEvent());
|
||||
@@ -140,8 +181,10 @@ class AuthenticatorService {
|
||||
debugPrint("Firing logout event");
|
||||
|
||||
Bus.instance.fire(TriggerLogoutEvent());
|
||||
return false;
|
||||
} catch (e) {
|
||||
_logger.severe("Failed to sync with remote", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +252,10 @@ class AuthenticatorService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List> getOrCreateAuthDataKey() async {
|
||||
Future<Uint8List> getOrCreateAuthDataKey(AccountMode mode) async {
|
||||
if(mode.isOffline) {
|
||||
return _config.getOfflineSecretKey()!;
|
||||
}
|
||||
if (_config.getAuthSecretKey() != null) {
|
||||
return _config.getAuthSecretKey()!;
|
||||
}
|
||||
|
||||
@@ -902,7 +902,7 @@ class UserService {
|
||||
}
|
||||
|
||||
bool? canDisableEmailMFA() {
|
||||
return _preferences.getBool(kCanDisableEmailMFA);;
|
||||
return _preferences.getBool(kCanDisableEmailMFA);
|
||||
}
|
||||
bool hasEmailMFAEnabled() {
|
||||
return _preferences.getBool(kIsEmailMFAEnabled) ?? true;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/codes_updated_event.dart';
|
||||
import 'package:ente_auth/models/authenticator/entity_result.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:ente_auth/store/offline_authenticator_db.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class CodeStore {
|
||||
@@ -20,9 +22,10 @@ class CodeStore {
|
||||
_authenticatorService = AuthenticatorService.instance;
|
||||
}
|
||||
|
||||
Future<List<Code>> getAllCodes() async {
|
||||
Future<List<Code>> getAllCodes({AccountMode? accountMode}) async {
|
||||
final mode = accountMode ?? _authenticatorService.getAccountMode();
|
||||
final List<EntityResult> entities =
|
||||
await _authenticatorService.getEntities();
|
||||
await _authenticatorService.getEntities(mode);
|
||||
final List<Code> codes = [];
|
||||
for (final entity in entities) {
|
||||
final decodeJson = jsonDecode(entity.rawData);
|
||||
@@ -43,38 +46,120 @@ class CodeStore {
|
||||
return codes;
|
||||
}
|
||||
|
||||
Future<void> addCode(
|
||||
Future<AddResult> addCode(
|
||||
Code code, {
|
||||
bool shouldSync = true,
|
||||
AccountMode? accountMode,
|
||||
}) async {
|
||||
final codes = await getAllCodes();
|
||||
final mode = accountMode ?? _authenticatorService.getAccountMode();
|
||||
final codes = await getAllCodes(accountMode: mode);
|
||||
bool isExistingCode = false;
|
||||
for (final existingCode in codes) {
|
||||
if (existingCode == code) {
|
||||
_logger.info("Found duplicate code, skipping add");
|
||||
return;
|
||||
return AddResult.duplicate;
|
||||
} else if (existingCode.generatedID == code.generatedID) {
|
||||
isExistingCode = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
late AddResult result;
|
||||
if (isExistingCode) {
|
||||
result = AddResult.updateCode;
|
||||
await _authenticatorService.updateEntry(
|
||||
code.generatedID!,
|
||||
jsonEncode(code.rawData),
|
||||
shouldSync,
|
||||
mode,
|
||||
);
|
||||
} else {
|
||||
result = AddResult.newCode;
|
||||
code.generatedID = await _authenticatorService.addEntry(
|
||||
jsonEncode(code.rawData),
|
||||
shouldSync,
|
||||
mode,
|
||||
);
|
||||
}
|
||||
Bus.instance.fire(CodesUpdatedEvent());
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> removeCode(Code code) async {
|
||||
await _authenticatorService.deleteEntry(code.generatedID!);
|
||||
Future<void> removeCode(Code code, {AccountMode? accountMode}) async {
|
||||
final mode = accountMode ?? _authenticatorService.getAccountMode();
|
||||
await _authenticatorService.deleteEntry(code.generatedID!, mode);
|
||||
Bus.instance.fire(CodesUpdatedEvent());
|
||||
}
|
||||
|
||||
bool _isOfflineImportRunning = false;
|
||||
|
||||
Future<void> importOfflineCodes() async {
|
||||
if(_isOfflineImportRunning) {
|
||||
return;
|
||||
}
|
||||
_isOfflineImportRunning = true;
|
||||
Logger logger = Logger('importOfflineCodes');
|
||||
try {
|
||||
Configuration config = Configuration.instance;
|
||||
if (!config.hasConfiguredAccount() ||
|
||||
!config.hasOptedForOfflineMode() ||
|
||||
config.getOfflineSecretKey() == null) {
|
||||
return;
|
||||
}
|
||||
logger.info('start import');
|
||||
|
||||
List<Code> offlineCodes = await CodeStore.instance
|
||||
.getAllCodes(accountMode: AccountMode.offline);
|
||||
if (offlineCodes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
bool isOnlineSyncDone = await AuthenticatorService.instance.onlineSync();
|
||||
if (!isOnlineSyncDone) {
|
||||
logger.info("skip as online sync is not done");
|
||||
return;
|
||||
}
|
||||
final List<Code> onlineCodes =
|
||||
await CodeStore.instance.getAllCodes(accountMode: AccountMode.online);
|
||||
logger.info(
|
||||
'importing ${offlineCodes.length} offline codes with ${onlineCodes.length} online codes',
|
||||
);
|
||||
for (Code eachCode in offlineCodes) {
|
||||
bool alreadyPresent = onlineCodes.any(
|
||||
(oc) =>
|
||||
oc.issuer == eachCode.issuer &&
|
||||
oc.account == eachCode.account &&
|
||||
oc.secret == eachCode.secret,
|
||||
);
|
||||
int? generatedID = eachCode.generatedID!;
|
||||
logger.info(
|
||||
'importingCode: genID ${eachCode.generatedID} & isAlreadyPresent $alreadyPresent',
|
||||
);
|
||||
if (!alreadyPresent) {
|
||||
// Avoid conflict with generatedID of online codes
|
||||
eachCode.generatedID = null;
|
||||
final AddResult result = await CodeStore.instance.addCode(
|
||||
eachCode,
|
||||
accountMode: AccountMode.online,
|
||||
shouldSync: false,
|
||||
);
|
||||
logger.info(
|
||||
'importedCode: genID ${eachCode.generatedID} result: ${result.name}',
|
||||
);
|
||||
}
|
||||
await OfflineAuthenticatorDB.instance.deleteByIDs(
|
||||
generatedIDs: [generatedID],
|
||||
);
|
||||
}
|
||||
AuthenticatorService.instance.onlineSync().ignore();
|
||||
} catch (e, s) {
|
||||
_logger.severe("error while importing offline codes", e, s);
|
||||
} finally {
|
||||
_isOfflineImportRunning = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AddResult {
|
||||
newCode,
|
||||
duplicate,
|
||||
updateCode,
|
||||
}
|
||||
|
||||
170
lib/store/offline_authenticator_db.dart
Normal file
170
lib/store/offline_authenticator_db.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/models/authenticator/auth_entity.dart';
|
||||
import 'package:ente_auth/models/authenticator/local_auth_entity.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
class OfflineAuthenticatorDB {
|
||||
static const _databaseName = "ente.offline_authenticator.db";
|
||||
static const _databaseVersion = 1;
|
||||
|
||||
static const entityTable = 'entities';
|
||||
|
||||
OfflineAuthenticatorDB._privateConstructor();
|
||||
static final OfflineAuthenticatorDB instance = OfflineAuthenticatorDB._privateConstructor();
|
||||
|
||||
static Future<Database>? _dbFuture;
|
||||
|
||||
Future<Database> get database async {
|
||||
_dbFuture ??= _initDatabase();
|
||||
return _dbFuture!;
|
||||
}
|
||||
|
||||
Future<Database> _initDatabase() async {
|
||||
final Directory documentsDirectory =
|
||||
await getApplicationDocumentsDirectory();
|
||||
final String path = join(documentsDirectory.path, _databaseName);
|
||||
debugPrint(path);
|
||||
return await openDatabase(
|
||||
path,
|
||||
version: _databaseVersion,
|
||||
onCreate: _onCreate,
|
||||
);
|
||||
}
|
||||
|
||||
Future _onCreate(Database db, int version) async {
|
||||
await db.execute(
|
||||
'''
|
||||
CREATE TABLE $entityTable (
|
||||
_generatedID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
id TEXT,
|
||||
encryptedData TEXT NOT NULL,
|
||||
header TEXT NOT NULL,
|
||||
createdAt INTEGER NOT NULL,
|
||||
updatedAt INTEGER NOT NULL,
|
||||
shouldSync INTEGER DEFAULT 0,
|
||||
UNIQUE(id)
|
||||
);
|
||||
''',
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> insert(String encData, String header) async {
|
||||
final db = await instance.database;
|
||||
final int timeInMicroSeconds = DateTime.now().microsecondsSinceEpoch;
|
||||
final insertedID = await db.insert(
|
||||
entityTable,
|
||||
{
|
||||
"encryptedData": encData,
|
||||
"header": header,
|
||||
"shouldSync": 1,
|
||||
"createdAt": timeInMicroSeconds,
|
||||
"updatedAt": timeInMicroSeconds,
|
||||
},
|
||||
);
|
||||
return insertedID;
|
||||
}
|
||||
|
||||
Future<int> updateEntry(
|
||||
int generatedID,
|
||||
String encData,
|
||||
String header,
|
||||
) async {
|
||||
final db = await instance.database;
|
||||
final int timeInMicroSeconds = DateTime.now().microsecondsSinceEpoch;
|
||||
int affectedRows = await db.update(
|
||||
entityTable,
|
||||
{
|
||||
"encryptedData": encData,
|
||||
"header": header,
|
||||
"shouldSync": 1,
|
||||
"updatedAt": timeInMicroSeconds,
|
||||
},
|
||||
where: '_generatedID = ?',
|
||||
whereArgs: [generatedID],
|
||||
);
|
||||
return affectedRows;
|
||||
}
|
||||
|
||||
Future<void> insertOrReplace(List<AuthEntity> authEntities) async {
|
||||
final db = await instance.database;
|
||||
final batch = db.batch();
|
||||
for (AuthEntity authEntity in authEntities) {
|
||||
final insertRow = authEntity.toMap();
|
||||
insertRow.remove('isDeleted');
|
||||
insertRow.putIfAbsent('shouldSync', () => 0);
|
||||
batch.insert(
|
||||
entityTable,
|
||||
insertRow,
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
Future<void> updateLocalEntity(LocalAuthEntity localAuthEntity) async {
|
||||
final db = await instance.database;
|
||||
await db.update(
|
||||
entityTable,
|
||||
localAuthEntity.toMap(),
|
||||
where: '_generatedID = ?',
|
||||
whereArgs: [localAuthEntity.generatedID],
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<LocalAuthEntity?> getEntryByID(int genID) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db
|
||||
.query(entityTable, where: '_generatedID = ?', whereArgs: [genID]);
|
||||
final listOfAuthEntities = _convertRows(rows);
|
||||
if (listOfAuthEntities.isEmpty) {
|
||||
return null;
|
||||
} else {
|
||||
return listOfAuthEntities.first;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<LocalAuthEntity>> getAll() async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.rawQuery("SELECT * from $entityTable");
|
||||
return _convertRows(rows);
|
||||
}
|
||||
|
||||
// deleteByID will prefer generated id if both ids are passed during deletion
|
||||
Future<void> deleteByIDs({List<int>? generatedIDs, List<String>? ids}) async {
|
||||
final db = await instance.database;
|
||||
final batch = db.batch();
|
||||
const whereGenID = '_generatedID = ?';
|
||||
const whereID = 'id = ?';
|
||||
if (generatedIDs != null) {
|
||||
for (int genId in generatedIDs) {
|
||||
batch.delete(entityTable, where: whereGenID, whereArgs: [genId]);
|
||||
}
|
||||
}
|
||||
if (ids != null) {
|
||||
for (String id in ids) {
|
||||
batch.delete(entityTable, where: whereID, whereArgs: [id]);
|
||||
}
|
||||
}
|
||||
final result = await batch.commit();
|
||||
debugPrint("Done");
|
||||
}
|
||||
|
||||
Future<void> clearTable() async {
|
||||
final db = await instance.database;
|
||||
await db.delete(entityTable);
|
||||
}
|
||||
|
||||
List<LocalAuthEntity> _convertRows(List<Map<String, dynamic>> rows) {
|
||||
final keys = <LocalAuthEntity>[];
|
||||
for (final row in rows) {
|
||||
keys.add(LocalAuthEntity.fromMap(row));
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ class DeleteAccountPage extends StatelessWidget {
|
||||
Center(
|
||||
child: Text(
|
||||
l10n.deleteAccountQuery,
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -67,7 +67,7 @@ class DeleteAccountPage extends StatelessWidget {
|
||||
text: ", maybe there is a way we can help.",
|
||||
),
|
||||
],
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
|
||||
@@ -135,13 +135,13 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
||||
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
child: Text(
|
||||
context.l10n.createNewAccount,
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
child: TextFormField(
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
decoration: InputDecoration(
|
||||
fillColor: _emailIsValid ? _validFieldValueColor : null,
|
||||
@@ -372,11 +372,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
||||
child: StyledText(
|
||||
text: context.l10n.signUpTerms,
|
||||
style:
|
||||
Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 12),
|
||||
Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 12),
|
||||
tags: {
|
||||
'u-terms': StyledTextActionTag(
|
||||
(String? text, Map<String?, String?> attrs) => {
|
||||
Navigator.of(context).push(
|
||||
(String? text, Map<String?, String?> attrs) => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WebPage(
|
||||
@@ -385,15 +384,13 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
'u-policy': StyledTextActionTag(
|
||||
(String? text, Map<String?, String?> attrs) => {
|
||||
Navigator.of(context).push(
|
||||
(String? text, Map<String?, String?> attrs) => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WebPage(
|
||||
@@ -402,8 +399,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
@@ -439,11 +435,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
||||
child: StyledText(
|
||||
text: context.l10n.ackPasswordLostWarning,
|
||||
style:
|
||||
Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 12),
|
||||
Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 12),
|
||||
tags: {
|
||||
'underline': StyledTextActionTag(
|
||||
(String? text, Map<String?, String?> attrs) => {
|
||||
Navigator.of(context).push(
|
||||
(String? text, Map<String?, String?> attrs) => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WebPage(
|
||||
@@ -452,8 +447,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
|
||||
@@ -107,7 +107,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
child: Text(
|
||||
l10n.welcomeBack,
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@@ -172,12 +172,11 @@ class _LoginPageState extends State<LoginPage> {
|
||||
text: context.l10n.loginTerms,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1!
|
||||
.titleMedium!
|
||||
.copyWith(fontSize: 12),
|
||||
tags: {
|
||||
'u-terms': StyledTextActionTag(
|
||||
(String? text, Map<String?, String?> attrs) => {
|
||||
Navigator.of(context).push(
|
||||
(String? text, Map<String?, String?> attrs) => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WebPage(
|
||||
@@ -186,15 +185,13 @@ class _LoginPageState extends State<LoginPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
'u-policy': StyledTextActionTag(
|
||||
(String? text, Map<String?, String?> attrs) => {
|
||||
Navigator.of(context).push(
|
||||
(String? text, Map<String?, String?> attrs) => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WebPage(
|
||||
@@ -203,8 +200,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
|
||||
@@ -68,8 +68,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
||||
body: _getBody(),
|
||||
floatingActionButton: DynamicFAB(
|
||||
isKeypadOpen: isKeypadOpen,
|
||||
isFormValid: !(_verificationCodeController.text == null ||
|
||||
_verificationCodeController.text.isEmpty),
|
||||
isFormValid: !(_verificationCodeController.text.isEmpty),
|
||||
buttonText: l10n.verify,
|
||||
onPressedFunction: () {
|
||||
if (widget.isChangeEmail) {
|
||||
@@ -102,7 +101,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
||||
padding: const EdgeInsets.fromLTRB(20, 30, 20, 15),
|
||||
child: Text(
|
||||
l10n.verifyEmail,
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@@ -119,7 +118,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
||||
text: l10n.weHaveSendEmailTo(widget.email),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1!
|
||||
.titleMedium!
|
||||
.copyWith(fontSize: 14),
|
||||
tags: {
|
||||
'green': StyledTextTag(
|
||||
@@ -144,7 +143,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
||||
l10n.checkInboxAndSpamFolder,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1!
|
||||
.titleMedium!
|
||||
.copyWith(fontSize: 14),
|
||||
),
|
||||
],
|
||||
@@ -160,7 +159,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
|
||||
child: TextFormField(
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
hintText: l10n.tapToEnterCode,
|
||||
@@ -199,7 +198,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
||||
},
|
||||
child: Text(
|
||||
l10n.resendEmail,
|
||||
style: Theme.of(context).textTheme.subtitle1!.copyWith(
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
|
||||
@@ -153,7 +153,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
||||
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
child: Text(
|
||||
buttonTextAndHeading,
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@@ -165,7 +165,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
||||
textAlign: TextAlign.start,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1!
|
||||
.titleMedium!
|
||||
.copyWith(fontSize: 14),
|
||||
),
|
||||
),
|
||||
@@ -176,11 +176,11 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
||||
text: context.l10n.passwordWarning,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1!
|
||||
.titleMedium!
|
||||
.copyWith(fontSize: 14),
|
||||
tags: {
|
||||
'underline': StyledTextTag(
|
||||
style: Theme.of(context).textTheme.subtitle1!.copyWith(
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
@@ -356,7 +356,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: context.l10n.howItWorks,
|
||||
style: Theme.of(context).textTheme.subtitle1!.copyWith(
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
|
||||
@@ -103,14 +103,14 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
widget.title ?? context.l10n.recoveryKey,
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(widget.showAppBar! ? 0 : 12),
|
||||
),
|
||||
Text(
|
||||
widget.text ?? context.l10n.recoveryKeyOnForgotPassword,
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(top: 24)),
|
||||
DottedBorder(
|
||||
@@ -164,7 +164,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
|
||||
child: Text(
|
||||
recoveryKey,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyText1,
|
||||
Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -177,7 +177,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
|
||||
child: Text(
|
||||
widget.subText ??
|
||||
context.l10n.recoveryKeySaveDescription,
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -88,7 +88,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
|
||||
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
child: Text(
|
||||
'Forgot password',
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@@ -140,7 +140,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
|
||||
child: Text(
|
||||
"No recovery key?",
|
||||
style:
|
||||
Theme.of(context).textTheme.subtitle1!.copyWith(
|
||||
Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
|
||||
@@ -162,7 +162,7 @@ class _SessionsPageState extends State<SessionsPage> {
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
Text(
|
||||
session.ua,
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
@@ -31,6 +32,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
final ValueNotifier<String> _nextCode = ValueNotifier<String>("");
|
||||
final Logger logger = Logger("_CodeWidgetState");
|
||||
bool _isInitialized = false;
|
||||
late bool hasConfiguredAccount;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -46,6 +48,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
}
|
||||
}
|
||||
});
|
||||
hasConfiguredAccount = Configuration.instance.hasConfiguredAccount();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -156,14 +159,14 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
children: [
|
||||
Text(
|
||||
safeDecode(widget.code.issuer).trim(),
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
safeDecode(widget.code.account).trim(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.caption
|
||||
.bodySmall
|
||||
?.copyWith(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
@@ -174,9 +177,9 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
widget.code.hasSynced != null &&
|
||||
widget.code.hasSynced!
|
||||
? Container()
|
||||
(widget.code.hasSynced != null &&
|
||||
widget.code.hasSynced!) || !hasConfiguredAccount
|
||||
? const SizedBox.shrink()
|
||||
: const Icon(
|
||||
Icons.sync_disabled,
|
||||
size: 20,
|
||||
@@ -216,7 +219,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
Text(
|
||||
l10n.nextTotpTitle,
|
||||
style:
|
||||
Theme.of(context).textTheme.caption,
|
||||
Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
ValueListenableBuilder<String>(
|
||||
valueListenable: _nextCode,
|
||||
@@ -238,7 +241,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
Text(
|
||||
l10n.nextTotpTitle,
|
||||
style:
|
||||
Theme.of(context).textTheme.caption,
|
||||
Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
InkWell(
|
||||
onTap: _onNextHotpTapped,
|
||||
|
||||
@@ -16,7 +16,7 @@ class BottomShadowWidget extends StatelessWidget {
|
||||
color: Colors.transparent,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: shadowColor ?? Theme.of(context).backgroundColor,
|
||||
color: shadowColor ?? Theme.of(context).colorScheme.background,
|
||||
spreadRadius: 42,
|
||||
blurRadius: 42,
|
||||
offset: Offset(0, offsetDy), // changes position of shadow
|
||||
|
||||
@@ -26,7 +26,7 @@ class DynamicFAB extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
spreadRadius: 200,
|
||||
blurRadius: 100,
|
||||
offset: const Offset(0, 230),
|
||||
|
||||
@@ -6,10 +6,10 @@ PopupMenuButton<dynamic> reportBugPopupMenu(BuildContext context) {
|
||||
itemBuilder: (context) {
|
||||
final List<PopupMenuItem> items = [];
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
const PopupMenuItem(
|
||||
value: 1,
|
||||
child: Row(
|
||||
children: const [
|
||||
children: [
|
||||
Text("Contact support"),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,75 +1,120 @@
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import "package:ente_auth/ente_theme_data.dart";
|
||||
import 'package:ente_auth/theme/colors.dart';
|
||||
import "package:ente_auth/theme/ente_theme.dart";
|
||||
import 'package:ente_auth/theme/text_style.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/icon_button_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NotificationWarningWidget extends StatelessWidget {
|
||||
final IconData warningIcon;
|
||||
// CreateNotificationType enum
|
||||
enum NotificationType {
|
||||
warning,
|
||||
banner,
|
||||
notice,
|
||||
}
|
||||
|
||||
class NotificationWidget extends StatelessWidget {
|
||||
final IconData startIcon;
|
||||
final IconData actionIcon;
|
||||
final String text;
|
||||
final String? subText;
|
||||
final GestureTapCallback onTap;
|
||||
final NotificationType type;
|
||||
|
||||
const NotificationWarningWidget({
|
||||
const NotificationWidget({
|
||||
Key? key,
|
||||
required this.warningIcon,
|
||||
required this.startIcon,
|
||||
required this.actionIcon,
|
||||
required this.text,
|
||||
required this.onTap,
|
||||
this.subText,
|
||||
this.type = NotificationType.warning,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EnteColorScheme colorScheme = getEnteColorScheme(context);
|
||||
EnteTextTheme textTheme = getEnteTextTheme(context);
|
||||
TextStyle mainTextStyle = darkTextTheme.bodyBold;
|
||||
TextStyle subTextStyle = darkTextTheme.miniMuted;
|
||||
LinearGradient? backgroundGradient;
|
||||
Color? backgroundColor;
|
||||
EnteColorScheme strokeColorScheme = darkScheme;
|
||||
List<BoxShadow>? boxShadow;
|
||||
switch (type) {
|
||||
case NotificationType.warning:
|
||||
backgroundColor = warning500;
|
||||
break;
|
||||
case NotificationType.banner:
|
||||
colorScheme = getEnteColorScheme(context);
|
||||
textTheme = getEnteTextTheme(context);
|
||||
backgroundColor = colorScheme.backgroundElevated2;
|
||||
mainTextStyle = textTheme.bodyBold;
|
||||
subTextStyle = textTheme.miniMuted;
|
||||
strokeColorScheme = colorScheme;
|
||||
boxShadow = [
|
||||
BoxShadow(color: Colors.black.withOpacity(0.25), blurRadius: 1),
|
||||
];
|
||||
break;
|
||||
|
||||
case NotificationType.notice:
|
||||
backgroundColor = colorScheme.backgroundElevated2;
|
||||
mainTextStyle = textTheme.bodyBold;
|
||||
subTextStyle = textTheme.miniMuted;
|
||||
strokeColorScheme = colorScheme;
|
||||
boxShadow = Theme.of(context).colorScheme.enteTheme.shadowMenu;
|
||||
break;
|
||||
}
|
||||
return Center(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
boxShadow: Theme.of(context).colorScheme.enteTheme.shadowMenu,
|
||||
color: warning500,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Icon(
|
||||
warningIcon,
|
||||
size: 36,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Flexible(
|
||||
child: Text(
|
||||
text,
|
||||
style: darkTextTheme.bodyBold,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ClipOval(
|
||||
child: Material(
|
||||
color: fillFaintDark,
|
||||
child: InkWell(
|
||||
splashColor: Colors.red, // Splash color
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Icon(
|
||||
actionIcon,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
boxShadow: boxShadow,
|
||||
color: backgroundColor,
|
||||
gradient: backgroundGradient,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Icon(
|
||||
startIcon,
|
||||
size: 36,
|
||||
color: strokeColorScheme.strokeBase,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
style: mainTextStyle,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
subText != null
|
||||
? Text(
|
||||
subText!,
|
||||
style: subTextStyle,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
IconButtonWidget(
|
||||
icon: actionIcon,
|
||||
iconButtonType: IconButtonType.rounded,
|
||||
iconColor: strokeColorScheme.strokeBase,
|
||||
defaultColor: strokeColorScheme.fillFaint,
|
||||
pressedColor: strokeColorScheme.fillMuted,
|
||||
onTap: onTap,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -41,7 +41,7 @@ class CoachMarkWidget extends StatelessWidget {
|
||||
),
|
||||
Text(
|
||||
l10n.swipeHint,
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 36,
|
||||
|
||||
@@ -36,7 +36,7 @@ class HomeEmptyStateWidget extends StatelessWidget {
|
||||
Text(
|
||||
l10n.setupFirstAccount,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 64),
|
||||
SizedBox(
|
||||
|
||||
@@ -65,7 +65,10 @@ class _HomePageState extends State<HomePage> {
|
||||
await autoLogoutAlert(context);
|
||||
});
|
||||
_initDeepLinks();
|
||||
|
||||
Future.delayed(
|
||||
const Duration(seconds: 1),
|
||||
() async => await CodeStore.instance.importOfflineCodes(),
|
||||
);
|
||||
}
|
||||
|
||||
void _loadCodes() {
|
||||
@@ -248,7 +251,7 @@ class _HomePageState extends State<HomePage> {
|
||||
Code? code;
|
||||
try {
|
||||
code = _filteredCodes[index];
|
||||
return CodeWidget(code!);
|
||||
return CodeWidget(code);
|
||||
} catch (e, s) {
|
||||
_logger.severe("code widget error", e, s);
|
||||
return Center(
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/services/update_service.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class AppUpdateDialog extends StatefulWidget {
|
||||
final LatestVersionInfo? latestVersionInfo;
|
||||
@@ -20,6 +23,7 @@ class AppUpdateDialog extends StatefulWidget {
|
||||
class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final enteTextTheme = getEnteTextTheme(context);
|
||||
final List<Widget> changelog = [];
|
||||
for (final log in widget.latestVersionInfo!.changelog) {
|
||||
changelog.add(
|
||||
@@ -27,7 +31,7 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 0, 4),
|
||||
child: Text(
|
||||
"- " + log,
|
||||
style: Theme.of(context).textTheme.caption!.copyWith(
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
@@ -41,18 +45,19 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
||||
Text(
|
||||
widget.latestVersionInfo!.name!,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
const Text(
|
||||
"Changelog",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
if (changelog.isNotEmpty)
|
||||
const Text(
|
||||
"Changelog",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(4)),
|
||||
if (changelog.isNotEmpty) const Padding(padding: EdgeInsets.all(4)),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: changelog,
|
||||
@@ -65,22 +70,16 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
||||
style: Theme.of(context).outlinedButtonTheme.style!.copyWith(
|
||||
textStyle: MaterialStateProperty.resolveWith<TextStyle?>(
|
||||
(Set<MaterialState> states) {
|
||||
return Theme.of(context).textTheme.subtitle1;
|
||||
return Theme.of(context).textTheme.titleMedium;
|
||||
},
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ApkDownloaderDialog(widget.latestVersionInfo);
|
||||
},
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
"Update",
|
||||
onPressed: () => launchUrlString(
|
||||
widget.latestVersionInfo!.url!,
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
child: Text(
|
||||
context.l10n.downloadUpdate,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -91,8 +90,24 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
||||
return WillPopScope(
|
||||
onWillPop: () async => !shouldForceUpdate,
|
||||
child: AlertDialog(
|
||||
title: Text(
|
||||
shouldForceUpdate ? "Critical update available" : "Update available",
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.auto_awesome_outlined,
|
||||
size: 24,
|
||||
color: getEnteColorScheme(context).strokeMuted,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Text(
|
||||
shouldForceUpdate
|
||||
? context.l10n.criticalUpdateAvailable
|
||||
: context.l10n.updateAvailable,
|
||||
style: enteTextTheme.h3Bold,
|
||||
),
|
||||
],
|
||||
),
|
||||
content: content,
|
||||
),
|
||||
@@ -147,15 +162,17 @@ class _ApkDownloaderDialogState extends State<ApkDownloaderDialog> {
|
||||
|
||||
Future<void> _downloadApk() async {
|
||||
try {
|
||||
await Network.instance.getDio().download(
|
||||
widget.versionInfo!.url!,
|
||||
_saveUrl,
|
||||
onReceiveProgress: (count, _) {
|
||||
setState(() {
|
||||
_downloadProgress = count / widget.versionInfo!.size!;
|
||||
});
|
||||
},
|
||||
);
|
||||
if (!File(_saveUrl!).existsSync()) {
|
||||
await Network.instance.getDio().download(
|
||||
widget.versionInfo!.url!,
|
||||
_saveUrl,
|
||||
onReceiveProgress: (count, _) {
|
||||
setState(() {
|
||||
_downloadProgress = count / widget.versionInfo!.size!;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
OpenFilex.open(_saveUrl);
|
||||
} catch (e) {
|
||||
|
||||
@@ -49,7 +49,7 @@ class _AppVersionWidgetState extends State<AppVersionWidget> {
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Text(
|
||||
"Version: " + snapshot.data!,
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class DataSectionWidget extends StatelessWidget {
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
handleExportClick(context);
|
||||
await handleExportClick(context);
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
|
||||
@@ -156,7 +156,7 @@ Future<int?> _processAegisExportFile(
|
||||
for (final code in parsedCodes) {
|
||||
await CodeStore.instance.addCode(code, shouldSync: false);
|
||||
}
|
||||
unawaited(AuthenticatorService.instance.sync());
|
||||
unawaited(AuthenticatorService.instance.onlineSync());
|
||||
int count = parsedCodes.length;
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,8 @@ Future<void> _decryptExportData(
|
||||
derivedKey,
|
||||
Sodium.base642bin(enteAuthExport.encryptionNonce),
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (e,s) {
|
||||
Logger("encryptedImport").warning('failed to decrypt',e,s);
|
||||
showToast(context, l10n.incorrectPasswordTitle);
|
||||
isPasswordIncorrect = true;
|
||||
}
|
||||
@@ -118,7 +119,7 @@ Future<void> _decryptExportData(
|
||||
for (final code in parsedCodes) {
|
||||
await CodeStore.instance.addCode(code, shouldSync: false);
|
||||
}
|
||||
unawaited(AuthenticatorService.instance.sync());
|
||||
unawaited(AuthenticatorService.instance.onlineSync());
|
||||
importedCodeCount = parsedCodes.length;
|
||||
await progressDialog.hide();
|
||||
} catch (e, s) {
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:ente_auth/ui/scanner_gauth_page.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/import_success.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
const kGoogleAuthExportPrefix = 'otpauth-migration://offline?data=';
|
||||
@@ -56,7 +55,7 @@ Future<void> showGoogleAuthInstruction(BuildContext context) async {
|
||||
for (final code in codes) {
|
||||
await CodeStore.instance.addCode(code, shouldSync: false);
|
||||
}
|
||||
unawaited(AuthenticatorService.instance.sync());
|
||||
unawaited(AuthenticatorService.instance.onlineSync());
|
||||
importSuccessDialog(context, codes.length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/import_success.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
@@ -59,7 +58,7 @@ Future<void> showImportInstructionDialog(BuildContext context) async {
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
title: Text(
|
||||
l10n.importCodes,
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
content: const SingleChildScrollView(
|
||||
child: PlainTextImport(),
|
||||
@@ -122,7 +121,7 @@ Future<void> _pickImportFile(BuildContext context) async {
|
||||
for (final code in parsedCodes) {
|
||||
await CodeStore.instance.addCode(code, shouldSync: false);
|
||||
}
|
||||
unawaited(AuthenticatorService.instance.sync());
|
||||
unawaited(AuthenticatorService.instance.onlineSync());
|
||||
await progressDialog.hide();
|
||||
await importSuccessDialog(context, parsedCodes.length);
|
||||
} catch (e) {
|
||||
|
||||
@@ -111,7 +111,7 @@ Future<int?> _processRaivoExportFile(BuildContext context,String path) async {
|
||||
for (final code in parsedCodes) {
|
||||
await CodeStore.instance.addCode(code, shouldSync: false);
|
||||
}
|
||||
unawaited(AuthenticatorService.instance.sync());
|
||||
unawaited(AuthenticatorService.instance.onlineSync());
|
||||
int count = parsedCodes.length;
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:ente_auth/ui/components/menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/components/title_bar_title_widget.dart';
|
||||
import 'package:ente_auth/ui/components/title_bar_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/import_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum ImportType {
|
||||
|
||||
@@ -31,9 +31,11 @@ class SecuritySectionWidget extends StatefulWidget {
|
||||
|
||||
class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
||||
final _config = Configuration.instance;
|
||||
late bool _hasLoggedIn;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_hasLoggedIn = _config.hasConfiguredAccount();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -53,49 +55,103 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
||||
}
|
||||
|
||||
Widget _getSectionOptions(BuildContext context) {
|
||||
final bool? canDisableMFA = UserService.instance.canDisableEmailMFA();
|
||||
if (canDisableMFA == null) {
|
||||
// We don't know if the user can disable MFA yet, so we fetch the info
|
||||
UserService.instance.getUserDetailsV2().ignore();
|
||||
}
|
||||
final l10n = context.l10n;
|
||||
final List<Widget> children = [];
|
||||
children.addAll([
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: l10n.recoveryKey,
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(
|
||||
context,
|
||||
l10n.authToViewYourRecoveryKey,
|
||||
);
|
||||
if (hasAuthenticated) {
|
||||
String recoveryKey;
|
||||
try {
|
||||
recoveryKey =
|
||||
Sodium.bin2hex(Configuration.instance.getRecoveryKey());
|
||||
} catch (e) {
|
||||
showGenericErrorDialog(context: context);
|
||||
return;
|
||||
}
|
||||
routeToPage(
|
||||
if (_hasLoggedIn) {
|
||||
final bool? canDisableMFA = UserService.instance.canDisableEmailMFA();
|
||||
if (canDisableMFA == null) {
|
||||
// We don't know if the user can disable MFA yet, so we fetch the info
|
||||
UserService.instance.getUserDetailsV2().ignore();
|
||||
}
|
||||
children.addAll([
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: l10n.recoveryKey,
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(
|
||||
context,
|
||||
RecoveryKeyPage(
|
||||
recoveryKey,
|
||||
l10n.ok,
|
||||
showAppBar: true,
|
||||
onDone: () {},
|
||||
),
|
||||
l10n.authToViewYourRecoveryKey,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (hasAuthenticated) {
|
||||
String recoveryKey;
|
||||
try {
|
||||
recoveryKey =
|
||||
Sodium.bin2hex(Configuration.instance.getRecoveryKey());
|
||||
} catch (e) {
|
||||
showGenericErrorDialog(context: context);
|
||||
return;
|
||||
}
|
||||
routeToPage(
|
||||
context,
|
||||
RecoveryKeyPage(
|
||||
recoveryKey,
|
||||
l10n.ok,
|
||||
showAppBar: true,
|
||||
onDone: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: l10n.emailVerificationToggle,
|
||||
),
|
||||
trailingWidget: ToggleSwitchWidget(
|
||||
value: () => UserService.instance.hasEmailMFAEnabled(),
|
||||
onChanged: () async {
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(
|
||||
context,
|
||||
l10n.authToChangeEmailVerificationSetting,
|
||||
);
|
||||
final isEmailMFAEnabled =
|
||||
UserService.instance.hasEmailMFAEnabled();
|
||||
if (hasAuthenticated) {
|
||||
await updateEmailMFA(!isEmailMFAEnabled);
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: context.l10n.viewActiveSessions,
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(
|
||||
context,
|
||||
context.l10n.authToViewYourActiveSessions,
|
||||
);
|
||||
if (hasAuthenticated) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const SessionsPage();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
children.add(sectionOptionSpacing);
|
||||
}
|
||||
children.addAll([
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: l10n.lockscreen,
|
||||
@@ -117,54 +173,6 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
||||
),
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: l10n.emailVerificationToggle,
|
||||
),
|
||||
trailingWidget: ToggleSwitchWidget(
|
||||
value: () => UserService.instance.hasEmailMFAEnabled(),
|
||||
onChanged: () async {
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(
|
||||
context,
|
||||
l10n.authToChangeEmailVerificationSetting,
|
||||
);
|
||||
final isEmailMFAEnabled = UserService.instance.hasEmailMFAEnabled();
|
||||
if (hasAuthenticated) {
|
||||
await updateEmailMFA(!isEmailMFAEnabled);
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: context.l10n.viewActiveSessions,
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(
|
||||
context,
|
||||
context.l10n.authToViewYourActiveSessions,
|
||||
);
|
||||
if (hasAuthenticated) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const SessionsPage();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
]);
|
||||
return Column(
|
||||
children: children,
|
||||
@@ -173,12 +181,19 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
||||
|
||||
Future<void> updateEmailMFA(bool isEnabled) async {
|
||||
try {
|
||||
final UserDetails details = await UserService.instance.getUserDetailsV2(memoryCount: false);
|
||||
if(details.profileData?.canDisableEmailMFA == false) {
|
||||
await routeToPage(context, RequestPasswordVerificationPage(onPasswordVerified: (Uint8List keyEncryptionKey) async {
|
||||
final Uint8List loginKey = await CryptoUtil.deriveLoginKey(keyEncryptionKey);
|
||||
await UserService.instance.registerOrUpdateSrp(loginKey);
|
||||
},),);
|
||||
final UserDetails details =
|
||||
await UserService.instance.getUserDetailsV2(memoryCount: false);
|
||||
if (details.profileData?.canDisableEmailMFA == false) {
|
||||
await routeToPage(
|
||||
context,
|
||||
RequestPasswordVerificationPage(
|
||||
onPasswordVerified: (Uint8List keyEncryptionKey) async {
|
||||
final Uint8List loginKey =
|
||||
await CryptoUtil.deriveLoginKey(keyEncryptionKey);
|
||||
await UserService.instance.registerOrUpdateSrp(loginKey);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
await UserService.instance.updateEmailMFA(isEnabled);
|
||||
} catch (e) {
|
||||
|
||||
@@ -24,9 +24,9 @@ class SettingsSectionTitle extends StatelessWidget {
|
||||
style: color != null
|
||||
? Theme.of(context)
|
||||
.textTheme
|
||||
.headline6!
|
||||
.titleLarge!
|
||||
.merge(TextStyle(color: color))
|
||||
: Theme.of(context).textTheme.headline6,
|
||||
: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(4)),
|
||||
|
||||
@@ -23,7 +23,7 @@ class SettingsTextItem extends StatelessWidget {
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(text, style: Theme.of(context).textTheme.subtitle1),
|
||||
child: Text(text, style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
Icon(icon),
|
||||
],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/subscription.dart';
|
||||
import 'package:ente_auth/services/billing_service.dart';
|
||||
@@ -18,50 +19,56 @@ class SupportDevWidget extends StatelessWidget {
|
||||
final l10n = context.l10n;
|
||||
|
||||
// fetch
|
||||
return FutureBuilder<Subscription>(
|
||||
future: BillingService.instance.getSubscription(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final subscription = snapshot.data;
|
||||
if (subscription != null && subscription.productID == "free") {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse("https://ente.io"));
|
||||
},
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 12.0, horizontal: 6),
|
||||
child: Column(
|
||||
children: [
|
||||
StyledText(
|
||||
text: l10n.supportDevs,
|
||||
tags: {
|
||||
'bold-green': StyledTextTag(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: getEnteColorScheme(context).primaryGreen,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(6)),
|
||||
Platform.isAndroid
|
||||
? Text(
|
||||
l10n.supportDiscount,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (Configuration.instance.hasConfiguredAccount()) {
|
||||
return FutureBuilder<Subscription>(
|
||||
future: BillingService.instance.getSubscription(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final subscription = snapshot.data;
|
||||
if (subscription != null && subscription.productID == "free") {
|
||||
return buildWidget(l10n, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return buildWidget(l10n, context);
|
||||
}
|
||||
}
|
||||
|
||||
GestureDetector buildWidget(AppLocalizations l10n, BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse("https://ente.io"));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
StyledText(
|
||||
text: l10n.supportDevs,
|
||||
style: getEnteTextTheme(context).large,
|
||||
tags: {
|
||||
'bold-green': StyledTextTag(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: getEnteColorScheme(context).primaryGreen,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(6)),
|
||||
Text(
|
||||
l10n.supportDiscount,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,41 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/onboarding/view/onboarding_page.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/theme/colors.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_result.dart';
|
||||
import 'package:ente_auth/ui/components/notification_warning_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/about_section_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/account_section_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/app_version_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/data/data_section_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/data/export_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/security_section_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/social_section_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/support_dev_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/support_section_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/theme_switch_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/title_bar_widget.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
final ValueNotifier<String?> emailNotifier;
|
||||
const SettingsPage({Key? key, required this.emailNotifier}) : super(key: key);
|
||||
|
||||
SettingsPage({Key? key, required this.emailNotifier}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
UserService.instance.getUserDetailsV2().ignore();
|
||||
final _hasLoggedIn = Configuration.instance.hasConfiguredAccount();
|
||||
if (_hasLoggedIn) {
|
||||
UserService.instance.getUserDetailsV2().ignore();
|
||||
}
|
||||
final enteColorScheme = getEnteColorScheme(context);
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
@@ -34,34 +46,71 @@ class SettingsPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _getBody(BuildContext context, EnteColorScheme colorScheme) {
|
||||
final _hasLoggedIn = Configuration.instance.hasConfiguredAccount();
|
||||
final enteTextTheme = getEnteTextTheme(context);
|
||||
const sectionSpacing = SizedBox(height: 8);
|
||||
final List<Widget> contents = [];
|
||||
contents.add(
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AnimatedBuilder(
|
||||
// [AnimatedBuilder] accepts any [Listenable] subtype.
|
||||
animation: emailNotifier,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return Text(
|
||||
emailNotifier.value!,
|
||||
style: enteTextTheme.body.copyWith(
|
||||
color: colorScheme.textMuted,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
},
|
||||
if (_hasLoggedIn) {
|
||||
contents.add(
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AnimatedBuilder(
|
||||
// [AnimatedBuilder] accepts any [Listenable] subtype.
|
||||
animation: emailNotifier,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return Text(
|
||||
emailNotifier.value!,
|
||||
style: enteTextTheme.body.copyWith(
|
||||
color: colorScheme.textMuted,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
const sectionSpacing = SizedBox(height: 8);
|
||||
contents.add(const SizedBox(height: 12));
|
||||
);
|
||||
contents.addAll([
|
||||
const SizedBox(height: 12),
|
||||
AccountSectionWidget(),
|
||||
sectionSpacing,
|
||||
]);
|
||||
} else {
|
||||
contents.addAll([
|
||||
NotificationWidget(
|
||||
startIcon: Icons.account_circle_sharp,
|
||||
actionIcon: Icons.arrow_forward,
|
||||
text: context.l10n.signInToBackup,
|
||||
type: NotificationType.notice,
|
||||
onTap: () async {
|
||||
ButtonResult? result = await showChoiceActionSheet(
|
||||
context,
|
||||
title: context.l10n.warning,
|
||||
body: context.l10n.sigInBackupReminder,
|
||||
secondButtonLabel: context.l10n.singIn,
|
||||
secondButtonAction: ButtonAction.second,
|
||||
firstButtonLabel: context.l10n.exportCodes,
|
||||
);
|
||||
if (result == null) return;
|
||||
if (result.action == ButtonAction.first) {
|
||||
await handleExportClick(context);
|
||||
} else {
|
||||
if (result.action == ButtonAction.second) {
|
||||
await routeToPage(
|
||||
context,
|
||||
const OnboardingPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
sectionSpacing,
|
||||
sectionSpacing,
|
||||
]);
|
||||
}
|
||||
contents.addAll([
|
||||
AccountSectionWidget(),
|
||||
sectionSpacing,
|
||||
DataSectionWidget(),
|
||||
sectionSpacing,
|
||||
const SecuritySectionWidget(),
|
||||
|
||||
@@ -65,6 +65,6 @@ class IconUtils {
|
||||
}
|
||||
|
||||
String _getProviderTitle(String provider) {
|
||||
return provider.split(".")[0].toLowerCase();
|
||||
return provider.split(RegExp(r'[.(]'))[0].trim().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,8 +201,8 @@ Widget getDayWidget(
|
||||
child: Text(
|
||||
getDayTitle(timestamp),
|
||||
style: (getDayTitle(timestamp) == "Today" && !smallerTodayFont)
|
||||
? Theme.of(context).textTheme.headline5
|
||||
: Theme.of(context).textTheme.caption?.copyWith(
|
||||
? Theme.of(context).textTheme.headlineSmall
|
||||
: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'Inter-SemiBold',
|
||||
|
||||
@@ -224,7 +224,7 @@ ProgressDialog createProgressDialog(
|
||||
);
|
||||
dialog.style(
|
||||
message: message,
|
||||
messageTextStyle: Theme.of(context).textTheme.caption,
|
||||
messageTextStyle: Theme.of(context).textTheme.labelMedium,
|
||||
backgroundColor: Theme.of(context).dialogTheme.backgroundColor,
|
||||
progressWidget: const EnteLoadingWidget(),
|
||||
borderRadius: 10,
|
||||
|
||||
@@ -229,7 +229,7 @@ Future<void> sendEmail(
|
||||
final Uri params = Uri(
|
||||
scheme: 'mailto',
|
||||
path: to,
|
||||
query: 'subject=$_subject&body=${_body}',
|
||||
query: 'subject=$_subject&body=$_body',
|
||||
);
|
||||
if (await canLaunchUrl(params)) {
|
||||
await launchUrl(params);
|
||||
|
||||
@@ -9,7 +9,7 @@ import connectivity_macos
|
||||
import device_info_plus
|
||||
import flutter_local_notifications
|
||||
import flutter_secure_storage_macos
|
||||
import package_info_plus_macos
|
||||
import package_info_plus
|
||||
import path_provider_foundation
|
||||
import sentry_flutter
|
||||
import share_plus_macos
|
||||
|
||||
@@ -88,14 +88,14 @@ SPEC CHECKSUMS:
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c
|
||||
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
|
||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
|
||||
Sentry: 4c9babff9034785067c896fd580b1f7de44da020
|
||||
sentry_flutter: b10ae7a5ddcbc7f04648eeb2672b5747230172f1
|
||||
sentry_flutter: 1346a880b24c0240807b53b10cf50ddad40f504e
|
||||
share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4
|
||||
shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
|
||||
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
||||
sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea
|
||||
url_launcher_macos: c04e4fa86382d4f94f6b38f14625708be3ae52e2
|
||||
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
||||
|
||||
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
|
||||
|
||||
|
||||
70
migration-guides/decrypt/crypt.go
Normal file
70
migration-guides/decrypt/crypt.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"github.com/jamesruan/sodium"
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
// deriveArgonKey generates a 32-bit cryptographic key using the Argon2id algorithm.
|
||||
// Parameters:
|
||||
// - password: The plaintext password to be hashed.
|
||||
// - salt: The salt as a base64 encoded string.
|
||||
// - memLimit: The memory limit in bytes.
|
||||
// - opsLimit: The number of iterations.
|
||||
//
|
||||
// Returns:
|
||||
// - A byte slice representing the derived key.
|
||||
// - An error object, which is nil if no error occurs.
|
||||
func deriveArgonKey(password, salt string, memLimit, opsLimit int) ([]byte, error) {
|
||||
if memLimit < 1024 || opsLimit < 1 {
|
||||
return nil, fmt.Errorf("invalid memory or operation limits")
|
||||
}
|
||||
|
||||
// Decode salt from base64
|
||||
saltBytes, err := base64.StdEncoding.DecodeString(salt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid salt: %v", err)
|
||||
}
|
||||
|
||||
// Generate key using Argon2id
|
||||
// Note: We're assuming a fixed key length of 32 bytes and changing the threads
|
||||
key := argon2.IDKey([]byte(password), saltBytes, uint32(opsLimit), uint32(memLimit/1024), 1, 32)
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// decryptChaCha20poly1305 decrypts the given data using the ChaCha20-Poly1305 algorithm.
|
||||
// Parameters:
|
||||
// - data: The encrypted data as a byte slice.
|
||||
// - key: The key for decryption as a byte slice.
|
||||
// - nonce: The nonce for decryption as a byte slice.
|
||||
//
|
||||
// Returns:
|
||||
// - A byte slice representing the decrypted data.
|
||||
// - An error object, which is nil if no error occurs.
|
||||
func decryptChaCha20poly1305(data []byte, key []byte, nonce []byte) ([]byte, error) {
|
||||
reader := bytes.NewReader(data)
|
||||
header := sodium.SecretStreamXCPHeader{Bytes: nonce}
|
||||
decoder, err := sodium.MakeSecretStreamXCPDecoder(
|
||||
sodium.SecretStreamXCPKey{Bytes: key},
|
||||
reader,
|
||||
header)
|
||||
if err != nil {
|
||||
log.Println("Failed to make secret stream decoder", err)
|
||||
return nil, err
|
||||
}
|
||||
// Buffer to store the decrypted data
|
||||
decryptedData := make([]byte, len(data))
|
||||
n, err := decoder.Read(decryptedData)
|
||||
if err != nil && err != io.EOF {
|
||||
log.Println("Failed to read from decoder", err)
|
||||
return nil, err
|
||||
}
|
||||
return decryptedData[:n], nil
|
||||
}
|
||||
53
migration-guides/decrypt/crypt_test.go
Normal file
53
migration-guides/decrypt/crypt_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
password = "test_password"
|
||||
kdfSalt = "vd0dcYMGNLKn/gpT6uTFTw=="
|
||||
memLimit = 64 * 1024 * 1024 // 64MB
|
||||
opsLimit = 2
|
||||
cipherText = "kBXQ2PuX6y/aje5r22H0AehRPh6sQ0ULoeAO"
|
||||
cipherNonce = "v7wsI+BFZsRMIjDm3rTxPhmi/CaUdkdJ"
|
||||
expectedPlainText = "plain_text"
|
||||
expectedDerivedKey = "vp8d8Nee0BbIML4ab8Cp34uYnyrN77cRwTl920flyT0="
|
||||
)
|
||||
|
||||
func TestDeriveArgonKey(t *testing.T) {
|
||||
derivedKey, err := deriveArgonKey(password, kdfSalt, memLimit, opsLimit)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to derive key: %v", err)
|
||||
}
|
||||
|
||||
if base64.StdEncoding.EncodeToString(derivedKey) != expectedDerivedKey {
|
||||
t.Fatalf("Derived key does not match expected key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecryptChaCha20poly1305(t *testing.T) {
|
||||
derivedKey, err := deriveArgonKey(password, kdfSalt, memLimit, opsLimit)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to derive key: %v", err)
|
||||
}
|
||||
|
||||
decodedCipherText, err := base64.StdEncoding.DecodeString(cipherText)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decode cipher text: %v", err)
|
||||
}
|
||||
|
||||
decodedCipherNonce, err := base64.StdEncoding.DecodeString(cipherNonce)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decode cipher nonce: %v", err)
|
||||
}
|
||||
|
||||
decryptedText, err := decryptChaCha20poly1305(decodedCipherText, derivedKey, decodedCipherNonce)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decrypt: %v", err)
|
||||
}
|
||||
if string(decryptedText) != expectedPlainText {
|
||||
t.Fatalf("Decrypted text : %s does not match the expected text: %s", string(decryptedText), expectedPlainText)
|
||||
}
|
||||
}
|
||||
BIN
migration-guides/decrypt/decrypt
Executable file
BIN
migration-guides/decrypt/decrypt
Executable file
Binary file not shown.
105
migration-guides/decrypt/decrypt.go
Normal file
105
migration-guides/decrypt/decrypt.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Export struct {
|
||||
Version int `json:"version"`
|
||||
KDFParams KDF `json:"kdfParams"`
|
||||
EncryptedData string `json:"encryptedData"`
|
||||
EncryptionNonce string `json:"encryptionNonce"`
|
||||
}
|
||||
|
||||
type KDF struct {
|
||||
MemLimit int `json:"memLimit"`
|
||||
OpsLimit int `json:"opsLimit"`
|
||||
Salt string `json:"salt"`
|
||||
}
|
||||
|
||||
func resolvePath(path string) (string, error) {
|
||||
if path[:2] != "~/" {
|
||||
return path, nil
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return home + path[1:], nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if len(os.Args) != 4 {
|
||||
fmt.Println("Usage: ./decrypt <export_file> <password> <output_file>")
|
||||
return
|
||||
}
|
||||
|
||||
exportFile, err := resolvePath(os.Args[1])
|
||||
if err != nil {
|
||||
fmt.Println("Error resolving exportFile path:", err)
|
||||
return
|
||||
}
|
||||
password := os.Args[2]
|
||||
outputFile, err := resolvePath(os.Args[3])
|
||||
if err != nil {
|
||||
fmt.Println("Error resolving outputFile path:", err)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(exportFile)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading file:", err)
|
||||
return
|
||||
}
|
||||
|
||||
var export Export
|
||||
if err := json.Unmarshal(data, &export); err != nil {
|
||||
fmt.Println("Error parsing JSON:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if export.Version != 1 {
|
||||
fmt.Println("Unsupported version")
|
||||
return
|
||||
}
|
||||
|
||||
encryptedData, err := base64.StdEncoding.DecodeString(export.EncryptedData)
|
||||
if err != nil {
|
||||
fmt.Println("Error decoding encrypted data:", err)
|
||||
return
|
||||
}
|
||||
|
||||
nonce, err := base64.StdEncoding.DecodeString(export.EncryptionNonce)
|
||||
if err != nil {
|
||||
fmt.Println("Error decoding nonce:", err)
|
||||
return
|
||||
}
|
||||
|
||||
key, err := deriveArgonKey(password, export.KDFParams.Salt, export.KDFParams.MemLimit, export.KDFParams.OpsLimit)
|
||||
if err != nil {
|
||||
fmt.Println("Error deriving key:", err)
|
||||
return
|
||||
}
|
||||
|
||||
decryptedData, err := decryptChaCha20poly1305(encryptedData, key, nonce)
|
||||
if err != nil {
|
||||
fmt.Println("Error decrypting data:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(outputFile, decryptedData, 0644); err != nil {
|
||||
fmt.Println("Error writing decrypted data to file:", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Decrypted data written to %s\n", outputFile)
|
||||
}
|
||||
10
migration-guides/decrypt/go.mod
Normal file
10
migration-guides/decrypt/go.mod
Normal file
@@ -0,0 +1,10 @@
|
||||
module decrypt
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/jamesruan/sodium v1.0.14
|
||||
golang.org/x/crypto v0.11.0
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.10.0 // indirect
|
||||
6
migration-guides/decrypt/go.sum
Normal file
6
migration-guides/decrypt/go.sum
Normal file
@@ -0,0 +1,6 @@
|
||||
github.com/jamesruan/sodium v1.0.14 h1:JfOHobip/lUWouxHV3PwYwu3gsLewPrDrZXO3HuBzUU=
|
||||
github.com/jamesruan/sodium v1.0.14/go.mod h1:GK2+LACf7kuVQ9k7Irk0MB2B65j5rVqkz+9ylGIggZk=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
## Overview
|
||||
|
||||
When we export the auth codes, the data is encrypted using a key derived from the user's password.
|
||||
This document describes the JSON structure used to organize exported data, including versioning and key derivation parameters.
|
||||
When we export the auth codes, the data is encrypted using a key derived from the user's password.
|
||||
This document describes the JSON structure used to organize exported data, including versioning and key derivation
|
||||
parameters.
|
||||
|
||||
## Export JSON Sample
|
||||
|
||||
@@ -27,10 +28,12 @@ The main object used to represent the export data. It contains the following key
|
||||
- `encryptedData"`: The encrypted authentication data.
|
||||
- `encryptionNonce`: The nonce used for encryption.
|
||||
|
||||
### Version
|
||||
### Version
|
||||
|
||||
Export version is used to identify the format of the export data.
|
||||
|
||||
Export version is used to identify the format of the export data.
|
||||
#### Ver: 1
|
||||
|
||||
* KDF Algorithm: `ARGON2ID`
|
||||
* Decrypted data format: `otpauth://totp/...`, separated by a new line.
|
||||
* Encryption Algo: `XChaCha20-Poly1305`
|
||||
@@ -44,9 +47,17 @@ This section contains the parameters that were using during KDF operation:
|
||||
- `salt`: The salt used in the derivation process.
|
||||
|
||||
#### Encrypted Data
|
||||
As mentioned above, the auth data is encrypted using a key that's derived by using user provided password & kdf params.
|
||||
For encryption, we are using `XChaCha20-Poly1305` algorithm.
|
||||
|
||||
## How to use the export data
|
||||
* **ente Authenticator app**: You can directly import the codes in the ente Authenticator app.
|
||||
>Settings -> Data -> Import Codes -> ente Encrypted export.
|
||||
As mentioned above, the auth data is encrypted using a key that's derived by using user provided password & kdf params.
|
||||
For encryption, we are using `XChaCha20-Poly1305` algorithm.
|
||||
|
||||
## How to use the exported data
|
||||
|
||||
* **ente Authenticator app**: You can directly import the codes in the ente Authenticator app.
|
||||
> Settings -> Data -> Import Codes -> ente Encrypted export.
|
||||
|
||||
* **Decryption Tool** : You can download the prebuilt [decryption tool](decrypt/decrypt) (or build it from [source](decrypt)) and run the following command.
|
||||
|
||||
```
|
||||
./decrypt <export_file> <password> <output_file>
|
||||
```
|
||||
|
||||
106
pubspec.lock
106
pubspec.lock
@@ -45,10 +45,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.0"
|
||||
version: "2.11.0"
|
||||
base32:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -157,10 +157,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.3.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -197,10 +197,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
|
||||
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.17.1"
|
||||
computer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -382,10 +382,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: expansion_tile_card
|
||||
sha256: "0812d52e4a2effa4957286b9037c81394e1881ac49bcd8f006f631f2ea5ef6eb"
|
||||
sha256: "27ce4cb518f00e21d0f2309aaa6462b26b148e93cee2029a73088cecf42b1eb0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "3.0.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -414,10 +414,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: b85eb92b175767fdaa0c543bf3b0d1f610fe966412ea72845fe5ba7801e763ff
|
||||
sha256: "9d6e95ec73abbd31ec54d0e0df8a961017e165aba1395e462e5b31ea0c165daf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.10"
|
||||
version: "5.3.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -699,10 +699,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
|
||||
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.17.0"
|
||||
version: "0.18.0"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -715,10 +715,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7"
|
||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.5"
|
||||
version: "0.6.7"
|
||||
json_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -795,10 +795,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72"
|
||||
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.13"
|
||||
version: "0.12.15"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -811,10 +811,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42"
|
||||
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
version: "1.9.1"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -891,50 +891,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: f62d7253edc197fe3c88d7c2ddab82d68f555e778d55390ccc3537eca8e8d637
|
||||
sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.3+1"
|
||||
package_info_plus_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_linux
|
||||
sha256: "04b575f44233d30edbb80a94e57cad9107aada334fc02aabb42b6becd13c43fc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
package_info_plus_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_macos
|
||||
sha256: a2ad8b4acf4cd479d4a0afa5a74ea3f5b1c7563b77e52cc32b3ee6956d5482a6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "4.1.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: f7a0c8f1e7e981bc65f8b64137a53fd3c195b18d429fba960babc59a5a1c7ae8
|
||||
sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
package_info_plus_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_web
|
||||
sha256: f0829327eb534789e0a16ccac8936a80beed4e2401c4d3a74f3f39094a822d3b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
package_info_plus_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_windows
|
||||
sha256: "79524f11c42dd9078b96d797b3cf79c0a2883a50c4920dc43da8562c115089bc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.0.1"
|
||||
password_strength:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -947,10 +915,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
|
||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
version: "1.8.3"
|
||||
path_drawing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1131,18 +1099,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry
|
||||
sha256: a1529c545fcbc899e5dcc7c94ff1c6ad0c334dfc99a3cda366b1da98af7c5678
|
||||
sha256: "39c23342fc96105da449914f7774139a17a0ca8a4e70d9ad5200171f7e47d6ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.22.0"
|
||||
version: "7.9.0"
|
||||
sentry_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry_flutter
|
||||
sha256: cab07e99a8f27af94f399cabceaff6968011660505b30a0e2286728a81bc476c
|
||||
sha256: ff68ab31918690da004a42e20204242a3ad9ad57da7e2712da8487060ac9767f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.22.0"
|
||||
version: "7.9.0"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1408,26 +1376,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
sha256: a5fcd2d25eeadbb6589e80198a47d6a464ba3e2049da473943b8af9797900c2d
|
||||
sha256: "3dac9aecf2c3991d09b9cdde4f98ded7b30804a88a0d7e4e7e1678e78d6b97f4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.22.0"
|
||||
version: "1.24.1"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206
|
||||
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.16"
|
||||
version: "0.5.1"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "0ef9755ec6d746951ba0aabe62f874b707690b5ede0fecc818b138fcc9b14888"
|
||||
sha256: "5138dbffb77b2289ecb12b81c11ba46036590b72a64a7a90d6ffb880f1a29e93"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.20"
|
||||
version: "0.5.1"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1632,10 +1600,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4
|
||||
sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
version: "4.1.4"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1669,5 +1637,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=2.19.0 <3.0.0"
|
||||
dart: ">=3.0.0 <4.0.0"
|
||||
flutter: ">=3.7.0"
|
||||
|
||||
14
pubspec.yaml
14
pubspec.yaml
@@ -1,10 +1,10 @@
|
||||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 1.0.55+55
|
||||
version: 2.0.1+201
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.0 <3.0.0"
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
adaptive_theme: ^3.1.0 # done
|
||||
@@ -25,7 +25,7 @@ dependencies:
|
||||
email_validator: ^2.0.1
|
||||
event_bus: ^2.0.0
|
||||
expandable: ^5.0.1
|
||||
expansion_tile_card: ^2.0.0
|
||||
expansion_tile_card: ^3.0.0
|
||||
file_picker: ^5.2.4
|
||||
fk_user_agent: ^2.1.0
|
||||
flutter:
|
||||
@@ -48,7 +48,7 @@ dependencies:
|
||||
fluttertoast: ^8.1.1
|
||||
google_nav_bar: ^5.0.5 #supported
|
||||
http: ^0.13.4
|
||||
intl: ^0.17.0
|
||||
intl: ^0.18.0
|
||||
json_annotation: ^4.5.0
|
||||
local_auth: ^2.1.3
|
||||
logging: ^1.0.1
|
||||
@@ -56,7 +56,7 @@ dependencies:
|
||||
move_to_background: ^1.0.2
|
||||
open_filex: ^4.3.2
|
||||
otp: ^3.1.1
|
||||
package_info_plus: ^1.0.1
|
||||
package_info_plus: ^4.1.0
|
||||
password_strength: ^0.2.0
|
||||
path_provider: ^2.0.11
|
||||
pinput: ^1.2.2
|
||||
@@ -64,8 +64,8 @@ dependencies:
|
||||
protobuf: ^3.0.0
|
||||
qr_code_scanner: ^1.0.1
|
||||
qr_flutter: 4.0.0
|
||||
sentry: ^6.12.1
|
||||
sentry_flutter: ^6.12.1
|
||||
sentry: ^7.9.0
|
||||
sentry_flutter: ^7.9.0
|
||||
share_plus: ^4.4.0
|
||||
shared_preferences: ^2.0.5
|
||||
sqflite: ^2.1.0
|
||||
|
||||
Reference in New Issue
Block a user