UNPKG

@quanle94/react-native-cert-pinner

Version:

For React Native, pins TLS connections to specific trusted certificates' public keys with configuration for enforcePinning

249 lines (213 loc) 6.44 kB
const fs = require("fs"); const path = require("path"); var plist = require("plist"); const Log = require("../Log"); const andHead = `package com.criticalblue.reactnative; import okhttp3.CertificatePinner; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class GeneratedCertificatePinner { public static CertificatePinner instance() { CertificatePinner.Builder builder = new CertificatePinner.Builder(); `; const andFoot = ` return builder.build(); } `; const getSelfSignedHead = ` public static List<String> getSelfSignedCerts () { return Arrays.asList( `; const getSelfSignedEnd = ` ); } `; const getPropertiesHead = ` public static List<List<Object>> getProperties () { List<List<Object>> result = new ArrayList<>(); `; const getPropertiesEnd = ` return result; } } `; module.exports = (args) => { // prep config file const count = args._.length; const srcPath = count > 1 ? args._[1] : "./pinset.json"; if (!fs.existsSync(srcPath)) { Log.fatal(`Missing config file: ${srcPath}`); } // prep android project path let andBase = "./android"; if (args.a) { if (args.a.length <= 0) { Log.fatal(`Option '-a' missing path to Android project`); } andBase = args.a; } const andExists = fs.existsSync(andBase); if (!andExists) { Log.warn(`Android project not found: ${andBase}`); } const andPath = `${andBase}/app/src/main/java/com/criticalblue/reactnative/GeneratedCertificatePinner.java`; // prep ios project path let iosBase = "./ios"; if (args.i) { if (args.i.length <= 0) { Log.fatal(`Option '-i' missing path to iOS project`); } iosBase = args.i; } const iosExists = fs.existsSync(iosBase); if (!iosExists) { Log.warn(`iOS project not found: ${iosBase}`); } let iosPath = ""; let iosInfo = {}; if (iosExists) { const iosXcodeprojs = fs .readdirSync(iosBase) .filter((fn) => fn.endsWith(".xcodeproj")); if (iosXcodeprojs.length != 1) { Log.fatal("Cannot find unique *.xcodeproj in ${iosBase)"); } iosPath = iosBase + "/" + path.basename(iosXcodeprojs[0], ".xcodeproj") + "/info.plist"; if (!fs.existsSync(iosPath)) { Log.fatal(`Missing iOS plist file: ${iosPath}`); } iosInfo = plist.parse(fs.readFileSync(iosPath, "utf8")); } // check any projects if (!andExists && !iosExists) { Log.fatal("Neither Android nor iOS projects found"); } // check file ages let andUpdate = andExists; if (andUpdate && fs.existsSync(andPath)) { if (fs.statSync(srcPath).mtimeMs < fs.statSync(andPath).mtimeMs) { if (!args.f) { andUpdate = false; Log.warn( `config file'${srcPath}' is older than Android file '${andPath}'; use '-f' to force update.` ); } } } let iosUpdate = iosExists; if (iosUpdate && fs.existsSync(iosPath)) { if (fs.statSync(srcPath).mtimeMs < fs.statSync(iosPath).mtimeMs) { if (!args.f) { iosUpdate = false; Log.warn( `config file'${srcPath}' is older than iOS file '${iosPath}'; use '-f' to force update.` ); } } } if (!andUpdate && !iosUpdate) { Log.warn("No files updated"); return; } // read pinset config let pinset = ""; try { pinset = JSON.parse(fs.readFileSync(srcPath, "utf8")); } catch (err) { Log.fatal(`Config file '${srcPath}' error: ${err}`); } Log.info(`Reading config file '${srcPath}'.`); // write android config if (andUpdate) { Log.info(`Updating java file '${andPath}'.`); try { let andBody = ""; let getSelfSignedBody = ""; let getPropertyBody = ""; for (let domain in pinset.domains) { const domainObj = pinset.domains[domain]; (pinset.domains[domain].pins || [domain]).forEach((pin) => { andBody += ` builder.add("${domain}", "${pin}");\n`; }); getPropertyBody += ` result.add( Arrays.asList("${domain}", Arrays.asList(\n${( domainObj.pins || [domain] ) .map((pin) => ` "${pin}"`) .join(",\n")}),\n ${ typeof domainObj.enforcePinning === "undefined" ? true : !!domainObj.enforcePinning }));\n`; } for (const selfSignedIndex in pinset.selfSignedCerts) { getSelfSignedBody += ` "${pinset.selfSignedCerts[selfSignedIndex]}"` + `${ selfSignedIndex >= pinset.selfSignedCerts.length - 1 ? "" : ",\n" }`; } fs.writeFileSync( andPath, andHead + andBody + andFoot + getSelfSignedHead + getSelfSignedBody + getSelfSignedEnd + getPropertiesHead + getPropertyBody + getPropertiesEnd ); } catch (err) { Log.fatal(`Java file '${andPath}' error: ${err}`); } } // write ios config if (iosUpdate) { Log.info(`Updating plist file '${iosPath}'.`); try { let iosDomains = {}; for (let domain in pinset.domains) { const domainObj = pinset.domains[domain]; let includeSubdomains = false; let baseDomain = domain; if (domain.startsWith("*.")) { includeSubdomains = true; baseDomain = domain.substring(2); } let iosDomain = { TSKEnforcePinning: typeof domainObj.enforcePinning === "undefined" ? true : !!domainObj.enforcePinning, TSKIncludeSubdomains: includeSubdomains, TSKPublicKeyAlgorithms: ["TSKAlgorithmRsa2048"], }; pinArray = []; (pinset.domains[domain].pins || [domain]).forEach((pin) => { if (pin.startsWith("sha256/")) { pinArray.push(pin.substring(7)); } else { pinArray.push(pin); } }); iosDomain["TSKPublicKeyHashes"] = pinArray; iosDomains[baseDomain] = iosDomain; } iosInfo["TSKConfiguration"] = { TSKPinnedDomains: iosDomains, TSKSwizzleNetworkDelegates: true, }; // WRITE OUT FILE HERE fs.writeFileSync(iosPath, plist.build(iosInfo)); } catch (err) { Log.fatal(`iOS plist file '${iosPath}' error: ${err}`); } } };