Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c9c9c53d1 | ||
|
|
aeaaddbe40 | ||
|
|
25782870c7 | ||
|
|
39e28dd63b | ||
|
|
517ce33fd9 | ||
|
|
02d2cb5733 | ||
|
|
2b5f349b2f | ||
|
|
08ad496975 | ||
|
|
f52ee5683b | ||
|
|
332faa9166 | ||
|
|
3d95c789f5 | ||
|
|
244d4d969a | ||
|
|
32605dc708 | ||
|
|
ef0c302c94 | ||
|
|
fba694de68 | ||
|
|
3d4cd106cb | ||
|
|
61e6e0ffaa | ||
|
|
e0b952e516 | ||
|
|
546a9234a4 | ||
|
|
da2083ef08 | ||
|
|
8a1177e7db | ||
|
|
8acd1faf03 | ||
|
|
0c28b83b46 | ||
|
|
fa0ac608f4 | ||
|
|
0965b367cc | ||
|
|
37db940720 | ||
|
|
334800472e | ||
|
|
0da4497857 | ||
|
|
0b05a21dc7 | ||
|
|
2558473399 | ||
|
|
4dd465fdfa | ||
|
|
1f20ba17f4 | ||
|
|
9ce8059212 | ||
|
|
e69a3adf14 | ||
|
|
8baa350056 | ||
|
|
2526b61620 | ||
|
|
ec4df467a0 | ||
|
|
41fd5337a6 | ||
|
|
8fbec8307e | ||
|
|
d902fdbf75 | ||
|
|
932dc53f6c | ||
|
|
745cb54ffd | ||
|
|
8ced5bf32e | ||
|
|
9b1a8fb4ca | ||
|
|
824c6d769b | ||
|
|
92c2247aa1 | ||
|
|
e84e9db70e | ||
|
|
be72db844d | ||
|
|
2386a5a10b | ||
|
|
09ae14a1d6 | ||
|
|
924ce5ff86 | ||
|
|
c47370163d | ||
|
|
3dd408af01 | ||
|
|
408e3bd2b6 | ||
|
|
ebf634ef1e | ||
|
|
62c21749b5 | ||
|
|
a9e8245b4d | ||
|
|
a088c61785 | ||
|
|
640fad7b3c | ||
|
|
f6df133e73 | ||
|
|
7f55e68d09 | ||
|
|
3c5cf55fbe | ||
|
|
7b834e258f | ||
|
|
fcc2d925d9 | ||
|
|
0a41f8cba1 | ||
|
|
c9f8046351 |
4
.github/workflows/ci.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: '3.3.2'
|
||||
flutter-version: '3.3.8'
|
||||
|
||||
# Fetch sub modules
|
||||
- run: git submodule update --init --recursive
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: release-checksum
|
||||
path: build/app/outputs/flutter-apk/checksum
|
||||
path: build/app/outputs/flutter-apk/sha256sum
|
||||
|
||||
# Create a Github release
|
||||
- uses: ncipollo/release-action@v1
|
||||
|
||||
25
.vscode/launch.json
vendored
@@ -10,18 +10,37 @@
|
||||
"args": ["--dart-define", "endpoint=http://localhost:8080"]
|
||||
},
|
||||
{
|
||||
"name": "Dev",
|
||||
"name": "Android Dev",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main.dart",
|
||||
"args": ["--dart-define", "endpoint=http://192.168.1.3:8080"]
|
||||
"args": [
|
||||
"--dart-define",
|
||||
"endpoint=http://192.168.1.3:8080",
|
||||
"--flavor",
|
||||
"independent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Prod",
|
||||
"name": "iOS Dev",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main.dart",
|
||||
"args": ["--dart-define", "endpoint=http://192.168.1.30:8080"]
|
||||
},
|
||||
{
|
||||
"name": "iOS Prod",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main.dart",
|
||||
"args": ["--target", "lib/main.dart"]
|
||||
},
|
||||
{
|
||||
"name": "Android Prod",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main.dart",
|
||||
"args": ["--target", "lib/main.dart", "--flavor", "independent"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
64
README.md
@@ -1,3 +1,63 @@
|
||||
# ente Auth
|
||||
# ente Authenticator
|
||||
|
||||
Open source authenticator app for your 2FA secrets, with end-to-end encrypted backups.
|
||||
ente's Authenticator helps you generate and store 2 step verification (2FA)
|
||||
tokens on your mobile devices.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### Secure Backups
|
||||
|
||||
ente provides end-to-end encrypted cloud backups so that you don't have to worry
|
||||
about losing your tokens. We use the same protocols [ente
|
||||
Photos](https://ente.io/photos) uses to encrypt and preserve your data.
|
||||
|
||||
|
||||
### Multi Device Synchronization
|
||||
|
||||
ente will automatically sync the 2FA tokens you add to your account, across all
|
||||
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.
|
||||
|
||||
### 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/ACCOUNT?secret=SUPERSECRET&issuer=SERVICE
|
||||
```
|
||||
The codes maybe separated by new lines or commas.
|
||||
|
||||
You can also export the codes you have added to ente, to an **unencrypted** text
|
||||
file, that adheres to the above format.
|
||||
|
||||
<br/>
|
||||
|
||||
## 🙋♂️ Support
|
||||
|
||||
If you need help, please reach out to support@ente.io, and a human will get in touch with you.
|
||||
|
||||
On the other hand, if you wish to support us, please [star](https://github.com/ente-io/auth/stargazers) this project.
|
||||
|
||||
<br/>
|
||||
|
||||
## 🧑💻 Building 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`
|
||||
3. Pull in all submodules with `git submodule update --init --recursive`
|
||||
4. For Android, run `flutter build apk --release --flavor independent`
|
||||
5. For iOS, run `flutter build ios`
|
||||
|
||||
<br/>
|
||||
|
||||
## 💜 Community
|
||||
- Follow us on [Twitter](https://twitter.com/enteio)
|
||||
- Join us on [Discord](https://ente.io/discord)
|
||||
|
||||
50
SECURITY.md
Normal file
@@ -0,0 +1,50 @@
|
||||
ente believes that working with security researchers across the globe is crucial
|
||||
to keeping our users safe. If you believe you've found a security issue in our
|
||||
product or service, we encourage you to notify us (security@ente.io). We welcome
|
||||
working with you to resolve the issue promptly. Thanks in advance!
|
||||
|
||||
# Disclosure Policy
|
||||
|
||||
- Let us know as soon as possible upon discovery of a potential security issue,
|
||||
and we'll make every effort to quickly resolve the issue.
|
||||
- Provide us a reasonable amount of time to resolve the issue before any
|
||||
disclosure to the public or a third-party. We may publicly disclose the issue
|
||||
before resolving it, if appropriate.
|
||||
- Make a good faith effort to avoid privacy violations, destruction of data, and
|
||||
interruption or degradation of our service. Only interact with accounts you
|
||||
own or with explicit permission of the account holder.
|
||||
- If you would like to encrypt your report, please use the PGP key with long ID
|
||||
`E273695C0403F34F74171932DF6DDDE98EBD2394` (available in the public keyserver
|
||||
pool).
|
||||
|
||||
# In-scope
|
||||
|
||||
- Security issues in any current release of ente. This includes the web app,
|
||||
desktop app, and mobile apps (iOS and Android). Product downloads are
|
||||
available at https://ente.io. Source code is available at
|
||||
https://github.com/ente-io.
|
||||
|
||||
# Exclusions
|
||||
|
||||
The following bug classes are out-of scope:
|
||||
|
||||
- Bugs that are already reported on any of ente's issue trackers
|
||||
(https://github.com/ente-io), or that we already know of. Note that some of
|
||||
our issue tracking is private.
|
||||
- Issues in an upstream software dependency (ex: Flutter, Next.js etc) which are
|
||||
already reported to the upstream maintainer.
|
||||
- Attacks requiring physical access to a user's device.
|
||||
- Self-XSS
|
||||
- Issues related to software or protocols not under ente's control
|
||||
- Vulnerabilities in outdated versions of ente
|
||||
- Missing security best practices that do not directly lead to a vulnerability
|
||||
- Issues that do not have any impact on the general public
|
||||
|
||||
While researching, we'd like to ask you to refrain from:
|
||||
|
||||
- Denial of service
|
||||
- Spamming
|
||||
- Social engineering (including phishing) of ente staff or contractors
|
||||
- Any physical attempts against ente property or data centers
|
||||
|
||||
Thank you for helping keep ente and our users safe!
|
||||
@@ -1,33 +1,57 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="io.ente.auth">
|
||||
<application
|
||||
android:label="auth"
|
||||
android:icon="@mipmap/launcher_icon">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="io.ente.auth">
|
||||
<application android:name="${applicationName}"
|
||||
android:label="auth"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:largeHeap="true">
|
||||
|
||||
<activity android:name=".MainActivity" android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="ente"/>
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
<meta-data android:name="flutterEmbedding" android:value="2"/>
|
||||
|
||||
<meta-data android:name="io.sentry.dsn"
|
||||
android:value="https://ed4ddd6309b847ba8849935e26e9b648@sentry.ente.io/9"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.SENDTO"/>
|
||||
<data android:scheme="mailto"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"/>
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29"
|
||||
tools:ignore="ScopedStorage"/>
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
||||
|
||||
</manifest>
|
||||
|
Before Width: | Height: | Size: 32 KiB |
BIN
android/app/src/main/res/drawable/notification_icon.png
Normal file
|
After Width: | Height: | Size: 901 B |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 15 KiB |
18
android/app/src/main/res/values-night-v31/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowSplashScreenBackground">#000000</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#ffffff</color>
|
||||
<color name="splash_color">#000000</color>
|
||||
<color name="ic_launcher_background">#ffffff</color>
|
||||
</resources>
|
||||
@@ -1,21 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<style name="LaunchTheme" parent="Theme.AppCompat.NoActionBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
35
architecture/authentication.svg
Normal file
|
After Width: | Height: | Size: 119 KiB |
11
architecture/e2ee.svg
Normal file
|
After Width: | Height: | Size: 22 KiB |
26
architecture/file-encryption.svg
Normal file
|
After Width: | Height: | Size: 105 KiB |
275
architecture/index.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Architecture
|
||||
|
||||
This is an overview of ente's end-to-end encrypted architecture and
|
||||
specifications of the underlying cryptography.
|
||||
|
||||
## Introduction
|
||||
|
||||
Your data is end-to-end encrypted with **ente**. Meaning, they are encrypted
|
||||
with your `keys` before they leave your device.
|
||||
|
||||
<img src="e2ee.svg" class="architecture-svg" style="max-width: 600px"
|
||||
title="End-to-end encryption in ente" />
|
||||
|
||||
<br/>
|
||||
|
||||
These `keys` are available only to you. Meaning only you can access your data
|
||||
else where.
|
||||
|
||||
What follows is an explanation of how we do what we do.
|
||||
|
||||
## Key Encryption
|
||||
|
||||
### Fundamentals
|
||||
|
||||
#### Master Key
|
||||
|
||||
When you sign up for **ente**, your client generates a `masterKey` for you. This
|
||||
never leaves your device unencrypted.
|
||||
|
||||
#### Key Encryption Key
|
||||
|
||||
Once you choose a password, a `keyEncryptionKey` is derived from it. This never
|
||||
leaves your device.
|
||||
|
||||
### Flows
|
||||
|
||||
#### Primary Device
|
||||
|
||||
During registration, your `masterKey` is encrypted with your`keyEncryptionKey`,
|
||||
and the resultant `encryptedMasterKey` is then sent to our servers for storage.
|
||||
|
||||
<img src="key-derivation.svg" class="architecture-svg" title="Key derivation" />
|
||||
|
||||
#### <a id="key-encryption-flows-secondary-device"></a> Secondary Device
|
||||
|
||||
When you sign in on a secondary device, after you successfully verify your
|
||||
email, our servers give you back your `encryptedMasterKey` that was sent to us
|
||||
by your primary device.
|
||||
|
||||
You are then prompted to enter your password. Once entered, your
|
||||
`keyEncryptionKey` is derived, and the client decrypts your `encryptedMasterKey`
|
||||
with this, to yield your original `masterKey`.
|
||||
|
||||
If the decryption fails, the client will know that the derived
|
||||
`keyEncryptionKey` was wrong, indicating an incorrect password, and this
|
||||
information will be surfaced to you.
|
||||
|
||||
### Privacy
|
||||
|
||||
- Since only you know your password, only you can derive your
|
||||
`keyEncryptionKey`.
|
||||
- Since only you can derive your `keyEncryptionKey`, only you have access to
|
||||
your `masterKey`.
|
||||
|
||||
> Keep reading to learn about how this `masterKey` is used to encrypt your data.
|
||||
|
||||
---
|
||||
|
||||
## Token Encryption
|
||||
|
||||
### Fundamentals
|
||||
|
||||
#### Token Key
|
||||
|
||||
Each of your tokens in **ente** are encrypted with a `tokenKey`. These never
|
||||
leave your device unencrypted.
|
||||
|
||||
#### Authenticator Key
|
||||
|
||||
Each of your `tokenKey`s are in turn encrypted with an `authKey`. This never
|
||||
leave your device unencrypted.
|
||||
|
||||
### Flows
|
||||
|
||||
#### Upload
|
||||
|
||||
- Each token and associated metadata is encrypted with randomly generated
|
||||
`tokenKey`s.
|
||||
- Each `tokenKey` is encrypted with your `authKey`. In case your account does
|
||||
not have an `authKey` yet, one is randomly generated and encrypted with your
|
||||
`masterKey`.
|
||||
- All of the above mentioned encrypted data is then pushed to the server for
|
||||
storage.
|
||||
|
||||
<img src="token-encryption.svg" class="architecture-svg" title="Token
|
||||
encryption" />
|
||||
|
||||
#### Download
|
||||
|
||||
- All of the above mentioned encrypted data is pulled from the server.
|
||||
- You first decrypt your `authKey` with your `masterKey`.
|
||||
- You then decrypt each token's `tokenKey` with your `authKey`.
|
||||
- Finally, you decrypt each token and associated metadata with the respective
|
||||
`tokenKey`s.
|
||||
|
||||
### Privacy
|
||||
|
||||
- As explained in the previous section, only you have access to your
|
||||
`masterKey`.
|
||||
- Since only you have access to your `masterKey`, only you can decrypt your
|
||||
`authKey`.
|
||||
- Since only you have access to your `authKey`, only you can decrypt the
|
||||
`tokenKey`s.
|
||||
- Since only you have access to the `tokenKey`s, only you can decrypt the tokens
|
||||
and their associated metadata.
|
||||
|
||||
---
|
||||
|
||||
## Key Recovery
|
||||
|
||||
### Fundamentals
|
||||
|
||||
#### Recovery Key
|
||||
|
||||
When you sign up for **ente**, your app generates a `recoveryKey` for you. This
|
||||
never leaves your device unencrypted.
|
||||
|
||||
### Flow
|
||||
|
||||
#### Storage
|
||||
|
||||
Your `recoveryKey` and `masterKey` are encrypted with each other and stored on
|
||||
the server.
|
||||
|
||||
#### Access
|
||||
|
||||
This encrypted `recoveryKey` is downloaded when you sign in on a new device.
|
||||
This is decrypted with your `masterKey` and surfaced to you whenever you request
|
||||
for it.
|
||||
|
||||
#### Recovery
|
||||
|
||||
Post email verification, if you're unable to unlock your account because you
|
||||
have forgotten your password, the client will prompt you to enter your
|
||||
`recoveryKey`.
|
||||
|
||||
The client then pulls the `masterKey` that was earlier encrypted and pushed to
|
||||
the server (as discussed in [Key Encryption](#key-encryption), and decrypts it
|
||||
with the entered `recoveryKey`. If the decryption succeeds, the client will know
|
||||
that you have entered the correct `recoveryKey`.
|
||||
|
||||
<img src="recovery.svg" class="architecture-svg" title="Recovery" />
|
||||
|
||||
Now that you have your `masterKey`, the client will prompt you to set a new
|
||||
password, using which it will derive a new `keyEncryptionKey`. This is then used
|
||||
to encrypt your `masterKey` and this new `encryptedMasterKey` is uploaded to our
|
||||
servers, similar to what was earlier discussed in [Key
|
||||
Encryption](#key-encryption).
|
||||
|
||||
### Privacy
|
||||
|
||||
- Since only you have access to your `masterKey`, only you can access your
|
||||
`recoveryKey`.
|
||||
- Since only you can access your `recoveryKey`, only you can reset your
|
||||
password.
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
### Fundamentals
|
||||
|
||||
#### One Time Token
|
||||
|
||||
When you attempt to verify ownership of an email address, our server generates a
|
||||
`oneTimeToken`, that if presented confirms your access to the said email
|
||||
address. This token is valid for a short time and can only be used once.
|
||||
|
||||
#### Authentication Token
|
||||
|
||||
When you successfully authenticate yourself against our server by proving
|
||||
ownership of your email (and in future any other configured vectors), the server
|
||||
generates an `authToken`, that can from there on be used to authenticate against
|
||||
our private APIs.
|
||||
|
||||
#### Encrypted Authentication Token
|
||||
|
||||
A generated `authToken` is returned to your client after being encrypted with
|
||||
your `publicKey`. This `encryptedAuthToken` can only be decrypted with your
|
||||
`privateKey`.
|
||||
|
||||
### Flow
|
||||
|
||||
- You are asked for an email address, to which a `oneTimeToken` is sent.
|
||||
- Once you present this information correctly to our server, an `authToken` is
|
||||
generated and an `encryptedAuthToken` is returned to you, along with your
|
||||
other encrypted keys.
|
||||
- You are then prompted to enter your password, using which your `masterKey` is
|
||||
derived (as discussed [here](#key-encryption-flows-secondary-device)).
|
||||
- Using this `masterKey`, the rest of your keys, including your `privateKey` is
|
||||
decrypted (as discussed [here](#private-key)).
|
||||
- Using your `privateKey`, the client will then decrypt the `encryptedAuthToken`
|
||||
that was earlier encrypted by our server with your `publicKey`.
|
||||
- This decrypted `authToken` can then from there on be used to authenticate all
|
||||
API calls against our servers.
|
||||
|
||||
<img src="authentication.svg" class="architecture-svg" title="Authentication" />
|
||||
|
||||
### Security
|
||||
|
||||
Only by verifying access to your email and knowing your password can you obtain
|
||||
an`authToken` that can be used to authenticate yourself against our servers.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
We rely on the high level APIs exposed by this wonderful library called
|
||||
[libsodium](https://libsodium.gitbook.io/doc/).
|
||||
|
||||
#### Key Generation
|
||||
|
||||
[`crypto_secretbox_keygen`](https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes)
|
||||
is used to generate all random keys within the application. Your `masterKey`,
|
||||
`recoveryKey`, `authKey`, `tokenKey` are all 256-bit keys generated using this
|
||||
API.
|
||||
|
||||
#### Key Derivation
|
||||
|
||||
[`crypto_pwhash`](https://libsodium.gitbook.io/doc/password_hashing/default_phf)
|
||||
is used to derive your `keyEncryptionKey` from your password.
|
||||
|
||||
`crypto_pwhash_OPSLIMIT_SENSITIVE` and `crypto_pwhash_MEMLIMIT_SENSITIVE` are
|
||||
used as the limits for computation and memory respectively. If the operation
|
||||
fails due to insufficient memory, the former is doubled and the latter is halved
|
||||
progressively, until a key can be derived. If during this process the memory
|
||||
limit is reduced to a value less than `crypto_pwhash_MEMLIMIT_MIN`, the client
|
||||
will not let you register from that device.
|
||||
|
||||
Internally, this uses [Argon2
|
||||
v1.3](https://github.com/P-H-C/phc-winner-argon2/raw/master/argon2-specs.pdf),
|
||||
which is regarded as [one of the best hashing
|
||||
algorithms](https://en.wikipedia.org/wiki/Argon2) currently available.
|
||||
|
||||
#### Symmetric Encryption
|
||||
|
||||
[`crypto_secretbox_easy`](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretbox)
|
||||
is used to encrypt your `masterKey`, `recoveryKey`, `authKey` and `tokenKey`s.
|
||||
Internally, this uses
|
||||
[XSalsa20](https://libsodium.gitbook.io/doc/advanced/stream_ciphers/xsalsa20)
|
||||
stream cipher with [Poly1305
|
||||
MAC](https://datatracker.ietf.org/doc/html/rfc8439#section-2.5) for
|
||||
authentication.
|
||||
|
||||
[`crypto_secretstream_*`](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream)
|
||||
APIs are used to encrypt your token data. Internally, this uses
|
||||
[XChaCha20](https://libsodium.gitbook.io/doc/advanced/stream_ciphers/xchacha20)
|
||||
stream cipher with [Poly1305
|
||||
MAC](https://datatracker.ietf.org/doc/html/rfc8439#section-2.5) for
|
||||
authentication.
|
||||
|
||||
#### Salt & Nonce Generation
|
||||
|
||||
[`randombytes_buf`](https://libsodium.gitbook.io/doc/generating_random_data) is
|
||||
used to generate a new salt/nonce every time data needs to be hashed/encrypted.
|
||||
|
||||
---
|
||||
|
||||
## Further Details
|
||||
|
||||
Thank you for reading this far! For implementation details, we request you to
|
||||
checkout [our code](https://github.com/ente-io).
|
||||
|
||||
If you'd like to help us improve this document, kindly email
|
||||
[security@ente.io](mailto:security@ente.io).
|
||||
17
architecture/key-derivation.svg
Normal file
|
After Width: | Height: | Size: 76 KiB |
33
architecture/recovery.svg
Normal file
|
After Width: | Height: | Size: 130 KiB |
21
architecture/sharing.svg
Normal file
|
After Width: | Height: | Size: 123 KiB |
26
architecture/token-encryption.svg
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
assets/2.0x/loading_photos_background.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
assets/2.0x/loading_photos_background_dark.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/3.0x/loading_photos_background.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
assets/3.0x/loading_photos_background_dark.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
assets/loading_photos_background.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/loading_photos_background_dark.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -4,6 +4,40 @@ PODS:
|
||||
- Reachability
|
||||
- device_info (0.0.1):
|
||||
- Flutter
|
||||
- DKImagePickerController/Core (4.3.4):
|
||||
- DKImagePickerController/ImageDataManager
|
||||
- DKImagePickerController/Resource
|
||||
- DKImagePickerController/ImageDataManager (4.3.4)
|
||||
- DKImagePickerController/PhotoGallery (4.3.4):
|
||||
- DKImagePickerController/Core
|
||||
- DKPhotoGallery
|
||||
- DKImagePickerController/Resource (4.3.4)
|
||||
- DKPhotoGallery (0.0.17):
|
||||
- DKPhotoGallery/Core (= 0.0.17)
|
||||
- DKPhotoGallery/Model (= 0.0.17)
|
||||
- DKPhotoGallery/Preview (= 0.0.17)
|
||||
- DKPhotoGallery/Resource (= 0.0.17)
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Core (0.0.17):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Preview
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Model (0.0.17):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Preview (0.0.17):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Resource
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Resource (0.0.17):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- fk_user_agent (2.0.0):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
@@ -44,6 +78,14 @@ PODS:
|
||||
- Flutter
|
||||
- MTBBarcodeScanner
|
||||
- Reachability (3.2)
|
||||
- SDWebImage (5.13.4):
|
||||
- SDWebImage/Core (= 5.13.4)
|
||||
- SDWebImage/Core (5.13.4)
|
||||
- Sentry/HybridSDK (7.30.2)
|
||||
- sentry_flutter (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 7.30.2)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_ios (0.0.1):
|
||||
@@ -51,6 +93,7 @@ PODS:
|
||||
- sqflite (0.0.2):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- SwiftyGif (5.4.3)
|
||||
- Toast (4.0.0)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
@@ -58,6 +101,7 @@ PODS:
|
||||
DEPENDENCIES:
|
||||
- connectivity (from `.symlinks/plugins/connectivity/ios`)
|
||||
- device_info (from `.symlinks/plugins/device_info/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- fk_user_agent (from `.symlinks/plugins/fk_user_agent/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`)
|
||||
@@ -72,6 +116,7 @@ DEPENDENCIES:
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||
- 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_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
@@ -79,10 +124,15 @@ DEPENDENCIES:
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- FMDB
|
||||
- MTBBarcodeScanner
|
||||
- OrderedSet
|
||||
- Reachability
|
||||
- SDWebImage
|
||||
- Sentry
|
||||
- SwiftyGif
|
||||
- Toast
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@@ -90,6 +140,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/connectivity/ios"
|
||||
device_info:
|
||||
:path: ".symlinks/plugins/device_info/ios"
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
fk_user_agent:
|
||||
:path: ".symlinks/plugins/fk_user_agent/ios"
|
||||
Flutter:
|
||||
@@ -118,6 +170,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
qr_code_scanner:
|
||||
:path: ".symlinks/plugins/qr_code_scanner/ios"
|
||||
sentry_flutter:
|
||||
:path: ".symlinks/plugins/sentry_flutter/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_ios:
|
||||
@@ -130,6 +184,9 @@ EXTERNAL SOURCES:
|
||||
SPEC CHECKSUMS:
|
||||
connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467
|
||||
device_info: d7d233b645a32c40dfdc212de5cf646ca482f175
|
||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: 817ab1d8cd2da9d2da412a417162deee3500fc95
|
||||
fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
|
||||
@@ -137,7 +194,7 @@ SPEC CHECKSUMS:
|
||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
||||
flutter_sodium: c84426b4de738514b5b66cfdeb8a06634e72fe0b
|
||||
fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
|
||||
fluttertoast: 74526702fea2c060ea55dde75895b7e1bde1c86b
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
in_app_purchase: 3e2155afa9d03d4fa32d9e62d567885080ce97d6
|
||||
local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c
|
||||
@@ -148,9 +205,13 @@ SPEC CHECKSUMS:
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
|
||||
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
|
||||
SDWebImage: e5cc87bf736e60f49592f307bdf9e157189298a3
|
||||
Sentry: 9be48e341494bc976c963b05aa4a8ca48308c684
|
||||
sentry_flutter: 544e6376e35b00eef9f0864f8bb7f10a0e204993
|
||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||
|
||||
|
||||
@@ -1,60 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>auth</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
<string>es</string>
|
||||
</array>
|
||||
<key>CFBundleName</key>
|
||||
<string>auth</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs camera access to scan QR codes</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>auth</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
<string>es</string>
|
||||
</array>
|
||||
<key>CFBundleName</key>
|
||||
<string>auth</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs camera access to scan QR codes</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Please allow auth to lock itself with FaceID or TouchID</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Please allow auth to pick a file to import data from</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -10,10 +10,11 @@ import 'package:ente_auth/events/signed_in_event.dart';
|
||||
import 'package:ente_auth/events/signed_out_event.dart';
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import "package:ente_auth/onboarding/view/onboarding_page.dart";
|
||||
import 'package:ente_auth/services/update_service.dart';
|
||||
import 'package:ente_auth/ui/home_page.dart';
|
||||
import 'package:ente_auth/ui/settings/app_update_dialog.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import "package:flutter_localizations/flutter_localizations.dart";
|
||||
|
||||
class App extends StatefulWidget {
|
||||
@@ -39,6 +40,21 @@ class _AppState extends State<App> {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
UpdateService.instance.shouldUpdate().then((shouldUpdate) {
|
||||
if (shouldUpdate) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AppUpdateDialog(
|
||||
UpdateService.instance.getLatestVersionInfo(),
|
||||
);
|
||||
},
|
||||
barrierColor: Colors.black.withOpacity(0.85),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -62,7 +78,6 @@ class _AppState extends State<App> {
|
||||
theme: lightTheme,
|
||||
darkTheme: dartTheme,
|
||||
debugShowCheckedModeBanner: false,
|
||||
builder: EasyLoading.init(),
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
@@ -80,7 +95,6 @@ class _AppState extends State<App> {
|
||||
theme: lightThemeData,
|
||||
darkTheme: darkThemeData,
|
||||
debugShowCheckedModeBanner: false,
|
||||
builder: EasyLoading.init(),
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
|
||||
@@ -4,9 +4,10 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
// import 'package:ente_auth/core/error-reporting/super_logging.dart';
|
||||
import 'package:ente_auth/core/errors.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
// ignore: import_of_legacy_library_into_null_safe
|
||||
import 'package:ente_auth/core/logging/super_logging.dart';
|
||||
import 'package:ente_auth/events/signed_in_event.dart';
|
||||
import 'package:ente_auth/events/signed_out_event.dart';
|
||||
import 'package:ente_auth/models/key_attributes.dart';
|
||||
@@ -14,7 +15,6 @@ import 'package:ente_auth/models/key_gen_result.dart';
|
||||
import 'package:ente_auth/models/private_key_attributes.dart';
|
||||
import 'package:ente_auth/store/authenticator_db.dart';
|
||||
import 'package:ente_auth/utils/crypto_util.dart';
|
||||
// import 'package:ente_auth/utils/validator_util.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -120,7 +120,7 @@ class Configuration {
|
||||
}
|
||||
await _migrateSecurityStorageToFirstUnlock();
|
||||
}
|
||||
// SuperLogging.setUserID(await _getOrCreateAnonymousUserID());
|
||||
SuperLogging.setUserID(await _getOrCreateAnonymousUserID());
|
||||
}
|
||||
|
||||
Future<void> logout({bool autoLogout = false}) async {
|
||||
|
||||
@@ -4,9 +4,7 @@ const int thumbnailLargeSize = 512;
|
||||
const int compressedThumbnailResolution = 1080;
|
||||
const int thumbnailDataLimit = 100 * 1024;
|
||||
const String sentryDSN =
|
||||
"https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4";
|
||||
const String sentryDebugDSN =
|
||||
"https://ca5e686dd7f149d9bf94e620564cceba@sentry.ente.io/3";
|
||||
"https://ed4ddd6309b847ba8849935e26e9b648@sentry.ente.io/9";
|
||||
const String sentryTunnel = "https://sentry-reporter.ente.io";
|
||||
const String roadmapURL = "https://roadmap.ente.io";
|
||||
const int microSecondsInDay = 86400000000;
|
||||
|
||||
@@ -7,13 +7,16 @@ import 'dart:collection';
|
||||
import 'dart:core';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/logging/tunneled_transport.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
typedef FutureOrVoidCallback = FutureOr<void> Function();
|
||||
|
||||
@@ -189,17 +192,17 @@ class SuperLogging {
|
||||
if (config.body == null) return;
|
||||
|
||||
if (enable && sentryIsEnabled) {
|
||||
// await SentryFlutter.init(
|
||||
// (options) {
|
||||
// options.dsn = config.sentryDsn;
|
||||
// options.httpClient = http.Client();
|
||||
// if (config.tunnel != null) {
|
||||
// options.transport =
|
||||
// TunneledTransport(Uri.parse(config.tunnel), options);
|
||||
// }
|
||||
// },
|
||||
// appRunner: () => config.body(),
|
||||
// );
|
||||
await SentryFlutter.init(
|
||||
(options) {
|
||||
options.dsn = config.sentryDsn;
|
||||
options.httpClient = http.Client();
|
||||
if (config.tunnel != null) {
|
||||
options.transport =
|
||||
TunneledTransport(Uri.parse(config.tunnel), options);
|
||||
}
|
||||
},
|
||||
appRunner: () => config.body(),
|
||||
);
|
||||
} else {
|
||||
await config.body();
|
||||
}
|
||||
@@ -207,21 +210,21 @@ class SuperLogging {
|
||||
|
||||
static void setUserID(String userID) async {
|
||||
if (config?.sentryDsn != null) {
|
||||
// Sentry.configureScope((scope) => scope.user = SentryUser(id: userID));
|
||||
Sentry.configureScope((scope) => scope.user = SentryUser(id: userID));
|
||||
$.info("setting sentry user ID to: $userID");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _sendErrorToSentry(Object error, StackTrace stack) async {
|
||||
// try {
|
||||
// await Sentry.captureException(
|
||||
// error,
|
||||
// stackTrace: stack,
|
||||
// );
|
||||
// } catch (e) {
|
||||
// $.info('Sending report to sentry.io failed: $e');
|
||||
// $.info('Original error: $error');
|
||||
// }
|
||||
try {
|
||||
await Sentry.captureException(
|
||||
error,
|
||||
stackTrace: stack,
|
||||
);
|
||||
} catch (e) {
|
||||
$.info('Sending report to sentry.io failed: $e');
|
||||
$.info('Original error: $error');
|
||||
}
|
||||
}
|
||||
|
||||
static String _lastExtraLines = '';
|
||||
@@ -249,8 +252,16 @@ class SuperLogging {
|
||||
}
|
||||
|
||||
// add error to sentry queue
|
||||
if (sentryIsEnabled && rec.error != null) {
|
||||
_sendErrorToSentry(rec.error, null);
|
||||
if (sentryIsEnabled) {
|
||||
if (rec.error != null) {
|
||||
_sendErrorToSentry(rec.error, null);
|
||||
} else if (rec.level == Level.SEVERE || rec.level == Level.SHOUT) {
|
||||
if (rec.error != null) {
|
||||
_sendErrorToSentry(rec.error, null);
|
||||
} else {
|
||||
_sendErrorToSentry(rec.message, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,16 +297,16 @@ class SuperLogging {
|
||||
|
||||
static Future<void> setupSentry() async {
|
||||
await for (final error in sentryQueueControl.stream.asBroadcastStream()) {
|
||||
// try {
|
||||
// Sentry.captureException(
|
||||
// error,
|
||||
// );
|
||||
// } catch (e) {
|
||||
// $.fine(
|
||||
// "sentry upload failed; will retry after ${config.sentryRetryDelay}",
|
||||
// );
|
||||
// doSentryRetry(error);
|
||||
// }
|
||||
try {
|
||||
Sentry.captureException(
|
||||
error,
|
||||
);
|
||||
} catch (e) {
|
||||
$.fine(
|
||||
"sentry upload failed; will retry after ${config.sentryRetryDelay}",
|
||||
);
|
||||
doSentryRetry(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,6 +383,6 @@ class SuperLogging {
|
||||
return false;
|
||||
}
|
||||
final pkgName = (await PackageInfo.fromPlatform()).packageName;
|
||||
return pkgName.startsWith("io.ente.photos.fdroid");
|
||||
return pkgName.endsWith("fdroid");
|
||||
}
|
||||
}
|
||||
|
||||
141
lib/core/logging/tunneled_transport.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:sentry/sentry.dart';
|
||||
|
||||
/// A transport is in charge of sending the event to the Sentry server.
|
||||
class TunneledTransport implements Transport {
|
||||
final Uri _tunnel;
|
||||
final SentryOptions _options;
|
||||
|
||||
final Dsn _dsn;
|
||||
|
||||
_CredentialBuilder _credentialBuilder;
|
||||
|
||||
final Map<String, String> _headers;
|
||||
|
||||
factory TunneledTransport(Uri tunnel, SentryOptions options) {
|
||||
return TunneledTransport._(tunnel, options);
|
||||
}
|
||||
|
||||
TunneledTransport._(this._tunnel, this._options)
|
||||
: _dsn = Dsn.parse(_options.dsn),
|
||||
_headers = _buildHeaders(
|
||||
_options.platformChecker.isWeb,
|
||||
_options.sdk.identifier,
|
||||
) {
|
||||
_credentialBuilder = _CredentialBuilder(
|
||||
_dsn,
|
||||
_options.sdk.identifier,
|
||||
_options.clock,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SentryId> send(SentryEnvelope envelope) async {
|
||||
final streamedRequest = await _createStreamedRequest(envelope);
|
||||
final response = await _options.httpClient
|
||||
.send(streamedRequest)
|
||||
.then(Response.fromStream);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
// body guard to not log the error as it has performance impact to allocate
|
||||
// the body String.
|
||||
if (_options.debug) {
|
||||
_options.logger(
|
||||
SentryLevel.error,
|
||||
'API returned an error, statusCode = ${response.statusCode}, '
|
||||
'body = ${response.body}',
|
||||
);
|
||||
}
|
||||
return const SentryId.empty();
|
||||
} else {
|
||||
_options.logger(
|
||||
SentryLevel.debug,
|
||||
'Envelope ${envelope.header.eventId ?? "--"} was sent successfully.',
|
||||
);
|
||||
}
|
||||
|
||||
final eventId = json.decode(response.body)['id'];
|
||||
if (eventId == null) {
|
||||
return null;
|
||||
}
|
||||
return SentryId.fromId(eventId);
|
||||
}
|
||||
|
||||
Future<StreamedRequest> _createStreamedRequest(
|
||||
SentryEnvelope envelope,
|
||||
) async {
|
||||
final streamedRequest = StreamedRequest('POST', _tunnel);
|
||||
envelope
|
||||
.envelopeStream(_options)
|
||||
.listen(streamedRequest.sink.add)
|
||||
.onDone(streamedRequest.sink.close);
|
||||
|
||||
streamedRequest.headers.addAll(_credentialBuilder.configure(_headers));
|
||||
|
||||
return streamedRequest;
|
||||
}
|
||||
}
|
||||
|
||||
class _CredentialBuilder {
|
||||
final String _authHeader;
|
||||
|
||||
final ClockProvider _clock;
|
||||
|
||||
int get timestamp => _clock().millisecondsSinceEpoch;
|
||||
|
||||
_CredentialBuilder._(String authHeader, ClockProvider clock)
|
||||
: _authHeader = authHeader,
|
||||
_clock = clock;
|
||||
|
||||
factory _CredentialBuilder(
|
||||
Dsn dsn,
|
||||
String sdkIdentifier,
|
||||
ClockProvider clock,
|
||||
) {
|
||||
final authHeader = _buildAuthHeader(
|
||||
publicKey: dsn.publicKey,
|
||||
secretKey: dsn.secretKey,
|
||||
sdkIdentifier: sdkIdentifier,
|
||||
);
|
||||
|
||||
return _CredentialBuilder._(authHeader, clock);
|
||||
}
|
||||
|
||||
static String _buildAuthHeader({
|
||||
String publicKey,
|
||||
String secretKey,
|
||||
String sdkIdentifier,
|
||||
}) {
|
||||
var header = 'Sentry sentry_version=7, sentry_client=$sdkIdentifier, '
|
||||
'sentry_key=$publicKey';
|
||||
|
||||
if (secretKey != null) {
|
||||
header += ', sentry_secret=$secretKey';
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
Map<String, String> configure(Map<String, String> headers) {
|
||||
return headers
|
||||
..addAll(
|
||||
<String, String>{
|
||||
'X-Sentry-Auth': '$_authHeader, sentry_timestamp=$timestamp'
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> _buildHeaders(bool isWeb, String sdkIdentifier) {
|
||||
final headers = {'Content-Type': 'application/x-sentry-envelope'};
|
||||
// NOTE(lejard_h) overriding user agent on VM and Flutter not sure why
|
||||
// for web it use browser user agent
|
||||
if (!isWeb) {
|
||||
headers['User-Agent'] = sdkIdentifier;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
@@ -224,6 +224,14 @@ extension CustomColorScheme on ColorScheme {
|
||||
Color get inverseBackgroundColor =>
|
||||
brightness != Brightness.light ? backgroundBaseLight : backgroundBaseDark;
|
||||
|
||||
Color get fabForegroundColor => brightness == Brightness.light
|
||||
? const Color.fromRGBO(255, 255, 255, 1)
|
||||
: const Color.fromRGBO(40, 40, 40, 1);
|
||||
|
||||
Color get fabBackgroundColor => brightness != Brightness.light
|
||||
? const Color.fromRGBO(255, 255, 255, 1)
|
||||
: const Color.fromRGBO(40, 40, 40, 1);
|
||||
|
||||
Color get defaultTextColor =>
|
||||
brightness == Brightness.light ? textBaseLight : textBaseDark;
|
||||
|
||||
@@ -344,6 +352,10 @@ extension CustomColorScheme on ColorScheme {
|
||||
? Colors.black.withOpacity(0.32)
|
||||
: Colors.black.withOpacity(0.64);
|
||||
|
||||
Color get codeCardBackgroundColor => brightness == Brightness.light
|
||||
? const Color.fromRGBO(246, 246, 246, 1)
|
||||
: const Color.fromRGBO(40, 40, 40, 0.6);
|
||||
|
||||
EnteTheme get enteTheme =>
|
||||
brightness == Brightness.light ? lightTheme : darkTheme;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// @dart=2.9
|
||||
import "package:ente_auth/app/view/app.dart";
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
import 'package:ente_auth/core/logging/super_logging.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:ente_auth/services/billing_service.dart';
|
||||
import 'package:ente_auth/services/notification_service.dart';
|
||||
import 'package:ente_auth/services/update_service.dart';
|
||||
import 'package:ente_auth/services/user_remote_flag_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
@@ -29,6 +31,7 @@ Future<void> _runInForeground() async {
|
||||
return await _runWithLogs(() async {
|
||||
_logger.info("Starting app in foreground");
|
||||
await _init(false, via: 'mainMethod');
|
||||
UpdateService.instance.showUpdateNotification();
|
||||
runApp(
|
||||
AppLock(
|
||||
builder: (args) => const App(),
|
||||
@@ -47,6 +50,8 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async {
|
||||
body: function,
|
||||
logDirPath: (await getApplicationSupportDirectory()).path + "/logs",
|
||||
maxLogFiles: 5,
|
||||
sentryDsn: sentryDSN,
|
||||
tunnel: sentryTunnel,
|
||||
enableInDebugMode: true,
|
||||
prefix: prefix,
|
||||
),
|
||||
@@ -61,7 +66,8 @@ Future<void> _init(bool bool, {String via}) async {
|
||||
await Network.instance.init();
|
||||
await UserService.instance.init();
|
||||
await UserRemoteFlagService.instance.init();
|
||||
await UpdateService.instance.init();
|
||||
await AuthenticatorService.instance.init();
|
||||
await BillingService.instance.init();
|
||||
await NotificationService.instance.init();
|
||||
await UpdateService.instance.init();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
|
||||
class Code {
|
||||
static const defaultDigits = 6;
|
||||
static const defaultPeriod = 30;
|
||||
@@ -51,7 +53,7 @@ class Code {
|
||||
_getIssuer(uri),
|
||||
_getDigits(uri),
|
||||
_getPeriod(uri),
|
||||
uri.queryParameters['secret']!,
|
||||
getSanitizedSecret(uri.queryParameters['secret']!),
|
||||
_getAlgorithm(uri),
|
||||
_getType(uri),
|
||||
rawData,
|
||||
|
||||
@@ -2,7 +2,6 @@ import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class SetupEnterSecretKeyPage extends StatefulWidget {
|
||||
@@ -15,8 +14,7 @@ class SetupEnterSecretKeyPage extends StatefulWidget {
|
||||
|
||||
class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
final _accountController = TextEditingController();
|
||||
final _secretController =
|
||||
TextEditingController(text: kDebugMode ? "JBSWY3DPEHPK3PXP" : "");
|
||||
final _secretController = TextEditingController(text: "");
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -62,7 +62,7 @@ class AuthenticatorService {
|
||||
return entries;
|
||||
}
|
||||
|
||||
Future<int> addEntry(String plainText) async {
|
||||
Future<int> addEntry(String plainText, bool shouldSync) async {
|
||||
var key = await getOrCreateAuthDataKey();
|
||||
final encryptedKeyData = await CryptoUtil.encryptChaCha(
|
||||
utf8.encode(plainText) as Uint8List,
|
||||
@@ -71,7 +71,9 @@ class AuthenticatorService {
|
||||
String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
|
||||
String header = Sodium.bin2base64(encryptedKeyData.header!);
|
||||
final insertedID = await _db.insert(encryptedData, header);
|
||||
unawaited(sync());
|
||||
if (shouldSync) {
|
||||
unawaited(sync());
|
||||
}
|
||||
return insertedID;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,53 +1,56 @@
|
||||
// import 'dart:io';
|
||||
//
|
||||
// import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
//
|
||||
// class NotificationService {
|
||||
// static final NotificationService instance =
|
||||
// NotificationService._privateConstructor();
|
||||
//
|
||||
// NotificationService._privateConstructor();
|
||||
// final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
|
||||
// FlutterLocalNotificationsPlugin();
|
||||
//
|
||||
// Future<void> init() async {
|
||||
// if (!Platform.isAndroid) {
|
||||
// return;
|
||||
// }
|
||||
// const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
// AndroidInitializationSettings('notification_icon');
|
||||
// const InitializationSettings initializationSettings =
|
||||
// InitializationSettings(
|
||||
// android: initializationSettingsAndroid,
|
||||
// );
|
||||
// await _flutterLocalNotificationsPlugin.initialize(
|
||||
// initializationSettings,
|
||||
// onSelectNotification: selectNotification,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// Future selectNotification(String? payload) async {}
|
||||
//
|
||||
// Future<void> showNotification(String title, String message) async {
|
||||
// if (!Platform.isAndroid) {
|
||||
// return;
|
||||
// }
|
||||
// const AndroidNotificationDetails androidPlatformChannelSpecifics =
|
||||
// AndroidNotificationDetails(
|
||||
// 'io.ente.photos',
|
||||
// 'ente',
|
||||
// channelDescription: 'ente alerts',
|
||||
// importance: Importance.max,
|
||||
// priority: Priority.high,
|
||||
// showWhen: false,
|
||||
// );
|
||||
// const NotificationDetails platformChannelSpecifics =
|
||||
// NotificationDetails(android: androidPlatformChannelSpecifics);
|
||||
// await _flutterLocalNotificationsPlugin.show(
|
||||
// 0,
|
||||
// title,
|
||||
// message,
|
||||
// platformChannelSpecifics,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
class NotificationService {
|
||||
static final NotificationService instance =
|
||||
NotificationService._privateConstructor();
|
||||
|
||||
NotificationService._privateConstructor();
|
||||
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
Future<void> init() async {
|
||||
if (!Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
AndroidInitializationSettings('notification_icon');
|
||||
const InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
);
|
||||
await _flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
);
|
||||
final implementation =
|
||||
_flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>();
|
||||
if (implementation != null) {
|
||||
implementation.requestPermission();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showNotification(String title, String message) async {
|
||||
if (!Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
const AndroidNotificationDetails androidPlatformChannelSpecifics =
|
||||
AndroidNotificationDetails(
|
||||
'io.ente.auth',
|
||||
'auth',
|
||||
channelDescription: 'auth alerts',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
showWhen: false,
|
||||
);
|
||||
const NotificationDetails platformChannelSpecifics =
|
||||
NotificationDetails(android: androidPlatformChannelSpecifics);
|
||||
await _flutterLocalNotificationsPlugin.show(
|
||||
0,
|
||||
title,
|
||||
message,
|
||||
platformChannelSpecifics,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/services/notification_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
@@ -55,33 +57,33 @@ class UpdateService {
|
||||
return _latestVersion;
|
||||
}
|
||||
|
||||
// Future<void> showUpdateNotification() async {
|
||||
// if (!isIndependent()) {
|
||||
// return;
|
||||
// }
|
||||
// final shouldUpdate = await this.shouldUpdate();
|
||||
// final lastNotificationShownTime =
|
||||
// _prefs.getInt(kUpdateAvailableShownTimeKey) ?? 0;
|
||||
// final now = DateTime.now().microsecondsSinceEpoch;
|
||||
// final hasBeen3DaysSinceLastNotification =
|
||||
// (now - lastNotificationShownTime) > (3 * microSecondsInDay);
|
||||
// if (shouldUpdate &&
|
||||
// hasBeen3DaysSinceLastNotification &&
|
||||
// _latestVersion.shouldNotify) {
|
||||
// NotificationService.instance.showNotification(
|
||||
// "update available",
|
||||
// "click to install our best version yet",
|
||||
// );
|
||||
// await _prefs.setInt(kUpdateAvailableShownTimeKey, now);
|
||||
// } else {
|
||||
// _logger.info("Debouncing notification");
|
||||
// }
|
||||
// }
|
||||
Future<void> showUpdateNotification() async {
|
||||
if (!isIndependent()) {
|
||||
return;
|
||||
}
|
||||
final shouldUpdate = await this.shouldUpdate();
|
||||
final lastNotificationShownTime =
|
||||
_prefs.getInt(kUpdateAvailableShownTimeKey) ?? 0;
|
||||
final now = DateTime.now().microsecondsSinceEpoch;
|
||||
final hasBeen3DaysSinceLastNotification =
|
||||
(now - lastNotificationShownTime) > (3 * microSecondsInDay);
|
||||
if (shouldUpdate &&
|
||||
hasBeen3DaysSinceLastNotification &&
|
||||
_latestVersion.shouldNotify) {
|
||||
NotificationService.instance.showNotification(
|
||||
"Update available",
|
||||
"Click to install our best version yet",
|
||||
);
|
||||
await _prefs.setInt(kUpdateAvailableShownTimeKey, now);
|
||||
} else {
|
||||
_logger.info("Debouncing notification");
|
||||
}
|
||||
}
|
||||
|
||||
Future<LatestVersionInfo> _getLatestVersionInfo() async {
|
||||
final response = await Network.instance
|
||||
.getDio()
|
||||
.get("https://ente.io/release-info/independent.json");
|
||||
.get("https://ente.io/release-info/auth-independent.json");
|
||||
return LatestVersionInfo.fromMap(response.data["latestVersion"]);
|
||||
}
|
||||
|
||||
@@ -89,18 +91,7 @@ class UpdateService {
|
||||
if (Platform.isIOS) {
|
||||
return false;
|
||||
}
|
||||
if (!kDebugMode &&
|
||||
_packageInfo.packageName != "io.ente.auth.independent") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isIndependentFlavor() {
|
||||
if (Platform.isIOS) {
|
||||
return false;
|
||||
}
|
||||
return _packageInfo.packageName.startsWith("io.ente.auth.independent");
|
||||
return kDebugMode || _packageInfo.packageName.endsWith("independent");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/events/user_details_changed_event.dart';
|
||||
@@ -12,12 +14,17 @@ import 'package:ente_auth/models/sessions.dart';
|
||||
import 'package:ente_auth/models/set_keys_request.dart';
|
||||
import 'package:ente_auth/models/set_recovery_key_request.dart';
|
||||
import 'package:ente_auth/models/user_details.dart';
|
||||
import 'package:ente_auth/ui/account/login_page.dart';
|
||||
import 'package:ente_auth/ui/account/ott_verification_page.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/two_factor_authentication_page.dart';
|
||||
import 'package:ente_auth/ui/two_factor_recovery_page.dart';
|
||||
import 'package:ente_auth/utils/crypto_util.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class UserService {
|
||||
@@ -265,11 +272,16 @@ class UserService {
|
||||
await dialog.hide();
|
||||
if (response != null && response.statusCode == 200) {
|
||||
Widget page;
|
||||
await _saveConfiguration(response);
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
page = const PasswordReentryPage();
|
||||
final String twoFASessionID = response.data["twoFactorSessionID"];
|
||||
if (twoFASessionID != null && twoFASessionID.isNotEmpty) {
|
||||
page = TwoFactorAuthenticationPage(twoFASessionID);
|
||||
} else {
|
||||
page = const PasswordEntryPage();
|
||||
await _saveConfiguration(response);
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
page = const PasswordReentryPage();
|
||||
} else {
|
||||
page = const PasswordEntryPage();
|
||||
}
|
||||
}
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
@@ -484,4 +496,204 @@ class UserService {
|
||||
await Configuration.instance.setToken(response.data["token"]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> recoverTwoFactor(BuildContext context, String sessionID) async {
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
await dialog.show();
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_config.getHttpEndpoint() + "/users/two-factor/recover",
|
||||
queryParameters: {
|
||||
"sessionID": sessionID,
|
||||
},
|
||||
);
|
||||
if (response != null && response.statusCode == 200) {
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return TwoFactorRecoveryPage(
|
||||
sessionID,
|
||||
response.data["encryptedSecret"],
|
||||
response.data["secretDecryptionNonce"],
|
||||
);
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
_logger.severe(e);
|
||||
if (e.response != null && e.response.statusCode == 404) {
|
||||
showToast(context, "Session expired");
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const LoginPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
} finally {
|
||||
await dialog.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> verifyTwoFactor(
|
||||
BuildContext context,
|
||||
String sessionID,
|
||||
String code,
|
||||
) async {
|
||||
final dialog = createProgressDialog(context, "Authenticating...");
|
||||
await dialog.show();
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
_config.getHttpEndpoint() + "/users/two-factor/verify",
|
||||
data: {
|
||||
"sessionID": sessionID,
|
||||
"code": code,
|
||||
},
|
||||
);
|
||||
await dialog.hide();
|
||||
if (response != null && response.statusCode == 200) {
|
||||
showToast(context, "Authentication successful!");
|
||||
await _saveConfiguration(response);
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const PasswordReentryPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e);
|
||||
if (e.response != null && e.response.statusCode == 404) {
|
||||
showToast(context, "Session expired");
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const LoginPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Incorrect code",
|
||||
"Authentication failed, please try again",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e);
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Oops",
|
||||
"Authentication failed, please try again",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeTwoFactor(
|
||||
BuildContext context,
|
||||
String sessionID,
|
||||
String recoveryKey,
|
||||
String encryptedSecret,
|
||||
String secretDecryptionNonce,
|
||||
) async {
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
await dialog.show();
|
||||
String secret;
|
||||
try {
|
||||
if (recoveryKey.contains(' ')) {
|
||||
if (recoveryKey.split(' ').length != mnemonicKeyWordCount) {
|
||||
throw AssertionError(
|
||||
'recovery code should have $mnemonicKeyWordCount words',
|
||||
);
|
||||
}
|
||||
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
||||
}
|
||||
secret = Sodium.bin2base64(
|
||||
await CryptoUtil.decrypt(
|
||||
Sodium.base642bin(encryptedSecret),
|
||||
Sodium.hex2bin(recoveryKey.trim()),
|
||||
Sodium.base642bin(secretDecryptionNonce),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Incorrect recovery key",
|
||||
"The recovery key you entered is incorrect",
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
_config.getHttpEndpoint() + "/users/two-factor/remove",
|
||||
data: {
|
||||
"sessionID": sessionID,
|
||||
"secret": secret,
|
||||
},
|
||||
);
|
||||
if (response != null && response.statusCode == 200) {
|
||||
showShortToast(context, "Two-factor authentication successfully reset");
|
||||
await _saveConfiguration(response);
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const PasswordReentryPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
_logger.severe(e);
|
||||
if (e.response != null && e.response.statusCode == 404) {
|
||||
showToast(context, "Session expired");
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const LoginPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
} finally {
|
||||
await dialog.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/codes_updated_event.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class CodeStore {
|
||||
static final CodeStore instance = CodeStore._privateConstructor();
|
||||
@@ -11,6 +12,7 @@ class CodeStore {
|
||||
CodeStore._privateConstructor();
|
||||
|
||||
late AuthenticatorService _authenticatorService;
|
||||
final _logger = Logger("CodeStore");
|
||||
|
||||
Future<void> init() async {
|
||||
_authenticatorService = AuthenticatorService.instance;
|
||||
@@ -26,13 +28,27 @@ class CodeStore {
|
||||
code.id = entry.key;
|
||||
codes.add(code);
|
||||
}
|
||||
codes.sort((c1, c2) {
|
||||
return c1.issuer.toLowerCase().compareTo(c2.issuer.toLowerCase());
|
||||
});
|
||||
return codes;
|
||||
}
|
||||
|
||||
Future<void> addCode(Code code) async {
|
||||
Future<void> addCode(
|
||||
Code code, {
|
||||
bool shouldSync = true,
|
||||
}) async {
|
||||
final codes = await getAllCodes();
|
||||
code.id = await _authenticatorService.addEntry(jsonEncode(code.rawData));
|
||||
codes.add(code);
|
||||
for (final existingCode in codes) {
|
||||
if (existingCode == code) {
|
||||
_logger.info("Found duplicate code, skipping add");
|
||||
return;
|
||||
}
|
||||
}
|
||||
code.id = await _authenticatorService.addEntry(
|
||||
jsonEncode(code.rawData),
|
||||
shouldSync,
|
||||
);
|
||||
Bus.instance.fire(CodesUpdatedEvent());
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ class _ChangeEmailDialogState extends State<ChangeEmailDialog> {
|
||||
child: const Text(
|
||||
"Verify",
|
||||
style: TextStyle(
|
||||
color: Colors.green,
|
||||
color: Colors.purple,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
|
||||
@@ -152,9 +152,10 @@ class DeleteAccountPage extends StatelessWidget {
|
||||
if (hasAuthenticated) {
|
||||
final choice = await showChoiceDialog(
|
||||
context,
|
||||
'Are you sure you want to delete your account?',
|
||||
'Your uploaded data will be scheduled for deletion, and your account '
|
||||
'will be permanently deleted. \n\nThis action is not reversible.',
|
||||
'Are you sure you want to delete your ente account?',
|
||||
'Your uploaded data, across all apps '
|
||||
'(Photos and Authenticator both), will be scheduled for deletion,'
|
||||
'and your account will be permanently deleted.',
|
||||
firstAction: 'Cancel',
|
||||
secondAction: 'Delete',
|
||||
firstActionColor: Theme.of(context).colorScheme.onSurface,
|
||||
|
||||
@@ -161,7 +161,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
||||
),
|
||||
),
|
||||
controller: _verificationCodeController,
|
||||
autofocus: false,
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (_) {
|
||||
|
||||
@@ -68,7 +68,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
|
||||
);
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
String errMessage = 'the recovery key you entered is incorrect';
|
||||
String errMessage = 'The recovery key you entered is incorrect';
|
||||
if (e is AssertionError) {
|
||||
errMessage = '$errMessage : ${e.message}';
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animation_progress_bar/flutter_animation_progress_bar.dart';
|
||||
// import 'package:flutter_animation_progress_bar/flutter_animation_progress_bar.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
|
||||
class CodeWidget extends StatefulWidget {
|
||||
@@ -48,92 +48,137 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Slidable(
|
||||
key: ValueKey(widget.code.hashCode),
|
||||
endActionPane: ActionPane(
|
||||
motion: const ScrollMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: _onDeletePressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor: const Color(0xFFFE4A49),
|
||||
icon: Icons.delete,
|
||||
label: 'Delete',
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
FlutterClipboard.copy(_getTotp())
|
||||
.then((value) => showToast(context, "Copied to clipboard"));
|
||||
},
|
||||
child: SizedBox(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FAProgressBar(
|
||||
currentValue: _timeRemaining / widget.code.period * 100,
|
||||
size: 4,
|
||||
animatedDuration: const Duration(milliseconds: 200),
|
||||
progressColor: Colors.orange,
|
||||
changeColorValue: 40,
|
||||
changeProgressColor: Colors.green,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Text(
|
||||
Uri.decodeFull(widget.code.issuer),
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8),
|
||||
child: Slidable(
|
||||
key: ValueKey(widget.code.hashCode),
|
||||
endActionPane: ActionPane(
|
||||
motion: const ScrollMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: _onDeletePressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor: const Color(0xFFFE4A49),
|
||||
icon: Icons.delete,
|
||||
label: 'Delete',
|
||||
padding: const EdgeInsets.only(left: 0, right: 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 10),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
onTap: () {
|
||||
_copyToClipboard();
|
||||
},
|
||||
onLongPress: () {
|
||||
_copyToClipboard();
|
||||
},
|
||||
child: SizedBox(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FAProgressBar(
|
||||
currentValue:
|
||||
_timeRemaining / widget.code.period * 100,
|
||||
size: 4,
|
||||
animatedDuration: const Duration(milliseconds: 200),
|
||||
progressColor: Colors.orange,
|
||||
changeColorValue: 40,
|
||||
changeProgressColor: Colors.green,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
Uri.decodeFull(widget.code.issuer).trim(),
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
Uri.decodeFull(
|
||||
widget.code.account,
|
||||
).trim(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.caption
|
||||
?.copyWith(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_getTotp(),
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"next",
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
Text(
|
||||
_getNextTotp(),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"next",
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_getTotp(),
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_getNextTotp(),
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _copyToClipboard() {
|
||||
FlutterClipboard.copy(_getTotp()).then(
|
||||
(value) => showToast(context, "Copied to clipboard"),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDeletePressed(_) {
|
||||
final AlertDialog alert = AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
@@ -195,16 +240,4 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
return "Error";
|
||||
}
|
||||
}
|
||||
|
||||
Color _getProgressColor() {
|
||||
final progress = _timeRemaining / widget.code.period;
|
||||
if (progress > 0.6) {
|
||||
return Colors.green;
|
||||
} else if (progress > 0.4) {
|
||||
return Colors.yellow;
|
||||
} else if (progress > 2) {
|
||||
return Colors.orange;
|
||||
}
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ class _HomePageState extends State<HomePage> {
|
||||
_loadCodes();
|
||||
_streamSubscription = Bus.instance.on<CodesUpdatedEvent>().listen((event) {
|
||||
_loadCodes();
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,8 +151,8 @@ class _HomePageState extends State<HomePage> {
|
||||
childPadding: const EdgeInsets.all(5),
|
||||
spaceBetweenChildren: 4,
|
||||
tooltip: 'Add Code',
|
||||
foregroundColor: Theme.of(context).colorScheme.background,
|
||||
backgroundColor: Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
foregroundColor: Theme.of(context).colorScheme.fabForegroundColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.fabBackgroundColor,
|
||||
overlayOpacity: 0.5,
|
||||
overlayColor: Theme.of(context).colorScheme.background,
|
||||
elevation: 8.0,
|
||||
@@ -161,16 +160,16 @@ class _HomePageState extends State<HomePage> {
|
||||
children: [
|
||||
SpeedDialChild(
|
||||
child: const Icon(Icons.qr_code),
|
||||
foregroundColor: Theme.of(context).colorScheme.background,
|
||||
backgroundColor: Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
label: 'Scan a QR Code',
|
||||
foregroundColor: Theme.of(context).colorScheme.fabForegroundColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.fabBackgroundColor,
|
||||
labelWidget: const SpeedDialLabelWidget("Scan a QR Code"),
|
||||
onTap: _redirectToScannerPage,
|
||||
),
|
||||
SpeedDialChild(
|
||||
child: const Icon(Icons.keyboard),
|
||||
foregroundColor: Theme.of(context).colorScheme.background,
|
||||
backgroundColor: Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
label: 'Enter details manually',
|
||||
foregroundColor: Theme.of(context).colorScheme.fabForegroundColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.fabBackgroundColor,
|
||||
labelWidget: const SpeedDialLabelWidget("Enter details manually"),
|
||||
onTap: _redirectToManualEntryPage,
|
||||
),
|
||||
],
|
||||
@@ -224,3 +223,30 @@ class _HomePageState extends State<HomePage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SpeedDialLabelWidget extends StatelessWidget {
|
||||
final String label;
|
||||
|
||||
const SpeedDialLabelWidget(
|
||||
this.label, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(4),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.fabBackgroundColor,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.fabForegroundColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/services/local_authentication_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/account/change_email_dialog.dart';
|
||||
import 'package:ente_auth/ui/account/password_entry_page.dart';
|
||||
@@ -17,13 +14,8 @@ import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class AccountSectionWidget extends StatelessWidget {
|
||||
final _codeFile = File(
|
||||
Configuration.instance.getTempDirectory() + "ente-authenticator-codes.txt",
|
||||
);
|
||||
|
||||
AccountSectionWidget({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -37,46 +29,42 @@ class AccountSectionWidget extends StatelessWidget {
|
||||
|
||||
Column _getSectionOptions(BuildContext context) {
|
||||
List<Widget> children = [];
|
||||
if (Configuration.instance.getRecoveryKey() != null) {
|
||||
children.addAll([
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Recovery key",
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(
|
||||
context,
|
||||
"Please authenticate to view your recovery key",
|
||||
);
|
||||
if (hasAuthenticated) {
|
||||
String recoveryKey;
|
||||
try {
|
||||
recoveryKey =
|
||||
Sodium.bin2base64(Configuration.instance.getRecoveryKey());
|
||||
} catch (e) {
|
||||
showGenericErrorDialog(context);
|
||||
return;
|
||||
}
|
||||
routeToPage(
|
||||
context,
|
||||
RecoveryKeyPage(
|
||||
recoveryKey,
|
||||
"OK",
|
||||
showAppBar: true,
|
||||
onDone: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
children.addAll([
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Recovery key",
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(
|
||||
context,
|
||||
"Please authenticate to view your recovery key",
|
||||
);
|
||||
if (hasAuthenticated) {
|
||||
String recoveryKey;
|
||||
try {
|
||||
recoveryKey =
|
||||
Sodium.bin2hex(Configuration.instance.getRecoveryKey());
|
||||
} catch (e) {
|
||||
showGenericErrorDialog(context);
|
||||
return;
|
||||
}
|
||||
routeToPage(
|
||||
context,
|
||||
RecoveryKeyPage(
|
||||
recoveryKey,
|
||||
"OK",
|
||||
showAppBar: true,
|
||||
onDone: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
@@ -131,90 +119,9 @@ class AccountSectionWidget extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Export secrets",
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
_showWarningDialog(context);
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
]);
|
||||
return Column(
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showWarningDialog(BuildContext context) async {
|
||||
final AlertDialog alert = AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
title: Text(
|
||||
"Warning",
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
content: const Text(
|
||||
"The exported file contains sensitive information. Please store this safely.",
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text(
|
||||
"I understand",
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
_exportCodes(context);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text(
|
||||
"Cancel",
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return alert;
|
||||
},
|
||||
barrierColor: Colors.black12,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _exportCodes(BuildContext context) async {
|
||||
final hasAuthenticated =
|
||||
await LocalAuthenticationService.instance.requestLocalAuthentication(
|
||||
context,
|
||||
"Please authenticate to export your codes",
|
||||
);
|
||||
if (!hasAuthenticated) {
|
||||
return;
|
||||
}
|
||||
if (_codeFile.existsSync()) {
|
||||
await _codeFile.delete();
|
||||
}
|
||||
final codes = await CodeStore.instance.getAllCodes();
|
||||
String data = "";
|
||||
for (final code in codes) {
|
||||
data += code.rawData + "\n";
|
||||
}
|
||||
_codeFile.writeAsStringSync(data);
|
||||
await Share.shareFiles([_codeFile.path]);
|
||||
Future.delayed(const Duration(seconds: 15), () async {
|
||||
if (_codeFile.existsSync()) {
|
||||
_codeFile.deleteSync();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// @dart=2.9
|
||||
|
||||
// import 'package:open_file/open_file.dart';
|
||||
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/services/update_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
|
||||
class AppUpdateDialog extends StatefulWidget {
|
||||
final LatestVersionInfo latestVersionInfo;
|
||||
@@ -25,7 +25,12 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
||||
changelog.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 0, 4),
|
||||
child: Text("- " + log, style: Theme.of(context).textTheme.caption),
|
||||
child: Text(
|
||||
"- " + log,
|
||||
style: Theme.of(context).textTheme.caption.copyWith(
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -152,7 +157,7 @@ class _ApkDownloaderDialogState extends State<ApkDownloaderDialog> {
|
||||
},
|
||||
);
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
// OpenFile.open(_saveUrl);
|
||||
OpenFile.open(_saveUrl);
|
||||
} catch (e) {
|
||||
Logger("ApkDownloader").severe(e);
|
||||
final AlertDialog alert = AlertDialog(
|
||||
|
||||
282
lib/ui/settings/data_section_widget.dart
Normal file
@@ -0,0 +1,282 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:ente_auth/services/local_authentication_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
|
||||
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/components/menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/common_settings.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class DataSectionWidget extends StatelessWidget {
|
||||
final _logger = Logger("AccountSectionWidget");
|
||||
|
||||
final _codeFile = File(
|
||||
Configuration.instance.getTempDirectory() + "ente-authenticator-codes.txt",
|
||||
);
|
||||
|
||||
DataSectionWidget({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExpandableMenuItemWidget(
|
||||
title: "Data",
|
||||
selectionOptionsWidget: _getSectionOptions(context),
|
||||
leadingIcon: Icons.key_outlined,
|
||||
);
|
||||
}
|
||||
|
||||
Column _getSectionOptions(BuildContext context) {
|
||||
List<Widget> children = [];
|
||||
children.addAll([
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Import codes",
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
_showImportInstructionDialog(context);
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Export codes",
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
_showExportWarningDialog(context);
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
]);
|
||||
return Column(
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showImportInstructionDialog(BuildContext context) async {
|
||||
final AlertDialog alert = AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
title: Text(
|
||||
"Import codes",
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
"Please select a file that contains a list of your codes in the following format",
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.gNavBackgroundColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
"otpauth://totp/provider.com:you@email.com?secret=YOUR_SECRET",
|
||||
style: TextStyle(
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
fontFamily: Platform.isIOS ? "Courier" : "monospace",
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
const Text(
|
||||
"The codes can be separated by a comma or a new line",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text(
|
||||
"Cancel",
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text(
|
||||
"Select file",
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
_pickImportFile(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return alert;
|
||||
},
|
||||
barrierColor: Colors.black12,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showExportWarningDialog(BuildContext context) async {
|
||||
final AlertDialog alert = AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
title: Text(
|
||||
"Warning",
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
content: const Text(
|
||||
"The exported file contains sensitive information. Please store this safely.",
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text(
|
||||
"I understand",
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
_exportCodes(context);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text(
|
||||
"Cancel",
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return alert;
|
||||
},
|
||||
barrierColor: Colors.black12,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _exportCodes(BuildContext context) async {
|
||||
final hasAuthenticated =
|
||||
await LocalAuthenticationService.instance.requestLocalAuthentication(
|
||||
context,
|
||||
"Please authenticate to export your codes",
|
||||
);
|
||||
if (!hasAuthenticated) {
|
||||
return;
|
||||
}
|
||||
if (_codeFile.existsSync()) {
|
||||
await _codeFile.delete();
|
||||
}
|
||||
final codes = await CodeStore.instance.getAllCodes();
|
||||
String data = "";
|
||||
for (final code in codes) {
|
||||
data += code.rawData + "\n";
|
||||
}
|
||||
_codeFile.writeAsStringSync(data);
|
||||
await Share.shareFiles([_codeFile.path]);
|
||||
Future.delayed(const Duration(seconds: 15), () async {
|
||||
if (_codeFile.existsSync()) {
|
||||
_codeFile.deleteSync();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pickImportFile(BuildContext context) async {
|
||||
FilePickerResult result = await FilePicker.platform.pickFiles();
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
await dialog.show();
|
||||
try {
|
||||
File file = File(result.files.single.path);
|
||||
final codes = await file.readAsString();
|
||||
List<String> splitCodes = codes.split(",");
|
||||
if (splitCodes.length == 1) {
|
||||
splitCodes = codes.split("\n");
|
||||
}
|
||||
final parsedCodes = [];
|
||||
for (final code in splitCodes) {
|
||||
try {
|
||||
parsedCodes.add(Code.fromRawData(code));
|
||||
} catch (e) {
|
||||
_logger.severe("Could not parse code", e);
|
||||
}
|
||||
}
|
||||
for (final code in parsedCodes) {
|
||||
await CodeStore.instance.addCode(code, shouldSync: false);
|
||||
}
|
||||
unawaited(AuthenticatorService.instance.sync());
|
||||
await dialog.hide();
|
||||
await showConfettiDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
title: Text(
|
||||
"Yay!",
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
content: Text(
|
||||
"You have imported " + parsedCodes.length.toString() + " codes!",
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
"Okay",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
await showErrorDialog(
|
||||
context,
|
||||
"Sorry",
|
||||
"Could not parse the selected file.\nPlease write to support@ente.io if you need help!",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
lib/ui/settings/made_with_love_widget.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class MadeWithLoveWidget extends StatelessWidget {
|
||||
const MadeWithLoveWidget({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse("https://ente.io"));
|
||||
},
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: "made with ❤️ at ",
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: const <TextSpan>[
|
||||
TextSpan(
|
||||
text: 'ente.io',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/services/update_service.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
|
||||
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
|
||||
@@ -33,19 +30,6 @@ class SocialSectionWidget extends StatelessWidget {
|
||||
const SocialsMenuItemWidget("Reddit", "https://reddit.com/r/enteio"),
|
||||
sectionOptionSpacing,
|
||||
];
|
||||
if (!UpdateService.instance.isIndependent()) {
|
||||
options.addAll(
|
||||
[
|
||||
SocialsMenuItemWidget(
|
||||
"Rate us! ✨",
|
||||
Platform.isAndroid
|
||||
? "https://play.google.com/store/apps/details?id=io.ente.photos"
|
||||
: "https://apps.apple.com/in/app/ente-photos/id1542026904",
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
],
|
||||
);
|
||||
}
|
||||
return Column(children: options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ 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/danger_section_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/data_section_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/made_with_love_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_section_widget.dart';
|
||||
@@ -60,6 +62,8 @@ class SettingsPage extends StatelessWidget {
|
||||
contents.addAll([
|
||||
AccountSectionWidget(),
|
||||
sectionSpacing,
|
||||
DataSectionWidget(),
|
||||
sectionSpacing,
|
||||
const SecuritySectionWidget(),
|
||||
sectionSpacing,
|
||||
]);
|
||||
@@ -80,6 +84,7 @@ class SettingsPage extends StatelessWidget {
|
||||
sectionSpacing,
|
||||
const DangerSectionWidget(),
|
||||
const AppVersionWidget(),
|
||||
const MadeWithLoveWidget(),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 60),
|
||||
),
|
||||
|
||||
@@ -34,7 +34,7 @@ class _LockScreenState extends State<LockScreen> {
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: 0.2,
|
||||
opacity: 0.3,
|
||||
child: Image.asset('assets/loading_photos_background.png'),
|
||||
),
|
||||
SizedBox(
|
||||
|
||||
153
lib/ui/two_factor_authentication_page.dart
Normal file
@@ -0,0 +1,153 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/ui/lifecycle_event_handler.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pinput/pin_put/pin_put.dart';
|
||||
|
||||
class TwoFactorAuthenticationPage extends StatefulWidget {
|
||||
final String sessionID;
|
||||
|
||||
const TwoFactorAuthenticationPage(this.sessionID, {Key key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<TwoFactorAuthenticationPage> createState() =>
|
||||
_TwoFactorAuthenticationPageState();
|
||||
}
|
||||
|
||||
class _TwoFactorAuthenticationPageState
|
||||
extends State<TwoFactorAuthenticationPage> {
|
||||
final _pinController = TextEditingController();
|
||||
String _code = "";
|
||||
LifecycleEventHandler _lifecycleEventHandler;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_lifecycleEventHandler = LifecycleEventHandler(
|
||||
resumeCallBack: () async {
|
||||
if (mounted) {
|
||||
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null && data.text.length == 6) {
|
||||
_pinController.text = data.text;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
WidgetsBinding.instance.addObserver(_lifecycleEventHandler);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(_lifecycleEventHandler);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
"Two-factor authentication",
|
||||
),
|
||||
),
|
||||
body: _getBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
final pinPutDecoration = BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
.inputDecorationTheme
|
||||
.focusedBorder
|
||||
.borderSide
|
||||
.color,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const Text(
|
||||
"Enter the 6-digit code from\nyour authenticator app",
|
||||
style: TextStyle(
|
||||
height: 1.4,
|
||||
fontSize: 16,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(32)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(40, 0, 40, 0),
|
||||
child: PinPut(
|
||||
fieldsCount: 6,
|
||||
onSubmit: (String code) {
|
||||
_verifyTwoFactorCode(code);
|
||||
},
|
||||
onChanged: (String pin) {
|
||||
setState(() {
|
||||
_code = pin;
|
||||
});
|
||||
},
|
||||
controller: _pinController,
|
||||
submittedFieldDecoration: pinPutDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
selectedFieldDecoration: pinPutDecoration,
|
||||
followingFieldDecoration: pinPutDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
inputDecoration: const InputDecoration(
|
||||
focusedBorder: InputBorder.none,
|
||||
border: InputBorder.none,
|
||||
counterText: '',
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(24)),
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
|
||||
width: double.infinity,
|
||||
height: 64,
|
||||
child: OutlinedButton(
|
||||
onPressed: _code.length == 6
|
||||
? () async {
|
||||
_verifyTwoFactorCode(_code);
|
||||
}
|
||||
: null,
|
||||
child: const Text("Verify"),
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(30)),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
UserService.instance.recoverTwoFactor(context, widget.sessionID);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
"Lost device?",
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _verifyTwoFactorCode(String code) async {
|
||||
await UserService.instance.verifyTwoFactor(context, widget.sessionID, code);
|
||||
}
|
||||
}
|
||||
112
lib/ui/two_factor_recovery_page.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TwoFactorRecoveryPage extends StatefulWidget {
|
||||
final String sessionID;
|
||||
final String encryptedSecret;
|
||||
final String secretDecryptionNonce;
|
||||
|
||||
const TwoFactorRecoveryPage(
|
||||
this.sessionID,
|
||||
this.encryptedSecret,
|
||||
this.secretDecryptionNonce, {
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TwoFactorRecoveryPage> createState() => _TwoFactorRecoveryPageState();
|
||||
}
|
||||
|
||||
class _TwoFactorRecoveryPageState extends State<TwoFactorRecoveryPage> {
|
||||
final _recoveryKey = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
"Recover account",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(60, 0, 60, 0),
|
||||
child: TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: "Enter your recovery key",
|
||||
contentPadding: EdgeInsets.all(20),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
controller: _recoveryKey,
|
||||
autofocus: false,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
onChanged: (_) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(24)),
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
|
||||
width: double.infinity,
|
||||
height: 64,
|
||||
child: OutlinedButton(
|
||||
onPressed: _recoveryKey.text.isNotEmpty
|
||||
? () async {
|
||||
await UserService.instance.removeTwoFactor(
|
||||
context,
|
||||
widget.sessionID,
|
||||
_recoveryKey.text,
|
||||
widget.encryptedSecret,
|
||||
widget.secretDecryptionNonce,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: const Text("Recover"),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Contact support",
|
||||
"Please drop an email to support@ente.io from your registered email address",
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(40),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"No recovery key?",
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: 12,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
||||
Future showToast(
|
||||
@@ -11,31 +8,16 @@ Future showToast(
|
||||
toastLength = Toast.LENGTH_LONG,
|
||||
iOSDismissOnTap = true,
|
||||
}) async {
|
||||
if (Platform.isAndroid) {
|
||||
await Fluttertoast.cancel();
|
||||
return Fluttertoast.showToast(
|
||||
msg: message,
|
||||
toastLength: toastLength,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
timeInSecForIosWeb: 1,
|
||||
backgroundColor: Theme.of(context).colorScheme.toastBackgroundColor,
|
||||
textColor: Theme.of(context).colorScheme.toastTextColor,
|
||||
fontSize: 16.0,
|
||||
);
|
||||
} else {
|
||||
EasyLoading.instance
|
||||
..backgroundColor = Theme.of(context).colorScheme.toastBackgroundColor
|
||||
..indicatorColor = Theme.of(context).colorScheme.toastBackgroundColor
|
||||
..textColor = Theme.of(context).colorScheme.toastTextColor
|
||||
..userInteractions = true
|
||||
..loadingStyle = EasyLoadingStyle.custom;
|
||||
return EasyLoading.showToast(
|
||||
message,
|
||||
duration: Duration(seconds: (toastLength == Toast.LENGTH_LONG ? 5 : 2)),
|
||||
toastPosition: EasyLoadingToastPosition.bottom,
|
||||
dismissOnTap: iOSDismissOnTap,
|
||||
);
|
||||
}
|
||||
await Fluttertoast.cancel();
|
||||
return Fluttertoast.showToast(
|
||||
msg: message,
|
||||
toastLength: toastLength,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
timeInSecForIosWeb: 1,
|
||||
backgroundColor: Theme.of(context).colorScheme.toastBackgroundColor,
|
||||
textColor: Theme.of(context).colorScheme.toastTextColor,
|
||||
fontSize: 16.0,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showShortToast(context, String message) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:otp/otp.dart' as otp;
|
||||
|
||||
String getTotp(Code code) {
|
||||
return otp.OTP.generateTOTPCodeString(
|
||||
code.secret,
|
||||
getSanitizedSecret(code.secret),
|
||||
DateTime.now().millisecondsSinceEpoch,
|
||||
length: code.digits,
|
||||
interval: code.period,
|
||||
@@ -14,7 +14,7 @@ String getTotp(Code code) {
|
||||
|
||||
String getNextTotp(Code code) {
|
||||
return otp.OTP.generateTOTPCodeString(
|
||||
code.secret,
|
||||
getSanitizedSecret(code.secret),
|
||||
DateTime.now().millisecondsSinceEpoch + code.period * 1000,
|
||||
length: code.digits,
|
||||
interval: code.period,
|
||||
@@ -33,3 +33,7 @@ otp.Algorithm _getAlgorithm(Code code) {
|
||||
return otp.Algorithm.SHA1;
|
||||
}
|
||||
}
|
||||
|
||||
String getSanitizedSecret(String secret) {
|
||||
return secret.toUpperCase().trim();
|
||||
}
|
||||
|
||||
@@ -7,12 +7,16 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <sentry_flutter/sentry_flutter_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) sentry_flutter_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin");
|
||||
sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_secure_storage_linux
|
||||
sentry_flutter
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import connectivity_macos
|
||||
import flutter_local_notifications
|
||||
import flutter_secure_storage_macos
|
||||
import package_info_plus_macos
|
||||
import path_provider_macos
|
||||
import sentry_flutter
|
||||
import share_plus_macos
|
||||
import shared_preferences_macos
|
||||
import sqflite
|
||||
@@ -16,9 +18,11 @@ import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
FlutterSecureStorageMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageMacosPlugin"))
|
||||
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
|
||||
79
pubspec.lock
@@ -260,6 +260,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
device_info:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -344,6 +351,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.6.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -377,13 +391,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.0.1"
|
||||
flutter_easyloading:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_easyloading
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
flutter_email_sender:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -405,6 +412,27 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.3"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "12.0.3"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -487,13 +515,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.2.0"
|
||||
flutter_spinkit:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_spinkit
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -517,7 +538,7 @@ packages:
|
||||
name: fluttertoast
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.0.9"
|
||||
version: "8.1.1"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -707,6 +728,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
open_file:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: open_file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
otp:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -917,6 +945,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
sentry:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.15.1"
|
||||
sentry_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry_flutter
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.15.1"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1167,6 +1209,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.16"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.0"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
75
pubspec.yaml
@@ -1,6 +1,6 @@
|
||||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 1.0.0+1
|
||||
version: 1.0.12+12
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@@ -8,64 +8,63 @@ environment:
|
||||
|
||||
dependencies:
|
||||
adaptive_theme: ^3.1.0 # done
|
||||
bloc: ^8.0.3 #done
|
||||
bip39: ^1.0.6 #done
|
||||
bloc: ^8.0.3 #done
|
||||
clipboard: ^0.1.3
|
||||
collection: # dart
|
||||
computer: ^2.0.0
|
||||
confetti: ^0.7.0
|
||||
connectivity: ^3.0.3
|
||||
cupertino_icons: ^1.0.0
|
||||
device_info: ^2.0.2
|
||||
dio: ^4.0.6
|
||||
dotted_border: ^2.0.0+2
|
||||
email_validator: ^2.0.1
|
||||
event_bus: ^2.0.0
|
||||
dio: ^4.0.6
|
||||
expandable: ^5.0.1
|
||||
expansion_tile_card: ^2.0.0
|
||||
file_picker: ^4.6.1
|
||||
fk_user_agent: ^2.1.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_animation_progress_bar: ^2.2.1
|
||||
flutter_bloc: ^8.0.1
|
||||
flutter_email_sender: ^5.1.0
|
||||
flutter_inappwebview: ^5.7.1
|
||||
flutter_launcher_icons: ^0.9.3
|
||||
dotted_border: ^2.0.0+2
|
||||
in_app_purchase: ^0.5.2
|
||||
flutter_secure_storage: ^6.0.0
|
||||
flutter_animation_progress_bar: ^2.2.1
|
||||
flutter_slidable: ^2.0.0
|
||||
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.0.1
|
||||
flutter_native_splash: ^2.2.13
|
||||
local_auth: ^1.1.5
|
||||
pinput: ^1.2.2
|
||||
password_strength: ^0.2.0
|
||||
flutter_sodium: ^0.2.0
|
||||
flutter_windowmanager: ^0.2.0
|
||||
flutter_local_notifications: ^12.0.3
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
# sentry:
|
||||
# path: thirdparty/sentry-dart/dart
|
||||
# sentry_flutter:
|
||||
# path: thirdparty/sentry-dart/flutter
|
||||
json_annotation: ^4.5.0
|
||||
fluttertoast: ^8.0.6
|
||||
flutter_native_splash: ^2.2.13
|
||||
flutter_secure_storage: ^6.0.0
|
||||
flutter_slidable: ^2.0.0
|
||||
flutter_sodium: ^0.2.0
|
||||
flutter_speed_dial: ^6.2.0
|
||||
flutter_windowmanager: ^0.2.0
|
||||
fluttertoast: ^8.1.1
|
||||
google_nav_bar: ^5.0.5 #supported
|
||||
http: ^0.13.4
|
||||
move_to_background: ^1.0.2
|
||||
otp: ^3.1.1
|
||||
path_provider: ^2.0.11
|
||||
in_app_purchase: ^0.5.2
|
||||
intl: ^0.17.0
|
||||
qr_code_scanner: ^1.0.1
|
||||
sqflite: ^2.1.0
|
||||
share_plus: ^4.4.0
|
||||
package_info_plus: ^1.0.1
|
||||
shared_preferences: ^2.0.5
|
||||
flutter_easyloading: ^3.0.5
|
||||
uuid: ^3.0.4
|
||||
url_launcher: ^6.1.5
|
||||
json_annotation: ^4.5.0
|
||||
local_auth: ^1.1.5
|
||||
logging: ^1.0.1
|
||||
move_to_background: ^1.0.2
|
||||
open_file: ^3.2.1
|
||||
otp: ^3.1.1
|
||||
package_info_plus: ^1.0.1
|
||||
password_strength: ^0.2.0
|
||||
path_provider: ^2.0.11
|
||||
pinput: ^1.2.2
|
||||
qr_code_scanner: ^1.0.1
|
||||
sentry: ^6.12.1
|
||||
sentry_flutter: ^6.12.1
|
||||
share_plus: ^4.4.0
|
||||
shared_preferences: ^2.0.5
|
||||
sqflite: ^2.1.0
|
||||
step_progress_indicator: ^1.0.2
|
||||
confetti: ^0.7.0
|
||||
clipboard: ^0.1.3
|
||||
flutter_speed_dial: ^6.2.0
|
||||
url_launcher: ^6.1.5
|
||||
uuid: ^3.0.4
|
||||
|
||||
dev_dependencies:
|
||||
bloc_test: ^9.0.3
|
||||
|
||||
@@ -7,11 +7,14 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <sentry_flutter/sentry_flutter_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
SentryFlutterPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SentryFlutterPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_secure_storage_windows
|
||||
sentry_flutter
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
||||