Compare commits

...

81 Commits

Author SHA1 Message Date
vishnukvmd
7207921838 v1.0.18 2022-11-23 17:23:06 +05:30
vishnukvmd
56a741b2ac Update copy 2022-11-23 17:23:02 +05:30
vishnukvmd
e52aa78fae v1.0.17 2022-11-23 16:26:47 +05:30
vishnukvmd
9bcd107de8 Display a coach mark to nudge the user to swipe left 2022-11-23 16:26:29 +05:30
vishnukvmd
8e5488eaf9 v1.0.16 2022-11-22 15:00:08 +05:30
vishnukvmd
b20139f3b8 Provide option to opt in to crash analytics 2022-11-22 14:59:25 +05:30
vishnukvmd
21b139687d Disable Sentry tunnel 2022-11-22 14:58:59 +05:30
vishnukvmd
385731196f Remove code for setting anonymous user ID from Configuration 2022-11-22 14:44:27 +05:30
vishnukvmd
71c2958078 Improve warning while sharing logs 2022-11-22 14:06:46 +05:30
vishnukvmd
7a25da3927 Simplify email to share logs with 2022-11-22 14:06:36 +05:30
vishnukvmd
34c9ee76b3 Improve logging 2022-11-22 14:06:25 +05:30
vishnukvmd
3fbad241b1 Remove irrelevant link 2022-11-22 13:57:38 +05:30
vishnukvmd
9143634000 Execute remote sync if local items were pushed 2022-11-22 13:56:00 +05:30
vishnukvmd
a573c012e0 Rename variable 2022-11-22 13:37:09 +05:30
vishnukvmd
94debda2c7 Add the ability to add and edit issuers 2022-11-22 13:24:37 +05:30
vishnukvmd
c9f37b4f5a Add ability to edit an existing code 2022-11-22 13:24:37 +05:30
Neeraj Gupta
05652656ae Update README.md 2022-11-16 16:15:13 +05:30
vishnukvmd
8e715b4cf9 v1.0.15 2022-11-15 17:44:18 +05:30
vishnukvmd
e264d3456b Add indicator for codes that haven't synced to remote yet 2022-11-15 17:44:04 +05:30
vishnukvmd
56a73ab7ee Send stack trace on error capture 2022-11-15 17:27:12 +05:30
vishnukvmd
963d18b501 Gracefully parse errors 2022-11-15 17:26:59 +05:30
vishnukvmd
b35d3bda16 Reset cache on logout 2022-11-15 17:21:44 +05:30
vishnukvmd
7ad76adcaa v1.0.14 2022-11-15 14:04:26 +05:30
vishnukvmd
691eaabf50 Switch to FlutterFragmentActivity 2022-11-15 14:04:17 +05:30
vishnukvmd
7ab16df330 Update readme 2022-11-14 22:46:08 +05:30
vishnukvmd
3c16cfd829 Update podfile 2022-11-14 22:46:02 +05:30
vishnukvmd
8c9c9c53d1 v1.0.12 2022-11-14 22:11:38 +05:30
vishnukvmd
aeaaddbe40 Update README 2022-11-14 22:11:14 +05:30
vishnukvmd
25782870c7 Remove wasteful refresh 2022-11-14 21:53:14 +05:30
vishnukvmd
39e28dd63b Sort codes by issuer 2022-11-14 21:51:25 +05:30
vishnukvmd
517ce33fd9 Display the account within the code widget 2022-11-14 21:49:00 +05:30
vishnukvmd
02d2cb5733 Document architecture 2022-11-14 20:46:43 +05:30
vishnukvmd
2b5f349b2f Copy code on long press as well as on tap 2022-11-14 20:30:15 +05:30
vishnukvmd
08ad496975 Update README 2022-11-14 20:02:32 +05:30
vishnukvmd
f52ee5683b v1.0.11 2022-11-14 18:57:45 +05:30
vishnukvmd
332faa9166 Send error messages to sentry, even if the error object is missing 2022-11-14 18:57:37 +05:30
vishnukvmd
3d95c789f5 v1.0.10 2022-11-14 18:33:56 +05:30
vishnukvmd
244d4d969a Enable auto-updates 2022-11-14 18:33:49 +05:30
vishnukvmd
32605dc708 Implement a functional notification service 2022-11-14 18:23:55 +05:30
vishnukvmd
ef0c302c94 Add configuration for production builds for Android 2022-11-14 18:12:07 +05:30
vishnukvmd
fba694de68 Sort dependencies 2022-11-14 18:10:06 +05:30
vishnukvmd
3d4cd106cb Add dependencies to power auto-update 2022-11-14 18:08:42 +05:30
vishnukvmd
61e6e0ffaa Define responsible disclosure policy 2022-11-14 14:00:00 +05:30
vishnukvmd
e0b952e516 Configure Sentry 2022-11-14 13:58:09 +05:30
vishnukvmd
546a9234a4 Allow resetting on 2FA with the mnemonic phrase 2022-11-13 13:57:18 +05:30
vishnukvmd
da2083ef08 Fix styles for 2FA entry 2022-11-13 13:56:49 +05:30
vishnukvmd
8a1177e7db Fix case 2022-11-13 13:29:18 +05:30
vishnukvmd
8acd1faf03 Remove link to rate 2022-11-13 12:02:45 +05:30
vishnukvmd
0c28b83b46 Fix button color 2022-11-13 11:58:40 +05:30
vishnukvmd
fa0ac608f4 Add illustration for the lock screen 2022-11-13 11:57:07 +05:30
vishnukvmd
0965b367cc Decode the recovery key into hex instead of base64 2022-11-13 11:41:40 +05:30
vishnukvmd
37db940720 v1.0.9 2022-11-12 01:19:42 +05:30
vishnukvmd
334800472e Fix iOS permission issue 2022-11-12 01:19:25 +05:30
vishnukvmd
0da4497857 v1.0.8 2022-11-12 01:07:04 +05:30
vishnukvmd
0b05a21dc7 Fix fab colors 2022-11-12 01:05:25 +05:30
vishnukvmd
2558473399 Remove unused import 2022-11-12 00:47:53 +05:30
vishnukvmd
4dd465fdfa Remove junk value 2022-11-12 00:47:35 +05:30
vishnukvmd
1f20ba17f4 Remove toast library that was blocking user interaction 2022-11-12 00:46:02 +05:30
vishnukvmd
9ce8059212 Fix ripple 2022-11-12 00:37:06 +05:30
vishnukvmd
e69a3adf14 Remove redundant import 2022-11-12 00:35:21 +05:30
vishnukvmd
8baa350056 Improve style of the code widget 2022-11-12 00:34:06 +05:30
vishnukvmd
2526b61620 v1.0.7 2022-11-11 20:24:02 +05:30
vishnukvmd
ec4df467a0 Up flutter version to 3.3.8 2022-11-11 20:23:48 +05:30
vishnukvmd
41fd5337a6 Fix link to checksum 2022-11-11 20:23:29 +05:30
vishnukvmd
8fbec8307e v1.0.6 2022-11-11 20:14:01 +05:30
vishnukvmd
d902fdbf75 Add separate section for import and export 2022-11-11 20:13:52 +05:30
vishnukvmd
932dc53f6c Made with love @ ente 2022-11-11 20:09:59 +05:30
vishnukvmd
745cb54ffd Update copy 2022-11-11 20:01:02 +05:30
vishnukvmd
8ced5bf32e Fix error dialog 2022-11-11 19:58:36 +05:30
vishnukvmd
9b1a8fb4ca v1.0.5 2022-11-11 19:52:23 +05:30
vishnukvmd
824c6d769b Sanitize the secret 2022-11-11 19:51:54 +05:30
vishnukvmd
92c2247aa1 Add option to bulk import codes 2022-11-11 18:51:09 +05:30
vishnukvmd
e84e9db70e Add parameter to disable sync post code addition 2022-11-11 18:50:26 +05:30
vishnukvmd
be72db844d De-duplicate codes before inserting them into the DB 2022-11-11 17:12:05 +05:30
vishnukvmd
2386a5a10b Remove redundant code 2022-11-11 17:05:28 +05:30
vishnukvmd
09ae14a1d6 Add dependency on file picker 2022-11-11 17:05:22 +05:30
vishnukvmd
924ce5ff86 Update launch config 2022-11-11 15:45:58 +05:30
vishnukvmd
c47370163d Auto focus on OTT entry screen 2022-11-11 15:45:52 +05:30
vishnukvmd
3dd408af01 Clarify that account deletion applies to all products 2022-11-03 16:13:48 +05:30
vishnukvmd
408e3bd2b6 Fix the option to view recovery key 2022-11-03 16:00:02 +05:30
vishnukvmd
ebf634ef1e Enable 2fa within 2fa :okaypepe: 2022-11-03 15:59:00 +05:30
66 changed files with 2472 additions and 590 deletions

View File

@@ -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

11
.vscode/launch.json vendored
View File

@@ -26,14 +26,21 @@
"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.30:8080"]
},
{
"name": "Prod",
"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"]
}
]
}

View File

@@ -1,3 +1,68 @@
# ente Auth
# ente Authenticator
Open source authenticator app for your 2FA secrets, with end-to-end encrypted backups.
ente's Authenticator app 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) 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.
## 🔩 Architecture
The architecture that powers end-to-end encrypted storage and sync of your
tokens has been documented [here](architecture/index.md).
## 🧑‍💻 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`
## 🙋‍♂️ 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.
## 💜 Community
- Follow us on [Twitter](https://twitter.com/enteio)
- Join us on [Discord](https://ente.io/discord)

50
SECURITY.md Normal file
View 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!

View File

@@ -35,7 +35,7 @@
<meta-data android:name="flutterEmbedding" android:value="2"/>
<meta-data android:name="io.sentry.dsn"
android:value="https://8aeb7f013be74f829f8b73b46b3d7a80@sentry.ente.io/8"/>
android:value="https://ed4ddd6309b847ba8849935e26e9b648@sentry.ente.io/9"/>
</application>
<queries>

View File

@@ -1,6 +1,6 @@
package io.ente.auth
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity: FlutterActivity() {
class MainActivity: FlutterFragmentActivity() {
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 119 KiB

11
architecture/e2ee.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 105 KiB

275
architecture/index.md Normal file
View 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).

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 76 KiB

33
architecture/recovery.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 130 KiB

21
architecture/sharing.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 123 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -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)
@@ -16,6 +50,8 @@ PODS:
- flutter_inappwebview/Core (0.0.1):
- Flutter
- OrderedSet (~> 5.0)
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (0.0.1):
- Flutter
- flutter_secure_storage (3.3.1):
@@ -35,6 +71,8 @@ PODS:
- move_to_background (0.0.1):
- Flutter
- MTBBarcodeScanner (5.0.11)
- open_file (0.0.1):
- Flutter
- OrderedSet (5.0.0)
- package_info_plus (0.4.5):
- Flutter
@@ -44,6 +82,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 +97,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,10 +105,12 @@ 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`)
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_sodium (from `.symlinks/plugins/flutter_sodium/ios`)
@@ -69,9 +118,11 @@ DEPENDENCIES:
- in_app_purchase (from `.symlinks/plugins/in_app_purchase/ios`)
- local_auth (from `.symlinks/plugins/local_auth/ios`)
- move_to_background (from `.symlinks/plugins/move_to_background/ios`)
- open_file (from `.symlinks/plugins/open_file/ios`)
- 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 +130,15 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- FMDB
- MTBBarcodeScanner
- OrderedSet
- Reachability
- SDWebImage
- Sentry
- SwiftyGif
- Toast
EXTERNAL SOURCES:
@@ -90,6 +146,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:
@@ -98,6 +156,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_email_sender/ios"
flutter_inappwebview:
:path: ".symlinks/plugins/flutter_inappwebview/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage:
@@ -112,12 +172,16 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/local_auth/ios"
move_to_background:
:path: ".symlinks/plugins/move_to_background/ios"
open_file:
:path: ".symlinks/plugins/open_file/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_ios:
: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,27 +194,36 @@ 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
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
flutter_sodium: c84426b4de738514b5b66cfdeb8a06634e72fe0b
fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
fluttertoast: 74526702fea2c060ea55dde75895b7e1bde1c86b
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
in_app_purchase: 3e2155afa9d03d4fa32d9e62d567885080ce97d6
local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c
move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
open_file: 02eb5cb6b21264bd3a696876f5afbfb7ca4f4b7d
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
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

View File

@@ -58,5 +58,7 @@
<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>

View File

@@ -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,

View File

@@ -4,9 +4,9 @@ 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/events/signed_in_event.dart';
import 'package:ente_auth/events/signed_out_event.dart';
import 'package:ente_auth/models/key_attributes.dart';
@@ -14,13 +14,11 @@ 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';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
class Configuration {
Configuration._privateConstructor();
@@ -43,7 +41,6 @@ class Configuration {
static const userIDKey = "user_id";
static const hasMigratedSecureStorageToFirstUnlockKey =
"has_migrated_secure_storage_to_first_unlock";
static const anonymousUserIDKey = "anonymous_user_id";
final kTempFolderDeletionTimeBuffer = const Duration(days: 1).inMicroseconds;
@@ -120,7 +117,6 @@ class Configuration {
}
await _migrateSecurityStorageToFirstUnlock();
}
// SuperLogging.setUserID(await _getOrCreateAnonymousUserID());
}
Future<void> logout({bool autoLogout = false}) async {
@@ -130,6 +126,7 @@ class Configuration {
_key = null;
_cachedToken = null;
_secretKey = null;
_authSecretKey = null;
Bus.instance.fire(SignedOutEvent());
}
@@ -494,12 +491,4 @@ class Configuration {
);
}
}
Future<String> _getOrCreateAnonymousUserID() async {
if (!_preferences.containsKey(anonymousUserIDKey)) {
//ignore: prefer_const_constructors
await _preferences.setString(anonymousUserIDKey, Uuid().v4());
}
return _preferences.getString(anonymousUserIDKey)!;
}
}

View File

@@ -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;

View File

@@ -7,13 +7,18 @@ 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';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
typedef FutureOrVoidCallback = FutureOr<void> Function();
@@ -141,65 +146,62 @@ class SuperLogging {
/// The current super logging configuration
static LogConfig config;
static SharedPreferences _preferences;
static const keyShouldReportErrors = "should_report_errors";
static const keyAnonymousUserID = "anonymous_user_id";
static Future<void> main([LogConfig config]) async {
config ??= LogConfig();
SuperLogging.config = config;
WidgetsFlutterBinding.ensureInitialized();
appVersion ??= await getAppVersion();
final isFDroidClient = await isFDroidBuild();
if (isFDroidClient) {
config.sentryDsn = null;
config.tunnel = null;
}
_preferences = await SharedPreferences.getInstance();
final enable = config.enableInDebugMode || kReleaseMode;
sentryIsEnabled = enable && config.sentryDsn != null && !isFDroidClient;
fileIsEnabled = enable && config.logDirPath != null;
appVersion ??= await getAppVersion();
final loggingEnabled = config.enableInDebugMode || kReleaseMode;
sentryIsEnabled =
loggingEnabled && config.sentryDsn != null && shouldReportErrors();
fileIsEnabled = loggingEnabled && config.logDirPath != null;
if (fileIsEnabled) {
await setupLogDir();
}
if (sentryIsEnabled) {
setupSentry();
}
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen(onLogRecord);
if (isFDroidClient) {
assert(
sentryIsEnabled == false,
"sentry dsn should be disabled for "
"f-droid config ${config.sentryDsn} & ${config.tunnel}",
);
if (sentryIsEnabled) {
setupSentry();
} else {
$.info("Sentry is disabled");
}
if (!enable) {
if (!loggingEnabled) {
$.info("detected debug mode; sentry & file logging disabled.");
}
if (fileIsEnabled) {
$.info("log file for today: $logFile with prefix ${config.prefix}");
}
if (sentryIsEnabled) {
$.info("sentry uploader started");
}
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(),
// );
if (loggingEnabled && 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(),
);
} else {
await config.body();
}
@@ -207,21 +209,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 +251,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);
}
}
}
}
@@ -285,17 +295,19 @@ class SuperLogging {
static bool sentryIsEnabled;
static Future<void> setupSentry() async {
$.info("Setting up sentry");
SuperLogging.setUserID(await _getOrCreateAnonymousUserID());
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);
}
}
}
@@ -304,6 +316,26 @@ class SuperLogging {
sentryQueueControl.add(error);
}
static bool shouldReportErrors() {
if (_preferences.containsKey(keyShouldReportErrors)) {
return _preferences.getBool(keyShouldReportErrors);
} else {
return false;
}
}
static Future<void> setShouldReportErrors(bool value) {
return _preferences.setBool(keyShouldReportErrors, value);
}
static Future<String> _getOrCreateAnonymousUserID() async {
if (!_preferences.containsKey(keyAnonymousUserID)) {
//ignore: prefer_const_constructors
await _preferences.setString(keyAnonymousUserID, Uuid().v4());
}
return _preferences.getString(keyAnonymousUserID);
}
/// The log file currently in use.
static File logFile;
@@ -372,6 +404,6 @@ class SuperLogging {
return false;
}
final pkgName = (await PackageInfo.fromPlatform()).packageName;
return pkgName.startsWith("io.ente.photos.fdroid");
return pkgName.endsWith("fdroid");
}
}

View 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;
}

View File

@@ -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;
}

View File

@@ -11,12 +11,13 @@
"importScanQrCode": "Scan a QR Code",
"importEnterSetupKey": "Enter a setup key",
"importAccountPageTitle": "Enter account details",
"accountNameHint": "Account name",
"accountKeyHint" : "Your key",
"codeIssuerHint": "Issuer",
"codeSecretKeyHint" : "Secret Key",
"codeAccountHint": "Account (you@domain.com)",
"accountKeyType": "Type of key",
"timeBasedKeyType": "Time based (TOTP)",
"counterBasedKeyType": "Counter based (HOTP)",
"importAddAction": "Add",
"saveAction": "Save",
"existingUser": "Existing User",
"newUser" : "New to ente"

View File

@@ -1,11 +1,14 @@
// @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/preference_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 +32,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 +51,7 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async {
body: function,
logDirPath: (await getApplicationSupportDirectory()).path + "/logs",
maxLogFiles: 5,
sentryDsn: sentryDSN,
enableInDebugMode: true,
prefix: prefix,
),
@@ -56,12 +61,14 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async {
Future<void> _init(bool bool, {String via}) async {
InAppPurchaseConnection.enablePendingPurchases();
CryptoUtil.init();
await PreferenceService.instance.init();
await CodeStore.instance.init();
await Configuration.instance.init();
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();
}

View File

@@ -0,0 +1,7 @@
class EntityResult {
final int generatedID;
final String rawData;
final bool hasSynced;
EntityResult(this.generatedID, this.rawData, this.hasSynced);
}

View File

@@ -1,8 +1,10 @@
import 'package:ente_auth/utils/totp_util.dart';
class Code {
static const defaultDigits = 6;
static const defaultPeriod = 30;
int? id;
int? generatedID;
final String account;
final String issuer;
final int digits;
@@ -11,6 +13,7 @@ class Code {
final Algorithm algorithm;
final Type type;
final String rawData;
bool? hasSynced;
Code(
this.account,
@@ -21,24 +24,28 @@ class Code {
this.algorithm,
this.type,
this.rawData, {
this.id,
this.generatedID,
});
static Code fromAccountAndSecret(String account, String secret) {
static Code fromAccountAndSecret(
String account,
String issuer,
String secret,
) {
return Code(
account,
"",
issuer,
defaultDigits,
defaultPeriod,
secret,
Algorithm.sha1,
Type.totp,
"otpauth://totp/" +
account +
issuer +
":" +
account +
"?algorithm=SHA1&digits=6&issuer=" +
account +
issuer +
"period=30&secret=" +
secret,
);
@@ -51,7 +58,7 @@ class Code {
_getIssuer(uri),
_getDigits(uri),
_getPeriod(uri),
uri.queryParameters['secret']!,
getSanitizedSecret(uri.queryParameters['secret']!),
_getAlgorithm(uri),
_getType(uri),
rawData,

View File

@@ -1,12 +1,14 @@
import "package:ente_auth/l10n/l10n.dart";
import 'package:ente_auth/models/code.dart';
// ignore: import_of_legacy_library_into_null_safe
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 {
SetupEnterSecretKeyPage({Key? key}) : super(key: key);
final Code? code;
SetupEnterSecretKeyPage({this.code, Key? key}) : super(key: key);
@override
State<SetupEnterSecretKeyPage> createState() =>
@@ -14,9 +16,27 @@ class SetupEnterSecretKeyPage extends StatefulWidget {
}
class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
final _accountController = TextEditingController();
final _secretController =
TextEditingController(text: kDebugMode ? "JBSWY3DPEHPK3PXP" : "");
late TextEditingController _issuerController;
late TextEditingController _accountController;
late TextEditingController _secretController;
@override
void initState() {
_issuerController = TextEditingController(
text: widget.code != null
? Uri.decodeFull(widget.code!.issuer).trim()
: null,
);
_accountController = TextEditingController(
text: widget.code != null
? Uri.decodeFull(widget.code!.account).trim()
: null,
);
_secretController = TextEditingController(
text: widget.code != null ? widget.code!.secret : null,
);
super.initState();
}
@override
Widget build(BuildContext context) {
@@ -40,9 +60,9 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
return null;
},
decoration: InputDecoration(
hintText: l10n.accountNameHint,
hintText: l10n.codeIssuerHint,
),
controller: _accountController,
controller: _issuerController,
autofocus: true,
),
const SizedBox(
@@ -57,10 +77,26 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
return null;
},
decoration: InputDecoration(
hintText: l10n.accountKeyHint,
hintText: l10n.codeSecretKeyHint,
),
controller: _secretController,
),
const SizedBox(
height: 20,
),
TextFormField(
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter some text";
}
return null;
},
decoration: InputDecoration(
hintText: l10n.codeAccountHint,
),
controller: _accountController,
),
const SizedBox(
height: 40,
),
@@ -76,10 +112,14 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
try {
final code = Code.fromAccountAndSecret(
_accountController.text.trim(),
_issuerController.text.trim(),
_secretController.text.trim(),
);
// Verify the validity of the code
getTotp(code);
if (widget.code != null) {
code.generatedID = widget.code!.generatedID;
}
Navigator.of(context).pop(code);
} catch (e) {
_showIncorrectDetailsDialog(context);
@@ -90,7 +130,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
horizontal: 16.0,
vertical: 4,
),
child: Text(l10n.importAddAction),
child: Text(l10n.saveAction),
),
),
)

View File

@@ -11,6 +11,7 @@ import 'package:ente_auth/events/signed_in_event.dart';
import 'package:ente_auth/gateway/authenticator.dart';
import 'package:ente_auth/models/authenticator/auth_entity.dart';
import 'package:ente_auth/models/authenticator/auth_key.dart';
import 'package:ente_auth/models/authenticator/entity_result.dart';
import 'package:ente_auth/models/authenticator/local_auth_entity.dart';
import 'package:ente_auth/store/authenticator_db.dart';
import 'package:ente_auth/utils/crypto_util.dart';
@@ -44,25 +45,36 @@ class AuthenticatorService {
});
}
Future<Map<int, String>> getAllIDtoStringMap() async {
Future<List<EntityResult>> getEntities() async {
final List<LocalAuthEntity> result = await _db.getAll();
final Map<int, String> entries = <int, String>{};
final List<EntityResult> entities = [];
if (result.isEmpty) {
return entries;
return entities;
}
final key = await getOrCreateAuthDataKey();
for (LocalAuthEntity e in result) {
final decryptedValue = await CryptoUtil.decryptChaCha(
Sodium.base642bin(e.encryptedData),
key,
Sodium.base642bin(e.header),
);
entries[e.generatedID] = utf8.decode(decryptedValue);
try {
final decryptedValue = await CryptoUtil.decryptChaCha(
Sodium.base642bin(e.encryptedData),
key,
Sodium.base642bin(e.header),
);
final hasSynced = !(e.id == null || e.shouldSync);
entities.add(
EntityResult(
e.generatedID,
utf8.decode(decryptedValue),
hasSynced,
),
);
} catch (e, s) {
_logger.severe(e, s);
}
}
return entries;
return entities;
}
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,11 +83,17 @@ 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;
}
Future<void> updateEntry(int generatedID, String plainText) async {
Future<void> updateEntry(
int generatedID,
String plainText,
bool shouldSync,
) async {
var key = await getOrCreateAuthDataKey();
final encryptedKeyData = await CryptoUtil.encryptChaCha(
utf8.encode(plainText) as Uint8List,
@@ -89,7 +107,9 @@ class AuthenticatorService {
affectedRows == 1,
"updateEntry should have updated exactly one row",
);
unawaited(sync());
if (shouldSync) {
unawaited(sync());
}
}
Future<void> deleteEntry(int genID) async {
@@ -107,15 +127,9 @@ class AuthenticatorService {
Future<void> sync() async {
try {
_logger.info("Sync");
_logger.info("State of DB before sync");
await _printDBState();
await _remoteToLocalSync();
_logger.info("remote fetch completed");
_logger.info("State of DB after remoteToLocal sync");
await _printDBState();
await _localToRemoteSync();
_logger.info("State of DB after localToRemote sync");
await _printDBState();
_logger.info("local push completed");
Bus.instance.fire(CodesUpdatedEvent());
} catch (e) {
@@ -126,24 +140,27 @@ class AuthenticatorService {
Future<void> _remoteToLocalSync() async {
_logger.info('Initiating remote to local sync');
final int lastSyncTime = _prefs.getInt(_lastEntitySyncTime) ?? 0;
_logger.info("Current synctime is " + lastSyncTime.toString());
const int fetchLimit = 500;
final List<AuthEntity> result =
await _gateway.getDiff(lastSyncTime, limit: fetchLimit);
_logger.info(result.length.toString() + " entries fetched from remote");
if (result.isEmpty) {
return;
} else {
_logger.info(result.length.toString() + " entries fetched from remote");
}
final maxSyncTime = result.map((e) => e.updatedAt).reduce(max);
List<String> deletedIDs =
result.where((element) => element.isDeleted).map((e) => e.id).toList();
_logger.info(deletedIDs.length.toString() + " entries deleted");
result.removeWhere((element) => element.isDeleted);
await _db.insertOrReplace(result);
if (deletedIDs.isNotEmpty) {
await _db.deleteByIDs(ids: deletedIDs);
}
_prefs.setInt(_lastEntitySyncTime, maxSyncTime);
_logger.info("Setting synctime to " + maxSyncTime.toString());
if (result.length == fetchLimit) {
_logger.info("Diff limit reached, pulling again");
await _remoteToLocalSync();
}
}
@@ -178,6 +195,10 @@ class AuthenticatorService {
await _db.updateLocalEntity(entity.copyWith(shouldSync: false));
}
}
if (pendingUpdate.isNotEmpty) {
_logger.info("Initiating remote sync since local entries were pushed");
await _remoteToLocalSync();
}
}
Future<Uint8List> getOrCreateAuthDataKey() async {
@@ -208,13 +229,4 @@ class AuthenticatorService {
rethrow;
}
}
Future<void> _printDBState() async {
_logger.info("_____");
final entities = await _db.getAll();
for (final entity in entities) {
_logger.info(entity.id);
}
_logger.info("_____");
}
}

View File

@@ -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,
);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:shared_preferences/shared_preferences.dart';
class PreferenceService {
PreferenceService._privateConstructor();
static final PreferenceService instance =
PreferenceService._privateConstructor();
late final SharedPreferences _prefs;
static const kHasShownCoachMarkKey = "has_shown_coach_mark";
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
bool hasShownCoachMark() {
if (_prefs.containsKey(kHasShownCoachMarkKey)) {
return _prefs.getBool(kHasShownCoachMarkKey)!;
} else {
return false;
}
}
Future<void> setHasShownCoachMark(bool value) {
return _prefs.setBool(kHasShownCoachMarkKey, value);
}
}

View File

@@ -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");
}
}

View File

@@ -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();
}
}
}

View File

@@ -2,8 +2,10 @@ import 'dart:convert';
import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/events/codes_updated_event.dart';
import 'package:ente_auth/models/authenticator/entity_result.dart';
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/services/authenticator_service.dart';
import 'package:logging/logging.dart';
class CodeStore {
static final CodeStore instance = CodeStore._privateConstructor();
@@ -11,33 +13,61 @@ class CodeStore {
CodeStore._privateConstructor();
late AuthenticatorService _authenticatorService;
final _logger = Logger("CodeStore");
Future<void> init() async {
_authenticatorService = AuthenticatorService.instance;
}
Future<List<Code>> getAllCodes() async {
final Map<int, String> rawCodesMap =
await _authenticatorService.getAllIDtoStringMap();
final List<EntityResult> entities =
await _authenticatorService.getEntities();
final List<Code> codes = [];
for (final entry in rawCodesMap.entries) {
final decodeJson = jsonDecode(entry.value);
for (final entity in entities) {
final decodeJson = jsonDecode(entity.rawData);
final code = Code.fromRawData(decodeJson);
code.id = entry.key;
code.generatedID = entity.generatedID;
code.hasSynced = entity.hasSynced;
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);
bool isExistingCode = false;
for (final existingCode in codes) {
if (existingCode == code) {
_logger.info("Found duplicate code, skipping add");
return;
} else if (existingCode.generatedID == code.generatedID) {
isExistingCode = true;
break;
}
}
if (isExistingCode) {
await _authenticatorService.updateEntry(
code.generatedID!,
jsonEncode(code.rawData),
shouldSync,
);
} else {
code.generatedID = await _authenticatorService.addEntry(
jsonEncode(code.rawData),
shouldSync,
);
}
Bus.instance.fire(CodesUpdatedEvent());
}
Future<void> removeCode(Code code) async {
await _authenticatorService.deleteEntry(code.id!);
await _authenticatorService.deleteEntry(code.generatedID!);
Bus.instance.fire(CodesUpdatedEvent());
}
}

View File

@@ -61,7 +61,7 @@ class _ChangeEmailDialogState extends State<ChangeEmailDialog> {
child: const Text(
"Verify",
style: TextStyle(
color: Colors.green,
color: Colors.purple,
),
),
onPressed: () {

View File

@@ -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,

View File

@@ -161,7 +161,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
),
),
controller: _verificationCodeController,
autofocus: false,
autofocus: true,
autocorrect: false,
keyboardType: TextInputType.number,
onChanged: (_) {

View File

@@ -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}';
}

View File

@@ -1,13 +1,14 @@
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/onboarding/view/setup_enter_secret_key_page.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 +49,180 @@ 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: _onEditPressed,
backgroundColor: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
foregroundColor:
Theme.of(context).colorScheme.inverseBackgroundColor,
icon: Icons.edit_outlined,
label: 'Edit',
padding: const EdgeInsets.only(left: 4, right: 0),
spacing: 8,
),
const SizedBox(
width: 4,
),
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),
spacing: 8,
),
],
),
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: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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,
),
),
],
),
widget.code.hasSynced != null &&
widget.code.hasSynced!
? Container()
: const Icon(
Icons.sync_disabled,
size: 20,
color: Colors.amber,
),
],
),
),
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"),
);
}
Future<void> _onEditPressed(_) async {
final Code? code = await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return SetupEnterSecretKeyPage(code: widget.code);
},
),
);
if (code != null) {
CodeStore.instance.addCode(code);
}
}
void _onDeletePressed(_) {
final AlertDialog alert = AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
@@ -195,16 +284,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;
}
}

View File

@@ -2,6 +2,7 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/ente_theme_data.dart';
@@ -9,6 +10,7 @@ import 'package:ente_auth/events/codes_updated_event.dart';
import "package:ente_auth/l10n/l10n.dart";
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/onboarding/view/setup_enter_secret_key_page.dart';
import 'package:ente_auth/services/preference_service.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/ui/code_widget.dart';
@@ -41,7 +43,6 @@ class _HomePageState extends State<HomePage> {
_loadCodes();
_streamSubscription = Bus.instance.on<CodesUpdatedEvent>().listen((event) {
_loadCodes();
setState(() {});
});
}
@@ -122,7 +123,11 @@ class _HomePageState extends State<HomePage> {
appBar: AppBar(
title: const Text('ente Authenticator'),
),
floatingActionButton: !_hasLoaded || _codes.isEmpty ? null : _getFab(),
floatingActionButton: !_hasLoaded ||
_codes.isEmpty ||
!PreferenceService.instance.hasShownCoachMark()
? null
: _getFab(),
),
);
}
@@ -132,12 +137,22 @@ class _HomePageState extends State<HomePage> {
if (_codes.isEmpty) {
return _getEmptyState();
} else {
return ListView.builder(
final list = ListView.builder(
itemBuilder: ((context, index) {
return CodeWidget(_codes[index]);
}),
itemCount: _codes.length,
);
if (!PreferenceService.instance.hasShownCoachMark()) {
return Stack(
children: [
list,
_getCoachMarkWidget(),
],
);
} else {
return list;
}
}
} else {
return const EnteLoadingWidget();
@@ -152,8 +167,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 +176,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,
),
],
@@ -223,4 +238,89 @@ class _HomePageState extends State<HomePage> {
),
);
}
Widget _getCoachMarkWidget() {
return GestureDetector(
onTap: () async {
await PreferenceService.instance.setHasShownCoachMark(true);
setState(() {});
},
child: Row(
children: [
Expanded(
child: Container(
width: double.infinity,
color: Theme.of(context).colorScheme.background.withOpacity(0.1),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.swipe_left,
size: 42,
),
const SizedBox(
height: 24,
),
Text(
"Swipe left to edit or remove codes",
style: Theme.of(context).textTheme.headline6,
),
const SizedBox(
height: 36,
),
SizedBox(
width: 160,
child: OutlinedButton(
onPressed: () async {
await PreferenceService.instance
.setHasShownCoachMark(true);
setState(() {});
},
child: const Text("OK"),
),
)
],
),
],
),
),
),
),
],
),
);
}
}
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,
),
),
);
}
}

View File

@@ -28,11 +28,6 @@ class AboutSectionWidget extends StatelessWidget {
Widget _getSectionOptions(BuildContext context) {
return Column(
children: [
sectionOptionSpacing,
const AboutMenuItemWidget(
title: "FAQ",
url: "https://ente.io/faq",
),
sectionOptionSpacing,
const AboutMenuItemWidget(
title: "Terms",

View File

@@ -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();
}
});
}
}

View File

@@ -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(

View 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!",
);
}
}
}

View 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,
),
),
],
),
),
);
}
}

View File

@@ -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);
}
}

View File

@@ -1,19 +1,24 @@
// @dart=2.9
import 'dart:io';
import 'package:ente_auth/core/constants.dart';
import 'package:ente_auth/core/logging/super_logging.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/components/toggle_switch_widget.dart';
import 'package:ente_auth/ui/settings/common_settings.dart';
import 'package:ente_auth/utils/email_util.dart';
import 'package:flutter/material.dart';
class SupportSectionWidget extends StatelessWidget {
class SupportSectionWidget extends StatefulWidget {
const SupportSectionWidget({Key key}) : super(key: key);
@override
State<SupportSectionWidget> createState() => _SupportSectionWidgetState();
}
class _SupportSectionWidgetState extends State<SupportSectionWidget> {
@override
Widget build(BuildContext context) {
return ExpandableMenuItemWidget(
@@ -24,8 +29,6 @@ class SupportSectionWidget extends StatelessWidget {
}
Widget _getSectionOptions(BuildContext context) {
final String bugsEmail =
Platform.isAndroid ? "android-bugs@ente.io" : "ios-bugs@ente.io";
return Column(
children: [
sectionOptionSpacing,
@@ -49,14 +52,27 @@ class SupportSectionWidget extends StatelessWidget {
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
await sendLogs(context, "Report bug", bugsEmail);
await sendLogs(context, "Report bug", "auth@ente.io");
},
onDoubleTap: () async {
final zipFilePath = await getZippedLogsFile(context);
await shareLogs(context, bugsEmail, zipFilePath);
await shareLogs(context, "auth@ente.io", zipFilePath);
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Crash & error reporting",
),
trailingSwitch: ToggleSwitchWidget(
value: SuperLogging.shouldReportErrors(),
onChanged: (value) async {
await SuperLogging.setShouldReportErrors(value);
setState(() {});
},
),
),
sectionOptionSpacing,
],
);
}

View File

@@ -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),
),

View File

@@ -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(

View 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);
}
}

View 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),
),
),
),
),
),
],
),
);
}
}

View File

@@ -89,7 +89,9 @@ Future<void> sendLogs(
content.addAll(
[
const Text(
"This will send across logs to help us debug your issue. Please note that file names will be included to help track issues with specific files.",
"This will send across logs to help us debug your issue. "
"While we take precautions to ensure that sensitive information is not "
"logged, we encourage you to view these logs before sharing them.",
style: TextStyle(
height: 1.5,
fontSize: 16,

View File

@@ -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) {

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_linux
sentry_flutter
url_launcher_linux
)

View File

@@ -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"))

View File

@@ -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:

View File

@@ -1,6 +1,6 @@
name: ente_auth
description: ente two-factor authenticator
version: 1.0.3+4
version: 1.0.18+18
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

View File

@@ -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"));
}

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_windows
sentry_flutter
url_launcher_windows
)