Skip to content

Commit

Permalink
Merge branch 'main' into discovery-3
Browse files Browse the repository at this point in the history
  • Loading branch information
ashilkn committed Jun 12, 2024
2 parents 7fdf523 + 0666fc0 commit 4ce913c
Show file tree
Hide file tree
Showing 18 changed files with 313 additions and 173 deletions.
52 changes: 31 additions & 21 deletions auth/lib/services/user_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -267,31 +267,41 @@ class UserService {
}

Future<void> onPassKeyVerified(BuildContext context, Map response) async {
final userPassword = Configuration.instance.getVolatilePassword();
if (userPassword == null) throw Exception("volatile password is null");
final ProgressDialog dialog =
createProgressDialog(context, context.l10n.pleaseWait);
await dialog.show();
try {
final userPassword = _config.getVolatilePassword();
if (userPassword == null) throw Exception("volatile password is null");

await _saveConfiguration(response);
await _saveConfiguration(response);

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

// ignore: unawaited_futures
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return page;
},
),
(route) => route.isFirst,
);
page = const HomePage();
} else {
throw Exception("unexpected response during passkey verification");
} catch (e) {
_logger.severe(e);
await dialog.hide();
rethrow;
}

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

Future<void> verifyEmail(
Expand Down
6 changes: 6 additions & 0 deletions auth/lib/ui/passkey_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:url_launcher/url_launcher_string.dart';
Expand Down Expand Up @@ -61,6 +62,11 @@ class _PasskeyPageState extends State<PasskeyPage> {
}
try {
if (mounted && link.toLowerCase().startsWith("enteauth://passkey")) {
if (Configuration.instance.isLoggedIn()) {
_logger.info('ignored deeplink: already configured');
showToast(context, 'Account is already configured.');
return;
}
final String? uri = Uri.parse(link).queryParameters['response'];
String base64String = uri!.toString();
while (base64String.length % 4 != 0) {
Expand Down
1 change: 1 addition & 0 deletions auth/macos/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<key>CFBundleURLSchemes</key>
<array>
<string>otpauth</string>
<string>enteauth</string>
</array>
</dict>
</array>
Expand Down
183 changes: 100 additions & 83 deletions desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,103 @@ export const allowWindowClose = (): void => {
shouldAllowWindowClose = true;
};

/**
* The app's entry point.
*
* We call this at the end of this file.
*/
const main = () => {
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
return;
}

let mainWindow: BrowserWindow | undefined;

initLogging();
logStartupBanner();
registerForEnteLinks();
// The order of the next two calls is important
setupRendererServer();
registerPrivilegedSchemes();
migrateLegacyWatchStoreIfNeeded();

/**
* Handle an open URL request, but ensuring that we have a mainWindow.
*/
const handleOpenURLEnsuringWindow = (url: string) => {
log.info(`Attempting to handle request to open URL: ${url}`);
if (mainWindow) handleEnteLinks(mainWindow, url);
else setTimeout(() => handleOpenURLEnsuringWindow(url), 1000);
};

app.on("second-instance", (_, argv: string[]) => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
mainWindow.show();
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
// On Windows and Linux, this is how we get deeplinks.
// See: registerForEnteLinks
const url = argv.pop();
if (url) handleOpenURLEnsuringWindow(url);
});

// Emitted once, when Electron has finished initializing.
//
// Note that some Electron APIs can only be used after this event occurs.
void app.whenReady().then(() => {
void (async () => {
// Create window and prepare for the renderer.
mainWindow = createMainWindow();

// Setup IPC and streams.
const watcher = createWatcher(mainWindow);
attachIPCHandlers();
attachFSWatchIPCHandlers(watcher);
attachLogoutIPCHandler(watcher);
registerStreamProtocol();

// Configure the renderer's environment.
const webContents = mainWindow.webContents;
setDownloadPath(webContents);
allowExternalLinks(webContents);
allowAllCORSOrigins(webContents);

// Start loading the renderer.
void mainWindow.loadURL(rendererURL);

// Continue on with the rest of the startup sequence.
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
setupTrayItem(mainWindow);
setupAutoUpdater(mainWindow);

try {
await deleteLegacyDiskCacheDirIfExists();
await deleteLegacyKeysStoreIfExists();
} catch (e) {
// Log but otherwise ignore errors during non-critical startup
// actions.
log.error("Ignoring startup error", e);
}
})();
});

// This is a macOS only event. Show our window when the user activates the
// app, e.g. by clicking on its dock icon.
app.on("activate", () => mainWindow?.show());

app.on("before-quit", () => {
if (mainWindow) saveWindowBounds(mainWindow);
allowWindowClose();
});

// On macOS, this is how we get deeplinks. See: registerForEnteLinks
app.on("open-url", (_, url) => handleOpenURLEnsuringWindow(url));
};

/**
* Log a standard startup banner.
*
Expand Down Expand Up @@ -145,6 +242,8 @@ const registerPrivilegedSchemes = () => {
* Implementation notes:
* - https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app
* - This works only when the app is packaged.
* - On Windows and Linux, we get the deeplink in the "second-instance" event.
* - On macOS, we get the deeplink in the "open-url" event.
*/
const registerForEnteLinks = () => app.setAsDefaultProtocolClient("ente");

Expand Down Expand Up @@ -464,87 +563,5 @@ const deleteLegacyKeysStoreIfExists = async () => {
}
};

const main = () => {
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
return;
}

let mainWindow: BrowserWindow | undefined;

initLogging();
logStartupBanner();
registerForEnteLinks();
// The order of the next two calls is important
setupRendererServer();
registerPrivilegedSchemes();
migrateLegacyWatchStoreIfNeeded();

app.on("second-instance", () => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
mainWindow.show();
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});

// Emitted once, when Electron has finished initializing.
//
// Note that some Electron APIs can only be used after this event occurs.
void app.whenReady().then(() => {
void (async () => {
// Create window and prepare for the renderer.
mainWindow = createMainWindow();

// Setup IPC and streams.
const watcher = createWatcher(mainWindow);
attachIPCHandlers();
attachFSWatchIPCHandlers(watcher);
attachLogoutIPCHandler(watcher);
registerStreamProtocol();

// Configure the renderer's environment.
const webContents = mainWindow.webContents;
setDownloadPath(webContents);
allowExternalLinks(webContents);
allowAllCORSOrigins(webContents);

// Start loading the renderer.
void mainWindow.loadURL(rendererURL);

// Continue on with the rest of the startup sequence.
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
setupTrayItem(mainWindow);
setupAutoUpdater(mainWindow);

try {
await deleteLegacyDiskCacheDirIfExists();
await deleteLegacyKeysStoreIfExists();
} catch (e) {
// Log but otherwise ignore errors during non-critical startup
// actions.
log.error("Ignoring startup error", e);
}
})();
});

// This is a macOS only event. Show our window when the user activates the
// app, e.g. by clicking on its dock icon.
app.on("activate", () => mainWindow?.show());

app.on("before-quit", () => {
if (mainWindow) saveWindowBounds(mainWindow);
allowWindowClose();
});

const handleOpenURLWithWindow = (url: string) => {
log.info(`Attempting to handle request to open URL: ${url}`);
if (mainWindow) handleEnteLinks(mainWindow, url);
else setTimeout(() => handleOpenURLWithWindow(url), 1000);
};
app.on("open-url", (_, url) => handleOpenURLWithWindow(url));
};

// Go for it.
main();
3 changes: 2 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ yarn dev
For an editor, VSCode is a good choice. Also install the Prettier extension for
VSCode, and set VSCode to format on save. This way the editor will automatically
format and wrap the text using the project's standard, so you can just focus on
the content.
the content. You can also format without VSCode by using the `yarn pretty`
command.

## Have fun!

Expand Down
4 changes: 4 additions & 0 deletions docs/docs/.vitepress/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export const sidebar = [
link: "/photos/features/location-tags",
},
{ text: "Map", link: "/photos/features/map" },
{
text: "Passkeys",
link: "/photos/features/passkeys",
},
{
text: "Public link",
link: "/photos/features/public-link",
Expand Down
3 changes: 3 additions & 0 deletions docs/docs/photos/faq/security-and-privacy.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,6 @@ your own instead of contacting support to ask them to delete your account.
Note that both Ente photos and Ente auth data will be deleted when you delete
your account (irrespective of which app you delete it from) since both photos
and auth use the same underlying account.

To know details of how your data is deleted, including when you delete your
account, please see https://ente.io/blog/how-ente-deletes-data/.
62 changes: 62 additions & 0 deletions docs/docs/photos/features/passkeys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: Passkeys
description: Using passkeys as a second factor for your Ente account
---

# Passkeys

> [!CAUTION]
>
> This is preview documentation for an upcoming feature. This feature has not
> yet been released yet, so the steps below will not work currently.
Passkeys are a new authentication mechanism that uses strong cryptography built
into devices, like Windows Hello or Apple's Touch ID. **You can use passkeys as
a second factor to secure your Ente account.**

> [!TIP]
>
> Passkeys are the colloquial term for a WebAuthn (Web Authentication)
> credentials. To know more technical details about how our passkey verification
> works, you can see this
> [technical note in our source code](https://github.com/ente-io/ente/blob/main/web/docs/webauthn-passkeys.md).
## Passkeys and TOTP

Ente already supports TOTP codes (in fact, we built an
[entire app](https://ente.io/auth/) to store them...). Passkeys serve as an
alternative 2FA (second factor) mechanism.

If you add a passkey to your Ente account, it will be used instead of any
existing 2FA codes that you have configured (if any).

## Enabling and disabling passkeys

Passkeys get enabled if you add one (or more) passkeys to your account.
Conversely, passkeys get disabled if you remove all your existing passkeys.

To add and remove passkeys, use the _Passkey_ option in the settings menu. This
will open up _accounts.ente.io_, where you can manage your passkeys.

## Login with passkeys

If passkeys are enabled, then _accounts.ente.io_ will automatically open when
you log into your Ente account on a new device. Here you can follow the
instructions given by the browser to verify your passkey.

> These instructions different for each browser and device, but generally they
> will ask you to use the same mechanism that you used when you created the
> passkey to verify it (scanning a QR code, using your fingerprint, pressing the
> key on your Yubikey or other security key hardware etc).
## Recovery

If you are unable to login with your passkey (e.g. if you have misplaced the
hardware key that you used to store your passkey), then you can **recover your
account by using your Ente recovery key**.

During login, press cancel on the browser dialog to verify your passkey, and
then select the "Recover two-factor" option in the error message that gets
shown. This will take you to a place where you can enter your Ente recovery key
and login into your account. Now you can go to the _Passkey_ page to delete the
lost passkey and/or add a new one.
Loading

0 comments on commit 4ce913c

Please sign in to comment.