UNPKG

anticlone-sdk

Version:

SDK for generating app fingerprints and protecting against clone/fake apps

483 lines (417 loc) • 17.4 kB
const crypto = require('crypto'); const fs = require('fs').promises; const path = require('path'); const https = require('https'); const AdmZip = require('adm-zip'); class AntiCloneSDK { constructor(options = {}) { this.apiBaseUrl = options.apiBaseUrl || 'https://sentinel-api-aryc.onrender.com'; this.developerId = options.developerId; this.apiKey = options.apiKey; this.timeout = options.timeout || 30000; } /** * Set developer credentials */ setCredentials(developerId, apiKey) { this.developerId = developerId; this.apiKey = apiKey; } /** * Generate fingerprint from project directory */ async generateFingerprintFromProject(projectPath, appInfo) { try { const stats = await fs.stat(projectPath); if (!stats.isDirectory()) { throw new Error('Project path must be a directory'); } console.log(`Analyzing project at: ${projectPath}`); const fingerprints = {}; const metadata = { totalFiles: 0, analysisTimestamp: new Date().toISOString(), projectSize: 0 }; // Analyze different file types await this._analyzeDirectory(projectPath, fingerprints, metadata); // Generate overall hash const overallHash = this._generateOverallHash(fingerprints); // Create developer credentials hash // const developerCredentials = this._generateDeveloperCredentials(); return { appId: appInfo.appId, appName: appInfo.appName, packageName: appInfo.packageName || appInfo.appId, version: appInfo.version, fileType: 'project', fileName: path.basename(projectPath), overallHash, fingerprints, // developerCredentials, metadata }; } catch (error) { throw new Error(`Failed to generate fingerprint from project: ${error.message}`); } } /** * Generate fingerprint from APK/IPA file */ async generateFingerprintFromFile(filePath, appInfo) { try { const stats = await fs.stat(filePath); if (!stats.isFile()) { throw new Error('File path must be a file'); } const fileExt = path.extname(filePath).toLowerCase(); if (!['.apk', '.ipa'].includes(fileExt)) { throw new Error('File must be APK or IPA format'); } console.log(`Analyzing ${fileExt} file: ${filePath}`); const fingerprints = {}; const metadata = { fileSize: stats.size, analysisTimestamp: new Date().toISOString(), fileType: fileExt.substring(1) }; if (fileExt === '.apk') { await this._analyzeAPK(filePath, fingerprints, metadata); } else { await this._analyzeIPA(filePath, fingerprints, metadata); } // Generate overall hash const overallHash = this._generateOverallHash(fingerprints); // Create developer credentials hash // const developerCredentials = this._generateDeveloperCredentials(); return { appId: appInfo.appId, appName: appInfo.appName, packageName: appInfo.packageName || appInfo.appId, version: appInfo.version, fileType: fileExt.substring(1), fileName: path.basename(filePath), overallHash, fingerprints, // developerCredentials, metadata }; } catch (error) { throw new Error(`Failed to generate fingerprint from file: ${error.message}`); } } /** * Upload fingerprint to API server */ async uploadFingerprint(fingerprint) { if (!this.developerId || !this.apiKey) { throw new Error('Developer credentials not set. Use setCredentials() first.'); } try { const response = await this._makeApiRequest('/api/fingerprints', 'POST', { fingerprint }); if (response.success) { console.log('āœ… Fingerprint uploaded successfully'); console.log(`Fingerprint ID: ${response.id}`); return response; } else { throw new Error(response.error || 'Upload failed'); } } catch (error) { throw new Error(`Failed to upload fingerprint: ${error.message}`); } } /** * Register new developer (one-time setup) */ async registerDeveloper(developerInfo) { try { const response = await this._makeApiRequest('/api/developers/register', 'POST', developerInfo); if (response.success) { console.log('āœ… Developer registration successful'); console.log('šŸ”‘ Save these credentials securely:'); console.log(`Developer ID: ${response.credentials.developerId}`); console.log(`API Key: ${response.credentials.apiKey}`); console.log(`Passkey: ${response.credentials.passkey}`); console.log('\nāš ļø Store these credentials - they will not be shown again!'); return response; } else { throw new Error(response.error || 'Registration failed'); } } catch (error) { throw new Error(`Failed to register developer: ${error.message}`); } } /** * Get developer profile */ async getDeveloperProfile() { if (!this.developerId || !this.apiKey) { throw new Error('Developer credentials not set'); } try { const response = await this._makeApiRequest('/api/developers/profile', 'GET'); return response; } catch (error) { throw new Error(`Failed to get profile: ${error.message}`); } } // Private methods async _analyzeDirectory(dirPath, fingerprints, metadata, level = 0) { if (level > 10) return; // Prevent deep recursion const items = await fs.readdir(dirPath); for (const item of items) { const itemPath = path.join(dirPath, item); const stats = await fs.stat(itemPath); if (stats.isDirectory()) { // Skip common build/temp directories if (['node_modules', '.git', 'build', 'dist', 'tmp'].includes(item)) { continue; } await this._analyzeDirectory(itemPath, fingerprints, metadata, level + 1); } else { await this._analyzeFile(itemPath, fingerprints, metadata); metadata.totalFiles++; metadata.projectSize += stats.size; } } } async _analyzeFile(filePath, fingerprints, _metadata) { const ext = path.extname(filePath).toLowerCase(); const fileName = path.basename(filePath); try { const content = await fs.readFile(filePath, 'utf8'); const fileHash = this._hashContent(content); // Categorize files if (['.js', '.jsx', '.ts', '.tsx'].includes(ext)) { fingerprints.javascript = fingerprints.javascript || {}; fingerprints.javascript[fileName] = { hash: fileHash, size: content.length, imports: this._extractImports(content), functions: this._extractFunctions(content) }; } else if (['.java', '.kt'].includes(ext)) { fingerprints.android = fingerprints.android || {}; fingerprints.android[fileName] = { hash: fileHash, size: content.length, packages: this._extractPackages(content), classes: this._extractClasses(content) }; } else if (['.swift', '.m', '.h'].includes(ext)) { fingerprints.ios = fingerprints.ios || {}; fingerprints.ios[fileName] = { hash: fileHash, size: content.length, imports: this._extractImports(content) }; } else if (['.json', '.xml'].includes(ext)) { fingerprints.config = fingerprints.config || {}; fingerprints.config[fileName] = { hash: fileHash, size: content.length }; } } catch (error) { // Skip binary files or files that can't be read as text if (error.code !== 'EISDIR') { const stats = await fs.stat(filePath); fingerprints.assets = fingerprints.assets || {}; fingerprints.assets[fileName] = { hash: this._hashContent(filePath), size: stats.size }; } } } async _analyzeAPK(filePath, fingerprints, metadata) { try { const zip = new AdmZip(filePath); const entries = zip.getEntries(); fingerprints.apk = { manifest: null, classes: {}, resources: {}, assets: {}, libs: {} }; for (const entry of entries) { const entryName = entry.entryName; const content = entry.getData(); const contentHash = this._hashContent(content); if (entryName === 'AndroidManifest.xml') { fingerprints.apk.manifest = { hash: contentHash, size: content.length }; } else if (entryName.startsWith('classes') && entryName.endsWith('.dex')) { fingerprints.apk.classes[entryName] = { hash: contentHash, size: content.length }; } else if (entryName.startsWith('res/')) { fingerprints.apk.resources[entryName] = { hash: contentHash, size: content.length }; } else if (entryName.startsWith('assets/')) { fingerprints.apk.assets[entryName] = { hash: contentHash, size: content.length }; } else if (entryName.startsWith('lib/')) { fingerprints.apk.libs[entryName] = { hash: contentHash, size: content.length }; } } metadata.totalEntries = entries.length; } catch (error) { throw new Error(`Failed to analyze APK: ${error.message}`); } } async _analyzeIPA(filePath, fingerprints, metadata) { try { const zip = new AdmZip(filePath); const entries = zip.getEntries(); fingerprints.ipa = { executable: null, resources: {}, frameworks: {}, plists: {} }; for (const entry of entries) { const entryName = entry.entryName; const content = entry.getData(); const contentHash = this._hashContent(content); if (entryName.includes('.app/') && !entryName.includes('/')) { const fileName = path.basename(entryName); if (!fileName.includes('.')) { fingerprints.ipa.executable = { hash: contentHash, size: content.length, name: fileName }; } } else if (entryName.endsWith('.plist')) { fingerprints.ipa.plists[entryName] = { hash: contentHash, size: content.length }; } else if (entryName.includes('.framework/')) { fingerprints.ipa.frameworks[entryName] = { hash: contentHash, size: content.length }; } else if (entryName.includes('.app/')) { fingerprints.ipa.resources[entryName] = { hash: contentHash, size: content.length }; } } metadata.totalEntries = entries.length; } catch (error) { throw new Error(`Failed to analyze IPA: ${error.message}`); } } _extractImports(content) { const imports = []; const importRegex = /(?:import|require|from)\s+['"`]([^'"`]+)['"`]/g; let match; while ((match = importRegex.exec(content)) !== null) { imports.push(match[1]); } return [...new Set(imports)]; // Remove duplicates } _extractFunctions(content) { const functions = []; const functionRegex = /(?:function|const|let|var)\s+(\w+)\s*(?:\(|=\s*(?:\(|async))/g; let match; while ((match = functionRegex.exec(content)) !== null) { functions.push(match[1]); } return [...new Set(functions)]; } _extractPackages(content) { const packages = []; const packageRegex = /package\s+([a-zA-Z0-9_.]+)/g; let match; while ((match = packageRegex.exec(content)) !== null) { packages.push(match[1]); } return [...new Set(packages)]; } _extractClasses(content) { const classes = []; const classRegex = /(?:class|interface)\s+(\w+)/g; let match; while ((match = classRegex.exec(content)) !== null) { classes.push(match[1]); } return [...new Set(classes)]; } _generateOverallHash(fingerprints) { const fingerprintString = JSON.stringify(fingerprints, Object.keys(fingerprints).sort()); return this._hashContent(fingerprintString); } _generateDeveloperCredentials() { if (!this.developerId || !this.apiKey) { throw new Error('Developer credentials not set'); } return { developerId: this.developerId, timestamp: new Date().toISOString(), credentialHash: this._hashContent(`${this.developerId}:${this.apiKey}:${Date.now()}`) }; } _hashContent(content) { return crypto.createHash('sha256').update(content.toString()).digest('hex'); } _makeApiRequest(endpoint, method = 'GET', data = null) { return new Promise((resolve, reject) => { const url = new URL(endpoint, this.apiBaseUrl); const options = { method, headers: { 'Content-Type': 'application/json', 'User-Agent': 'anticlone-sdk/1.0.0' }, timeout: this.timeout }; if (this.developerId && this.apiKey) { options.headers['X-Developer-Id'] = this.developerId; options.headers['X-API-Key'] = this.apiKey; } const req = https.request(url, options, (res) => { let responseData = ''; res.on('data', (chunk) => { responseData += chunk; }); res.on('end', () => { try { const parsedData = JSON.parse(responseData); resolve(parsedData); } catch (error) { reject(new Error(`Invalid JSON response: ${error.message}`)); } }); }); req.on('error', (error) => { reject(new Error(`Request failed: ${error.message}`)); }); req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); }); if (data && method !== 'GET') { req.write(JSON.stringify(data)); } req.end(); }); } } module.exports = AntiCloneSDK;