feat(supersync): improve encryption UX with confirmation dialogs

Add two encryption UX improvements for SuperSync:

1. Enable encryption confirmation dialog: When enabling encryption,
   show a warning dialog explaining that all server data will be deleted
   and re-uploaded with encryption. On confirm, performs the operation
   automatically. On cancel, reverts the checkbox.

2. Password prompt dialog for missing encryption key: When receiving
   encrypted data without a configured password, show a dialog prompting
   the user to enter their encryption password instead of just showing
   a snack bar error. On save, stores the password and re-syncs.
This commit is contained in:
Johannes Millan 2026-01-24 14:52:32 +01:00
parent 1ba4c0ca7f
commit e2db30cfc2
13 changed files with 685 additions and 16 deletions

View file

@ -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<SyncConfig> = {
},
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<SyncConfig> = {
} 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;
}

View file

@ -0,0 +1,75 @@
<h1 mat-dialog-title>
{{ T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_TITLE | translate }}
</h1>
@if (!canProceed()) {
<mat-dialog-content>
<div class="info-box">
<mat-icon aria-hidden="true">info</mat-icon>
<div>
<p>{{ errorReason() | translate }}</p>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button
mat-button
type="button"
(click)="cancel()"
>
{{ T.G.CLOSE | translate }}
</button>
</mat-dialog-actions>
} @else {
<mat-dialog-content>
<div class="warning-box">
<mat-icon aria-hidden="true">warning</mat-icon>
<p>{{ T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_WARNING | translate }}</p>
</div>
<div class="info-section">
<h3>{{ T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_WHAT_HAPPENS | translate }}</h3>
<ul>
<li>{{ T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_ITEM_1 | translate }}</li>
<li>{{ T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_ITEM_2 | translate }}</li>
<li>{{ T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_ITEM_3 | translate }}</li>
</ul>
</div>
<p class="note">
<mat-icon aria-hidden="true">info</mat-icon>
<span>{{ T.F.SYNC.FORM.SUPER_SYNC.ENABLE_ENCRYPTION_NOTE | translate }}</span>
</p>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button
mat-button
type="button"
[disabled]="isLoading()"
(click)="cancel()"
>
{{ T.G.CANCEL | translate }}
</button>
<button
mat-flat-button
color="warn"
type="button"
[disabled]="isLoading()"
(click)="confirm()"
>
@if (isLoading()) {
<mat-spinner
diameter="20"
aria-label="Processing"
></mat-spinner>
} @else {
<ng-container>
<mat-icon aria-hidden="true">lock</mat-icon>
{{ T.F.SYNC.FORM.SUPER_SYNC.BTN_ENABLE_ENCRYPTION | translate }}
</ng-container>
}
</button>
</mat-dialog-actions>
}

View file

@ -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;
}

View file

@ -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<EnableEncryptionDialogData>(MAT_DIALOG_DATA);
private _matDialogRef =
inject<MatDialogRef<DialogEnableEncryptionComponent, EnableEncryptionResult>>(
MatDialogRef,
);
T: typeof T = T;
isLoading = signal(false);
canProceed = signal(true);
errorReason = signal<string | null>(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<void> {
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 });
}
}

View file

@ -0,0 +1,47 @@
<h1 mat-dialog-title>
<mat-icon aria-hidden="true">lock</mat-icon>
{{ T.F.SYNC.D_ENTER_PASSWORD.TITLE | translate }}
</h1>
<mat-dialog-content>
<div class="content">
<p>{{ T.F.SYNC.D_ENTER_PASSWORD.MESSAGE | translate }}</p>
<form
#formEl="ngForm"
(ngSubmit)="formEl.valid && saveAndSync()"
>
<mat-form-field>
<mat-label>{{ T.F.SYNC.D_ENTER_PASSWORD.PASSWORD_LABEL | translate }}</mat-label>
<input
[(ngModel)]="passwordVal"
name="passwordVal"
matInput
type="password"
required
/>
</mat-form-field>
</form>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button
mat-button
type="button"
(click)="cancel()"
>
<mat-icon>close</mat-icon>
{{ T.G.CANCEL | translate }}
</button>
<button
mat-flat-button
color="primary"
type="button"
[disabled]="!passwordVal"
(click)="saveAndSync()"
>
<mat-icon>sync</mat-icon>
{{ T.F.SYNC.D_ENTER_PASSWORD.BTN_SAVE_AND_SYNC | translate }}
</button>
</mat-dialog-actions>

View file

@ -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;
}

View file

@ -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<DialogEnterEncryptionPasswordComponent, EnterEncryptionPasswordResult>
>(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({});
}
}

View file

@ -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<void> {
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}`,
);
}
}
}

View file

@ -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<EnableEncryptionResult | undefined> {
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<EnableEncryptionResult | undefined> => {
if (!dialogOpenerInstance) {
console.error('EncryptionPasswordDialogOpenerService not initialized');
return Promise.resolve(undefined);
}
return dialogOpenerInstance.openEnableEncryptionDialog(encryptKey);
};

View file

@ -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');

View file

@ -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 {

View file

@ -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',

View file

@ -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",