import { RedisClient, RedisService } from "../services/redis.service"; import { DVCheckCmd, DVCheckResult, DVHash, DVRedisKey, FraudType, Type, } from "../models/dvFraud.model"; import { config } from "../utils/config"; import logger from "../utils/logger"; import crypto from "crypto"; const psl = require("psl"); export class DVFraudService { private readonly redisClientApp: RedisClient; private readonly redisClientDevice: RedisClient; public readonly activeFraudTypes = new Set([ FraudType.APP_BUNDLE, FraudType.DOMAIN, FraudType.SUBDOMAIN, FraudType.IP_UA, FraudType.IP, ]); constructor() { this.redisClientApp = RedisService.getClient("dv_app", true, parseInt(config.REDIS_APP_DB)); this.redisClientDevice = RedisService.getClient( "dv_device", true, parseInt(config.REDIS_DEVICE_DB) ); } getAppCmd(appId: string | null | undefined): DVCheckCmd { return { fraudType: FraudType.APP_BUNDLE, type: Type.App, toCheck: appId, }; } getWebCmds(domain: string | null | undefined): DVCheckCmd[] { const checkCmds: DVCheckCmd[] = []; const parsed = psl.parse(domain ?? ""); if (parsed.subdomain) { // check subdomain checkCmds.push({ fraudType: FraudType.SUBDOMAIN, type: Type.App, toCheck: parsed.input, }); } // check domain checkCmds.push({ fraudType: FraudType.DOMAIN, type: Type.App, toCheck: parsed.domain, }); return checkCmds; } /** * Hashes given key and checks if given key exists in redis. * @return {Promise} * @private * @param checkCmd */ async isBlacklisted(checkCmd: DVCheckCmd): Promise { const hash: DVHash = DVFraudService.hashDV(checkCmd.toCheck ?? ""); const keys: DVRedisKey = DVFraudService.getDVKeys(hash.bytes); const redisClient = checkCmd.type === Type.Device ? this.redisClientDevice : this.redisClientApp; logger.debug( () => `[DV FRAUD ${checkCmd.type}] key1: ${keys.key1.toString("hex")}, key2: ${keys.key2.toString( "hex" )}` ); return { fraudType: checkCmd.fraudType, isFraud: await redisClient.hexists(keys.key1, keys.key2), hash, }; } isActiveFraudType(t: FraudType): boolean { return this.activeFraudTypes.has(t); } isRedisConnected(): boolean { return this.redisClientApp.isConnected() && this.redisClientDevice.isConnected(); } /** * Hashes given string in a DV-like way. * @param toHash * @return {Buffer} a 8-bytes array * @private */ static hashDV(toHash: string): DVHash { const hash: string = crypto.createHash("md5").update(toHash).digest("hex"); const buf: Buffer = Buffer.from(hash, "hex"); const lsb: bigint = buf.readBigInt64LE(0); const msb: bigint = buf.readBigInt64LE(8); const xor: bigint = lsb ^ msb; const bytes = Buffer.alloc(8); bytes.writeBigInt64BE(xor); return { bytes, decimal: xor.toString(), }; } /** * Takes a 8-bytes buffer and split it into 2 keys. * The default behavior is to split into a 3 bytes key and a 5 bytes key, but this can change depending on db used. * @param bytes: number[] * @return {{key1: number, key2: number}} */ static getDVKeys(bytes: Buffer): DVRedisKey { const key1: Buffer = bytes.slice(0, 3); const key2: Buffer = bytes.slice(3, 8); return { key1, key2 }; } }