Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/mobile/vercel.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"buildCommand": "pnpm expo export -p web",
"buildCommand": "pnpm run build",
"outputDirectory": "dist",
"rewrites": [
{
Expand Down
313 changes: 313 additions & 0 deletions apps/next-app/__tests__/crypto-web-effect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
/**
* Comprehensive integration tests for crypto-web-effect implementation
* Tests all security fixes identified in GitHub Claude bot review
*/

import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest';
import { Effect, pipe } from 'effect';
import { WebCryptoLive, encryptWithAutoKey } from '../app/lib/crypto-web-effect';
import { CryptoProvider } from '@rite/shared-types';

// Mock Web Crypto API for testing
const mockGenerateKey = vi.fn(() => Promise.resolve({ type: 'secret', algorithm: { name: 'AES-GCM' } }));
const mockEncrypt = vi.fn(() => Promise.resolve(new ArrayBuffer(48))); // 32 bytes data + 16 bytes tag
const mockDecrypt = vi.fn(() => Promise.resolve(new ArrayBuffer(16)));
const mockImportKey = vi.fn(() => Promise.resolve({}));
const mockExportKey = vi.fn(() => Promise.resolve(new ArrayBuffer(32)));
const mockDeriveKey = vi.fn(() => Promise.resolve({}));

Object.defineProperty(global, 'window', {
value: {
crypto: {
subtle: {
generateKey: mockGenerateKey,
encrypt: mockEncrypt,
decrypt: mockDecrypt,
importKey: mockImportKey,
exportKey: mockExportKey,
deriveKey: mockDeriveKey,
},
getRandomValues: (arr: Uint8Array) => {
for (let i = 0; i < arr.length; i++) {
arr[i] = Math.floor(Math.random() * 256);
}
return arr;
},
},
},
writable: true,
});

// Mock sessionStorage
Object.defineProperty(global, 'sessionStorage', {
value: {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
key: () => null,
length: 0,
},
writable: true,
});

// Mock TextEncoder/TextDecoder
Object.defineProperty(global, 'TextEncoder', {
value: class {
encode(str: string) {
return new Uint8Array(Buffer.from(str, 'utf8'));
}
},
});

Object.defineProperty(global, 'TextDecoder', {
value: class {
decode(buffer: ArrayBuffer) {
return Buffer.from(buffer).toString('utf8');
}
},
});

describe('Crypto Web Effect Implementation - Security Review Tests', () => {
beforeAll(() => {
// Ensure global objects are properly set up
});

beforeEach(() => {
// Reset mocks for each test
vi.clearAllMocks();
});

it('should compile without type errors', () => {
// This test ensures the module can be imported without circular dependency issues
expect(encryptWithAutoKey).toBeDefined();
expect(WebCryptoLive).toBeDefined();
});

it('should have the correct layer structure', () => {
// Test that the layer can be created without errors
expect(() => WebCryptoLive).not.toThrow();
});

describe('Critical Security Fix Tests', () => {
it('CRITICAL: decrypt function should fail when no key is provided', async () => {
// This addresses the critical issue identified by GitHub Claude bot:
// "Decrypt function generates random key when none provided (can't decrypt)"

const mockEncryptedData = {
encryptedData: 'dGVzdA==', // base64 "test"
iv: 'aXZkYXRh', // base64 "ivdata"
authTag: 'dGFn', // base64 "tag"
version: 'AES_V1' as const,
algorithm: 'AES-256-GCM' as const,
};

// Create a test Effect that tries to decrypt without providing a key
const decryptWithoutKey = pipe(
CryptoProvider,
Effect.flatMap((crypto) => crypto.decrypt(mockEncryptedData))
);

const program = Effect.provide(decryptWithoutKey, WebCryptoLive);

// This should fail with the specific error message
await expect(Effect.runPromise(program)).rejects.toThrow(
'Decryption key required but not provided'
);
});

it('CRITICAL: encrypt function should work with auto-generated keys', async () => {
// Test that encryption still works correctly with auto-generated keys
const mockCryptoKey = { type: 'secret', algorithm: { name: 'AES-GCM' } };
mockGenerateKey.mockResolvedValue(mockCryptoKey as any);

// Mock encrypt to return consistent encrypted data
const mockEncryptedBuffer = new ArrayBuffer(48); // 32 bytes data + 16 bytes tag
const mockView = new Uint8Array(mockEncryptedBuffer);
mockView.fill(1, 0, 32); // Mock encrypted data
mockView.fill(2, 32, 48); // Mock auth tag
mockEncrypt.mockResolvedValue(mockEncryptedBuffer);

// Test the convenience function that generates its own key
const result = await Effect.runPromise(encryptWithAutoKey('test data'));

expect(result).toHaveProperty('encryptedData');
expect(result).toHaveProperty('iv');
expect(result).toHaveProperty('authTag');
expect(result.version).toBe('AES_V1');
expect(result.algorithm).toBe('AES-256-GCM');
});

it('HIGH: should decrypt data successfully when key is provided', async () => {
const mockCryptoKey = { type: 'secret', algorithm: { name: 'AES-GCM' } };
const mockDecryptedBuffer = new TextEncoder().encode('test data');
mockDecrypt.mockResolvedValue(mockDecryptedBuffer.buffer);

const mockEncryptedData = {
encryptedData: 'dGVzdA==',
iv: 'aXZkYXRh',
authTag: 'dGFn',
version: 'AES_V1' as const,
algorithm: 'AES-256-GCM' as const,
};

// Test decryption with a provided key
const decryptWithKey = pipe(
CryptoProvider,
Effect.flatMap((crypto) => crypto.decrypt(mockEncryptedData, mockCryptoKey as CryptoKey))
);

const program = Effect.provide(decryptWithKey, WebCryptoLive);
const result = await Effect.runPromise(program);

expect(result).toBe('test data');
});

it('HIGH: should generate cryptographically secure keys', async () => {
const mockCryptoKey = { type: 'secret', algorithm: { name: 'AES-GCM' } };
mockGenerateKey.mockResolvedValue(mockCryptoKey as any);

const generateKey = pipe(
CryptoProvider,
Effect.flatMap((crypto) => crypto.generateKey())
);

const program = Effect.provide(generateKey, WebCryptoLive);
const result = await Effect.runPromise(program);

expect(result).toBe(mockCryptoKey);
expect(mockGenerateKey).toHaveBeenCalledWith(
{ name: 'AES-GCM', length: 256 },
false, // not extractable
['encrypt', 'decrypt']
);
});

it('MEDIUM: should sanitize error messages in production', async () => {
// Test that error messages are sanitized in production
// Mock the NODE_ENV check in a safe way
vi.stubEnv('NODE_ENV', 'production');

try {
mockGenerateKey.mockRejectedValue(new Error('Detailed internal error'));

const generateKey = pipe(
CryptoProvider,
Effect.flatMap((crypto) => crypto.generateKey())
);

const program = Effect.provide(generateKey, WebCryptoLive);

// In production, error should be sanitized
await expect(Effect.runPromise(program)).rejects.toThrow('Key generation failed');
} finally {
// Restore original environment
vi.unstubAllEnvs();
}
});

it('MEDIUM: should handle encryption errors gracefully', async () => {
const mockCryptoKey = { type: 'secret', algorithm: { name: 'AES-GCM' } };
mockGenerateKey.mockResolvedValue(mockCryptoKey as any);
mockEncrypt.mockRejectedValue(new Error('Crypto API failed'));

const encryptData = pipe(
CryptoProvider,
Effect.flatMap((crypto) => crypto.encrypt('test data'))
);

const program = Effect.provide(encryptData, WebCryptoLive);

await expect(Effect.runPromise(program)).rejects.toThrow('Encryption failed');
});

it('MEDIUM: should handle decryption errors gracefully', async () => {
const mockCryptoKey = { type: 'secret', algorithm: { name: 'AES-GCM' } };
mockDecrypt.mockRejectedValue(new Error('Invalid key'));

const mockEncryptedData = {
encryptedData: 'dGVzdA==',
iv: 'aXZkYXRh',
authTag: 'dGFn',
version: 'AES_V1' as const,
algorithm: 'AES-256-GCM' as const,
};

const decryptData = pipe(
CryptoProvider,
Effect.flatMap((crypto) => crypto.decrypt(mockEncryptedData, mockCryptoKey as CryptoKey))
);

const program = Effect.provide(decryptData, WebCryptoLive);

await expect(Effect.runPromise(program)).rejects.toThrow('Decryption failed');
});

it('LOW: should detect web platform correctly', async () => {
// Test platform detection (addressing the LOW priority issue)
const checkSupport = pipe(
CryptoProvider,
Effect.flatMap((crypto) => crypto.isSupported())
);

const program = Effect.provide(checkSupport, WebCryptoLive);
const result = await Effect.runPromise(program);

expect(result).toBe(true);
});

it('LOW: should improve platform detection logic', async () => {
// Test the improved platform detection functionality
const { PlatformDetector } = await import('@rite/shared-types');

const detectPlatform = pipe(
PlatformDetector,
Effect.flatMap((detector) => detector.detectPlatform())
);

const program = Effect.provide(detectPlatform, WebCryptoLive);
const result = await Effect.runPromise(program);

// Our improved detection should return a valid platform type
expect(['web', 'mobile', 'unknown']).toContain(result);

// In JSDOM test environment, our stricter detection may return 'unknown'
// but that's correct behavior as JSDOM doesn't have all browser APIs
expect(typeof result).toBe('string');
});
});

describe('End-to-End Integration Tests', () => {
it('should complete full encrypt/decrypt cycle with auto-generated key', async () => {
// This test ensures the entire crypto flow works correctly
const plaintext = 'sensitive data for testing';

// Mock successful key generation
const mockCryptoKey = { type: 'secret', algorithm: { name: 'AES-GCM' } };
mockGenerateKey.mockResolvedValue(mockCryptoKey as any);

// Mock successful encryption
const mockEncryptedBuffer = new ArrayBuffer(48);
const mockView = new Uint8Array(mockEncryptedBuffer);
mockView.fill(42, 0, 32); // Mock encrypted data
mockView.fill(24, 32, 48); // Mock auth tag
mockEncrypt.mockResolvedValue(mockEncryptedBuffer);

// Test encryption
const encryptedResult = await Effect.runPromise(encryptWithAutoKey(plaintext));

expect(encryptedResult).toHaveProperty('encryptedData');
expect(encryptedResult).toHaveProperty('iv');
expect(encryptedResult).toHaveProperty('authTag');
expect(encryptedResult.version).toBe('AES_V1');
expect(encryptedResult.algorithm).toBe('AES-256-GCM');

// Verify that the Web Crypto API was called correctly
expect(mockGenerateKey).toHaveBeenCalledWith(
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
expect(mockEncrypt).toHaveBeenCalled();
});
});
});
Loading