[core] Copy security file to WAHA Core

fix #1069
This commit is contained in:
devlikepro committed 2025-06-25 13:38:48 +07:00
1 parent 3b6fa3343d
commit 75b06d3ba1
4 files changed
+135

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
import * as url from 'url';
import { validateApiKey } from './apiKey.strategy';
export class WebSocketAuth {
private key: string;
constructor() {
this.key = process.env.WHATSAPP_API_KEY || '';
}
verifyClient = (info: any, callback: any) => {
if (!this.key) {
callback(true);
return;
}
// Do something with the info
let query = url.parse(info.req.url, true).query;
// case insensitive
query = Object.keys(query).reduce((acc, key) => {
acc[key.toLowerCase()] = query[key];
return acc;
}, {});
const apiKey = query['x-api-key'];
// @ts-ignore
const isValid = validateApiKey(apiKey, this.key);
callback(isValid);
};
}
+56
View File
@@ -0,0 +1,56 @@
import { Injectable, Logger } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import * as crypto from 'crypto';
import { HeaderAPIKeyStrategy } from 'passport-headerapikey';
import { WhatsappConfigService } from '../../config.service';
/**
* Securely validates API key using constant-time comparison
* @param providedKey The key provided in the request
* @param storedKey The key stored in the configuration
* @returns boolean indicating if the keys match
*/
export function validateApiKey(
providedKey: string,
storedKey: string | undefined,
): boolean {
if (!storedKey || !providedKey) {
return false;
}
try {
// Convert strings to buffers for constant-time comparison
const providedKeyBuffer = Buffer.from(providedKey);
const storedKeyBuffer = Buffer.from(storedKey);
// If lengths are different, return false but use a dummy comparison to prevent timing attacks
if (providedKeyBuffer.length !== storedKeyBuffer.length) {
// Create a dummy buffer of the same length as the provided key
const dummyBuffer = Buffer.alloc(providedKeyBuffer.length);
// Perform comparison with dummy buffer to maintain constant time
crypto.timingSafeEqual(providedKeyBuffer, dummyBuffer);
return false;
}
// Perform constant-time comparison
return crypto.timingSafeEqual(providedKeyBuffer, storedKeyBuffer);
} catch (error) {
return false;
}
}
@Injectable()
export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy) {
constructor(private config: WhatsappConfigService) {
super({ header: 'X-Api-Key', prefix: '' }, true, (apikey, done) => {
const isValid = validateApiKey(apikey, this.config.getApiKey());
return done(isValid);
});
}
validate(apikey: string, done: (result: boolean) => void): void {
const isValid = validateApiKey(apikey, this.config.getApiKey());
return done(isValid);
}
}
+28
View File
@@ -0,0 +1,28 @@
import {
Injectable,
NestMiddleware,
UnauthorizedException,
} from '@nestjs/common';
import * as passport from 'passport';
import { WhatsappConfigService } from '../../config.service';
@Injectable()
export class AuthMiddleware implements NestMiddleware {
constructor(private config: WhatsappConfigService) {}
use(req: any, res: any, next: () => void) {
// No api key - skip the validation path
if (!this.config.getApiKey()) {
next();
return;
}
passport.authenticate('headerapikey', { session: false }, (value) => {
if (!value) {
throw new UnauthorizedException();
}
next();
})(req, res, next);
}
}
+21
View File
@@ -0,0 +1,21 @@
import * as basicAuth from 'express-basic-auth';
export function BasicAuthFunction(username, password, exclude: string[] = []) {
function authFunction(req, res, next) {
const ignore = exclude.filter((url) => req.url.startsWith(url)).length > 0;
if (ignore) {
next();
return;
}
const auth = basicAuth({
challenge: true,
users: {
[username]: password,
},
});
auth(req, res, next);
}
return authFunction;
}