feat: auth passkeys

This commit is contained in:
Prateek Sunal 2024-03-04 13:46:05 +05:30
parent 751db8be35
commit b35d942eac
13 changed files with 334 additions and 95 deletions

View file

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

View file

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

View file

@ -37,6 +37,7 @@
<key>CFBundleURLSchemes</key>
<array>
<string>otpauth</string>
<string>enteauth</string>
</array>
</dict>
</array>

View file

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

View file

@ -59,7 +59,7 @@
}
},
"contactSupport": "Contact support",
"rateUsOnStore" : "Rate us on {storeName}",
"rateUsOnStore": "Rate us on {storeName}",
"blog": "Blog",
"merchandise": "Merchandise",
"verifyPassword": "Verify password",
@ -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!",
@ -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",
@ -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",
@ -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"
}

View file

@ -0,0 +1,33 @@
import 'package:ente_auth/core/network.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 {
try {
final response = await _enteDio.get(
"/users/accounts-token",
);
if (response.data?["accountsToken"] == null) return null;
return response.data["accountsToken"] as String?;
} catch (e) {
return null;
}
}
Future<void> openPasskeyPage() async {
final jwtToken = await getJwtToken();
final url = jwtToken != null
? "https://accounts.ente.io/account-handoff?token=$jwtToken"
: "https://accounts.ente.io/";
await launchUrlString(
url,
mode: LaunchMode.externalApplication,
);
}
}

View file

@ -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';
@ -264,6 +265,38 @@ class UserService {
}
}
Future<void> acceptPasskey(
BuildContext context,
Map response,
Uint8List keyEncryptionKey,
) 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()!,
keyEncryptionKey: keyEncryptionKey,
);
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, {
@ -487,9 +520,9 @@ 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,
@ -497,7 +530,7 @@ class UserService {
},
);
} else {
srpCompleteResponse = await _enteDio.post(
_ = await _enteDio.post(
"/users/srp/update",
data: {
'setupID': setupSRPResponse.setupID,
@ -581,11 +614,18 @@ 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,
keyEncryptionKey: keyEncryptionKey,
);
} else {
await _saveConfiguration(response);
if (Configuration.instance.getEncryptedToken() != null) {
@ -603,7 +643,7 @@ class UserService {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return page;
return page!;
},
),
(route) => route.isFirst,
@ -861,16 +901,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"]);
}
}

View file

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

View file

@ -0,0 +1,124 @@
import 'dart:convert';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:logging/logging.dart';
import 'package:uni_links/uni_links.dart';
import 'package:url_launcher/url_launcher_string.dart';
class PasskeyPage extends StatefulWidget {
final String sessionID;
final Uint8List keyEncryptionKey;
const PasskeyPage(
this.sessionID, {
Key? key,
required this.keyEncryptionKey,
}) : super(key: key);
@override
State<PasskeyPage> createState() => _PasskeyPageState();
}
class _PasskeyPageState extends State<PasskeyPage> {
final Logger _logger = Logger("PasskeyPage");
@override
void initState() {
launchPasskey();
_initDeepLinks();
super.initState();
}
@override
void dispose() {
super.dispose();
}
Future<void> launchPasskey() async {
await launchUrlString(
"https://accounts.ente.io/passkeys/flow?"
"passkeySessionID=${widget.sessionID}"
"&redirect=enteauth://passkey",
mode: LaunchMode.externalApplication,
);
}
Future<void> _handleDeeplink(String? link) async {
if (!context.mounted ||
Configuration.instance.hasConfiguredAccount() ||
link == null) {
return;
}
if (mounted && link.toLowerCase().startsWith("enteauth://passkey")) {
final uri = Uri.parse(link).queryParameters['response'];
// response to json
final res = utf8.decode(base64.decode(uri!));
final json = jsonDecode(res) as Map<String, dynamic>;
await UserService.instance.acceptPasskey(
context,
json,
widget.keyEncryptionKey,
);
}
}
Future<bool> _initDeepLinks() async {
// Attach a listener to the stream
linkStream.listen(
_handleDeeplink,
onError: (err) {
_logger.severe(err);
},
);
return false;
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
title: Text(
l10n.passkeyAuthTitle,
),
),
body: _getBody(),
);
}
Widget _getBody() {
final l10n = context.l10n;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.waitingForBrowserRequest,
style: const TextStyle(
height: 1.4,
fontSize: 16,
),
),
const SizedBox(height: 16),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 32),
child: ElevatedButton(
style: Theme.of(context).colorScheme.optionalActionButtonStyle,
onPressed: launchPasskey,
child: Text(l10n.launchPasskeyUrlAgain),
),
),
],
),
);
}
}

View file

@ -1,5 +1,4 @@
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/local_authentication_service.dart';
import 'package:ente_auth/services/user_service.dart';
@ -7,6 +6,7 @@ import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/account/change_email_dialog.dart';
import 'package:ente_auth/ui/account/delete_account_page.dart';
import 'package:ente_auth/ui/account/password_entry_page.dart';
import 'package:ente_auth/ui/account/recovery_key_page.dart';
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
import 'package:ente_auth/ui/components/menu_item_widget.dart';
@ -14,6 +14,7 @@ import 'package:ente_auth/ui/settings/common_settings.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/navigation_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
class AccountSectionWidget extends StatelessWidget {
AccountSectionWidget({Key? key}) : super(key: key);
@ -86,6 +87,41 @@ class AccountSectionWidget extends StatelessWidget {
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: l10n.recoveryKey,
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
final hasAuthenticated = await LocalAuthenticationService.instance
.requestLocalAuthentication(
context,
l10n.authToViewYourRecoveryKey,
);
if (hasAuthenticated) {
String recoveryKey;
try {
recoveryKey =
Sodium.bin2hex(Configuration.instance.getRecoveryKey());
} catch (e) {
showGenericErrorDialog(context: context);
return;
}
routeToPage(
context,
RecoveryKeyPage(
recoveryKey,
l10n.ok,
showAppBar: true,
onDone: () {},
),
);
}
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: context.l10n.logout,
@ -115,6 +151,7 @@ class AccountSectionWidget extends StatelessWidget {
children: children,
);
}
void _onLogoutTapped(BuildContext context) {
showChoiceActionSheet(
context,

View file

@ -1,4 +1,3 @@
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/components/captioned_text_widget.dart';

View file

@ -5,9 +5,9 @@ import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/user_details.dart';
import 'package:ente_auth/services/local_authentication_service.dart';
import 'package:ente_auth/services/passkey_service.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/account/recovery_key_page.dart';
import 'package:ente_auth/ui/account/request_pwd_verification_page.dart';
import 'package:ente_auth/ui/account/sessions_page.dart';
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
@ -20,7 +20,6 @@ import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/navigation_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
class SecuritySectionWidget extends StatefulWidget {
const SecuritySectionWidget({Key? key}) : super(key: key);
@ -67,38 +66,14 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: l10n.recoveryKey,
title: l10n.passkey,
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
final hasAuthenticated = await LocalAuthenticationService.instance
.requestLocalAuthentication(
context,
l10n.authToViewYourRecoveryKey,
);
if (hasAuthenticated) {
String recoveryKey;
try {
recoveryKey =
Sodium.bin2hex(Configuration.instance.getRecoveryKey());
} catch (e) {
showGenericErrorDialog(context: context);
return;
}
routeToPage(
context,
RecoveryKeyPage(
recoveryKey,
l10n.ok,
showAppBar: true,
onDone: () {},
),
);
}
},
onTap: () => PasskeyService.instance.openPasskeyPage(),
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: l10n.emailVerificationToggle,

View file

@ -197,10 +197,10 @@ packages:
dependency: "direct main"
description:
name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
version: "1.17.2"
version: "1.18.0"
computer:
dependency: "direct main"
description:
@ -262,10 +262,10 @@ packages:
dependency: transitive
description:
name: coverage
sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097"
sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76"
url: "https://pub.dev"
source: hosted
version: "1.6.3"
version: "1.7.2"
cross_file:
dependency: transitive
description:
@ -751,6 +751,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.6.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
url: "https://pub.dev"
source: hosted
version: "10.0.0"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
url: "https://pub.dev"
source: hosted
version: "2.0.1"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
url: "https://pub.dev"
source: hosted
version: "2.0.1"
lints:
dependency: "direct dev"
description:
@ -811,26 +835,26 @@ packages:
dependency: transitive
description:
name: matcher
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16"
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
version: "0.8.0"
meta:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.11.0"
mime:
dependency: transitive
description:
@ -931,10 +955,10 @@ packages:
dependency: transitive
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.8.3"
version: "1.9.0"
path_drawing:
dependency: transitive
description:
@ -1304,10 +1328,10 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
source: hosted
version: "1.11.0"
version: "1.11.1"
step_progress_indicator:
dependency: "direct main"
description:
@ -1320,10 +1344,10 @@ packages:
dependency: transitive
description:
name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
stream_transform:
dependency: transitive
description:
@ -1368,26 +1392,26 @@ packages:
dependency: transitive
description:
name: test
sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46"
sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f
url: "https://pub.dev"
source: hosted
version: "1.24.3"
version: "1.24.9"
test_api:
dependency: transitive
description:
name: test_api
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
version: "0.6.1"
test_core:
dependency: transitive
description:
name: test_core
sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e"
sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a
url: "https://pub.dev"
source: hosted
version: "0.5.3"
version: "0.5.9"
timezone:
dependency: transitive
description:
@ -1560,10 +1584,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
url: "https://pub.dev"
source: hosted
version: "9.4.0"
version: "13.0.0"
watcher:
dependency: transitive
description:
@ -1572,14 +1596,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
web_socket_channel:
dependency: transitive
description:
@ -1637,5 +1653,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.1.0-185.0.dev <4.0.0"
dart: ">=3.2.0-0 <4.0.0"
flutter: ">=3.10.0"