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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ jobs:
- name: TypeScript type check
run: pnpm run typecheck

security:
name: Security Scan
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup ThumbCode environment
uses: ./.github/actions/setup-thumbcode

- name: Run vulnerability scan
run: pnpm run audit:prod

test:
name: Run Tests
runs-on: ubuntu-latest
Expand Down
56 changes: 56 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Security Policy

This document outlines the security procedures and policies for the ThumbCode project.

## Reporting a Vulnerability

If you discover a security vulnerability, please report it to us as soon as possible. We take all security reports seriously and will investigate them promptly.

To report a vulnerability, please email us at `security@thumbcode.com` with the following information:

- A detailed description of the vulnerability, including the steps to reproduce it.
- The version of the application you are using.
- Any proof-of-concept code or screenshots that can help us understand the issue.

We will acknowledge your report within 48 hours and will keep you informed of our progress. We ask that you do not disclose the vulnerability publicly until we have had a chance to address it.

## Security Features

The ThumbCode application includes several security features to protect user data and ensure the integrity of the application.

### Credential Storage

- All sensitive credentials, such as API keys and tokens, are stored using `expo-secure-store`, which leverages hardware-backed encryption on both iOS and Android.
- Credentials are encrypted at rest and are only accessible when the device is unlocked.
- Biometric authentication (Face ID or fingerprint) is required to access or modify credentials.

### API Communication

- All API communication is protected by TLS encryption.
- Certificate pinning is implemented to prevent man-in-the-middle attacks. The application will only trust the public keys of the pre-configured API endpoints.
- All requests to the `mcp_server` are signed with an HMAC-SHA256 signature to prevent tampering and ensure authenticity.

### Input Sanitization

- All user-provided input, including API keys and project information, is sanitized and validated using `zod` before being stored or used.
- This helps to prevent a range of injection attacks and ensures the integrity of the data.

### Runtime Security

- The application includes runtime security checks to detect if it is running on a rooted or jailbroken device.
- If a compromised environment is detected, the user will be alerted and the application will exit.

### Web Security

- The web version of the application includes a strict Content Security Policy (CSP) to mitigate cross-site scripting (XSS) and other injection attacks.
- Other security headers, such as `X-Content-Type-Options`, `X-Frame-Options`, and `Referrer-Policy`, are also in place.

## Secure Development Practices

- All dependencies are regularly scanned for vulnerabilities using `pnpm audit`.
- The principle of least privilege is followed when requesting permissions.
- All code is reviewed for security vulnerabilities before being merged into the main branch.

## Security Audits

The application will undergo regular security audits to identify and address any potential vulnerabilities. The results of these audits will be made available to the public.
2 changes: 2 additions & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export default ({ config }: ConfigContext): ExpoConfig => {
bundleIdentifier:
appEnv === 'production' ? 'com.thumbcode.app' : `com.thumbcode.app.${appEnv}`,
buildNumber: '1',
// Permission Review: Biometric permissions are essential for securing user credentials.
infoPlist: {
NSFaceIDUsageDescription: 'ThumbCode uses Face ID to secure your API keys and credentials.',
ITSAppUsesNonExemptEncryption: false,
Expand All @@ -98,6 +99,7 @@ export default ({ config }: ConfigContext): ExpoConfig => {
},
package: appEnv === 'production' ? 'com.thumbcode.app' : `com.thumbcode.app.${appEnv}`,
versionCode: 1,
// Permission Review: Biometric permissions are essential for securing user credentials.
permissions: ['USE_BIOMETRIC', 'USE_FINGERPRINT'],
},

Expand Down
26 changes: 12 additions & 14 deletions app/(onboarding)/api-keys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Collects AI provider API keys (Anthropic/OpenAI).
*/

import { CredentialService } from '@thumbcode/core/src/credentials/CredentialService';
import { useRouter } from 'expo-router';
import { useState } from 'react';
import { ActivityIndicator, Pressable, ScrollView, View } from 'react-native';
Expand Down Expand Up @@ -36,21 +37,13 @@ export default function ApiKeysScreen() {
});

const validateAnthropicKey = async (key: string) => {
if (!key.startsWith('sk-ant-')) {
return { isValid: false, error: 'Key should start with sk-ant-' };
}
// TODO: Actual API validation
await new Promise((resolve) => setTimeout(resolve, 1000));
return { isValid: true };
const result = await CredentialService.validateCredential('anthropic', key);
return { isValid: result.isValid, error: result.message };
};

const validateOpenAIKey = async (key: string) => {
if (!key.startsWith('sk-')) {
return { isValid: false, error: 'Key should start with sk-' };
}
// TODO: Actual API validation
await new Promise((resolve) => setTimeout(resolve, 1000));
return { isValid: true };
const result = await CredentialService.validateCredential('openai', key);
return { isValid: result.isValid, error: result.message };
};

const handleAnthropicChange = async (value: string) => {
Expand Down Expand Up @@ -89,8 +82,13 @@ export default function ApiKeysScreen() {
router.push('/(onboarding)/create-project');
};

const handleContinue = () => {
// TODO: Save keys to SecureStore
const handleContinue = async () => {
if (anthropicKey.isValid) {
await CredentialService.store('anthropic', anthropicKey.key);
}
if (openaiKey.isValid) {
await CredentialService.store('openai', openaiKey.key);
}
router.push('/(onboarding)/create-project');
};

Expand Down
21 changes: 17 additions & 4 deletions app/(onboarding)/create-project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* Helps user create their first project by connecting a repository.
*/

import { CredentialService } from '@thumbcode/core/src/credentials/CredentialService';
import * as Crypto from 'expo-crypto';
import { useRouter } from 'expo-router';
import { useState } from 'react';
import { ActivityIndicator, Pressable, ScrollView, View } from 'react-native';
Expand Down Expand Up @@ -73,10 +75,21 @@ export default function CreateProjectScreen() {
if (isLoading || !selectedRepo || !projectName) return;

setIsLoading(true);
// TODO: Create project via service
await new Promise((resolve) => setTimeout(resolve, 1500));
setIsLoading(false);
router.push('/(onboarding)/complete');
try {
// Generate and store the signing secret
const secret = Crypto.getRandomBytes(32);
await CredentialService.store('mcp_signing_secret', secret.toString());

// TODO: Create project via service
await new Promise((resolve) => setTimeout(resolve, 1500));

router.push('/(onboarding)/complete');
} catch (error) {
console.error('Failed to create project:', error);
// Optionally, show an error message to the user
} finally {
setIsLoading(false);
}
};

const canCreate = selectedRepo && projectName.trim().length > 0;
Expand Down
33 changes: 16 additions & 17 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,20 @@
* Root Layout
*
* Main app layout that provides navigation stack, theme providers,
* global context, and error handling for ThumbCode.
* and global context for ThumbCode.
*/

import { certificatePinningService } from '@thumbcode/core/src/security/CertificatePinningService';
import { runtimeSecurityService } from '@thumbcode/core/src/security/RuntimeSecurityService';
import { Stack, useRouter, useSegments } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { useEffect } from 'react';
import { ActivityIndicator, View } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { ErrorBoundary } from '@/components/error';
import { OnboardingProvider, useOnboarding } from '@/contexts/onboarding';
import { logger, setupGlobalErrorHandlers } from '@/lib';
import '../global.css';

// Initialize global error handlers
setupGlobalErrorHandlers();
logger.info('ThumbCode app started');

function RootLayoutNav() {
const { isLoading, hasCompletedOnboarding } = useOnboarding();
const segments = useSegments();
Expand Down Expand Up @@ -90,16 +86,19 @@ function RootLayoutNav() {
}

export default function RootLayout() {
useEffect(() => {
certificatePinningService.initialize();
runtimeSecurityService.checkAndHandleRootedStatus();
}, []);

return (
<ErrorBoundary>
<OnboardingProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<StatusBar style="light" />
<RootLayoutNav />
</SafeAreaProvider>
</GestureHandlerRootView>
</OnboardingProvider>
</ErrorBoundary>
<OnboardingProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<StatusBar style="light" />
<RootLayoutNav />
</SafeAreaProvider>
</GestureHandlerRootView>
</OnboardingProvider>
);
}
6 changes: 6 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
<title>ThumbCode - Code with your thumbs</title>
<meta name="description" content="A decentralized multi-agent mobile development platform. Full git workflow, AI-powered agents, from your phone.">

<!-- Security Headers -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self' https://api.github.com https://api.anthropic.com https://api.openai.com;">
<meta http-equiv="X-Content-Type-Options" content="nosniff">
<meta http-equiv="X-Frame-Options" content="DENY">
<meta name="referrer" content="no-referrer">

<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
Expand Down
2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ module.exports = {
testMatch: [
'<rootDir>/src/**/__tests__/**/*.test.{ts,tsx}',
'<rootDir>/packages/state/src/__tests__/**/*.test.{ts,tsx}',
'<rootDir>/packages/core/src/__tests__/**/*.test.{ts,tsx}',
],
// Exclude packages with their own jest config (e.g., agent-intelligence uses ts-jest)
testPathIgnorePatterns: ['/node_modules/', '/packages/agent-intelligence/'],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'app/**/*.{ts,tsx}',
'packages/state/src/**/*.{ts,tsx}',
'packages/core/src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/types/**/*',
'!**/__tests__/**/*',
Expand Down
5 changes: 5 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ jest.mock('expo-local-authentication', () => ({
},
}));

// Mock expo-device
jest.mock('expo-device', () => ({
isRootedExperimentalAsync: jest.fn(() => Promise.resolve(false)),
}));

// Mock expo-router
jest.mock('expo-router', () => ({
useRouter: jest.fn(),
Expand Down
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@
"docs:preview": "vitepress preview docs",
"generate:tokens": "pnpm --filter @thumbcode/dev-tools run generate:tokens",
"generate:icons": "pnpm --filter @thumbcode/dev-tools run generate:icons",
"generate:all": "pnpm --filter @thumbcode/dev-tools run generate:all"
"generate:all": "pnpm --filter @thumbcode/dev-tools run generate:all",
"audit:prod": "pnpm audit --prod --audit-level=moderate"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.32.0",
"@react-native-async-storage/async-storage": "^2.0.0",
"@react-native-community/netinfo": "^11.4.1",
"react-native-ssl-public-key-pinning": "^1.2.6",
"@react-navigation/native": "^7.0.0",
"@thumbcode/config": "workspace:*",
"@thumbcode/core": "workspace:*",
Expand Down Expand Up @@ -102,5 +104,10 @@
"typescript": "~5.6.0",
"vitepress": "^1.5.0"
},
"pnpm": {
"overrides": {
"tar": ">=7.5.3"
}
},
"private": true
}
27 changes: 27 additions & 0 deletions packages/core/src/__tests__/CredentialService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CredentialService } from '../credentials/CredentialService';

describe('CredentialService', () => {
it('should be defined', () => {
expect(CredentialService).toBeDefined();
});

describe('store', () => {
it('should reject an invalid Anthropic key', async () => {
const result = await CredentialService.store('anthropic', 'invalid-key');
expect(result.isValid).toBe(false);
expect(result.message).toBe('Invalid credential format');
});

it('should reject an invalid OpenAI key', async () => {
const result = await CredentialService.store('openai', 'invalid-key');
expect(result.isValid).toBe(false);
expect(result.message).toBe('Invalid credential format');
});

it('should reject an invalid GitHub token', async () => {
const result = await CredentialService.store('github', 'invalid-token');
expect(result.isValid).toBe(false);
expect(result.message).toBe('Invalid credential format');
});
});
});
26 changes: 26 additions & 0 deletions packages/core/src/__tests__/RequestSigningService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { requestSigningService } from '../security/RequestSigningService';
import { CredentialService } from '../credentials/CredentialService';

jest.mock('../credentials/CredentialService');

describe('RequestSigningService', () => {
it('should be defined', () => {
expect(requestSigningService).toBeDefined();
});

describe('signRequest', () => {
it('should return null if no signing secret is found', async () => {
(CredentialService.retrieve as jest.Mock).mockResolvedValue({ secret: null });
const headers = await requestSigningService.signRequest('https://mcp.thumbcode.com/test', 'POST', '{}');
expect(headers).toBeNull();
});

it('should return the correct signing headers', async () => {
(CredentialService.retrieve as jest.Mock).mockResolvedValue({ secret: 'test-secret' });
const headers = await requestSigningService.signRequest('https://mcp.thumbcode.com/test', 'POST', '{}');
expect(headers).toHaveProperty('X-Request-Timestamp');
expect(headers).toHaveProperty('X-Request-Nonce');
expect(headers).toHaveProperty('X-Request-Signature');
});
});
});
Loading