Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[auth][photos] Support for passkey #435

Merged
merged 5 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
4 changes: 2 additions & 2 deletions auth/assets/custom-icons/_data/custom-icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
},
{
"title": "Healthchecks.io",
"slug": "healthchecks",
"slug": "healthchecks"
},
{
"title": "ING"
Expand Down Expand Up @@ -302,7 +302,7 @@
},
{
"title": "Synology DSM",
"slug": "synology_dsm",
"slug": "synology_dsm"
},
{
"title": "TCPShield",
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
Loading