diff --git a/src/common/messages/combinedMessages.prod.ts b/src/common/messages/combinedMessages.prod.ts index fa2b3a9a..83bf24f3 100644 --- a/src/common/messages/combinedMessages.prod.ts +++ b/src/common/messages/combinedMessages.prod.ts @@ -2550,6 +2550,10 @@ export const allMessages: Readonly ${remote})": { def: "Higher (${local} > ${remote})", es: "Superior (${local} > ${remote})", @@ -9443,6 +9447,10 @@ export const allMessages: Readonly ${remote})": "Higher (${local} > ${remote})", "Highlight diff": "Highlight diff", "How to display network errors when the sync server is unreachable.": "How to display network errors when the sync server is unreachable.", @@ -1026,6 +1027,7 @@ "Show history": "Show history", "Show icon only": "Show icon only", "Show only notifications": "Show only notifications", + "Show password": "Show password", "Show status as icons only": "Show status as icons only", "Show status icon instead of file warnings banner": "Show status icon instead of file warnings banner", "Show status inside the editor": "Show status inside the editor", diff --git a/src/common/messagesJson/es.json b/src/common/messagesJson/es.json index 91e1ed16..4853a33f 100644 --- a/src/common/messagesJson/es.json +++ b/src/common/messagesJson/es.json @@ -309,6 +309,7 @@ "Hidden Files": "Archivos ocultos", "Hide completely": "Ocultar por completo", "Hide not applicable items": "Ocultar elementos no aplicables", + "Hide password": "Ocultar contraseña", "Higher (${local} > ${remote})": "Superior (${local} > ${remote})", "Highlight diff": "Resaltar las diferencias", "How to display network errors when the sync server is unreachable.": "Cómo mostrar los errores de red cuando el servidor de sincronización no está disponible.", @@ -1060,6 +1061,7 @@ "Show history": "Mostrar el historial", "Show icon only": "Mostrar solo el icono", "Show only notifications": "Mostrar solo notificaciones", + "Show password": "Mostrar contraseña", "Show status as icons only": "Mostrar estado solo con íconos", "Show status icon instead of file warnings banner": "Mostrar icono de estado en lugar del banner de advertencia de archivos", "Show status inside the editor": "Mostrar estado dentro del editor", diff --git a/src/common/messagesYAML/en.yaml b/src/common/messagesYAML/en.yaml index 7c4cdf95..8c0377bf 100644 --- a/src/common/messagesYAML/en.yaml +++ b/src/common/messagesYAML/en.yaml @@ -409,6 +409,7 @@ Hidden file synchronization have been temporarily disabled. Please enable them a Hidden Files: Hidden Files Hide completely: Hide completely Hide not applicable items: Hide not applicable items +Hide password: Hide password Higher (${local} > ${remote}): Higher (${local} > ${remote}) Highlight diff: Highlight diff How to display network errors when the sync server is unreachable.: How to display network errors when the sync server is unreachable. @@ -1812,6 +1813,7 @@ Show full banner: Show full banner Show history: Show history Show icon only: Show icon only Show only notifications: Show only notifications +Show password: Show password Show status as icons only: Show status as icons only Show status icon instead of file warnings banner: Show status icon instead of file warnings banner Show status inside the editor: Show status inside the editor diff --git a/src/common/messagesYAML/es.yaml b/src/common/messagesYAML/es.yaml index 77e975c5..cdf280a0 100644 --- a/src/common/messagesYAML/es.yaml +++ b/src/common/messagesYAML/es.yaml @@ -432,6 +432,7 @@ Hidden file synchronization have been temporarily disabled. Please enable them a Hidden Files: Archivos ocultos Hide completely: Ocultar por completo Hide not applicable items: Ocultar elementos no aplicables +Hide password: Ocultar contraseña Higher (${local} > ${remote}): Superior (${local} > ${remote}) Highlight diff: Resaltar las diferencias How to display network errors when the sync server is unreachable.: @@ -1921,6 +1922,7 @@ Show full banner: Mostrar banner completo Show history: Mostrar el historial Show icon only: Mostrar solo el icono Show only notifications: Mostrar solo notificaciones +Show password: Mostrar contraseña Show status as icons only: Mostrar estado solo con íconos Show status icon instead of file warnings banner: Mostrar icono de estado en lugar del banner de advertencia de archivos Show status inside the editor: Mostrar estado dentro del editor diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte index efce2429..7340f31d 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte @@ -48,80 +48,94 @@ } - -{translateMessage("Please configure your end-to-end encryption settings.")} - - - - - - {translateMessage( - "Enabling end-to-end encryption ensures that your data is encrypted on your device before being sent to the remote server. This means that even if someone gains access to the server, they won't be able to read your data without the passphrase. Make sure to remember your passphrase, as it will be required to decrypt your data on other devices." - )} -
- {translateMessage( - "Also, please note that if you are using Peer-to-Peer synchronization, this configuration will be used when you switch to other methods and connect to a remote server in the future." - )} -
- - {translateMessage("This setting must be the same even when connecting to multiple synchronisation destinations.")} - - - - - - - {translateMessage( - "Obfuscating properties (e.g., path of file, size, creation and modification dates) adds an additional layer of security by making it harder to identify the structure and names of your files and folders on the remote server. This helps protect your privacy and makes it more difficult for unauthorized users to infer information about your data." - )} - - - - - +
+ + {translateMessage("Please configure your end-to-end encryption settings.")} + + - + {translateMessage( - "In most cases, you should stick with the default algorithm (${algorithm}), This setting is only required if you have an existing Vault encrypted in a different format.", - { algorithm: E2EEAlgorithmNames[DEFAULT_SETTINGS.E2EEAlgorithm] } + "Enabling end-to-end encryption ensures that your data is encrypted on your device before being sent to the remote server. This means that even if someone gains access to the server, they won't be able to read your data without the passphrase. Make sure to remember your passphrase, as it will be required to decrypt your data on other devices." + )} +
+ {translateMessage( + "Also, please note that if you are using Peer-to-Peer synchronization, this configuration will be used when you switch to other methods and connect to a remote server in the future." )}
+ {#if encryptionSettings.encrypt} + + + + + {translateMessage( + "This setting must be the same even when connecting to multiple synchronisation destinations." + )} + + + + + + {translateMessage( + "Obfuscating properties (e.g., path of file, size, creation and modification dates) adds an additional layer of security by making it harder to identify the structure and names of your files and folders on the remote server. This helps protect your privacy and makes it more difficult for unauthorized users to infer information about your data." + )} + + {/if} + + + + + + + {translateMessage( + "In most cases, you should stick with the default algorithm (${algorithm}), This setting is only required if you have an existing Vault encrypted in a different format.", + { algorithm: E2EEAlgorithmNames[DEFAULT_SETTINGS.E2EEAlgorithm] } + )} + + + {translateMessage( + "Changing the encryption algorithm will prevent access to any data previously encrypted with a different algorithm. Ensure that all your devices are configured to use the same algorithm to maintain access to your data." + )} + + + - {translateMessage( - "Changing the encryption algorithm will prevent access to any data previously encrypted with a different algorithm. Ensure that all your devices are configured to use the same algorithm to maintain access to your data." - )} +

+ {translateMessage( + "Please be aware that the End-to-End Encryption passphrase is not validated until the synchronisation process actually commences. This is a security measure designed to protect your data." + )} +

+

+ {translateMessage( + "Therefore, we ask that you exercise extreme caution when configuring server information manually. If an incorrect passphrase is entered, the data on the server will become corrupted." + )}

+ {translateMessage("Please understand that this is intended behaviour.")} +

- - -

- {translateMessage( - "Please be aware that the End-to-End Encryption passphrase is not validated until the synchronisation process actually commences. This is a security measure designed to protect your data." - )} -

-

- {translateMessage( - "Therefore, we ask that you exercise extreme caution when configuring server information manually. If an incorrect passphrase is entered, the data on the server will become corrupted." - )}

- {translateMessage("Please understand that this is intended behaviour.")} -

-
+ + commit()} /> + setResult(TYPE_CANCELLED)} /> + +
- - commit()} /> - setResult(TYPE_CANCELLED)} /> - + diff --git a/src/modules/services/LiveSyncUI/components/Password.svelte b/src/modules/services/LiveSyncUI/components/Password.svelte index 5ebf3884..6642ae0b 100644 --- a/src/modules/services/LiveSyncUI/components/Password.svelte +++ b/src/modules/services/LiveSyncUI/components/Password.svelte @@ -18,6 +18,7 @@ let showPassword = $state(false); const type = $derived.by(() => (showPassword ? "text" : "password")); const translatedPlaceholder = $derived.by(() => translate(placeholder)); + const toggleLabel = $derived.by(() => translate(showPassword ? "Hide password" : "Show password")); - + + + diff --git a/styles.css b/styles.css index 6788414f..462eab07 100644 --- a/styles.css +++ b/styles.css @@ -345,6 +345,12 @@ body { } .sls-onboarding-invitation-action { + display: inline; +} + +/* Touch devices need a larger tap target; on desktop this would just pad the + link with blank space and make it look like a misplaced button. */ +body.is-mobile .sls-onboarding-invitation-action { display: inline-flex; min-width: 44px; min-height: 44px; diff --git a/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts b/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts index 3bd2eaef..f604059c 100644 --- a/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts +++ b/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts @@ -123,19 +123,55 @@ async function enterManualCouchDBSettings(port: number, couchDb: CouchDbConfig, const encryption = modalByTitle(page, "End-to-End Encryption"); await encryption.waitFor({ state: "visible", timeout: uiTimeoutMs }); + assertEqual( + await encryption.locator('input[name="e2ee-passphrase"]').count(), + 0, + "The passphrase field was present before end-to-end encryption was enabled." + ); + assertEqual( + await encryption.locator("label.row").filter({ hasText: "Obfuscate Properties" }).count(), + 0, + "The Obfuscate Properties row was present before end-to-end encryption was enabled." + ); await encryption .locator("label.row") .filter({ hasText: "End-to-End Encryption" }) .locator('input[type="checkbox"]') .first() .check({ timeout: uiTimeoutMs }); + const passphraseInput = encryption.locator('input[name="e2ee-passphrase"]'); + await passphraseInput.waitFor({ state: "visible", timeout: uiTimeoutMs }); await encryption .locator("label.row") .filter({ hasText: "Obfuscate Properties" }) .locator('input[type="checkbox"]') .first() .check({ timeout: uiTimeoutMs }); - await encryption.locator('input[name="e2ee-passphrase"]').fill(randomBytes(24).toString("base64url")); + const passphraseValue = randomBytes(24).toString("base64url"); + await passphraseInput.fill(passphraseValue); + const passwordToggle = encryption.locator("button.sls-password-toggle"); + await passwordToggle.click({ timeout: uiTimeoutMs }); + assertEqual( + await passphraseInput.getAttribute("type"), + "text", + "Toggling visibility did not reveal the passphrase." + ); + assertEqual( + await passphraseInput.inputValue(), + passphraseValue, + "Toggling visibility changed the passphrase value." + ); + await passwordToggle.click({ timeout: uiTimeoutMs }); + assertEqual( + await passphraseInput.getAttribute("type"), + "password", + "Toggling visibility again did not re-mask the passphrase." + ); + assertEqual( + await passphraseInput.inputValue(), + passphraseValue, + "Re-masking the passphrase changed its value." + ); }); screenshots.push(await captureGuideDialogue(port, "guide-couchdb-manual-encryption.png", "End-to-End Encryption")); await withObsidianPage(port, async (page) => { diff --git a/test/e2e-obsidian/scripts/onboarding-invitation.ts b/test/e2e-obsidian/scripts/onboarding-invitation.ts index 16441e2f..92b38543 100644 --- a/test/e2e-obsidian/scripts/onboarding-invitation.ts +++ b/test/e2e-obsidian/scripts/onboarding-invitation.ts @@ -131,7 +131,57 @@ async function captureAndSelectMobileInvitation(): Promise { return screenshot; } -async function captureAndCloseIntro(filename: string, mobile: boolean): Promise { +async function captureMobilePasswordToggle(): Promise { + const port = obsidianRemoteDebuggingPort(); + await withObsidianPage(port, async (page) => { + const intro = onboardingDialogue(page); + await intro + .locator("label") + .filter({ hasText: "I am setting this up for the first time" }) + .locator('input[type="radio"]') + .first() + .check({ timeout: uiTimeoutMs }); + await intro + .getByRole("button", { name: "Yes, I want to set up a new synchronisation" }) + .click({ timeout: uiTimeoutMs }); + + const method = page.locator(".modal-container").filter({ hasText: "Connection Method" }); + await method.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await method + .locator("label") + .filter({ hasText: "Configure a remote manually" }) + .locator('input[type="radio"]') + .first() + .check({ timeout: uiTimeoutMs }); + await method.getByRole("button", { name: "Proceed with manual configuration" }).click({ timeout: uiTimeoutMs }); + + const encryption = page.locator(".modal-container").filter({ hasText: "End-to-End Encryption" }); + await encryption.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await encryption + .locator("label.row") + .filter({ hasText: "End-to-End Encryption" }) + .locator('input[type="checkbox"]') + .first() + .check({ timeout: uiTimeoutMs }); + }); + const screenshot = await captureObsidianDialogue(port, "onboarding-e2ee-mobile.png", async (page) => { + const encryption = page.locator(".modal-container").filter({ hasText: "End-to-End Encryption" }); + const passwordToggle = encryption.locator("button.sls-password-toggle"); + await passwordToggle.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await passwordToggle.evaluate((element) => element.scrollIntoView({ block: "center" })); + await assertLocatorHasMinimumTouchTarget(page, passwordToggle, { + label: "mobile password visibility button", + }); + }); + await withObsidianPage(port, async (page) => { + const encryption = page.locator(".modal-container").filter({ hasText: "End-to-End Encryption" }); + await encryption.getByRole("button", { name: "Cancel", exact: true }).click({ timeout: uiTimeoutMs }); + await encryption.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + return screenshot; +} + +async function captureIntro(filename: string, mobile: boolean, closeAfterCapture = true): Promise { const port = obsidianRemoteDebuggingPort(); const screenshot = await captureObsidianDialogue(port, filename, async (page) => { const container = onboardingDialogue(page); @@ -145,11 +195,13 @@ async function captureAndCloseIntro(filename: string, mobile: boolean): Promise< .waitFor({ state: "visible", timeout: uiTimeoutMs }); if (mobile) await assertMobileDialogueLayout(page, container, "mobile onboarding introduction"); }); - await withObsidianPage(port, async (page) => { - const container = onboardingDialogue(page); - await container.getByRole("button", { name: "No, please take me back" }).click({ timeout: uiTimeoutMs }); - await container.waitFor({ state: "hidden", timeout: uiTimeoutMs }); - }); + if (closeAfterCapture) { + await withObsidianPage(port, async (page) => { + const container = onboardingDialogue(page); + await container.getByRole("button", { name: "No, please take me back" }).click({ timeout: uiTimeoutMs }); + await container.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + } return screenshot; } @@ -231,10 +283,11 @@ async function main(): Promise { const desktopInvitation = await captureDesktopInvitation(); const mobileInvitation = await captureAndSelectMobileInvitation(); - const mobileIntro = await captureAndCloseIntro("onboarding-intro-mobile.png", true); + const mobileIntro = await captureIntro("onboarding-intro-mobile.png", true, false); + const mobileEncryption = await captureMobilePasswordToggle(); await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), false, uiTimeoutMs); await openOnboardingFromSettings(); - const settingsIntro = await captureAndCloseIntro("onboarding-intro-settings-desktop.png", false); + const settingsIntro = await captureIntro("onboarding-intro-settings-desktop.png", false); await dismissVisibleNotices(); await closeSettings(); @@ -243,6 +296,7 @@ async function main(): Promise { desktopInvitation, mobileInvitation, mobileIntro, + mobileEncryption, settingsIntro, ].join(", ")}` );