mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
fix(sync): show clear error when encryption unavailable on Android
WebCrypto (crypto.subtle) is unavailable in insecure contexts like Android Capacitor (serves from http://localhost). Previously, encryption operations would fail silently, making sync appear successful when data wasn't actually syncing. Changes: - Add isCryptoSubtleAvailable() check function in encryption.ts - Add assertCryptoSubtleAvailable() guard to encrypt/decrypt functions - Add WebCryptoNotAvailableError handler in sync-wrapper.service.ts - Check crypto availability BEFORE deleting server data in encryption-enable.service.ts and import-encryption-handler.service.ts - Add WEB_CRYPTO_NOT_AVAILABLE translation key with clear error message This ensures users see a clear error message instead of silent failure when trying to use encryption on unsupported platforms.
This commit is contained in:
parent
ab6df00a72
commit
5f44e18aa2
6 changed files with 83 additions and 0 deletions
|
|
@ -4,6 +4,8 @@ import { SyncLog } from '../../core/log';
|
|||
import { SnapshotUploadService } from './snapshot-upload.service';
|
||||
import { OperationEncryptionService } from '../../op-log/sync/operation-encryption.service';
|
||||
import { WrappedProviderService } from '../../op-log/sync-providers/wrapped-provider.service';
|
||||
import { isCryptoSubtleAvailable } from '../../op-log/encryption/encryption';
|
||||
import { WebCryptoNotAvailableError } from '../../op-log/core/errors/sync-errors';
|
||||
|
||||
const LOG_PREFIX = 'EncryptionEnableService';
|
||||
|
||||
|
|
@ -38,6 +40,16 @@ export class EncryptionEnableService {
|
|||
throw new Error('Encryption key is required');
|
||||
}
|
||||
|
||||
// CRITICAL: Check crypto availability BEFORE deleting server data
|
||||
// to prevent data loss if encryption will fail
|
||||
if (!isCryptoSubtleAvailable()) {
|
||||
throw new WebCryptoNotAvailableError(
|
||||
'Cannot enable encryption: WebCrypto API is not available. ' +
|
||||
'Encryption requires a secure context (HTTPS). ' +
|
||||
'On Android, encryption is not supported.',
|
||||
);
|
||||
}
|
||||
|
||||
// Gather all data needed for upload (validates provider)
|
||||
const { syncProvider, existingCfg, state, vectorClock, clientId } =
|
||||
await this._snapshotUploadService.gatherSnapshotData(LOG_PREFIX);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { SyncLog } from '../../core/log';
|
|||
import { AppDataComplete } from '../../op-log/model/model-config';
|
||||
import { OperationEncryptionService } from '../../op-log/sync/operation-encryption.service';
|
||||
import { SnapshotUploadService } from './snapshot-upload.service';
|
||||
import { isCryptoSubtleAvailable } from '../../op-log/encryption/encryption';
|
||||
|
||||
export interface EncryptionStateChangeResult {
|
||||
encryptionStateChanged: boolean;
|
||||
|
|
@ -132,6 +133,20 @@ export class ImportEncryptionHandlerService {
|
|||
|
||||
const { syncProvider, existingCfg, state, vectorClock, clientId } = snapshotData;
|
||||
|
||||
// CRITICAL: Check crypto availability BEFORE deleting server data
|
||||
// to prevent data loss if encryption will fail (only needed when enabling encryption)
|
||||
if (isEncryptionEnabled && !isCryptoSubtleAvailable()) {
|
||||
return {
|
||||
encryptionStateChanged: false,
|
||||
serverDataDeleted: false,
|
||||
snapshotUploaded: false,
|
||||
error:
|
||||
'Cannot enable encryption: WebCrypto API is not available. ' +
|
||||
'Encryption requires a secure context (HTTPS). ' +
|
||||
'On Android, encryption is not supported.',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Delete all server data (encrypted ops can't mix with unencrypted)
|
||||
SyncLog.normal(`${LOG_PREFIX}: Deleting server data...`);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { toObservable } from '@angular/core/rxjs-interop';
|
|||
import {
|
||||
SyncAlreadyInProgressError,
|
||||
LocalDataConflictError,
|
||||
WebCryptoNotAvailableError,
|
||||
} from '../../op-log/core/errors/sync-errors';
|
||||
import { SyncConfig } from '../../features/config/global-config.model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
|
@ -407,6 +408,16 @@ export class SyncWrapperService {
|
|||
// File-based sync: Local data exists and remote snapshot would overwrite it
|
||||
// Show conflict dialog to let user choose between local and remote data
|
||||
return this._handleLocalDataConflict(error);
|
||||
} else if (error instanceof WebCryptoNotAvailableError) {
|
||||
// WebCrypto (crypto.subtle) is unavailable in insecure contexts
|
||||
// (e.g., Android Capacitor serves from http://localhost)
|
||||
this._providerManager.setSyncStatus('ERROR');
|
||||
this._snackService.open({
|
||||
msg: T.F.SYNC.S.WEB_CRYPTO_NOT_AVAILABLE,
|
||||
type: 'ERROR',
|
||||
config: { duration: 15000 },
|
||||
});
|
||||
return 'HANDLED_ERROR';
|
||||
} else if (this._isTimeoutError(error)) {
|
||||
this._snackService.open({
|
||||
msg: T.F.SYNC.S.TIMEOUT_ERROR,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,50 @@
|
|||
import { argon2id } from 'hash-wasm';
|
||||
import { WebCryptoNotAvailableError } from '../core/errors/sync-errors';
|
||||
|
||||
const ALGORITHM = 'AES-GCM' as const;
|
||||
const SALT_LENGTH = 16;
|
||||
const IV_LENGTH = 12;
|
||||
const KEY_LENGTH = 32;
|
||||
|
||||
// ============================================================================
|
||||
// WEBCRYPTO AVAILABILITY CHECK
|
||||
// ============================================================================
|
||||
// WebCrypto (crypto.subtle) is unavailable in insecure contexts:
|
||||
// - Android Capacitor: serves from http://localhost (not https)
|
||||
// - iOS Capacitor: capacitor:// scheme may not be recognized as secure
|
||||
//
|
||||
// When WebCrypto is unavailable, encryption operations will fail.
|
||||
// Call isCryptoSubtleAvailable() before attempting encryption to show
|
||||
// a clear error message to the user.
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Checks if WebCrypto API (crypto.subtle) is available in the current context.
|
||||
* Returns false in insecure contexts (http://, some custom schemes like Android Capacitor).
|
||||
*/
|
||||
export const isCryptoSubtleAvailable = (): boolean => {
|
||||
return (
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.crypto !== 'undefined' &&
|
||||
typeof window.crypto.subtle !== 'undefined'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Throws WebCryptoNotAvailableError if crypto.subtle is not available.
|
||||
* Call this at the start of encryption/decryption functions to fail fast
|
||||
* with a clear error message.
|
||||
*/
|
||||
const assertCryptoSubtleAvailable = (): void => {
|
||||
if (!isCryptoSubtleAvailable()) {
|
||||
throw new WebCryptoNotAvailableError(
|
||||
'WebCrypto API (crypto.subtle) is not available. ' +
|
||||
'Encryption requires a secure context (HTTPS). ' +
|
||||
'On Android, encryption is not supported.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Holds a derived CryptoKey along with its salt for reuse across operations.
|
||||
* Used to avoid expensive Argon2id key derivation for each operation.
|
||||
|
|
@ -209,6 +249,7 @@ const decryptArgon = async (data: string, password: string): Promise<string> =>
|
|||
};
|
||||
|
||||
export const encrypt = async (data: string, password: string): Promise<string> => {
|
||||
assertCryptoSubtleAvailable();
|
||||
const enc = new TextEncoder();
|
||||
const dataBuffer = enc.encode(data);
|
||||
const salt = window.crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
||||
|
|
@ -229,6 +270,7 @@ export const encrypt = async (data: string, password: string): Promise<string> =
|
|||
};
|
||||
|
||||
export const decrypt = async (data: string, password: string): Promise<string> => {
|
||||
assertCryptoSubtleAvailable();
|
||||
try {
|
||||
return await decryptArgon(data, password);
|
||||
} catch (e) {
|
||||
|
|
@ -296,6 +338,7 @@ export const deriveKeyFromPassword = async (
|
|||
password: string,
|
||||
salt?: Uint8Array,
|
||||
): Promise<DerivedKeyInfo> => {
|
||||
assertCryptoSubtleAvailable();
|
||||
const actualSalt = salt ?? window.crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
||||
const key = await _deriveKeyArgon(password, actualSalt);
|
||||
return { key, salt: actualSalt };
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,7 @@ const T = {
|
|||
ENCRYPTION_PASSWORD_REQUIRED: 'F.SYNC.S.ENCRYPTION_PASSWORD_REQUIRED',
|
||||
ENCRYPTION_DISABLED_ON_OTHER_DEVICE:
|
||||
'F.SYNC.S.ENCRYPTION_DISABLED_ON_OTHER_DEVICE',
|
||||
WEB_CRYPTO_NOT_AVAILABLE: 'F.SYNC.S.WEB_CRYPTO_NOT_AVAILABLE',
|
||||
PERSIST_FAILED: 'F.SYNC.S.PERSIST_FAILED',
|
||||
DEFERRED_ACTION_FAILED: 'F.SYNC.S.DEFERRED_ACTION_FAILED',
|
||||
REMOTE_DATA_TOO_OLD: 'F.SYNC.S.REMOTE_DATA_TOO_OLD',
|
||||
|
|
|
|||
|
|
@ -1127,6 +1127,7 @@
|
|||
"DECRYPTION_FAILED": "Failed to decrypt synced data. Please check your encryption password.",
|
||||
"ENCRYPTION_PASSWORD_REQUIRED": "Encrypted data received but no encryption password is configured. Please set your encryption password in sync settings.",
|
||||
"ENCRYPTION_DISABLED_ON_OTHER_DEVICE": "Encryption was disabled on another device. Your sync settings have been updated.",
|
||||
"WEB_CRYPTO_NOT_AVAILABLE": "Encryption is not available on this device. On Android, encryption requires a secure context (HTTPS) which is not available. Please disable encryption in sync settings or sync from a desktop browser.",
|
||||
"PERSIST_FAILED": "Failed to save changes. State may be inconsistent - please reload.",
|
||||
"DEFERRED_ACTION_FAILED": "Some changes made during sync could not be saved. Please reload to recover.",
|
||||
"REMOTE_DATA_TOO_OLD": "Remote data is too old to be processed. Please update your remote app.",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue