@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
JavaScript
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}`);
}
}
};