${alias}
${nonNullish(dupCount) && dupCount > 0
diff --git a/src/frontend/src/flows/manage/deviceSettings.ts b/src/frontend/src/flows/manage/deviceSettings.ts
index 0d70757839..ebd604dbe1 100644
--- a/src/frontend/src/flows/manage/deviceSettings.ts
+++ b/src/frontend/src/flows/manage/deviceSettings.ts
@@ -216,9 +216,11 @@ export const resetPhrase = async ({
if ("ok" in res) {
// If the user was authenticated with the phrase, then replace the connection
// to use the new phrase to void logging them out
- const nextConnection = sameDevice
- ? await connection.fromIdentity(userNumber, res.ok)
- : undefined;
+ let nextConnection = undefined;
+ if (sameDevice) {
+ nextConnection = (await connection.fromIdentity(userNumber, res.ok))
+ .connection;
+ }
return reload(nextConnection);
} else if ("error" in res) {
await displayError({
diff --git a/src/frontend/src/flows/manage/index.ts b/src/frontend/src/flows/manage/index.ts
index a2f913b6cc..c7439488a3 100644
--- a/src/frontend/src/flows/manage/index.ts
+++ b/src/frontend/src/flows/manage/index.ts
@@ -2,7 +2,6 @@ import {
DeviceData,
IdentityAnchorInfo,
} from "$generated/internet_identity_types";
-import { showWarning } from "$src/banner";
import {
AuthnTemplates,
authenticateBox,
@@ -22,18 +21,23 @@ import { addDevice } from "$src/flows/addDevice/manage/addDevice";
import { dappsExplorer } from "$src/flows/dappsExplorer";
import { KnownDapp, getDapps } from "$src/flows/dappsExplorer/dapps";
import { dappsHeader, dappsTeaser } from "$src/flows/dappsExplorer/teaser";
+import {
+ TempKeyWarningAction,
+ tempKeyWarningBox,
+ tempKeysSection,
+} from "$src/flows/manage/tempKeys";
import { addPhrase, recoveryWizard } from "$src/flows/recovery/recoveryWizard";
import { setupKey, setupPhrase } from "$src/flows/recovery/setupRecovery";
import { I18n } from "$src/i18n";
import { AuthenticatedConnection, Connection } from "$src/utils/iiConnection";
import { TemplateElement, renderPage } from "$src/utils/lit-html";
import {
- hasRecoveryPhrase,
isProtected,
isRecoveryDevice,
isRecoveryPhrase,
} from "$src/utils/recoveryDevice";
import { OmitParams, shuffleArray, unreachable } from "$src/utils/utils";
+import { Principal } from "@dfinity/principal";
import { isNullish, nonNullish } from "@dfinity/utils";
import { TemplateResult, html } from "lit-html";
import { authenticatorsSection } from "./authenticatorsSection";
@@ -142,13 +146,14 @@ const displayFailedToListDevices = (error: Error) =>
// recovery devices.
const displayManageTemplate = ({
userNumber,
- devices: { authenticators, recoveries },
+ devices: { authenticators, recoveries, pinAuthenticators },
onAddDevice,
addRecoveryPhrase,
addRecoveryKey,
dapps,
exploreDapps,
identityBackground,
+ tempKeysWarning,
}: {
userNumber: bigint;
devices: Devices;
@@ -158,18 +163,20 @@ const displayManageTemplate = ({
dapps: KnownDapp[];
exploreDapps: () => void;
identityBackground: IdentityBackground;
+ tempKeysWarning?: TempKeyWarningAction;
}): TemplateResult => {
- // Nudge the user to add a device iff there is one or fewer authenticators and no recoveries
- const warnFewDevices =
- authenticators.length <= 1 &&
- isNullish(recoveries.recoveryPhrase) &&
- isNullish(recoveries.recoveryKey);
+ // Nudge the user to add a passkey if there is none
+ const warnNoPasskeys = authenticators.length === 0;
+ const i18n = new I18n();
const pageContentSlot = html`
Manage your
Internet Identity
${anchorSection({ userNumber, identityBackground })}
+ ${nonNullish(tempKeysWarning)
+ ? tempKeyWarningBox({ i18n, warningAction: tempKeysWarning })
+ : ""}
${dappsTeaser({
dapps,
@@ -180,17 +187,19 @@ const displayManageTemplate = ({
},
})}
+ ${pinAuthenticators.length > 0
+ ? tempKeysSection({ authenticators: pinAuthenticators, i18n })
+ : ""}
${authenticatorsSection({
authenticators,
onAddDevice,
- warnFewDevices,
+ warnNoPasskeys,
})}
${recoveryMethodsSection({ recoveries, addRecoveryPhrase, addRecoveryKey })}
${logoutSection()}
`;
return mainWindow({
- isWideContainer: true,
slot: pageContentSlot,
});
};
@@ -267,6 +276,22 @@ export const renderManage = async ({
export const displayManagePage = renderPage(displayManageTemplate);
+function isPinAuthenticated(
+ devices_: DeviceData[],
+ connection: AuthenticatedConnection
+): boolean {
+ const connectionPrincipal = connection.identity.getPrincipal();
+ const currentDevice = devices_.find(({ pubkey }) => {
+ const devicePrincipal = Principal.selfAuthenticating(
+ new Uint8Array(pubkey)
+ );
+ return devicePrincipal.toText() === connectionPrincipal.toText();
+ });
+ return (
+ nonNullish(currentDevice) && "browser_storage_key" in currentDevice.key_type
+ );
+}
+
export const displayManage = (
userNumber: bigint,
connection: AuthenticatedConnection,
@@ -293,24 +318,53 @@ export const displayManage = (
"More than one recovery keys are registered, which is unexpected. Only one will be shown."
);
}
+
+ const onAddDevice = async () => {
+ await addDevice({ userNumber, connection });
+ resolve();
+ };
+ const addRecoveryPhrase = async () => {
+ const doAdd = await addPhrase({ intent: "userInitiated" });
+ if (doAdd === "cancel") {
+ resolve();
+ return;
+ }
+ doAdd satisfies "ok";
+ await setupPhrase(userNumber, connection);
+ resolve();
+ };
+
+ // Function to figure out what temp keys warning should be shown, if any.
+ const determineTempKeysWarning = (): TempKeyWarningAction | undefined => {
+ if (!isPinAuthenticated(devices_, connection)) {
+ // Don't show the warning, if the user is not authenticated using a PIN
+ // protected browser storage key
+ return undefined;
+ }
+ // First priority, nudge to add a recovery phrase
+ if (devices.recoveries.recoveryPhrase === undefined) {
+ return {
+ tag: "add_recovery",
+ action: addRecoveryPhrase,
+ };
+ }
+ // Second priority, nudge to add a passkey
+ if (devices.authenticators.length === 0) {
+ return {
+ tag: "add_passkey",
+ action: onAddDevice,
+ };
+ }
+ // If both, recovery phrase and passkey are present, don't show a warning
+ return undefined;
+ };
+
const display = () =>
displayManagePage({
userNumber,
devices,
- onAddDevice: async () => {
- await addDevice({ userNumber, connection });
- resolve();
- },
- addRecoveryPhrase: async () => {
- const doAdd = await addPhrase({ intent: "userInitiated" });
- if (doAdd === "cancel") {
- resolve();
- return;
- }
- doAdd satisfies "ok";
- await setupPhrase(userNumber, connection);
- resolve();
- },
+ onAddDevice,
+ addRecoveryPhrase,
addRecoveryKey: async () => {
const confirmed = confirm(
"Add a Recovery Device\n\nUse a FIDO Security Key, like a YubiKey, as an additional recovery method."
@@ -331,30 +385,10 @@ export const displayManage = (
display();
},
identityBackground,
+ tempKeysWarning: determineTempKeysWarning(),
});
display();
-
- // When visiting the legacy URL (ic0.app) we extra-nudge the users to create a recovery phrase,
- // if they don't have one already. We lead them straight to recovery phrase creation, because
- // recovery _device_ would be tied to the domain (which we want to avoid).
- if (
- window.location.origin === LEGACY_II_URL &&
- !hasRecoveryPhrase(devices_)
- ) {
- const elem = showWarning(html`
Important!
- Create a recovery phrase.
-
`);
- }
});
};
@@ -446,17 +480,29 @@ export const devicesFromDeviceDatas = ({
return acc;
}
- acc.authenticators.push({
+ const authenticator = {
alias: device.alias,
warn: domainWarning(device),
rename: () => renameDevice({ connection, device, reload }),
remove: hasSingleDevice
? undefined
: () => deleteDevice({ connection, device, reload }),
- });
+ };
+
+ if ("browser_storage_key" in device.key_type) {
+ acc.pinAuthenticators.push(authenticator);
+ } else {
+ acc.authenticators.push(authenticator);
+ }
return acc;
},
- { authenticators: [], recoveries: {}, dupPhrase: false, dupKey: false }
+ {
+ authenticators: [],
+ recoveries: {},
+ pinAuthenticators: [],
+ dupPhrase: false,
+ dupKey: false,
+ }
);
};
diff --git a/src/frontend/src/flows/manage/recoveryMethodsSection.json b/src/frontend/src/flows/manage/recoveryMethodsSection.json
index cdf5cce094..89f2502b27 100644
--- a/src/frontend/src/flows/manage/recoveryMethodsSection.json
+++ b/src/frontend/src/flows/manage/recoveryMethodsSection.json
@@ -1,6 +1,8 @@
{
"en": {
+ "security_warning": "Security Warning",
"recovery_phrase_enabled": "You enabled a recovery phrase.",
- "recovery_key_enabled": "You enabled a recovery device."
+ "recovery_key_enabled": "You enabled a recovery device.",
+ "enable_recovery_to_make_secure": "Enable recovery methods to make your Internet Identity more secure."
}
}
diff --git a/src/frontend/src/flows/manage/recoveryMethodsSection.ts b/src/frontend/src/flows/manage/recoveryMethodsSection.ts
index 68753fa00b..81938b2ecb 100644
--- a/src/frontend/src/flows/manage/recoveryMethodsSection.ts
+++ b/src/frontend/src/flows/manage/recoveryMethodsSection.ts
@@ -26,19 +26,45 @@ export const recoveryMethodsSection = ({
addRecoveryPhrase: () => void;
addRecoveryKey: () => void;
}): TemplateResult => {
+ const i18n = new I18n();
+
+ const copy = i18n.i18n(copyJson);
+ const warnNoRecovery = isNullish(recoveryPhrase) && isNullish(recoveryKey);
+ const wrapClasses = [
+ "l-stack",
+ "c-card",
+ "c-card--narrow",
+ ...(warnNoRecovery ? ["c-card--warning"] : []),
+ ];
+
return html`
-