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.
This commit is contained in:
Johannes Millan 2026-01-27 11:29:52 +01:00
parent 3fc0be6924
commit e2c585ee45
2 changed files with 34 additions and 12 deletions

View file

@ -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 () => {

View file

@ -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<ArrayBuffer> => {
const sha256 = async (plain: string): Promise<ArrayBuffer> => {
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