From e2c585ee45b77118256e794e2a03e683cda2dd6a Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 27 Jan 2026 11:29:52 +0100 Subject: [PATCH] fix(dropbox): use hash-wasm fallback for PKCE on Android On Android, Capacitor serves the app from http://localhost which is not a secure context. WebCrypto's crypto.subtle API is unavailable in insecure contexts, causing Dropbox OAuth PKCE code generation to fail. Use hash-wasm (already a dependency for Argon2id) as fallback when crypto.subtle is unavailable. --- .../dropbox/generate-pkce-codes.spec.ts | 18 ++++++++---- .../file-based/dropbox/generate-pkce-codes.ts | 28 ++++++++++++++----- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.spec.ts b/src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.spec.ts index 7507dd711d..6b414c822c 100644 --- a/src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.spec.ts +++ b/src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.spec.ts @@ -29,8 +29,8 @@ describe('generatePKCECodes', () => { ); }); - it('should throw error when crypto.subtle is unavailable', async () => { - // Mock crypto with getRandomValues but without subtle + it('should successfully generate PKCE codes when crypto.subtle is unavailable (fallback)', async () => { + // Mock crypto with getRandomValues but without subtle (simulates Android Capacitor insecure context) Object.defineProperty(window, 'crypto', { value: { getRandomValues: (array: Uint32Array) => { @@ -46,9 +46,17 @@ describe('generatePKCECodes', () => { configurable: true, }); - await expectAsync(generatePKCECodes(128)).toBeRejectedWithError( - /WebCrypto API.*subtle/i, - ); + const result = await generatePKCECodes(128); + + expect(result).toBeDefined(); + expect(result.codeVerifier).toBeDefined(); + expect(result.codeChallenge).toBeDefined(); + expect(typeof result.codeVerifier).toBe('string'); + expect(typeof result.codeChallenge).toBe('string'); + expect(result.codeVerifier.length).toBeGreaterThan(0); + expect(result.codeChallenge.length).toBeGreaterThan(0); + // Code challenge should be different from verifier (it's hashed) + expect(result.codeChallenge).not.toBe(result.codeVerifier); }); it('should successfully generate PKCE codes when WebCrypto is available', async () => { diff --git a/src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.ts b/src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.ts index 7f80df171d..0103b4a497 100644 --- a/src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.ts +++ b/src/app/op-log/sync-providers/file-based/dropbox/generate-pkce-codes.ts @@ -1,4 +1,14 @@ // taken from https://github.com/aaronpk/pkce-vanilla-js/blob/master/index.html +import { sha256 as hashWasmSha256 } from 'hash-wasm'; + +// Convert hex string to ArrayBuffer (needed for hash-wasm fallback) +const hexStringToArrayBuffer = (hex: string): ArrayBuffer => { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); + } + return bytes.buffer; +}; // Generate a secure random string using the browser crypto functions const generateRandomString = (length: number): string => { @@ -15,18 +25,22 @@ const generateRandomString = (length: number): string => { // Calculate the SHA256 hash of the input text. // Returns a promise that resolves to an ArrayBuffer -const sha256 = (plain: string): Promise => { +const sha256 = async (plain: string): Promise => { const encoder = new TextEncoder(); const data = encoder.encode(plain); - // NOTE: crypto.subtle is supposed to be undefined in insecure contexts + + // Use WebCrypto if available (secure context) + // NOTE: crypto.subtle is undefined in insecure contexts (e.g., Android Capacitor http://localhost) // @see https://www.chromium.org/blink/webcrypto - if (window.crypto?.subtle === undefined) { - throw new Error( - 'WebCrypto API (subtle.digest) is only supported in secure contexts.', - ); + if (window.crypto?.subtle !== undefined) { + return window.crypto.subtle.digest('SHA-256', data); } - return window.crypto.subtle.digest('SHA-256', data); + // Fallback to hash-wasm for insecure contexts (e.g., Android Capacitor serves from http://localhost) + // We can't use WebCrypto in insecure contexts, so we use hash-wasm which is already a dependency + // (used for Argon2id encryption) and provides a pure WebAssembly implementation + const hexHash = await hashWasmSha256(data); + return hexStringToArrayBuffer(hexHash); }; // Base64-urlencodes the input string