diff --git a/src/app/features/config/form-cfgs/sync-form.const.ts b/src/app/features/config/form-cfgs/sync-form.const.ts index 83ba1d8292..31c5f39a81 100644 --- a/src/app/features/config/form-cfgs/sync-form.const.ts +++ b/src/app/features/config/form-cfgs/sync-form.const.ts @@ -7,7 +7,10 @@ import { IS_ELECTRON } from '../../../app.constants'; import { fileSyncDroid, fileSyncElectron } from '../../../op-log/model/model-config'; import { FormlyFieldConfig } from '@ngx-formly/core'; import { IS_NATIVE_PLATFORM } from '../../../util/is-native-platform'; -import { openDisableEncryptionDialog } from '../../../imex/sync/encryption-password-dialog-opener.service'; +import { + openDisableEncryptionDialog, + openEnableEncryptionDialog, +} from '../../../imex/sync/encryption-password-dialog-opener.service'; /** * Creates form fields for WebDAV-based sync providers. @@ -240,14 +243,14 @@ export const SYNC_FORM: ConfigFormSection = { }, hooks: { onInit: (field: FormlyFieldConfig) => { - // Track previous value to detect changes from true to false + // Track previous value to detect changes let previousValue = field?.formControl?.value; // Guard to prevent multiple dialogs opening simultaneously let isDialogOpen = false; const subscription = field?.formControl?.valueChanges.subscribe( async (newValue) => { - // Only intercept when changing from true (enabled) to false (disabled) + // Intercept when changing from true (enabled) to false (disabled) if (previousValue === true && newValue === false && !isDialogOpen) { isDialogOpen = true; try { @@ -265,6 +268,41 @@ export const SYNC_FORM: ConfigFormSection = { } finally { isDialogOpen = false; } + } + // Intercept when changing from false (disabled) to true (enabled) + else if ( + previousValue === false && + newValue === true && + !isDialogOpen + ) { + // Get the encryption password from the model + // The encryptKey field is a sibling in the same fieldGroup + const encryptKey = field?.model?.encryptKey; + + if (!encryptKey) { + // No password yet - just update previousValue + // The user will need to enter a password and the dialog + // will be triggered when they save the form + previousValue = newValue; + return; + } + + isDialogOpen = true; + try { + // Open confirmation dialog with the encrypt key + const result = await openEnableEncryptionDialog(encryptKey); + + if (!result?.success) { + // User cancelled - reset to false + field?.formControl?.setValue(false, { emitEvent: false }); + previousValue = false; + return; + } + // User confirmed and encryption was enabled successfully + previousValue = newValue; + } finally { + isDialogOpen = false; + } } else if (!isDialogOpen) { previousValue = newValue; } diff --git a/src/app/imex/sync/dialog-enable-encryption/dialog-enable-encryption.component.html b/src/app/imex/sync/dialog-enable-encryption/dialog-enable-encryption.component.html new file mode 100644 index 0000000000..eed930dc34 --- /dev/null +++ b/src/app/imex/sync/dialog-enable-encryption/dialog-enable-encryption.component.html @@ -0,0 +1,75 @@ +

+ {{ T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_TITLE | translate }} +

+ +@if (!canProceed()) { + +
+ +
+

{{ errorReason() | translate }}

+
+
+
+ + + + +} @else { + +
+ +

{{ T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_WARNING | translate }}

+
+ +
+

{{ T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_WHAT_HAPPENS | translate }}

+
    +
  • {{ T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_ITEM_1 | translate }}
  • +
  • {{ T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_ITEM_2 | translate }}
  • +
  • {{ T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_ITEM_3 | translate }}
  • +
+
+ +

+ + {{ T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_NOTE | translate }} +

+
+ + + + + +} diff --git a/src/app/imex/sync/dialog-enable-encryption/dialog-enable-encryption.component.scss b/src/app/imex/sync/dialog-enable-encryption/dialog-enable-encryption.component.scss new file mode 100644 index 0000000000..bdb5db9eb7 --- /dev/null +++ b/src/app/imex/sync/dialog-enable-encryption/dialog-enable-encryption.component.scss @@ -0,0 +1,87 @@ +.warning-box { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px; + margin-bottom: 16px; + background: rgba(255, 152, 0, 0.1); + border: 1px solid rgba(255, 152, 0, 0.3); + border-radius: 4px; + + mat-icon { + color: #ff9800; + flex-shrink: 0; + } + + p { + margin: 0; + line-height: 1.5; + } +} + +.info-box { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px; + background: rgba(33, 150, 243, 0.1); + border: 1px solid rgba(33, 150, 243, 0.3); + border-radius: 4px; + + mat-icon { + color: #2196f3; + flex-shrink: 0; + } + + p { + margin: 0 0 8px; + line-height: 1.5; + + &:last-child { + margin-bottom: 0; + } + } +} + +.info-section { + margin-bottom: 16px; + + h3 { + margin: 0 0 8px; + font-size: 14px; + font-weight: 500; + } + + ul { + margin: 0; + padding-left: 20px; + + li { + line-height: 1.6; + } + } +} + +.note { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + font-size: 13px; + color: rgba(0, 0, 0, 0.6); + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + flex-shrink: 0; + } +} + +:host-context(.isDarkTheme) .note { + color: rgba(255, 255, 255, 0.6); +} + +mat-spinner { + display: inline-block; +} diff --git a/src/app/imex/sync/dialog-enable-encryption/dialog-enable-encryption.component.ts b/src/app/imex/sync/dialog-enable-encryption/dialog-enable-encryption.component.ts new file mode 100644 index 0000000000..958fc28f7e --- /dev/null +++ b/src/app/imex/sync/dialog-enable-encryption/dialog-enable-encryption.component.ts @@ -0,0 +1,110 @@ +import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; +import { + MatDialogActions, + MatDialogContent, + MatDialogRef, + MatDialogTitle, + MAT_DIALOG_DATA, +} from '@angular/material/dialog'; +import { T } from '../../../t.const'; +import { MatButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { TranslatePipe } from '@ngx-translate/core'; +import { EncryptionEnableService } from '../encryption-enable.service'; +import { SnackService } from '../../../core/snack/snack.service'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { SyncProviderManager } from '../../../op-log/sync-providers/provider-manager.service'; +import { SyncProviderId } from '../../../op-log/sync-providers/provider.const'; + +export interface EnableEncryptionDialogData { + encryptKey: string; +} + +export interface EnableEncryptionResult { + success: boolean; +} + +@Component({ + selector: 'dialog-enable-encryption', + templateUrl: './dialog-enable-encryption.component.html', + styleUrls: ['./dialog-enable-encryption.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatButton, + MatIcon, + TranslatePipe, + MatProgressSpinner, + ], +}) +export class DialogEnableEncryptionComponent { + private _encryptionEnableService = inject(EncryptionEnableService); + private _snackService = inject(SnackService); + private _providerManager = inject(SyncProviderManager); + private _data = inject(MAT_DIALOG_DATA); + private _matDialogRef = + inject>( + MatDialogRef, + ); + + T: typeof T = T; + isLoading = signal(false); + canProceed = signal(true); + errorReason = signal(null); + + constructor() { + this._checkPreconditions(); + } + + private _checkPreconditions(): void { + const provider = this._providerManager.getActiveProvider(); + + if (!provider) { + this.canProceed.set(false); + this.errorReason.set(T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_NOT_READY); + return; + } + + if (provider.id !== SyncProviderId.SuperSync) { + this.canProceed.set(false); + this.errorReason.set(T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_SUPERSYNC_ONLY); + return; + } + + if (!this._data?.encryptKey) { + this.canProceed.set(false); + this.errorReason.set(T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_PASSWORD_REQUIRED); + return; + } + } + + async confirm(): Promise { + if (this.isLoading() || !this.canProceed()) { + return; + } + + this.isLoading.set(true); + + try { + await this._encryptionEnableService.enableEncryption(this._data.encryptKey); + this._snackService.open({ + type: 'SUCCESS', + msg: T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_SUCCESS, + }); + this._matDialogRef.close({ success: true }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + this._snackService.open({ + type: 'ERROR', + msg: `Failed to enable encryption: ${message}`, + }); + this.isLoading.set(false); + } + } + + cancel(): void { + this._matDialogRef.close({ success: false }); + } +} diff --git a/src/app/imex/sync/dialog-enter-encryption-password/dialog-enter-encryption-password.component.html b/src/app/imex/sync/dialog-enter-encryption-password/dialog-enter-encryption-password.component.html new file mode 100644 index 0000000000..a165cda26a --- /dev/null +++ b/src/app/imex/sync/dialog-enter-encryption-password/dialog-enter-encryption-password.component.html @@ -0,0 +1,47 @@ +

+ + {{ T.F.SYNC.D_ENTER_PASSWORD.TITLE | translate }} +

+ + +
+

{{ T.F.SYNC.D_ENTER_PASSWORD.MESSAGE | translate }}

+ +
+ + {{ T.F.SYNC.D_ENTER_PASSWORD.PASSWORD_LABEL | translate }} + + +
+
+
+ + + + + diff --git a/src/app/imex/sync/dialog-enter-encryption-password/dialog-enter-encryption-password.component.scss b/src/app/imex/sync/dialog-enter-encryption-password/dialog-enter-encryption-password.component.scss new file mode 100644 index 0000000000..4414ce77f8 --- /dev/null +++ b/src/app/imex/sync/dialog-enter-encryption-password/dialog-enter-encryption-password.component.scss @@ -0,0 +1,18 @@ +.content { + max-width: 400px; + + p { + line-height: 1.5; + margin-bottom: 16px; + } +} + +:host mat-form-field { + width: 100%; + margin-bottom: 0; +} + +h1 mat-icon { + vertical-align: middle; + margin-right: 8px; +} diff --git a/src/app/imex/sync/dialog-enter-encryption-password/dialog-enter-encryption-password.component.ts b/src/app/imex/sync/dialog-enter-encryption-password/dialog-enter-encryption-password.component.ts new file mode 100644 index 0000000000..b164df47cd --- /dev/null +++ b/src/app/imex/sync/dialog-enter-encryption-password/dialog-enter-encryption-password.component.ts @@ -0,0 +1,60 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { + MatDialogActions, + MatDialogContent, + MatDialogRef, + MatDialogTitle, +} from '@angular/material/dialog'; +import { T } from '../../../t.const'; +import { MatFormField, MatLabel } from '@angular/material/form-field'; +import { MatInput } from '@angular/material/input'; +import { FormsModule } from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { TranslatePipe } from '@ngx-translate/core'; +import { SyncConfigService } from '../sync-config.service'; + +export interface EnterEncryptionPasswordResult { + password?: string; +} + +@Component({ + selector: 'dialog-enter-encryption-password', + templateUrl: './dialog-enter-encryption-password.component.html', + styleUrls: ['./dialog-enter-encryption-password.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + MatDialogTitle, + MatDialogContent, + MatFormField, + MatLabel, + MatInput, + FormsModule, + MatDialogActions, + MatButton, + MatIcon, + TranslatePipe, + ], +}) +export class DialogEnterEncryptionPasswordComponent { + private _syncConfigService = inject(SyncConfigService); + private _matDialogRef = + inject< + MatDialogRef + >(MatDialogRef); + + T: typeof T = T; + passwordVal: string = ''; + + saveAndSync(): void { + if (!this.passwordVal) { + return; + } + this._syncConfigService.updateEncryptionPassword(this.passwordVal); + this._matDialogRef.close({ password: this.passwordVal }); + } + + cancel(): void { + this._matDialogRef.close({}); + } +} diff --git a/src/app/imex/sync/encryption-enable.service.ts b/src/app/imex/sync/encryption-enable.service.ts new file mode 100644 index 0000000000..94bbdedf38 --- /dev/null +++ b/src/app/imex/sync/encryption-enable.service.ts @@ -0,0 +1,138 @@ +import { inject, Injectable } from '@angular/core'; +import { SyncProviderManager } from '../../op-log/sync-providers/provider-manager.service'; +import { StateSnapshotService } from '../../op-log/backup/state-snapshot.service'; +import { VectorClockService } from '../../op-log/sync/vector-clock.service'; +import { + CLIENT_ID_PROVIDER, + ClientIdProvider, +} from '../../op-log/util/client-id.provider'; +import { isOperationSyncCapable } from '../../op-log/sync/operation-sync.util'; +import { SyncProviderId } from '../../op-log/sync-providers/provider.const'; +import { SuperSyncPrivateCfg } from '../../op-log/sync-providers/super-sync/super-sync.model'; +import { CURRENT_SCHEMA_VERSION } from '../../op-log/persistence/schema-migration.service'; +import { SyncLog } from '../../core/log'; +import { uuidv7 } from '../../util/uuid-v7'; + +/** + * Service for enabling encryption for SuperSync. + * + * Enable encryption flow: + * 1. Delete all data on server (unencrypted operations can't be mixed with encrypted) + * 2. Upload current state as encrypted snapshot + * 3. Update local config to enable encryption and set the key + */ +@Injectable({ + providedIn: 'root', +}) +export class EncryptionEnableService { + private _providerManager = inject(SyncProviderManager); + private _stateSnapshotService = inject(StateSnapshotService); + private _vectorClockService = inject(VectorClockService); + private _clientIdProvider: ClientIdProvider = inject(CLIENT_ID_PROVIDER); + + /** + * Enables encryption by deleting all server data + * and uploading a new encrypted snapshot. + * + * @param encryptKey The encryption key to use + * @throws Error if sync provider is not SuperSync or not ready + */ + async enableEncryption(encryptKey: string): Promise { + SyncLog.normal('EncryptionEnableService: Starting encryption enable...'); + + if (!encryptKey) { + throw new Error('Encryption key is required'); + } + + // Get the sync provider + const syncProvider = this._providerManager.getActiveProvider(); + if (!syncProvider) { + throw new Error('No active sync provider. Please enable sync first.'); + } + if (syncProvider.id !== SyncProviderId.SuperSync) { + throw new Error( + `Enable encryption is only supported for SuperSync (current: ${syncProvider.id})`, + ); + } + + if (!isOperationSyncCapable(syncProvider)) { + throw new Error('Sync provider does not support operation sync'); + } + + // Get current config + const existingCfg = + (await syncProvider.privateCfg.load()) as SuperSyncPrivateCfg | null; + + // Get current state + SyncLog.normal('EncryptionEnableService: Getting current state...'); + const currentState = this._stateSnapshotService.getStateSnapshot(); + const vectorClock = await this._vectorClockService.getCurrentVectorClock(); + const clientId = await this._clientIdProvider.loadClientId(); + if (!clientId) { + throw new Error('Client ID not available'); + } + + // Delete all server data (unencrypted ops can't be mixed with encrypted) + SyncLog.normal('EncryptionEnableService: Deleting server data...'); + await syncProvider.deleteAllData(); + + // Update local config first - enable encryption and set the key + // This must happen BEFORE upload so the upload uses the new key + SyncLog.normal('EncryptionEnableService: Updating local config...'); + await syncProvider.setPrivateCfg({ + ...existingCfg, + encryptKey, + isEncryptionEnabled: true, + } as SuperSyncPrivateCfg); + + // Upload encrypted snapshot + SyncLog.normal('EncryptionEnableService: Uploading encrypted snapshot...'); + try { + const response = await syncProvider.uploadSnapshot( + currentState, + clientId, + 'recovery', + vectorClock, + CURRENT_SCHEMA_VERSION, + true, // isPayloadEncrypted = true + uuidv7(), // opId - server must use this ID + ); + + if (!response.accepted) { + throw new Error(`Snapshot upload failed: ${response.error}`); + } + + // Update lastServerSeq to the new snapshot's seq + if (response.serverSeq !== undefined) { + await syncProvider.setLastServerSeq(response.serverSeq); + } else { + SyncLog.err( + 'EncryptionEnableService: Snapshot accepted but serverSeq is missing. ' + + 'Sync state may be inconsistent - consider using "Sync Now" to verify.', + ); + } + + SyncLog.normal('EncryptionEnableService: Encryption enabled successfully!'); + } catch (uploadError) { + // CRITICAL: Server data was deleted but new snapshot failed to upload. + // Try to revert local config to unencrypted state + SyncLog.err( + 'EncryptionEnableService: Snapshot upload failed after deleting server data!', + uploadError, + ); + + // Revert local config + await syncProvider.setPrivateCfg({ + ...existingCfg, + encryptKey: undefined, + isEncryptionEnabled: false, + } as SuperSyncPrivateCfg); + + throw new Error( + 'CRITICAL: Failed to upload encrypted snapshot after deleting server data. ' + + 'Your local data is safe. Encryption has been reverted. Please use "Sync Now" to re-upload your data. ' + + `Original error: ${uploadError instanceof Error ? uploadError.message : uploadError}`, + ); + } + } +} diff --git a/src/app/imex/sync/encryption-password-dialog-opener.service.ts b/src/app/imex/sync/encryption-password-dialog-opener.service.ts index e79bccdfc3..f0e4c0571e 100644 --- a/src/app/imex/sync/encryption-password-dialog-opener.service.ts +++ b/src/app/imex/sync/encryption-password-dialog-opener.service.ts @@ -8,6 +8,11 @@ import { DialogDisableEncryptionComponent, DisableEncryptionResult, } from './dialog-disable-encryption/dialog-disable-encryption.component'; +import { + DialogEnableEncryptionComponent, + EnableEncryptionDialogData, + EnableEncryptionResult, +} from './dialog-enable-encryption/dialog-enable-encryption.component'; /** * Singleton service to open the encryption password change dialog. @@ -36,6 +41,18 @@ export class EncryptionPasswordDialogOpenerService { return dialogRef.afterClosed().toPromise(); } + + openEnableEncryptionDialog( + encryptKey: string, + ): Promise { + const dialogRef = this._matDialog.open(DialogEnableEncryptionComponent, { + width: '450px', + disableClose: true, + data: { encryptKey } as EnableEncryptionDialogData, + }); + + return dialogRef.afterClosed().toPromise(); + } } /** @@ -80,3 +97,17 @@ export const openDisableEncryptionDialog = (): Promise< } return dialogOpenerInstance.openDisableEncryptionDialog(); }; + +/** + * Opens the enable encryption confirmation dialog. + * Can be called from form config onChange handlers. + */ +export const openEnableEncryptionDialog = ( + encryptKey: string, +): Promise => { + if (!dialogOpenerInstance) { + console.error('EncryptionPasswordDialogOpenerService not initialized'); + return Promise.resolve(undefined); + } + return dialogOpenerInstance.openEnableEncryptionDialog(encryptKey); +}; diff --git a/src/app/imex/sync/sync-wrapper.service.ts b/src/app/imex/sync/sync-wrapper.service.ts index bfc71fc1c0..4718903073 100644 --- a/src/app/imex/sync/sync-wrapper.service.ts +++ b/src/app/imex/sync/sync-wrapper.service.ts @@ -48,6 +48,7 @@ import { DataInitService } from '../../core/data-init/data-init.service'; import { DialogSyncInitialCfgComponent } from './dialog-sync-initial-cfg/dialog-sync-initial-cfg.component'; import { DialogIncompleteSyncComponent } from './dialog-incomplete-sync/dialog-incomplete-sync.component'; import { DialogHandleDecryptErrorComponent } from './dialog-handle-decrypt-error/dialog-handle-decrypt-error.component'; +import { DialogEnterEncryptionPasswordComponent } from './dialog-enter-encryption-password/dialog-enter-encryption-password.component'; import { DialogIncoherentTimestampsErrorComponent } from './dialog-incoherent-timestamps-error/dialog-incoherent-timestamps-error.component'; import { SyncLog } from '../../core/log'; import { promiseTimeout } from '../../util/promise-timeout'; @@ -315,10 +316,10 @@ export class SyncWrapperService { actionStr: T.F.SYNC.S.BTN_FORCE_OVERWRITE, }); return 'HANDLED_ERROR'; - } else if ( - error instanceof DecryptNoPasswordError || - error instanceof DecryptError - ) { + } else if (error instanceof DecryptNoPasswordError) { + this._handleMissingPasswordDialog(); + return 'HANDLED_ERROR'; + } else if (error instanceof DecryptError) { this._handleDecryptionError(); return 'HANDLED_ERROR'; } else if (error instanceof CanNotMigrateMajorDownError) { @@ -530,6 +531,33 @@ export class SyncWrapperService { return undefined; } + /** + * Handles missing encryption password when receiving encrypted data. + * Opens a simple dialog to prompt for the password, then re-syncs. + */ + private _handleMissingPasswordDialog(): void { + // Set ERROR status so sync button shows error icon + this._providerManager.setSyncStatus('ERROR'); + + // Open dialog for password entry + this._matDialog + .open(DialogEnterEncryptionPasswordComponent, { + width: '450px', + disableClose: true, + autoFocus: false, + }) + .afterClosed() + .subscribe((result) => { + if (result?.password) { + // Password was entered and saved, re-sync + this.sync(); + } else { + // User cancelled - set status to unknown + this._providerManager.setSyncStatus('UNKNOWN_OR_CHANGED'); + } + }); + } + private _handleDecryptionError(): void { // Set ERROR status so sync button shows error icon this._providerManager.setSyncStatus('ERROR'); diff --git a/src/app/op-log/sync/operation-log-download.service.ts b/src/app/op-log/sync/operation-log-download.service.ts index 3518ebb201..e7afd7bc52 100644 --- a/src/app/op-log/sync/operation-log-download.service.ts +++ b/src/app/op-log/sync/operation-log-download.service.ts @@ -17,7 +17,7 @@ import { CLOCK_DRIFT_THRESHOLD_MS, } from '../core/operation-log.const'; import { OperationEncryptionService } from './operation-encryption.service'; -import { DecryptError } from '../core/errors/sync-errors'; +import { DecryptError, DecryptNoPasswordError } from '../core/errors/sync-errors'; import { SuperSyncStatusService } from './super-sync-status.service'; import { DownloadResult } from '../core/types/sync-results.types'; import { CLIENT_ID_PROVIDER } from '../util/client-id.provider'; @@ -197,16 +197,13 @@ export class OperationLogDownloadService { const hasEncryptedOps = syncOps.some((op) => op.isPayloadEncrypted); if (hasEncryptedOps) { if (!encryptKey) { - // No encryption key available - fail with a helpful message + // No encryption key available - throw error to let sync wrapper show password dialog OpLog.error( 'OperationLogDownloadService: Received encrypted operations but no encryption key is configured.', ); - this.snackService.open({ - type: 'ERROR', - msg: T.F.SYNC.S.ENCRYPTION_PASSWORD_REQUIRED, - }); - downloadFailed = true; - return; + throw new DecryptNoPasswordError( + 'Encrypted data received but no encryption password is configured', + ); } try { diff --git a/src/app/t.const.ts b/src/app/t.const.ts index cd134e12e4..515995e58e 100644 --- a/src/app/t.const.ts +++ b/src/app/t.const.ts @@ -964,6 +964,12 @@ const T = { P2: 'F.SYNC.D_DECRYPT_ERROR.P2', PASSWORD: 'F.SYNC.D_DECRYPT_ERROR.PASSWORD', }, + D_ENTER_PASSWORD: { + TITLE: 'F.SYNC.D_ENTER_PASSWORD.TITLE', + MESSAGE: 'F.SYNC.D_ENTER_PASSWORD.MESSAGE', + PASSWORD_LABEL: 'F.SYNC.D_ENTER_PASSWORD.PASSWORD_LABEL', + BTN_SAVE_AND_SYNC: 'F.SYNC.D_ENTER_PASSWORD.BTN_SAVE_AND_SYNC', + }, D_FRESH_CLIENT_CONFIRM: { MESSAGE: 'F.SYNC.D_FRESH_CLIENT_CONFIRM.MESSAGE', OK: 'F.SYNC.D_FRESH_CLIENT_CONFIRM.OK', @@ -1068,6 +1074,22 @@ const T = { 'F.SYNC.FORM.SUPER_SYNC.DISABLE_ENCRYPTION_NOT_READY', DISABLE_ENCRYPTION_SUPERSYNC_ONLY: 'F.SYNC.FORM.SUPER_SYNC.DISABLE_ENCRYPTION_SUPERSYNC_ONLY', + ENABLE_ENCRYPTION_TITLE: 'F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_TITLE', + ENABLE_ENCRYPTION_WARNING: 'F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_WARNING', + ENABLE_ENCRYPTION_WHAT_HAPPENS: + 'F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_WHAT_HAPPENS', + ENABLE_ENCRYPTION_ITEM_1: 'F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_ITEM_1', + ENABLE_ENCRYPTION_ITEM_2: 'F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_ITEM_2', + ENABLE_ENCRYPTION_ITEM_3: 'F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_ITEM_3', + ENABLE_ENCRYPTION_NOTE: 'F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_NOTE', + BTN_ENABLE_ENCRYPTION: 'F.SYNC.FORM.SUPER_SYNC.BTN_ENABLE_ENCRYPTION', + ENABLE_ENCRYPTION_SUCCESS: 'F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_SUCCESS', + ENABLE_ENCRYPTION_NOT_READY: + 'F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_NOT_READY', + ENABLE_ENCRYPTION_SUPERSYNC_ONLY: + 'F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_SUPERSYNC_ONLY', + ENABLE_ENCRYPTION_PASSWORD_REQUIRED: + 'F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_PASSWORD_REQUIRED', }, IMPORT_ENCRYPTION_WARNING: { TITLE: 'F.SYNC.FORM.IMPORT_ENCRYPTION_WARNING.TITLE', diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 8d219b7ef6..4ff3d5465f 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -939,6 +939,12 @@ "P2": "Or you can also change your password, which will overwrite all remote data.", "PASSWORD": "Password" }, + "D_ENTER_PASSWORD": { + "TITLE": "Encryption Password Required", + "MESSAGE": "The sync server contains encrypted data, but no encryption password is configured on this device. Please enter your encryption password to decrypt and sync your data.", + "PASSWORD_LABEL": "Encryption Password", + "BTN_SAVE_AND_SYNC": "Save & Sync" + }, "D_FRESH_CLIENT_CONFIRM": { "MESSAGE": "This appears to be a fresh installation. Remote data with {{count}} changes was found. Do you want to download and apply this data?", "OK": "Download Remote Data", @@ -1036,7 +1042,19 @@ "DISABLE_ENCRYPTION_SUCCESS": "Encryption disabled successfully", "DISABLE_ENCRYPTION_NOT_READY": "Sync is not enabled or configured.", "DISABLE_ENCRYPTION_SUPERSYNC_ONLY": "This feature is only available for SuperSync.", - "DISABLE_ENCRYPTION_ENABLE_FIRST": "Please enable sync and save your SuperSync configuration first, then try again." + "DISABLE_ENCRYPTION_ENABLE_FIRST": "Please enable sync and save your SuperSync configuration first, then try again.", + "ENABLE_ENCRYPTION_TITLE": "Enable Encryption?", + "ENABLE_ENCRYPTION_WARNING": "WARNING: This will permanently delete ALL unencrypted sync data on the server. This is necessary because encrypted operations cannot be mixed with unencrypted ones. If you forget your encryption password, your data cannot be recovered.", + "ENABLE_ENCRYPTION_WHAT_HAPPENS": "What will happen:", + "ENABLE_ENCRYPTION_ITEM_1": "All sync history and backups on the server will be deleted", + "ENABLE_ENCRYPTION_ITEM_2": "Your current data will be re-uploaded with encryption", + "ENABLE_ENCRYPTION_ITEM_3": "All other devices will need to enter this encryption password", + "ENABLE_ENCRYPTION_NOTE": "Your local data will NOT be deleted. You can disable encryption at any time.", + "BTN_ENABLE_ENCRYPTION": "Enable Encryption", + "ENABLE_ENCRYPTION_SUCCESS": "Encryption enabled successfully", + "ENABLE_ENCRYPTION_NOT_READY": "Sync is not enabled or configured.", + "ENABLE_ENCRYPTION_SUPERSYNC_ONLY": "This feature is only available for SuperSync.", + "ENABLE_ENCRYPTION_PASSWORD_REQUIRED": "Please enter an encryption password first." }, "IMPORT_ENCRYPTION_WARNING": { "TITLE": "Encryption Settings Will Change",