Skip to content

Commit

Permalink
[auth][photos] Support for passkey (#435)
Browse files Browse the repository at this point in the history
<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Description

Passkey implementation (similar will be done in ente Photos)

<!--- Describe your changes in detail -->

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] 🖼️ New icon
- [x] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ] ❌ Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
  • Loading branch information
ua741 authored Mar 6, 2024
2 parents b36c136 + 82573f2 commit 4744434
Show file tree
Hide file tree
Showing 23 changed files with 585 additions and 72 deletions.
10 changes: 5 additions & 5 deletions auth/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ android {

signingConfigs {
release {
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : file(System.getenv("SIGNING_KEY_PATH"))
keyAlias keystoreProperties['keyAlias'] ? keystoreProperties['keyAlias'] : System.getenv("SIGNING_KEY_ALIAS")
keyPassword keystoreProperties['keyPassword'] ? keystoreProperties['keyPassword'] : System.getenv("SIGNING_KEY_PASSWORD")
storePassword keystoreProperties['storePassword'] ? keystoreProperties['storePassword'] : System.getenv("SIGNING_STORE_PASSWORD")
}
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : System.getenv("SIGNING_KEY_PATH") ? file(System.getenv("SIGNING_KEY_PATH")) : null
keyAlias keystoreProperties['keyAlias'] ? keystoreProperties['keyAlias'] : System.getenv("SIGNING_KEY_ALIAS")
keyPassword keystoreProperties['keyPassword'] ? keystoreProperties['keyPassword'] : System.getenv("SIGNING_KEY_PASSWORD")
storePassword keystoreProperties['storePassword'] ? keystoreProperties['storePassword'] : System.getenv("SIGNING_STORE_PASSWORD")
}
}

flavorDimensions "default"
Expand Down
7 changes: 7 additions & 0 deletions auth/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
<data android:scheme="otpauth" />
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="enteauth" />
</intent-filter>

</activity>

<!-- Don't delete the meta-data below.
Expand Down
1 change: 1 addition & 0 deletions auth/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<key>CFBundleURLSchemes</key>
<array>
<string>otpauth</string>
<string>enteauth</string>
</array>
</dict>
</array>
Expand Down
4 changes: 2 additions & 2 deletions auth/lib/core/network.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ class EnteRequestInterceptor extends Interceptor {
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (kDebugMode) {
assert(
options.baseUrl == enteEndpoint,
"interceptor should only be used for API endpoint",
options.baseUrl == enteEndpoint,
"interceptor should only be used for API endpoint",
);
}
// ignore: prefer_const_constructors
Expand Down
15 changes: 9 additions & 6 deletions auth/lib/l10n/arb/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
}
},
"contactSupport": "Contact support",
"rateUsOnStore" : "Rate us on {storeName}",
"rateUsOnStore": "Rate us on {storeName}",
"blog": "Blog",
"merchandise": "Merchandise",
"verifyPassword": "Verify password",
Expand Down Expand Up @@ -133,7 +133,6 @@
"faq_q_5": "How can I enable FaceID lock in ente Auth",
"faq_a_5": "You can enable FaceID lock under Settings → Security → Lockscreen.",
"somethingWentWrongMessage": "Something went wrong, please try again",

"leaveFamily": "Leave family",
"leaveFamilyMessage": "Are you sure that you want to leave the family plan?",
"inFamilyPlanMessage": "You are on a family plan!",
Expand All @@ -145,6 +144,7 @@
"enterCodeHint": "Enter the 6-digit code from\nyour authenticator app",
"lostDeviceTitle": "Lost device?",
"twoFactorAuthTitle": "Two-factor authentication",
"passkeyAuthTitle": "Passkey authentication",
"recoverAccount": "Recover account",
"enterRecoveryKeyHint": "Enter your recovery key",
"recover": "Recover",
Expand Down Expand Up @@ -337,10 +337,10 @@
"offlineModeWarning": "You have chosen to proceed without backups. Please take manual backups to make sure your codes are safe.",
"showLargeIcons": "Show large icons",
"shouldHideCode": "Hide codes",
"doubleTapToViewHiddenCode" : "You can double tap on an entry to view code",
"doubleTapToViewHiddenCode": "You can double tap on an entry to view code",
"focusOnSearchBar": "Focus search on app start",
"confirmUpdatingkey": "Are you sure you want to update the secret key?",
"minimizeAppOnCopy": "Minimize app on copy",
"minimizeAppOnCopy": "Minimize app on copy",
"editCodeAuthMessage": "Authenticate to edit code",
"deleteCodeAuthMessage": "Authenticate to delete code",
"showQRAuthMessage": "Authenticate to show QR code",
Expand Down Expand Up @@ -405,5 +405,8 @@
"signOutOtherDevices": "Sign out other devices",
"doNotSignOut": "Do not sign out",
"hearUsWhereTitle": "How did you hear about Ente? (optional)",
"hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!"
}
"hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!",
"waitingForBrowserRequest": "Waiting for browser request...",
"launchPasskeyUrlAgain": "Launch passkey URL again",
"passkey": "Passkey"
}
23 changes: 23 additions & 0 deletions auth/lib/services/auth_feature_flag.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:ente_auth/core/configuration.dart';
import 'package:flutter/foundation.dart';

class FeatureFlagService {
FeatureFlagService._privateConstructor();
static final FeatureFlagService instance =
FeatureFlagService._privateConstructor();

static final _internalUserIDs = const String.fromEnvironment(
"internal_user_ids",
defaultValue: "1,2,3,4,191,125,1580559962388044,1580559962392434,10000025",
).split(",").map((element) {
return int.parse(element);
}).toSet();

bool isInternalUserOrDebugBuild() {
final String? email = Configuration.instance.getEmail();
final userID = Configuration.instance.getUserID();
return (email != null && email.endsWith("@ente.io")) ||
_internalUserIDs.contains(userID) ||
kDebugMode;
}
}
33 changes: 33 additions & 0 deletions auth/lib/services/passkey_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:ente_auth/core/network.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:url_launcher/url_launcher_string.dart';

class PasskeyService {
PasskeyService._privateConstructor();
static final PasskeyService instance = PasskeyService._privateConstructor();

final _enteDio = Network.instance.enteDio;

Future<String> getJwtToken() async {
final response = await _enteDio.get(
"/users/accounts-token",
);
return response.data!["accountsToken"] as String;
}

Future<void> openPasskeyPage(BuildContext context) async {
try {
final jwtToken = await getJwtToken();
final url = "https://accounts.ente.io/account-handoff?token=$jwtToken";
await launchUrlString(
url,
mode: LaunchMode.externalApplication,
);
} catch (e) {
Logger('PasskeyService').severe("failed to open passkey page", e);
showGenericErrorDialog(context: context).ignore();
}
}
}
57 changes: 46 additions & 11 deletions auth/lib/services/user_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import 'package:ente_auth/ui/account/password_reentry_page.dart';
import 'package:ente_auth/ui/account/recovery_page.dart';
import 'package:ente_auth/ui/common/progress_dialog.dart';
import 'package:ente_auth/ui/home_page.dart';
import 'package:ente_auth/ui/passkey_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';
Expand Down Expand Up @@ -264,6 +265,33 @@ class UserService {
}
}

Future<void> onPassKeyVerified(BuildContext context, Map response) async {
final userPassword = Configuration.instance.getVolatilePassword();
if (userPassword == null) throw Exception("volatile password is null");

await _saveConfiguration(response);

Widget page;
if (Configuration.instance.getEncryptedToken() != null) {
await Configuration.instance.decryptSecretsAndGetKeyEncKey(
userPassword,
Configuration.instance.getKeyAttributes()!,
);
page = const HomePage();
} else {
throw Exception("unexpected response during passkey verification");
}

Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return page;
},
),
(route) => route.isFirst,
);
}

Future<void> verifyEmail(
BuildContext context,
String ott, {
Expand Down Expand Up @@ -487,17 +515,17 @@ class UserService {
final clientS = client.calculateSecret(serverB);
final clientM = client.calculateClientEvidenceMessage();
// ignore: unused_local_variable
late Response srpCompleteResponse;
late Response _;
if (setKeysRequest == null) {
srpCompleteResponse = await _enteDio.post(
_ = await _enteDio.post(
"/users/srp/complete",
data: {
'setupID': setupSRPResponse.setupID,
'srpM1': base64Encode(SRP6Util.encodeBigInt(clientM!)),
},
);
} else {
srpCompleteResponse = await _enteDio.post(
_ = await _enteDio.post(
"/users/srp/update",
data: {
'setupID': setupSRPResponse.setupID,
Expand Down Expand Up @@ -581,11 +609,15 @@ class UserService {
},
);
if (response.statusCode == 200) {
Widget page;
Widget? page;
final String passkeySessionID = response.data["passkeySessionID"];
final String twoFASessionID = response.data["twoFactorSessionID"];
Configuration.instance.setVolatilePassword(userPassword);

if (twoFASessionID.isNotEmpty) {
page = TwoFactorAuthenticationPage(twoFASessionID);
} else if (passkeySessionID.isNotEmpty) {
page = PasskeyPage(passkeySessionID);
} else {
await _saveConfiguration(response);
if (Configuration.instance.getEncryptedToken() != null) {
Expand All @@ -603,7 +635,7 @@ class UserService {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return page;
return page!;
},
),
(route) => route.isFirst,
Expand Down Expand Up @@ -861,16 +893,19 @@ class UserService {
}
}

Future<void> _saveConfiguration(Response response) async {
await Configuration.instance.setUserID(response.data["id"]);
if (response.data["encryptedToken"] != null) {
Future<void> _saveConfiguration(dynamic response) async {
final responseData = response is Map ? response : response.data as Map?;
if (responseData == null) return;

await Configuration.instance.setUserID(responseData["id"]);
if (responseData["encryptedToken"] != null) {
await Configuration.instance
.setEncryptedToken(response.data["encryptedToken"]);
.setEncryptedToken(responseData["encryptedToken"]);
await Configuration.instance.setKeyAttributes(
KeyAttributes.fromMap(response.data["keyAttributes"]),
KeyAttributes.fromMap(responseData["keyAttributes"]),
);
} else {
await Configuration.instance.setToken(response.data["token"]);
await Configuration.instance.setToken(responseData["token"]);
}
}

Expand Down
13 changes: 7 additions & 6 deletions auth/lib/store/offline_authenticator_db.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ class OfflineAuthenticatorDB {
static const entityTable = 'entities';

OfflineAuthenticatorDB._privateConstructor();
static final OfflineAuthenticatorDB instance = OfflineAuthenticatorDB._privateConstructor();
static final OfflineAuthenticatorDB instance =
OfflineAuthenticatorDB._privateConstructor();

static Future<Database>? _dbFuture;

Expand All @@ -26,7 +27,7 @@ class OfflineAuthenticatorDB {

Future<Database> _initDatabase() async {
final Directory documentsDirectory =
await getApplicationDocumentsDirectory();
await getApplicationDocumentsDirectory();
final String path = join(documentsDirectory.path, _databaseName);
debugPrint(path);
return await openDatabase(
Expand Down Expand Up @@ -70,10 +71,10 @@ class OfflineAuthenticatorDB {
}

Future<int> updateEntry(
int generatedID,
String encData,
String header,
) async {
int generatedID,
String encData,
String header,
) async {
final db = await instance.database;
final int timeInMicroSeconds = DateTime.now().microsecondsSinceEpoch;
int affectedRows = await db.update(
Expand Down
Loading

0 comments on commit 4744434

Please sign in to comment.