Skip to content

Commit

Permalink
fix: add hmac check (#256)
Browse files Browse the repository at this point in the history
* up api

* hotifx

* add app logic

* Update hmac.js

* Update index.js
  • Loading branch information
tangimds authored Jan 23, 2025
1 parent 67f3d4c commit 66cdfd8
Show file tree
Hide file tree
Showing 14 changed files with 470 additions and 1,203 deletions.
4 changes: 3 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"crypto-js": "^4.2.0",
"date-fns": "^2.30.0",
"date-fns-tz": "^1.3.7",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-rate-limit": "^7.5.0",
"helmet": "^4.0.0",
"morgan": "^1.10.0",
"node-cron": "^3.0.2",
Expand All @@ -46,4 +48,4 @@
"node": ">= 14"
},
"packageManager": "[email protected]"
}
}
6 changes: 5 additions & 1 deletion api/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ const DATABASE_URL = process.env.DATABASE_URL;
const CRONJOBS_ENABLED = process.env.CRONJOBS_ENABLED === "true";

const PUSH_NOTIFICATION_GCM_ID = process.env.PUSH_NOTIFICATION_GCM_ID;
const PUSH_NOTIFICATION_APN_KEY = process.env.PUSH_NOTIFICATION_APN_KEY.replace(/\\n/g, "\n");
const PUSH_NOTIFICATION_APN_KEY = process.env.PUSH_NOTIFICATION_APN_KEY?.replace(/\\n/g, "\n");
const PUSH_NOTIFICATION_APN_KEY_ID = process.env.PUSH_NOTIFICATION_APN_KEY_ID;
const PUSH_NOTIFICATION_APN_TEAM_ID = process.env.PUSH_NOTIFICATION_APN_TEAM_ID;

const TIPIMAIL_API_KEY = process.env.TIPIMAIL_API_KEY;
const TIPIMAIL_API_USER = process.env.TIPIMAIL_API_USER;

const HMAC_SECRET = process.env.HMAC_SECRET;

if (process.env.NODE_ENV === "development") {
console.log("✍️ ~CONFIG ", {
PORT,
Expand All @@ -37,6 +39,7 @@ if (process.env.NODE_ENV === "development") {
CRONJOBS_ENABLED,
TIPIMAIL_API_KEY,
TIPIMAIL_API_USER,
HMAC_SECRET,
});
}

Expand All @@ -57,4 +60,5 @@ module.exports = {
PUSH_NOTIFICATION_APN_TEAM_ID,
TIPIMAIL_API_KEY,
TIPIMAIL_API_USER,
HMAC_SECRET,
};
6 changes: 5 additions & 1 deletion api/src/controllers/mail.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ const { TIPIMAIL_API_USER, TIPIMAIL_API_KEY, ENVIRONMENT } = require("../config"
const { catchErrors } = require("../middlewares/errors");
const router = express.Router();
const { capture } = require("../third-parties/sentry");
const { validateHMAC } = require("../middlewares/hmac");
const { mailLimiter } = require("../middlewares/rateLimit");

router.post(
"/",
validateHMAC,
mailLimiter,
catchErrors(async (req, res) => {
let { to, replyTo, replyToName, subject, text, html } = req.body || {};

if (!subject || (!text && !html)) return res.status(400).json({ ok: false, error: "wrong parameters" });

if (!to) {
to = ENVIRONMENT === "development" ? "[email protected]" : "[email protected]";
to = ENVIRONMENT === "development" ? process.env.MAIL_TO_DEV : "[email protected]";
}

if (!replyTo) {
Expand Down
5 changes: 4 additions & 1 deletion api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ app.get("/config", async (req, res) => {
// hello world
const now = new Date();
app.get("/", async (req, res) => {
res.send(`api MSP • ${now.toISOString()}`);
res.send({
name: "api jardin mental",
last_deployed_at: now.toISOString(),
});
});

// Add header with API version to compare with client.
Expand Down
32 changes: 32 additions & 0 deletions api/src/middlewares/hmac.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const crypto = require("crypto");
const { HMAC_SECRET } = require("../config");

const validateHMAC = (req, res, next) => {
const secret = HMAC_SECRET;
if (!secret) {
return next();
}
const { "x-signature": signature, "x-timestamp": timestamp } = req.headers;

if (!signature || !timestamp) {
return res.status(400).json({ error: "Missing signature or timestamp" });
}

const now = Date.now();
if (Math.abs(now - timestamp) > 5 * 60 * 1000) {
// Vérifie un délai de 5 minutes
return res.status(400).json({ error: "Timestamp expired" });
}

const payload = JSON.stringify(req.body);
const dataToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto.createHmac("sha256", secret).update(dataToSign).digest("hex");

if (signature !== expectedSignature) {
return res.status(401).json({ error: "Invalid signature" });
}

next();
};

module.exports = { validateHMAC };
25 changes: 25 additions & 0 deletions api/src/middlewares/rateLimit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const rateLimit = require("express-rate-limit");

// Middleware de rate limiting pour la route /reminder
const reminderLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // Fenêtre de 15 minutes
max: 10, // Maximum de 10 requêtes dans cette période
keyGenerator: (req) => req.body.pushNotifToken || req.ip, // Limite basée sur pushNotifToken ou IP
message: {
ok: false,
error: "Too many requests. Please try again later.",
},
});

// Middleware de rate limiting pour la route /mail
const mailLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // Fenêtre de 1 minute
max: 5, // Maximum de 5 mails par minute
keyGenerator: (req) => req.ip, // Limite basée sur l'IP uniquement
message: {
ok: false,
error: "Too many emails sent. Please wait a moment and try again.",
},
});

module.exports = { reminderLimiter, mailLimiter };
18 changes: 18 additions & 0 deletions api/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -357,10 +357,12 @@ __metadata:
bcryptjs: ^2.4.3
cors: ^2.8.5
cross-env: ^7.0.3
crypto-js: ^4.2.0
date-fns: ^2.30.0
date-fns-tz: ^1.3.7
dotenv: ^16.3.1
express: ^4.18.2
express-rate-limit: ^7.5.0
helmet: ^4.0.0
morgan: ^1.10.0
node-cron: ^3.0.2
Expand Down Expand Up @@ -841,6 +843,13 @@ __metadata:
languageName: node
linkType: hard

"crypto-js@npm:^4.2.0":
version: 4.2.0
resolution: "crypto-js@npm:4.2.0"
checksum: f051666dbc077c8324777f44fbd3aaea2986f198fe85092535130d17026c7c2ccf2d23ee5b29b36f7a4a07312db2fae23c9094b644cc35f7858b1b4fcaf27774
languageName: node
linkType: hard

"crypto-random-string@npm:^2.0.0":
version: 2.0.0
resolution: "crypto-random-string@npm:2.0.0"
Expand Down Expand Up @@ -1105,6 +1114,15 @@ __metadata:
languageName: node
linkType: hard

"express-rate-limit@npm:^7.5.0":
version: 7.5.0
resolution: "express-rate-limit@npm:7.5.0"
peerDependencies:
express: ^4.11 || 5 || ^5.0.0-beta.1
checksum: 2807341039c111eed292e28768aff3c69515cb96ff15799976a44ead776c41931d6947fe3da3cea021fa0490700b1ab468b4832bbed7d231bed63c195d22b959
languageName: node
linkType: hard

"express@npm:^4.18.2":
version: 4.18.2
resolution: "express@npm:4.18.2"
Expand Down
14 changes: 4 additions & 10 deletions app/eas.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,24 @@
"distribution": "internal",
"env": {
"EXPO_PUBLIC_SCHEME": "http",
"EXPO_PUBLIC_HOST": "localhost",
"EXPO_PUBLIC_APP_ENV": "development",
"EXPO_PUBLIC_TIPIMAIL_API_KEY": "1234567890",
"EXPO_PUBLIC_TIPIMAIL_API_USER": "1234567890"
"EXPO_PUBLIC_HOST": "localhost:3000",
"EXPO_PUBLIC_APP_ENV": "development"
}
},
"preview": {
"distribution": "internal",
"env": {
"EXPO_PUBLIC_SCHEME": "https",
"EXPO_PUBLIC_HOST": "api-monsuivipsy.fabrique.social.gouv.fr",
"EXPO_PUBLIC_APP_ENV": "production",
"EXPO_PUBLIC_TIPIMAIL_API_KEY": "1234567890",
"EXPO_PUBLIC_TIPIMAIL_API_USER": "1234567890"
"EXPO_PUBLIC_APP_ENV": "production"
}
},
"production": {
"autoIncrement": true,
"env": {
"EXPO_PUBLIC_SCHEME": "https",
"EXPO_PUBLIC_HOST": "api-monsuivipsy.fabrique.social.gouv.fr",
"EXPO_PUBLIC_APP_ENV": "production",
"EXPO_PUBLIC_TIPIMAIL_API_KEY": "1234567890",
"EXPO_PUBLIC_TIPIMAIL_API_USER": "1234567890"
"EXPO_PUBLIC_APP_ENV": "production"
},
"android": {
"credentialsSource": "local"
Expand Down
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@react-navigation/material-top-tabs": "^6.6.14",
"@react-navigation/native": "^6.1.18",
"@react-navigation/stack": "^6.4.1",
"crypto-js": "^4.2.0",
"csv-parser": "^3.0.0",
"date-fns": "^2.16.1",
"dayjs": "^1.11.3",
Expand Down
9 changes: 3 additions & 6 deletions app/src/config.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
// import envConfig from "react-native-config";
import {version, buildNumber} from '../package.json';

// const SCHEME = envConfig.SCHEME;
// const HOST = envConfig.HOST;
// const APP_ENV = envConfig.APP_ENV;
const SCHEME = process.env.EXPO_PUBLIC_SCHEME;
const HOST = process.env.EXPO_PUBLIC_HOST;
const APP_ENV = process.env.EXPO_PUBLIC_APP_ENV;
const VERSION = version;
const BUILD_NUMBER = buildNumber;
// const TIPIMAIL_API_KEY = envConfig.TIPIMAIL_API_KEY;
// const TIPIMAIL_API_USER = envConfig.TIPIMAIL_API_USER;

export {SCHEME, HOST, APP_ENV, VERSION, BUILD_NUMBER};
const HMAC_SECRET = process.env.EXPO_PUBLIC_HMAC_SECRET;

export {SCHEME, HOST, APP_ENV, VERSION, BUILD_NUMBER, HMAC_SECRET};
4 changes: 3 additions & 1 deletion app/src/scenes/presentation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const PdfViewer = ({navigation}) => {
<Text className="text-gray-700 text-sm">Retour</Text>
</Pressable>
</View>
<WebView style={{width: '100%', height: '100%'}} source={{uri}} originWhitelist={['*']} javaScriptEnabled={true} domStorageEnabled={true} />
<View className="flex-1">
<WebView style={{width: '100%', height: '100%'}} source={{uri}} originWhitelist={['*']} javaScriptEnabled={true} domStorageEnabled={true} />
</View>
</SafeAreaView>
);
};
Expand Down
47 changes: 28 additions & 19 deletions app/src/services/api.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import URI from "urijs";
import { Alert, Linking, Platform } from "react-native";
import NetInfo from "@react-native-community/netinfo";
import fetchRetry from "fetch-retry";
import URI from 'urijs';
import {Alert, Linking, Platform} from 'react-native';
import NetInfo from '@react-native-community/netinfo';
import fetchRetry from 'fetch-retry';

import { SCHEME, HOST, BUILD_NUMBER } from "../config";
import matomo from "./matomo";
import {SCHEME, HOST, BUILD_NUMBER} from '../config';
import matomo from './matomo';
import {generateHMAC} from '../utils/generateHmac';

export const checkNetwork = async (test = false) => {
const isConnected = await NetInfo.fetch().then((state) => state.isConnected);
const isConnected = await NetInfo.fetch().then(state => state.isConnected);
if (!isConnected || test) {
await new Promise((res) => setTimeout(res, 1500));
Alert.alert("Pas de réseau", "Veuillez vérifier votre connexion");
await new Promise(res => setTimeout(res, 1500));
Alert.alert('Pas de réseau', 'Veuillez vérifier votre connexion');
return false;
}
return true;
Expand All @@ -23,13 +24,21 @@ class ApiService {
getUrl = (path, query) => {
return new URI().host(this.host).scheme(this.scheme).path(path).setSearch(query).toString();
};
execute = async ({ method = "GET", path = "", query = {}, headers = {}, body = null }) => {
execute = async ({method = 'GET', path = '', query = {}, headers = {}, body = null}) => {
try {
if (body) {
const {signature, timestamp} = generateHMAC(body);
headers = {
...headers,
'x-signature': signature,
'x-timestamp': timestamp,
};
}
const config = {
method,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
'Content-Type': 'application/json',
Accept: 'application/json',
appversion: BUILD_NUMBER,
appdevice: Platform.OS,
currentroute: this.navigation?.getCurrentRoute?.()?.name,
Expand All @@ -41,7 +50,7 @@ class ApiService {
};

const url = this.getUrl(path, query);
console.log("api: ", { url });
// console.log('api: ', {url});
const canFetch = await checkNetwork();
if (!canFetch) return;

Expand All @@ -65,15 +74,15 @@ class ApiService {

needUpdate = false;

get = async (args) => this.execute({ method: "GET", ...args });
post = async (args) => this.execute({ method: "POST", ...args });
put = async (args) => this.execute({ method: "PUT", ...args });
delete = async (args) => this.execute({ method: "DELETE", ...args });
get = async args => this.execute({method: 'GET', ...args});
post = async args => this.execute({method: 'POST', ...args});
put = async args => this.execute({method: 'PUT', ...args});
delete = async args => this.execute({method: 'DELETE', ...args});

handleInAppMessage = (inAppMessage) => {
handleInAppMessage = inAppMessage => {
const [title, subTitle, actions = [], options = {}] = inAppMessage;
if (!actions || !actions.length) return Alert.alert(title, subTitle);
const actionsWithNavigation = actions.map((action) => {
const actionsWithNavigation = actions.map(action => {
if (action.navigate) {
action.onPress = () => {
API.navigation.navigate(...action.navigate);
Expand Down
10 changes: 10 additions & 0 deletions app/src/utils/generateHmac.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import crypto from "crypto-js";
import { HMAC_SECRET } from "../config";

export const generateHMAC = (payload) => {
const timestamp = Date.now().toString();
const dataToSign = `${timestamp}:${JSON.stringify(payload)}`;
const signature = crypto.HmacSHA256(dataToSign, HMAC_SECRET).toString();

return { signature, timestamp };
};
Loading

0 comments on commit 66cdfd8

Please sign in to comment.