From 5ded730ed7e3d5ef29fcffe83038b44387e11007 Mon Sep 17 00:00:00 2001 From: Maxime GRANDCOLAS Date: Wed, 16 Oct 2024 10:34:48 +0200 Subject: [PATCH 1/6] [MS] Updated recovery device and migrated tests to pw --- client/src/locales/en-US.json | 34 ++-- client/src/locales/fr-FR.json | 34 ++-- client/src/parsec/device.ts | 172 +++++------------- client/src/parsec/types.ts | 5 + client/src/views/devices/DevicesPage.vue | 36 ++-- .../devices/ExportRecoveryDevicePage.vue | 59 +++--- .../devices/ImportRecoveryDevicePage.vue | 140 +++++++------- client/src/views/home/HomePage.vue | 27 ++- client/src/views/home/LoginPage.vue | 1 - .../views/home/UserJoinOrganizationModal.vue | 2 +- .../specs/test_export_recovery_device_page.ts | 56 ------ .../specs/test_import_recovery_device_page.ts | 78 -------- .../pw/data/recovery/fake_recovery_file.txt | 1 + .../tests/pw/data/recovery/recovery_file.psrk | 1 + .../pw/e2e/export_recovery_device.spec.ts | 47 +++++ .../pw/e2e/import_recovery_device.spec.ts | 126 +++++++++++++ client/tests/pw/e2e/my_profile_page.spec.ts | 13 +- 17 files changed, 417 insertions(+), 415 deletions(-) delete mode 100644 client/tests/e2e/specs/test_export_recovery_device_page.ts delete mode 100644 client/tests/e2e/specs/test_import_recovery_device_page.ts create mode 100644 client/tests/pw/data/recovery/fake_recovery_file.txt create mode 100644 client/tests/pw/data/recovery/recovery_file.psrk create mode 100644 client/tests/pw/e2e/export_recovery_device.spec.ts create mode 100644 client/tests/pw/e2e/import_recovery_device.spec.ts diff --git a/client/src/locales/en-US.json b/client/src/locales/en-US.json index e0257629b66..51b5116468b 100644 --- a/client/src/locales/en-US.json +++ b/client/src/locales/en-US.json @@ -173,7 +173,13 @@ "archiveFailure": "Failed to archive the device." }, "organizationActionSheet": "Organization", - "keyringFailed": "Could not log in", + "loginErrors": { + "keyringFailed": "Could not log in", + "keyringFailedTitle": "Login failed", + "keyringFailedQuestion": "Could not login to this device using the system authentication. Would you like to use a recovery device?", + "keyringFailedUsedRecovery": "Use a recovery device", + "keyringFailedAbort": "No" + }, "bmsOrganizationNotFound": "Could not find the organization {organization} on this device.", "noDevices": { "titleCreateOrga": "New to Parsec?", @@ -702,16 +708,16 @@ "joinedOn": "Joined:", "now": "Now", "restorePassword": { - "title": "Password recovery files", + "title": "Authentication recovery files", "done": { - "subtitle": "Password recovery files have been created and downloaded.", + "subtitle": "Authentication recovery files have been created and downloaded.", "label": "Files already downloaded", "button": "Re-download recovery files" }, "notDone": { "label": "Action required", "subtitle": "Parsec does not store your password.", - "subtitle2": "These files can be used to reset your password on a device if you forget it.", + "subtitle2": "These files can be used to reset your authentication on a device.", "button": "Create recovery files" } }, @@ -807,20 +813,20 @@ "forgottenPassword": "Forgotten password", "recoveryFile": "Recovery file", "recoveryKey": "Secret key", - "setNewPassword": "Choose a new password", - "passwordChanged": "Password was successfully changed!" + "setNewPassword": "Choose a new authentication", + "passwordChanged": "Authentication was successfully updated!" }, "subtitles": { - "recoveryFilesMustExistWarning": "You must have created recovery files in order to reset your password.", + "recoveryFilesMustExistWarning": "You must have created recovery files in order to reset your authentication.", "password": "Password", - "passwordModified": "You can now login with your new password.", + "passwordModified": "You can now login with your new authentication.", "noFileSelected": "No file selected" }, "actions": { "browse": "Browse", "next": "Next", - "validatePassword": "Validate password", - "goBackToLogin": "Go back to login" + "validateAuth": "Confirm", + "login": "Login" }, "errors": { "fileErrorMessage": "Invalid recovery file.", @@ -828,7 +834,8 @@ "passwordErrorMessage": "Passwords do not match or are not strong enough.", "saveDeviceErrorMessage": "An error has occurred, the device recovery failed.", "internalErrorMessage": "An error has occurred" - } + }, + "secretKeyPlaceholder": "FH3H-N3DW-ABED-A6Q7-..." }, "ExportRecoveryDevicePage": { "titles": { @@ -854,8 +861,11 @@ "keyDownloadOk": "The secret key was successfully downloaded" }, "filenames": { - "recoveryFile": "Parsec_Recovery_File_{org}.data", + "recoveryFile": "Parsec_Recovery_File_{org}.psrk", "recoveryKey": "Parsec_Recovery_Code_{org}.txt" + }, + "errors": { + "exportFailed": "Failed to create a recovery device." } }, "SasCodeChoice": { diff --git a/client/src/locales/fr-FR.json b/client/src/locales/fr-FR.json index ddea86606bf..1b4d7973150 100644 --- a/client/src/locales/fr-FR.json +++ b/client/src/locales/fr-FR.json @@ -173,7 +173,13 @@ "archiveFailure": "Impossible d'archiver l'appareil." }, "organizationActionSheet": "Organisation", - "keyringFailed": "Impossible de se connecter", + "loginErrors": { + "keyringFailed": "Impossible de se connecter", + "keyringFailedTitle": "Impossible de se connecter", + "keyringFailedQuestion": "La connexion à cet appareil en utilisant l'authentification du système a échoué. Souhaitez-vous utiliser un appareil de récupération ?", + "keyringFailedUsedRecovery": "Utiliser un appareil de récupération", + "keyringFailedAbort": "Non" + }, "bmsOrganizationNotFound": "Impossible de trouver l'organisation {organization} sur cet appareil.", "noDevices": { "titleCreateOrga": "Vous débutez sur Parsec ?", @@ -702,16 +708,16 @@ "joinedOn": "Rejoint :", "now": "À l'instant", "restorePassword": { - "title": "Fichiers de récupération de mot de passe", + "title": "Fichiers de récupération d'authentification'", "done": { - "subtitle": "Vous avez créé et sauvegardé les fichiers de récupération de mot de passe.", + "subtitle": "Vous avez créé et sauvegardé les fichiers de récupération d'authentification'.", "label": "Fichiers déjà téléchargés", "button": "Re-télécharger les fichiers" }, "notDone": { "label": "Action requise", "subtitle": "Parsec ne garde pas votre mot de passe.", - "subtitle2": "Ces fichiers vous permettent de réinitialiser votre mot de passe sur un appareil en cas d'oubli.", + "subtitle2": "Ces fichiers vous permettent de réinitialiser votre authentification sur un appareil en cas d'oubli.", "button": "Créer des fichiers de récupération" } }, @@ -807,20 +813,20 @@ "forgottenPassword": "Mot de passe oublié", "recoveryFile": "Fichier de récupération", "recoveryKey": "Clé secrète", - "setNewPassword": "Définissez un nouveau mot de passe", - "passwordChanged": "Le mot de passe a bien été modifié" + "setNewPassword": "Choisissez votre méthode d'authentification", + "passwordChanged": "L'authentification a bien été modifiée" }, "subtitles": { - "recoveryFilesMustExistWarning": "Vous devez avoir créé des fichiers de récupération pour réinitialiser votre mot de passe.", + "recoveryFilesMustExistWarning": "Vous devez avoir créé des fichiers de récupération pour réinitialiser votre authentification.", "password": "Mot de passe", - "passwordModified": "Vous pouvez vous connecter avec votre nouveau mot de passe.", + "passwordModified": "Vous pouvez vous connecter avec votre nouvelle authentification.", "noFileSelected": "Aucun fichier sélectionné" }, "actions": { "browse": "Parcourir", "next": "Suivant", - "validatePassword": "Valider mon mot de passe", - "goBackToLogin": "Revenir à la connexion" + "validateAuth": "Confirmer", + "login": "Se connecter" }, "errors": { "fileErrorMessage": "Le fichier de récupération est invalide.", @@ -828,7 +834,8 @@ "passwordErrorMessage": "Les mots de passe ne correspondent pas ou ne sont pas assez forts.", "saveDeviceErrorMessage": "Une erreur est survenue, la récupération d'appareil a échoué.", "internalErrorMessage": "Une erreur est survenue" - } + }, + "secretKeyPlaceholder": "FH3H-N3DW-ABED-A6Q7-.." }, "ExportRecoveryDevicePage": { "titles": { @@ -854,8 +861,11 @@ "keyDownloadOk": "La clé secrète a bien été téléchargée" }, "filenames": { - "recoveryFile": "Parsec_Fichier_Récupération_{org}.data", + "recoveryFile": "Parsec_Fichier_Récupération_{org}.psrk", "recoveryKey": "Parsec_Code_Récupération_{org}.txt" + }, + "errors": { + "exportFailed": "Impossible de créer un appareil de récupération." } }, "SasCodeChoice": { diff --git a/client/src/parsec/device.ts b/client/src/parsec/device.ts index 3a2b58405f1..8a4daa7ccd6 100644 --- a/client/src/parsec/device.ts +++ b/client/src/parsec/device.ts @@ -1,14 +1,19 @@ // Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS import { needsMocks } from '@/parsec/environment'; +import { getClientConfig } from '@/parsec/internals'; import { getClientInfo } from '@/parsec/login'; import { getParsecHandle } from '@/parsec/routing'; import { + ClientExportRecoveryDeviceError, ClientListUserDevicesError, ClientListUserDevicesErrorTag, DeviceFileType, DeviceInfo, DevicePurpose, + DeviceSaveStrategy, + ImportRecoveryDeviceError, + ImportRecoveryDeviceErrorTag, OwnDeviceInfo, Result, UserID, @@ -18,113 +23,52 @@ import { DateTime } from 'luxon'; const RECOVERY_DEVICE_PREFIX = 'recovery'; -export type SecretKey = string; - -export interface RecoveryDeviceData { - code: string; - file: string; -} - -export enum RecoveryDeviceErrorTag { - Internal = 'internal', - Invalid = 'invalid', -} - -export enum RecoveryImportErrorTag { - Internal = 'internal', - KeyError = 'keyError', - RecoveryFileError = 'recoveryFileError', -} - -export interface RecoveryDeviceError { - tag: RecoveryDeviceErrorTag.Internal; -} - -export interface WrongAuthenticationError { - tag: RecoveryDeviceErrorTag.Invalid; -} - -export interface RecoveryImportInternalError { - tag: RecoveryImportErrorTag.Internal; -} - -export interface RecoveryImportFileError { - tag: RecoveryImportErrorTag.RecoveryFileError; -} - -export interface RecoveryImportKeyError { - tag: RecoveryImportErrorTag.KeyError; +function generateRecoveryDeviceLabel(): string { + return `${RECOVERY_DEVICE_PREFIX}_${DateTime.utc().toMillis()}`; } -export type RecoveryImportError = RecoveryImportInternalError | RecoveryImportFileError | RecoveryImportKeyError; - -export async function exportRecoveryDevice(_password: string): Promise> { +export async function exportRecoveryDevice(): Promise> { const handle = getParsecHandle(); - if (_password !== 'P@ssw0rd.') { - return { ok: false, error: { tag: RecoveryDeviceErrorTag.Invalid } }; - } - if (handle !== null && !needsMocks()) { - return { - ok: true, - value: { - code: 'ABCDEF', - file: 'Q2lnYXJlQU1vdXN0YWNoZQ==', - }, - }; + return await libparsec.clientExportRecoveryDevice(handle, generateRecoveryDeviceLabel()); } else { return { ok: true, - value: { - code: 'ABCDEF', - file: 'Q2lnYXJlQU1vdXN0YWNoZQ==', - }, + value: ['ABCDEF', new Uint8Array([0x6d, 0x65, 0x6f, 0x77])], }; } } +function areArraysEqual(a: Uint8Array, b: Uint8Array): boolean { + return a.every((val, index) => { + return val === b[index]; + }); +} + export async function importRecoveryDevice( deviceLabel: string, - file: File, - _passphrase: string, -): Promise> { - const handle = getParsecHandle(); + recoveryData: Uint8Array, + passphrase: string, + saveStrategy: DeviceSaveStrategy, +): Promise> { + if (!needsMocks()) { + const result = await libparsec.importRecoveryDevice(getClientConfig(), recoveryData, passphrase, deviceLabel, saveStrategy); + if (result.ok) { + result.value.createdOn = DateTime.fromSeconds(result.value.createdOn as any as number); + result.value.protectedOn = DateTime.fromSeconds(result.value.protectedOn as any as number); + } + return result; + } // cspell:disable-next-line - if (_passphrase !== 'ABCD-EFGH-IJKL-MNOP-QRST-UVWX-YZ12-3456-7890-ABCD-EFGH-IJKL-MNOP') { - return { ok: false, error: { tag: RecoveryImportErrorTag.KeyError } }; + if (passphrase !== 'ABCD-EFGH-IJKL-MNOP-QRST-UVWX-YZ12-3456-7890-ABCD-EFGH-IJKL-MNOP') { + return { ok: false, error: { tag: ImportRecoveryDeviceErrorTag.InvalidPassphrase, error: 'Wrong passphrase' } }; } - if (!file.name.endsWith('.psrk')) { - return { ok: false, error: { tag: RecoveryImportErrorTag.RecoveryFileError } }; + if (areArraysEqual(recoveryData, new Uint8Array([78, 79, 80, 10]))) { + return { ok: false, error: { tag: ImportRecoveryDeviceErrorTag.InvalidData, error: 'Wrong data' } }; } - if (handle !== null && !needsMocks()) { - return { - ok: true, - value: { - id: 'fake_id', - deviceLabel: deviceLabel, - purpose: DevicePurpose.Standard, - createdOn: DateTime.now(), - createdBy: null, - }, - }; - } else { - return { - ok: true, - value: { - id: 'fake_id', - deviceLabel: deviceLabel, - purpose: DevicePurpose.Standard, - createdOn: DateTime.now(), - createdBy: null, - }, - }; - } -} -export async function saveDevice(deviceInfo: DeviceInfo, _password: string): Promise> { - // const _saveStrategy: DeviceSaveStrategyPassword = { tag: DeviceSaveStrategyTag.Password, password: password }; return { ok: true, value: { @@ -134,29 +78,17 @@ export async function saveDevice(deviceInfo: DeviceInfo, _password: string): Pro protectedOn: DateTime.utc(), organizationId: 'dummy_org', userId: 'dummy_user_id', - deviceId: deviceInfo.id, + deviceId: 'device_id', humanHandle: { email: 'dummy_email@email.dum', label: 'dummy_label', }, - deviceLabel: deviceInfo.deviceLabel, + deviceLabel: deviceLabel, ty: DeviceFileType.Password, }, }; } -export async function deleteDevice(_device: AvailableDevice): Promise> { - return { ok: true, value: true }; -} - -export async function hasRecoveryDevice(): Promise { - const result = await listOwnDevices(); - if (!result.ok) { - return false; - } - return result.value.some((deviceInfo: OwnDeviceInfo) => deviceInfo.id.startsWith(RECOVERY_DEVICE_PREFIX)); -} - export async function listOwnDevices(): Promise, ClientListUserDevicesError>> { const handle = getParsecHandle(); @@ -168,6 +100,7 @@ export async function listOwnDevices(): Promise, Cli if (result.ok) { result.value.map((device) => { (device as OwnDeviceInfo).isCurrent = device.id === clientResult.value.deviceId; + (device as OwnDeviceInfo).isRecovery = device.deviceLabel.startsWith(RECOVERY_DEVICE_PREFIX); return device; }); } @@ -181,32 +114,17 @@ export async function listOwnDevices(): Promise, Cli } else { return { ok: true, - value: [ - { - id: 'device1', - deviceLabel: 'Web', - purpose: DevicePurpose.Standard, + value: [1, 2, 3].map((n) => { + return { + id: `device${n}`, + deviceLabel: n === 3 ? `${RECOVERY_DEVICE_PREFIX}_device${n}` : `device${n}`, createdOn: DateTime.now(), createdBy: 'some_device', - isCurrent: true, - }, - { - id: 'device2', - deviceLabel: 'Web', - purpose: DevicePurpose.Standard, - createdOn: DateTime.now(), - createdBy: 'device1', - isCurrent: false, - }, - { - id: `${RECOVERY_DEVICE_PREFIX}_device`, - deviceLabel: 'Recovery Device', - purpose: DevicePurpose.PassphraseRecovery, - createdOn: DateTime.now(), - createdBy: 'device1', - isCurrent: false, - }, - ], + isCurrent: n === 1, + isRecovery: n === 3, + purpose: n === 3 ? DevicePurpose.PassphraseRecovery : DevicePurpose.Standard, + }; + }), }; } } @@ -222,7 +140,7 @@ export async function listUserDevices(user: UserID): Promise, ClientListUserDevicesError>>; + return result; } else { return { ok: true, diff --git a/client/src/parsec/types.ts b/client/src/parsec/types.ts index ddbcbaaed93..8e1c287dc82 100644 --- a/client/src/parsec/types.ts +++ b/client/src/parsec/types.ts @@ -9,6 +9,7 @@ export { ClientChangeAuthenticationErrorTag, ClientCreateWorkspaceErrorTag, ClientEventTag, + ClientExportRecoveryDeviceErrorTag, ClientInfoErrorTag, ClientListUserDevicesErrorTag, ClientListUsersErrorTag, @@ -29,6 +30,7 @@ export { DeviceSaveStrategyTag, EntryStatTag as FileType, GreetInProgressErrorTag, + ImportRecoveryDeviceErrorTag, InvitationEmailSentStatus, InvitationStatus, ListInvitationsErrorTag, @@ -74,6 +76,7 @@ export type { ClientEvent, ClientEventInvitationChanged, ClientEventPing, + ClientExportRecoveryDeviceError, ClientGetTosError, ClientInfo, ClientInfoError, @@ -111,6 +114,7 @@ export type { FileDescriptor, VlobID as FileID, GreetInProgressError, + ImportRecoveryDeviceError, InvitationToken, ListInvitationsError, MountpointToOsPathError, @@ -204,6 +208,7 @@ interface UserInfo extends ParsecUserInfo { interface OwnDeviceInfo extends DeviceInfo { isCurrent: boolean; + isRecovery: boolean; } interface EntryStatFolder extends ParsecEntryStatFolder { diff --git a/client/src/views/devices/DevicesPage.vue b/client/src/views/devices/DevicesPage.vue index 07040160e0b..3831018b971 100644 --- a/client/src/views/devices/DevicesPage.vue +++ b/client/src/views/devices/DevicesPage.vue @@ -33,6 +33,7 @@ v-for="device in devices" :key="device.id" class="device-list-item ion-no-padding" + v-show="!device.isRecovery" > -
+
{{ $msTranslate('DevicesPage.restorePassword.notDone.label') }} @@ -64,10 +65,7 @@
- + {{ $msTranslate('DevicesPage.restorePassword.notDone.subtitle') }} @@ -130,22 +128,35 @@