anticlone-sdk
Version:
SDK for generating app fingerprints and protecting against clone/fake apps
483 lines (417 loc) ⢠17.4 kB
JavaScript
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;