Compare commits

...

42 Commits

Author SHA1 Message Date
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
51 changed files with 1223 additions and 358 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

9
.vscode/launch.json vendored
View File

@@ -29,11 +29,18 @@
"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/photos) uses to encrypt and preserve your data.
### Multi Device Synchronization
ente will automatically sync the 2FA tokens you add to your account, across all
your devices. Every new device you sign into will have access to these tokens.
### Offline Mode
ente generates 2FA tokens offline, so your network connectivity will not get in
the way of your workflow.
### Import and Export Tokens
You can add tokens to ente by one of the following methods:
1. Scanning a QR code
2. Manually entering (copy-pasting) a 2FA secret
3. Bulk importing from a file that contains a list of codes in the following
format:
```
otpauth://totp/ACCOUNT?secret=SUPERSECRET&issuer=SERVICE
```
The codes maybe separated by new lines or commas.
You can also export the codes you have added to ente, to an **unencrypted** text
file, that adheres to the above format.
## 🔩 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

@@ -50,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):
@@ -69,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
@@ -81,6 +85,11 @@ PODS:
- 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):
@@ -101,6 +110,7 @@ DEPENDENCIES:
- 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`)
@@ -108,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`)
@@ -125,6 +137,7 @@ SPEC REPOS:
- OrderedSet
- Reachability
- SDWebImage
- Sentry
- SwiftyGif
- Toast
@@ -143,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:
@@ -157,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:
@@ -182,21 +201,25 @@ SPEC CHECKSUMS:
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

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,10 @@ import 'dart:typed_data';
import 'package:bip39/bip39.dart' as bip39;
import 'package:ente_auth/core/constants.dart';
// import 'package:ente_auth/core/error-reporting/super_logging.dart';
import 'package:ente_auth/core/errors.dart';
import 'package:ente_auth/core/event_bus.dart';
// ignore: import_of_legacy_library_into_null_safe
import 'package:ente_auth/core/logging/super_logging.dart';
import 'package:ente_auth/events/signed_in_event.dart';
import 'package:ente_auth/events/signed_out_event.dart';
import 'package:ente_auth/models/key_attributes.dart';
@@ -14,7 +15,6 @@ import 'package:ente_auth/models/key_gen_result.dart';
import 'package:ente_auth/models/private_key_attributes.dart';
import 'package:ente_auth/store/authenticator_db.dart';
import 'package:ente_auth/utils/crypto_util.dart';
// import 'package:ente_auth/utils/validator_util.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:logging/logging.dart';
@@ -120,7 +120,7 @@ class Configuration {
}
await _migrateSecurityStorageToFirstUnlock();
}
// SuperLogging.setUserID(await _getOrCreateAnonymousUserID());
SuperLogging.setUserID(await _getOrCreateAnonymousUserID());
}
Future<void> logout({bool autoLogout = false}) async {

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,16 @@ import 'dart:collection';
import 'dart:core';
import 'dart:io';
import 'package:ente_auth/core/logging/tunneled_transport.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
typedef FutureOrVoidCallback = FutureOr<void> Function();
@@ -189,17 +192,17 @@ class SuperLogging {
if (config.body == null) return;
if (enable && sentryIsEnabled) {
// await SentryFlutter.init(
// (options) {
// options.dsn = config.sentryDsn;
// options.httpClient = http.Client();
// if (config.tunnel != null) {
// options.transport =
// TunneledTransport(Uri.parse(config.tunnel), options);
// }
// },
// appRunner: () => config.body(),
// );
await SentryFlutter.init(
(options) {
options.dsn = config.sentryDsn;
options.httpClient = http.Client();
if (config.tunnel != null) {
options.transport =
TunneledTransport(Uri.parse(config.tunnel), options);
}
},
appRunner: () => config.body(),
);
} else {
await config.body();
}
@@ -207,21 +210,21 @@ class SuperLogging {
static void setUserID(String userID) async {
if (config?.sentryDsn != null) {
// Sentry.configureScope((scope) => scope.user = SentryUser(id: userID));
Sentry.configureScope((scope) => scope.user = SentryUser(id: userID));
$.info("setting sentry user ID to: $userID");
}
}
static Future<void> _sendErrorToSentry(Object error, StackTrace stack) async {
// try {
// await Sentry.captureException(
// error,
// stackTrace: stack,
// );
// } catch (e) {
// $.info('Sending report to sentry.io failed: $e');
// $.info('Original error: $error');
// }
try {
await Sentry.captureException(
error,
stackTrace: stack,
);
} catch (e) {
$.info('Sending report to sentry.io failed: $e');
$.info('Original error: $error');
}
}
static String _lastExtraLines = '';
@@ -249,8 +252,16 @@ class SuperLogging {
}
// add error to sentry queue
if (sentryIsEnabled && rec.error != null) {
_sendErrorToSentry(rec.error, null);
if (sentryIsEnabled) {
if (rec.error != null) {
_sendErrorToSentry(rec.error, null);
} else if (rec.level == Level.SEVERE || rec.level == Level.SHOUT) {
if (rec.error != null) {
_sendErrorToSentry(rec.error, null);
} else {
_sendErrorToSentry(rec.message, null);
}
}
}
}
@@ -286,16 +297,16 @@ class SuperLogging {
static Future<void> setupSentry() async {
await for (final error in sentryQueueControl.stream.asBroadcastStream()) {
// try {
// Sentry.captureException(
// error,
// );
// } catch (e) {
// $.fine(
// "sentry upload failed; will retry after ${config.sentryRetryDelay}",
// );
// doSentryRetry(error);
// }
try {
Sentry.captureException(
error,
);
} catch (e) {
$.fine(
"sentry upload failed; will retry after ${config.sentryRetryDelay}",
);
doSentryRetry(error);
}
}
}
@@ -372,6 +383,6 @@ class SuperLogging {
return false;
}
final pkgName = (await PackageInfo.fromPlatform()).packageName;
return pkgName.startsWith("io.ente.photos.fdroid");
return pkgName.endsWith("fdroid");
}
}

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

@@ -1,11 +1,13 @@
// @dart=2.9
import "package:ente_auth/app/view/app.dart";
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/constants.dart';
import 'package:ente_auth/core/logging/super_logging.dart';
import 'package:ente_auth/core/network.dart';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/services/authenticator_service.dart';
import 'package:ente_auth/services/billing_service.dart';
import 'package:ente_auth/services/notification_service.dart';
import 'package:ente_auth/services/update_service.dart';
import 'package:ente_auth/services/user_remote_flag_service.dart';
import 'package:ente_auth/services/user_service.dart';
@@ -29,6 +31,7 @@ Future<void> _runInForeground() async {
return await _runWithLogs(() async {
_logger.info("Starting app in foreground");
await _init(false, via: 'mainMethod');
UpdateService.instance.showUpdateNotification();
runApp(
AppLock(
builder: (args) => const App(),
@@ -47,6 +50,8 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async {
body: function,
logDirPath: (await getApplicationSupportDirectory()).path + "/logs",
maxLogFiles: 5,
sentryDsn: sentryDSN,
tunnel: sentryTunnel,
enableInDebugMode: true,
prefix: prefix,
),
@@ -61,7 +66,8 @@ Future<void> _init(bool bool, {String via}) async {
await Network.instance.init();
await UserService.instance.init();
await UserRemoteFlagService.instance.init();
await UpdateService.instance.init();
await AuthenticatorService.instance.init();
await BillingService.instance.init();
await NotificationService.instance.init();
await UpdateService.instance.init();
}

View File

@@ -2,7 +2,6 @@ import "package:ente_auth/l10n/l10n.dart";
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/totp_util.dart';
import 'package:flutter/foundation.dart';
import "package:flutter/material.dart";
class SetupEnterSecretKeyPage extends StatefulWidget {
@@ -15,8 +14,7 @@ class SetupEnterSecretKeyPage extends StatefulWidget {
class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
final _accountController = TextEditingController();
final _secretController =
TextEditingController(text: kDebugMode ? "JBSWY3DPEHPK3PXP" : "");
final _secretController = TextEditingController(text: "");
@override
Widget build(BuildContext context) {

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

@@ -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';
@@ -620,6 +622,14 @@ class UserService {
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),

View File

@@ -28,6 +28,9 @@ class CodeStore {
code.id = entry.key;
codes.add(code);
}
codes.sort((c1, c2) {
return c1.issuer.toLowerCase().compareTo(c2.issuer.toLowerCase());
});
return codes;
}

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

@@ -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,13 @@
import 'dart:async';
import 'package:clipboard/clipboard.dart';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:ente_auth/utils/totp_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animation_progress_bar/flutter_animation_progress_bar.dart';
// import 'package:flutter_animation_progress_bar/flutter_animation_progress_bar.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
class CodeWidget extends StatefulWidget {
@@ -48,92 +48,137 @@ class _CodeWidgetState extends State<CodeWidget> {
@override
Widget build(BuildContext context) {
return Slidable(
key: ValueKey(widget.code.hashCode),
endActionPane: ActionPane(
motion: const ScrollMotion(),
children: [
SlidableAction(
onPressed: _onDeletePressed,
backgroundColor: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
foregroundColor: const Color(0xFFFE4A49),
icon: Icons.delete,
label: 'Delete',
),
],
),
child: InkWell(
onTap: () {
FlutterClipboard.copy(_getTotp())
.then((value) => showToast(context, "Copied to clipboard"));
},
child: SizedBox(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FAProgressBar(
currentValue: _timeRemaining / widget.code.period * 100,
size: 4,
animatedDuration: const Duration(milliseconds: 200),
progressColor: Colors.orange,
changeColorValue: 40,
changeProgressColor: Colors.green,
),
const SizedBox(
height: 10,
),
Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Text(
Uri.decodeFull(widget.code.issuer),
style: Theme.of(context).textTheme.headline6,
return Container(
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8),
child: Slidable(
key: ValueKey(widget.code.hashCode),
endActionPane: ActionPane(
motion: const ScrollMotion(),
children: [
SlidableAction(
onPressed: _onDeletePressed,
backgroundColor: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
foregroundColor: const Color(0xFFFE4A49),
icon: Icons.delete,
label: 'Delete',
padding: const EdgeInsets.only(left: 0, right: 0),
),
],
),
child: Container(
margin: const EdgeInsets.only(right: 10),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
child: Material(
color: Colors.transparent,
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
onTap: () {
_copyToClipboard();
},
onLongPress: () {
_copyToClipboard();
},
child: SizedBox(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FAProgressBar(
currentValue:
_timeRemaining / widget.code.period * 100,
size: 4,
animatedDuration: const Duration(milliseconds: 200),
progressColor: Colors.orange,
changeColorValue: 40,
changeProgressColor: Colors.green,
),
const SizedBox(
height: 16,
),
Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
Uri.decodeFull(widget.code.issuer).trim(),
style: Theme.of(context).textTheme.headline6,
),
const SizedBox(height: 2),
Text(
Uri.decodeFull(
widget.code.account,
).trim(),
style: Theme.of(context)
.textTheme
.caption
?.copyWith(
fontSize: 12,
color: Colors.grey,
),
),
],
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Text(
_getTotp(),
style: const TextStyle(fontSize: 24),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"next",
style: Theme.of(context).textTheme.caption,
),
Text(
_getNextTotp(),
style: const TextStyle(
fontSize: 18,
color: Colors.grey,
),
),
],
),
],
),
),
const SizedBox(
height: 20,
),
],
),
),
),
),
Container(
padding: const EdgeInsets.only(right: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
"next",
style: Theme.of(context).textTheme.caption,
),
],
),
),
Container(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: Text(
_getTotp(),
style: const TextStyle(fontSize: 24),
),
),
Text(
_getNextTotp(),
style: const TextStyle(
fontSize: 24,
color: Colors.grey,
),
),
],
),
),
const SizedBox(
height: 32,
),
],
),
),
),
),
);
}
void _copyToClipboard() {
FlutterClipboard.copy(_getTotp()).then(
(value) => showToast(context, "Copied to clipboard"),
);
}
void _onDeletePressed(_) {
final AlertDialog alert = AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
@@ -195,16 +240,4 @@ class _CodeWidgetState extends State<CodeWidget> {
return "Error";
}
}
Color _getProgressColor() {
final progress = _timeRemaining / widget.code.period;
if (progress > 0.6) {
return Colors.green;
} else if (progress > 0.4) {
return Colors.yellow;
} else if (progress > 2) {
return Colors.orange;
}
return Colors.red;
}
}

View File

@@ -41,7 +41,6 @@ class _HomePageState extends State<HomePage> {
_loadCodes();
_streamSubscription = Bus.instance.on<CodesUpdatedEvent>().listen((event) {
_loadCodes();
setState(() {});
});
}
@@ -152,8 +151,8 @@ class _HomePageState extends State<HomePage> {
childPadding: const EdgeInsets.all(5),
spaceBetweenChildren: 4,
tooltip: 'Add Code',
foregroundColor: Theme.of(context).colorScheme.background,
backgroundColor: Theme.of(context).colorScheme.inverseBackgroundColor,
foregroundColor: Theme.of(context).colorScheme.fabForegroundColor,
backgroundColor: Theme.of(context).colorScheme.fabBackgroundColor,
overlayOpacity: 0.5,
overlayColor: Theme.of(context).colorScheme.background,
elevation: 8.0,
@@ -161,16 +160,16 @@ class _HomePageState extends State<HomePage> {
children: [
SpeedDialChild(
child: const Icon(Icons.qr_code),
foregroundColor: Theme.of(context).colorScheme.background,
backgroundColor: Theme.of(context).colorScheme.inverseBackgroundColor,
label: 'Scan a QR Code',
foregroundColor: Theme.of(context).colorScheme.fabForegroundColor,
backgroundColor: Theme.of(context).colorScheme.fabBackgroundColor,
labelWidget: const SpeedDialLabelWidget("Scan a QR Code"),
onTap: _redirectToScannerPage,
),
SpeedDialChild(
child: const Icon(Icons.keyboard),
foregroundColor: Theme.of(context).colorScheme.background,
backgroundColor: Theme.of(context).colorScheme.inverseBackgroundColor,
label: 'Enter details manually',
foregroundColor: Theme.of(context).colorScheme.fabForegroundColor,
backgroundColor: Theme.of(context).colorScheme.fabBackgroundColor,
labelWidget: const SpeedDialLabelWidget("Enter details manually"),
onTap: _redirectToManualEntryPage,
),
],
@@ -224,3 +223,30 @@ class _HomePageState extends State<HomePage> {
);
}
}
class SpeedDialLabelWidget extends StatelessWidget {
final String label;
const SpeedDialLabelWidget(
this.label, {
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(4),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.fabBackgroundColor,
),
child: Text(
label,
style: TextStyle(
color: Theme.of(context).colorScheme.fabForegroundColor,
),
),
);
}
}

View File

@@ -48,7 +48,7 @@ class AccountSectionWidget extends StatelessWidget {
String recoveryKey;
try {
recoveryKey =
Sodium.bin2base64(Configuration.instance.getRecoveryKey());
Sodium.bin2hex(Configuration.instance.getRecoveryKey());
} catch (e) {
showGenericErrorDialog(context);
return;

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

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

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

@@ -20,10 +20,6 @@ class TwoFactorAuthenticationPage extends StatefulWidget {
class _TwoFactorAuthenticationPageState
extends State<TwoFactorAuthenticationPage> {
final _pinController = TextEditingController();
final _pinPutDecoration = BoxDecoration(
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
borderRadius: BorderRadius.circular(15.0),
);
String _code = "";
LifecycleEventHandler _lifecycleEventHandler;
@@ -62,6 +58,16 @@ class _TwoFactorAuthenticationPageState
}
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,
@@ -89,15 +95,12 @@ class _TwoFactorAuthenticationPageState
});
},
controller: _pinController,
submittedFieldDecoration: _pinPutDecoration.copyWith(
submittedFieldDecoration: pinPutDecoration.copyWith(
borderRadius: BorderRadius.circular(20.0),
),
selectedFieldDecoration: _pinPutDecoration,
followingFieldDecoration: _pinPutDecoration.copyWith(
selectedFieldDecoration: pinPutDecoration,
followingFieldDecoration: pinPutDecoration.copyWith(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(
color: const Color.fromRGBO(45, 194, 98, 0.5),
),
),
inputDecoration: const InputDecoration(
focusedBorder: InputBorder.none,

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

@@ -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:
@@ -384,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:
@@ -412,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
@@ -494,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
@@ -524,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:
@@ -714,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:
@@ -924,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:
@@ -1174,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.6+6
version: 1.0.14+14
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
file_picker: ^4.6.1
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
)