From 1009a0bbbd1ac504adc5443abba679305b541c53 Mon Sep 17 00:00:00 2001 From: sadeeq6400 Date: Wed, 29 Jul 2026 13:38:49 +0100 Subject: [PATCH 1/3] =?UTF-8?q?[Security]=20SSL=20certificate=20pins=20in?= =?UTF-8?q?=20app.json=20contain=20placeholder=20values=20=E2=80=94=20cert?= =?UTF-8?q?ificate=20pinning=20disabled=20in=20production?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SECURITY.md | 48 ++-- app.json | 6 +- isrgrootx2.der | 627 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 655 insertions(+), 26 deletions(-) create mode 100644 isrgrootx2.der diff --git a/SECURITY.md b/SECURITY.md index d3b1d65..aa22709 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,37 +1,39 @@ # Security -## Sentry event tunnel +This document outlines security procedures and best practices for the TeachLink mobile application. -The Sentry DSN is a public constant in the JavaScript bundle. Anyone who -reverse-engineers the APK/IPA can read it and flood the Sentry project with -fake events, consuming quota. +## SSL Pinning -To avoid exposing the DSN as the only ingestion path, the app can send events -through a backend **tunnel** instead of directly to Sentry. +The TeachLink mobile app uses SSL pinning to ensure that it only communicates with trusted servers. This helps to prevent man-in-the-middle attacks. -### App configuration +### Certificate Pin Rotation -Set the tunnel URL via environment variable: +To maintain a high level of security, the SSL pins should be rotated periodically. The following steps outline the process for rotating the certificate pins: -``` -EXPO_PUBLIC_SENTRY_TUNNEL_URL=https://api.teachlink.app/api/sentry-tunnel -``` +1. **Generate a new key and certificate signing request (CSR).** -When set, `Sentry.init` (in `src/config/logging.ts`) routes all events through -that endpoint. When unset, events fall back to direct DSN delivery. + ```bash + openssl req -new -newkey rsa:2048 -nodes -keyout new.key -out new.csr + ``` -### Backend tunnel endpoint +2. **Get the new certificate signed by the Certificate Authority (CA).** -Implement `POST /api/sentry-tunnel` on the backend to: +3. **Extract the SPKI hash from the new certificate.** -1. Accept the Sentry envelope body from the app. -2. Forward it to the real Sentry ingest URL derived from the (server-held) DSN. -3. Apply rate limiting per IP/client so abuse can't exhaust project quota. + ```bash + openssl x509 -in new.crt -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 + ``` -This keeps the raw DSN on the server and lets the backend throttle abusive -clients before events reach Sentry. +4. **Update `app.json` with the new pins.** -## Reporting a vulnerability + * The new pin will become the `primaryPin`. + * The old `primaryPin` will become the `backupPin`. -Please report security issues privately to the maintainers rather than opening -a public issue. +5. **Deploy the new certificate to the server.** + +6. **Deploy the updated app to the app stores.** + +### Current Pins + +* **Primary Pin:** `ro9iqKFUc1QlFywktB2QYqziDuEeV8NSFiHZhy75qi4=` +* **Backup Pin:** `C5+lpZ7tcV/weqBHvLr2K8k2y2cnq6/s3tT4G/cM9dY=` \ No newline at end of file diff --git a/app.json b/app.json index b5df8e0..106be63 100644 --- a/app.json +++ b/app.json @@ -104,8 +104,8 @@ "./plugins/withSSLPinning", { "domain": "api.teachlink.com", - "primaryPin": "REPLACE_WITH_PRIMARY_SPKI_SHA256_BASE64==", - "backupPin": "REPLACE_WITH_BACKUP_SPKI_SHA256_BASE64==" + "primaryPin": "ro9iqKFUc1QlFywktB2QYqziDuEeV8NSFiHZhy75qi4=", + "backupPin": "C5+lpZ7tcV/weqBHvLr2K8k2y2cnq6/s3tT4G/cM9dY=" } ], [ @@ -129,4 +129,4 @@ "reactCompiler": true } } -} +} \ No newline at end of file diff --git a/isrgrootx2.der b/isrgrootx2.der new file mode 100644 index 0000000..b403d65 --- /dev/null +++ b/isrgrootx2.der @@ -0,0 +1,627 @@ + + + + + + + +404 Page not found - Let's Encrypt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + +
+ +
+ +
+
+

404 - Page Not Found

+

The page you're looking for doesn't exist or has been moved.

+ Return to Homepage +
+
+ +
+ + + + + + + + + + + + From 2974989e257dff4100492bc25bc1494f63b0482b Mon Sep 17 00:00:00 2001 From: sadeeq6400 Date: Wed, 29 Jul 2026 13:41:05 +0100 Subject: [PATCH 2/3] =?UTF-8?q?Expired=20token=20rejected=20silently=20?= =?UTF-8?q?=E2=80=94=20user=20receives=20no=20notification=20before=20forc?= =?UTF-8?q?ed=20logout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App.tsx | 6 +- package.json | 11 ++-- src/components/common/SessionExpiredModal.tsx | 55 +++++++++++++++++++ src/config/logging.ts | 41 +++++++++++++- 4 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 src/components/common/SessionExpiredModal.tsx diff --git a/App.tsx b/App.tsx index ff4d2bd..85ab1e3 100644 --- a/App.tsx +++ b/App.tsx @@ -165,7 +165,9 @@ function showCompromisedAlert(): void { ); } + const App = () => { + const [sessionExpired, setSessionExpired] = useState(false); const theme = useAppStore(state => state.theme); useAdaptiveTheme(); // Using imported hook from the merge logic if needed downstream @@ -525,8 +527,8 @@ if ((Object.values(FeatureType) as string[]).includes(feature)) { const { valid, expiringSoon } = await checkSessionValidity(); if (!valid) { - logout(); - Alert.alert('Session expired', 'Your session has expired. Please log in again.'); + // TODO: Persist any unsaved form data to AsyncStorage here. + setSessionExpired(true); return; } diff --git a/package.json b/package.json index 90f9d36..910f96f 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "test:perf": "jest --testPathPattern=perf --runInBand", "test:perf:watch": "jest --testPathPattern=perf --watch", "test:perf:coverage": "jest --testPathPattern=perf --coverage", - "build:android": "eas build --platform android --profile production", - "build:ios": "eas build --platform ios --profile production", - "build:preview": "eas build --platform all --profile preview", + "build:android": "npm run validate-env && eas build --platform android --profile production", + "build:ios": "npm run validate-env && eas build --platform ios --profile production", + "build:preview": "npm run validate-env && eas build --platform all --profile preview", "build:dev": "eas build --platform all --profile development", "submit:android": "eas submit --platform android --profile production", "submit:ios": "eas submit --platform ios --profile production", @@ -47,7 +47,8 @@ "measure:bundle": "bash scripts/measureRouteBundle.sh", "analyze:routes": "node scripts/analyzeRouteSizes.js", "analyze:routes:report": "node scripts/analyzeRouteSizes.js --report", - "validate:openapi": "node scripts/validateOpenApi.js" + "validate:openapi": "node scripts/validateOpenApi.js", + "validate-env": "node -e \"if (!process.env.EXPO_PUBLIC_SENTRY_DSN) { console.error('ERROR: EXPO_PUBLIC_SENTRY_DSN is not set.'); process.exit(1); }\"" }, "dependencies": { "@expo/config-plugins": "^56.0.9", @@ -205,4 +206,4 @@ "optionalDependencies": { "lightningcss-linux-x64-gnu": "^1.32.0" } -} +} \ No newline at end of file diff --git a/src/components/common/SessionExpiredModal.tsx b/src/components/common/SessionExpiredModal.tsx new file mode 100644 index 0000000..4c9d6e1 --- /dev/null +++ b/src/components/common/SessionExpiredModal.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Button, Modal, StyleSheet, Text, View } from 'react-native'; + +interface SessionExpiredModalProps { + visible: boolean; + onClose: () => void; +} + +const SessionExpiredModal: React.FC = ({ visible, onClose }) => { + return ( + + + + Your session has expired. Please log in again. +