applesign
Version:
API to resign IPA files
965 lines (899 loc) • 33.5 kB
JavaScript
'use strict';
const tools = require('./lib/tools');
const config = require('./lib/config');
const idprov = require('./lib/idprov');
const EventEmitter = require('events').EventEmitter;
const path = require('path');
const { execSync } = require('child_process');
const uuid = require('uuid');
const fs = require('fs-extra');
const walk = require('fs-walk');
const plist = require('simple-plist');
const fchk = require('./lib/fchk');
const { tmpdir, homedir } = require('os');
const { AppDirectory } = require('./lib/appdir');
const depSolver = require('./lib/depsolver');
const adjustInfoPlist = require('./lib/info-plist');
const defaultEntitlements = require('./lib/entitlements');
const plistBuild = require('plist').build;
const bin = require('./lib/bin');
class Applesign {
constructor (options) {
this.config = config.fromOptions(options || {});
this.events = new EventEmitter();
this.nested = [];
this.debugObject = {};
this.tmpDir = this._makeTmpDir();
}
_makeTmpDir () {
const tmp = tmpdir();
const base = path.join(tmp, 'applesign');
const result = path.join(base, uuid.v4());
fs.mkdirSync(result, { recursive: true });
return result;
}
_fullPathInTmp (filePath) {
const dirname = path.dirname(filePath);
const dirnameInTmp = path.join(this.tmpDir, dirname);
fs.mkdirpSync(dirnameInTmp);
return path.join(this.tmpDir, filePath);
}
async getDeviceProvision () {
const installedProvisions = await tools.ideviceprovision('list');
const pd = path.join(homedir(), 'Library', 'MobileDevice', 'Provisioning Profiles');
for (const ip of installedProvisions) {
const absPath = path.join(pd, ip + '.mobileprovision');
if (fs.existsSync(absPath)) {
return absPath;
}
}
throw new Error('Cannot find provisioning file automatically. Please use -m');
}
async signXCarchive (file) {
fchk(arguments, ['string']);
const ipaFile = file + '.ipa';
await tools.xcaToIpa(file, ipaFile);
await this.signIPA(ipaFile);
}
async getIdentities () {
fchk(arguments, []);
return tools.getIdentities();
}
async signIPA (file) {
fchk(arguments, ['string']);
if (typeof file === 'string') {
this.setFile(file);
}
tools.setOptions({
use7zip: this.config.use7zip,
useOpenSSL: this.config.useOpenSSL
});
await this._pullMobileProvision();
this.emit('message', 'File: ' + this.config.file);
this.emit('message', 'Outdir: ' + this.config.outdir);
if (tools.isDirectory(this.config.file)) {
throw new Error('This is a directory');
}
try {
await this.unzipIPA(this.config.file, this.config.outdir);
const appDirectory = path.join(this.config.outdir, '/Payload');
this.config.appdir = getAppDirectory(appDirectory);
if (this.config.debug) {
this.debugObject = {};
}
const tasks = [];
if (this.config.withoutWatchapp) {
tasks.push(this.removeWatchApp());
}
// TODO: this .withoutSigningFiles option doesnt exist yet
if (this.config.withoutSigningFiles) {
tasks.push(this.removeSigningFiles());
}
if (this.config.withoutPlugins) {
tasks.push(this.removePlugins());
}
if (this.config.withoutXCTests) {
tasks.push(this.removeXCTests());
}
if (tasks.length > 0) {
await Promise.all(tasks);
}
await this.signAppDirectory(appDirectory);
await this.zipIPA();
} finally {
await this.cleanup();
}
return this;
}
async _pullMobileProvision () {
if (this.config.deviceProvision === true) {
this.config.mobileprovision = await this.getDeviceProvision();
this.config.mobileprovisions = [this.config.mobileprovision];
this.config.identity = idprov(this.config.mobileprovision);
}
this.config.mobileprovision = this.config.mobileprovisions[0];
if (this.config.mobileprovisions.length > 1) {
this.config.mobileprovisions.slice(1);
}
}
async signAppDirectoryInternal (ipadir, skipNested) {
fchk(arguments, ['string', 'boolean']);
await this._pullMobileProvision();
if (this.config.run) {
runScriptSync(this.config.run, this);
}
if (this.config.appdir === undefined) {
this.config.appdir = ipadir;
}
const binname = getExecutable(this.config.appdir);
this.emit('msg', 'Main Executable Name: ' + binname);
this.config.appbin = path.join(this.config.appdir, binname);
if (!fs.lstatSync(this.config.appbin).isFile()) {
throw new Error('This was supposed to be a file');
}
if (bin.isBitcode(this.config.appbin)) {
throw new Error('This IPA contains only bitcode. Must be transpiled for the target device to run.');
}
if (bin.isEncrypted(this.config.appbin)) {
if (!this.config.unfairPlay) {
throw new Error('This IPA is encrypted');
}
this.emit('warning', 'Main IPA executable is encrypted');
} else {
this.emit('message', 'Main IPA executable is not encrypted');
}
if (this.config.insertLibrary !== undefined) {
await insertLibrary(this.config);
}
const infoPlistPath = path.join(this.config.appdir, 'Info.plist');
adjustInfoPlist(infoPlistPath, this.config, this.emit.bind(this));
if (!this.config.pseudoSign) {
if (!this.config.mobileprovision) {
throw new Error('warning: No mobile provisioning file provided');
}
await this.checkProvision(this.config.appdir, this.config.mobileprovision);
}
await this.adjustEntitlements(this.config.appbin);
await this.signLibraries(this.config.appbin, this.config.appdir);
if (skipNested !== true) {
for (const nest of this.nested) {
if (tools.isDirectory(nest)) {
await this.signAppDirectoryInternal(nest, true);
} else {
this.emit('warning', 'Cannot find ' + nest);
}
}
}
}
async signAppDirectory (ipadir) {
fchk(arguments, ['string']);
return this.signAppDirectoryInternal(ipadir, false);
}
async removeWatchApp () {
fchk(arguments, []);
const watchdir = path.join(this.config.appdir, 'Watch');
this.emit('message', 'Stripping out the WatchApp at ' + watchdir);
await tools.asyncRimraf(watchdir);
const placeholderdir = path.join(this.config.appdir, 'com.apple.WatchPlaceholder');
this.emit('message', 'Stripping out the WatchApp at ' + placeholderdir);
await tools.asyncRimraf(placeholderdir);
}
// XXX some directory leftovers
async removeXCTests () {
fchk(arguments, []);
const dir = this.config.appdir;
walk.walkSync(dir, (basedir, filename, stat) => {
const target = path.join(basedir, filename);
// if (target.toLowerCase().indexOf('/xct') !== -1)
if (target.toLowerCase().indexOf('xctest') !== -1) {
this.emit('message', 'Deleting ' + target);
fs.unlinkSync(target);
}
});
}
async removeSigningFiles () {
fchk(arguments, []);
const dir = this.config.appdir;
walk.walkSync(dir, (basedir, filename, stat) => {
if (filename.endsWith('.entitlements') || filename.endsWith('.mobileprovision')) {
const target = path.join(basedir, filename);
this.emit('message', 'Deleting ' + target);
fs.unlinkSync(target);
}
});
}
async removePlugins () {
fchk(arguments, []);
const plugdir = path.join(this.config.appdir, 'PlugIns');
const tmpdir = path.join(this.config.appdir, 'applesign_xctest_tmp');
this.emit('message', 'Stripping out the PlugIns at ' + plugdir);
let tests = [];
if (!this.config.withoutXCTests) {
tests = await enumerateTestFiles(plugdir);
if (tests.length > 0) {
await moveFiles(tests, plugdir, tmpdir);
}
}
await tools.asyncRimraf(plugdir);
if (tests.length > 0) {
await moveFiles(tests, tmpdir, plugdir);
await fs.rmdir(tmpdir);
}
}
findProvisioningsSync () {
fchk(arguments, []);
const files = [];
walk.walkSync(this.config.appdir, (basedir, filename, stat) => {
const file = path.join(basedir, filename);
// only walk on files. Symlinks and other special files are forbidden
if (!fs.lstatSync(file).isFile()) {
return;
}
if (filename === 'embedded.mobileprovision') {
files.push(file);
}
});
return files;
}
/*
TODO: verify is mobileprovision app-id glob string matches the bundleid
read provision file in raw
search for application-identifier and <string>...</string>
check if prefix matches and last dot separated word is an asterisk
const identifierInProvisioning = 'x'
Read the one in Info.plist and compare with bundleid
*/
async checkProvision (appdir, file) {
fchk(arguments, ['string', 'string']);
/* Deletes the embedded.mobileprovision from the ipa? */
const withoutMobileProvision = false;
if (withoutMobileProvision) {
const files = this.findProvisioningsSync();
files.forEach((file) => {
console.error('Deleting ', file);
fs.unlinkSync(file);
});
}
if (appdir && file && !withoutMobileProvision) {
this.emit('message', 'Embedding new mobileprovision');
const mobileProvision = path.join(appdir, 'embedded.mobileprovision');
if (this.config.selfSignedProvision) {
/* update entitlements */
const data = await tools.getMobileProvisionPlist(this.config.mobileprovision);
const mainBin = path.join(appdir, getExecutable(appdir));
let ent = bin.entitlements(mainBin);
if (ent === null) {
this.emit('warning', 'Cannot find entitlements in binary. Using defaults');
const entMobProv = data.Entitlements;
const teamId = entMobProv['com.apple.developer.team-identifier'];
const appId = entMobProv['application-identifier'];
ent = defaultEntitlements(appId, teamId);
}
data.Entitlements = plist.parse(ent.toString().trim());
fs.writeFileSync(mobileProvision, plistBuild(data).toString());
/* TODO: self-sign mobile provisioning */
}
return fs.copySync(file, mobileProvision);
}
}
debugInfo (path, key, val) {
if (!val) {
return;
}
const f = path.replace(this.config.outdir + '/', '');
if (!this.debugObject) {
this.debugObject = {};
}
if (this.debugObject[f] === undefined) {
this.debugObject[f] = {};
}
this.debugObject[f][key] = val;
}
addEntitlementsSync (orig) {
if (this.config.addEntitlements === undefined) {
return orig;
}
this.emit('message', 'Adding entitlements from file');
const addEnt = plist.readFileSync(this.config.addEntitlements);
// TODO: deepmerge
return Object.assign(orig, addEnt);
}
adjustEntitlementsSync (file, entMobProv) {
if (this.config.pseudoSign) {
const ent = bin.entitlements(file);
if (ent === null) {
return;
}
let entMacho = plist.parse(ent.toString().trim());
entMacho = this.addEntitlementsSync(entMacho);
// TODO: merge additional entitlements here
const newEntitlements = plistBuild(entMacho).toString();
const newEntitlementsFile = file + '.entitlements';
const tmpEntitlementsFile = this._fullPathInTmp(newEntitlementsFile);
fs.writeFileSync(tmpEntitlementsFile, newEntitlements);
this.config.entitlement = tmpEntitlementsFile;
return;
}
fchk(arguments, ['string', 'object']);
this.debugInfo(file, 'before', entMobProv);
const teamId = entMobProv['com.apple.developer.team-identifier'];
const appId = entMobProv['application-identifier'];
let ent = bin.entitlements(file);
if (ent === null && !this.config.cloneEntitlements) {
console.error('Cannot find entitlements in binary. Using defaults');
ent = defaultEntitlements(appId, teamId);
}
let entMacho;
if (ent !== null) {
entMacho = plist.parse(ent.toString().trim());
entMacho = this.addEntitlementsSync(entMacho);
this.debugInfo(file, 'fullPath', file);
this.debugInfo(file, 'oldEntitlements', entMacho || 'TODO');
if (this.config.selfSignedProvision) {
this.emit('message', 'Using an unsigned provisioning');
const newEntitlementsFile = file + '.entitlements';
const newEntitlements = plistBuild(entMacho).toString();
const tmpEntitlementsFile = this._fullPathInTmp(newEntitlementsFile);
fs.writeFileSync(tmpEntitlementsFile, newEntitlements);
this.config.entitlement = tmpEntitlementsFile;
if (!this.config.noEntitlementsFile) {
fs.writeFileSync(newEntitlementsFile, tmpEntitlementsFile);
}
this.debugInfo(file, 'newEntitlements', plist.parse(newEntitlements));
return;
}
}
let changed = false;
if (this.config.cloneEntitlements) {
this.emit('message', 'Cloning entitlements');
entMacho = entMobProv;
changed = true;
} else {
const k = 'com.apple.developer.icloud-container-identifiers';
if (entMacho[k]) {
entMacho[k] = 'iCloud.' + appId;
}
['application-identifier', 'com.apple.developer.team-identifier'].forEach((id) => {
if (entMacho[id] !== entMobProv[id]) {
changed = true;
entMacho[id] = entMobProv[id];
}
});
if (this.config.massageEntitlements === true) {
if (typeof entMacho['keychain-access-groups'] === 'object') {
changed = true;
// keychain access groups makes the resigning fail with -M
delete entMacho['keychain-access-groups'];
// entMacho['keychain-access-groups'][0] = appId;
}
[
'com.apple.developer.ubiquity-kvstore-identifier',
'com.apple.developer.ubiquity-container-identifiers',
'com.apple.developer.icloud-container-identifiers',
'com.apple.developer.icloud-container-environment',
'com.apple.developer.icloud-services',
'com.apple.developer.payment-pass-provisioning',
'com.apple.developer.default-data-protection',
'com.apple.networking.vpn.configuration',
'com.apple.developer.associated-domains',
'com.apple.security.application-groups',
'com.apple.developer.in-app-payments',
'com.apple.developer.siri',
'beta-reports-active', /* our entitlements doesnt support beta */
'aps-environment'
].forEach((id) => {
if (typeof entMacho[id] !== 'undefined') {
delete entMacho[id];
changed = true;
}
});
} else if (!this.config.cloneEntitlements) {
delete entMacho['com.apple.developer.icloud-container-identifiers'];
delete entMacho['com.apple.developer.icloud-container-environment'];
delete entMacho['com.apple.developer.ubiquity-kvstore-identifier'];
delete entMacho['com.apple.developer.icloud-services'];
delete entMacho['com.apple.developer.siri'];
delete entMacho['com.apple.developer.in-app-payments'];
delete entMacho['aps-environment'];
delete entMacho['com.apple.security.application-groups'];
delete entMacho['com.apple.developer.associated-domains'];
delete entMacho['keychain-access-groups'];
changed = true;
}
}
if (typeof this.config.withGetTaskAllow !== 'undefined') {
this.emit('message', 'get-task-allow set to ' + this.config.withGetTaskAllow);
entMacho['get-task-allow'] = this.config.withGetTaskAllow;
changed = true;
}
const additionalKeychainGroups = [];
if (typeof this.config.customKeychainGroup === 'string') {
additionalKeychainGroups.push(this.config.customKeychainGroup);
}
const infoPlist = path.join(this.config.appdir, 'Info.plist');
const plistData = plist.readFileSync(infoPlist);
if (this.config.bundleIdKeychainGroup) {
if (typeof this.config.bundleid === 'string') {
additionalKeychainGroups.push(this.config.bundleid);
} else {
const bundleid = plistData.CFBundleIdentifier;
additionalKeychainGroups.push(bundleid);
}
}
if (this.config.osversion !== undefined) {
// DTPlatformVersion
plistData.MinimumOSVersion = this.config.osversion;
plist.writeFileSync(infoPlist, plistData);
}
if (additionalKeychainGroups.length > 0) {
const newGroups = additionalKeychainGroups.map(group => `${teamId}.${group}`);
const groups = entMacho['keychain-access-groups'];
if (typeof groups === 'undefined') {
entMacho['keychain-access-groups'] = newGroups;
} else {
groups.push(...newGroups);
}
changed = true;
}
if (changed || this.config.entry) {
const newEntitlementsFile = file + '.entitlements';
let newEntitlements = (appId && teamId && this.config.entry)
? defaultEntitlements(appId, teamId)
: (this.config.entitlement)
? fs.readFileSync(this.config.entitlement).toString()
: plistBuild(entMacho).toString();
const ent = plist.parse(newEntitlements.trim());
const shouldRenameGroups = !this.config.mobileprovision && !this.config.cloneEntitlements;
if (shouldRenameGroups && ent['com.apple.security.application-groups']) {
const ids = appId.split('.');
ids.shift();
const id = ids.join('.');
const groups = [];
for (const group of ent['com.apple.security.application-groups']) {
const cols = group.split('.');
if (cols.length === 4) {
groups.push('group.' + id);
} else {
groups.push('group.' + id + '.' + cols.pop());
}
}
ent['com.apple.security.application-groups'] = groups;
}
delete ent['beta-reports-active']; /* our entitlements doesnt support beta */
if (this.config.massageEntitlements === true) {
delete ent['com.apple.developer.ubiquity-container-identifiers']; // TODO should be massaged
}
newEntitlements = plistBuild(ent).toString();
this.debugInfo(file, 'newEntitlements', ent);
const tmpEntitlementsFile = this._fullPathInTmp(newEntitlementsFile);
fs.writeFileSync(tmpEntitlementsFile, newEntitlements);
this.config.entitlement = tmpEntitlementsFile;
if (!this.config.noEntitlementsFile) {
fs.writeFileSync(tmpEntitlementsFile, newEntitlements);
this.emit('message', 'Updated binary entitlements' + tmpEntitlementsFile);
}
this.debugInfo(file, 'after', newEntitlements);
} else {
this.debugInfo(file, 'nothing-changed', true);
}
}
async adjustEntitlements (file) {
fchk(arguments, ['string']);
let newEntitlements = null;
if (!this.config.pseudoSign) {
const mp = this.config.mobileprovision ? this.config.mobileprovision : path.join(this.config.appdir, 'embedded.mobileprovision');
newEntitlements = await tools.getEntitlementsFromMobileProvision(mp);
this.emit('message', JSON.stringify(newEntitlements));
}
this.adjustEntitlementsSync(file, newEntitlements);
}
async signFile (file) {
const config = this.config;
function customOptions (config, file) {
if (typeof config.json === 'object' && typeof config.json.custom === 'object') {
for (const c of config.json.custom) {
if (!c.filematch) {
continue;
}
const re = new RegExp(c.filematch);
if (re.test(file)) {
// console.error('Debug: '+ JSON.stringify(c, null, 2))
return c;
}
}
}
return false;
}
const custom = customOptions(config, file);
function getKeychain () { return (custom !== false && custom.keychain !== undefined) ? custom.keychain : config.keychain; }
function getIdentity () { return (custom !== false && custom.identity !== undefined) ? custom.identity : config.identity; }
function getEntitlements () { return (custom !== false && custom.entitlements !== undefined) ? custom.entitlements : config.entitlement; }
fchk(arguments, ['string']);
if (this.config.lipoArch !== undefined) {
this.emit('message', '[lipo] ' + this.config.lipoArch + ' ' + file);
try {
await tools.lipoFile(file, this.config.lipoArch);
} catch (ignored) {
}
}
function codesignHasFailed (config, error, errmsg) {
if (error && errmsg.indexOf('Error:') !== -1) {
throw error;
}
return ((errmsg && errmsg.indexOf('no identity found') !== -1) || !config.ignoreCodesignErrors);
}
const identity = getIdentity();
let entitlements = '';
if (this.config.cloneEntitlements) {
const mp = await tools.getMobileProvisionPlist(this.config.mobileprovision);
const newEntitlementsFile = file + '.entitlements';
const tmpEntitlementsFile = this._fullPathInTmp(newEntitlementsFile);
const entstr = plistBuild(mp.Entitlements, { pretty: true, allowEmpty: false }).toString();
fs.writeFileSync(tmpEntitlementsFile, entstr);
entitlements = tmpEntitlementsFile;
} else {
entitlements = getEntitlements();
}
let res;
if (this.config.pseudoSign) {
const newEntitlementsFile = file + '.entitlements';
const tmpEntitlementsFile = this._fullPathInTmp(newEntitlementsFile);
const entitlements = fs.existsSync(tmpEntitlementsFile) ? tmpEntitlementsFile : null;
res = await tools.pseudoSign(entitlements, file);
} else {
const keychain = getKeychain();
res = await tools.codesign(identity, entitlements, keychain, file);
if (res.code !== 0 && codesignHasFailed(config, res.code, res.stderr)) {
return this.emit('end', res.stderr);
}
}
this.emit('message', 'Signed ' + file);
if (config.verifyTwice) {
this.emit('message', 'Verify ' + file);
const res = await tools.verifyCodesign(file, config.keychain);
if (res.code !== 0) {
const type = (config.ignoreVerificationErrors) ? 'warning' : 'error';
return this.emit(type, res.stderr);
}
}
return this;
}
filterLibraries (libraries) {
fchk(arguments, ['object']);
return libraries.filter(library => {
// Resign all frameworks. even if not referenced :?
if (library.indexOf('Frameworks/') !== -1) {
return true;
}
if (this.config.all) {
return true;
}
// check if there's a Plist to inform us which is the right executable
const exe = getExecutable(path.dirname(library));
if (path.basename(library) !== exe) {
this.emit('warning', 'Not signing ' + library);
return false;
}
return true;
});
}
findLibrariesSync () {
fchk(arguments, []);
const libraries = [];
const nested = [];
const exe = path.sep + getExecutable(this.config.appdir);
const folders = this.config.appbin.split(path.sep);
const exe2 = path.sep + folders[folders.length - 1];
let found = false;
walk.walkSync(this.config.appdir, (basedir, filename, stat) => {
const file = path.join(basedir, filename);
// only walk on files. Symlinks and other special files are forbidden
if (!fs.lstatSync(file).isFile()) {
return;
}
if (file.endsWith(exe) || file.endsWith(exe2)) {
this.emit('message', 'Executable found at ' + file);
libraries.push(file);
found = true;
return;
}
const nest = nestedApp(file);
if (nest !== false) {
if (nested.indexOf(nest) === -1) {
nested.push(nest);
}
return;
}
if (bin.isMacho(file)) {
libraries.push(file);
}
});
if (!found) {
throw new Error('Cannot find any MACH0 binary to sign');
}
console.error('Found nested', nested);
this.nested = nested;
// return this.filterLibraries(libraries);
return libraries;
}
async signLibraries (bpath, appdir) {
fchk(arguments, ['string', 'string']);
this.emit('message', 'Signing libraries and frameworks');
const parallelVerify = async (libs) => {
if (!this.config.verify) {
return;
}
this.emit('message', 'Verifying ' + libs);
const promises = libs.map(lib => tools.verifyCodesign);
return Promise.all(promises);
};
const layeredSigning = async (libs) => {
const libsCopy = libs.slice(0).reverse();
for (const deps of libsCopy) {
const promises = deps.map(dep => { return this.signFile(dep); });
await Promise.all(promises);
}
await parallelVerify(libs);
};
const serialSigning = async (libs) => {
const libsCopy = libs.slice(0).reverse();
for (const lib of libsCopy) {
await this.signFile(lib);
if (this.config.verify) {
this.emit('message', 'Verifying ' + lib);
await tools.verifyCodesign(lib);
}
}
};
this.emit('message', 'Resolving signing order using layered list');
let libs = [];
const ls = new AppDirectory();
await ls.loadFromDirectory(appdir);
if (this.config.parallel) {
// known to be buggy in some situations, must use AppDirectory
const libraries = this.findLibrariesSync();
libs = await depSolver(bpath, libraries, true);
for (const appex of ls.appexs) {
libs.push([appex]);
}
} else {
for (const appex of ls.appexs) {
await this.adjustEntitlements(appex);
await this.signFile(appex);
}
this.emit('message', 'Nested: ' + JSON.stringify(ls.nestedApplications()));
this.emit('message', 'SystemLibraries: ' + JSON.stringify(ls.systemLibraries()));
this.emit('message', 'DiskLibraries: ' + JSON.stringify(ls.diskLibraries()));
this.emit('message', 'UnavailableLibraries: ' + JSON.stringify(ls.unavailableLibraries()));
this.emit('message', 'AppLibraries: ' + JSON.stringify(ls.appLibraries()));
this.emit('message', 'Orphan: ' + JSON.stringify(ls.orphanedLibraries()));
const libraries = ls.appLibraries();
if (this.config.all) {
libraries.push(...ls.orphanedLibraries());
} else {
for (const ol of ls.orphanedLibraries()) {
console.error('Warning: Orphaned library not signed, try -a: ' + ol);
}
}
this.debugInfo('analysis', 'orphan', ls.orphanedLibraries());
// const libraries = ls.diskLibraries ();
libs = libraries.filter(library => !(ls.appexs.includes(library))); // remove already-signed appexs
}
if (libs.length === 0) {
libs.push(bpath);
}
return (typeof libs[0] === 'object')
? layeredSigning(libs)
: serialSigning(libs);
}
async cleanup () {
fchk(arguments, []);
if (this.config.noclean) {
return;
}
const outdir = this.config.outdir;
this.emit('message', 'Cleaning up ' + outdir);
// await tools.asyncRimraf(this.config.outfile);
return tools.asyncRimraf(outdir);
}
async cleanupTmp () {
this.emit('message', 'Cleaning up temp dir ' + this.tmpDir);
await tools.asyncRimraf(this.tmpDir);
}
async zipIPA () {
fchk(arguments, []);
const ipaIn = this.config.file;
const ipaOut = getOutputPath(this.config.outdir, this.config.outfile);
try {
fs.unlinkSync(ipaOut); // await for it
} catch (e) {
/* do nothing */
}
this.events.emit('message', 'Zipifying into ' + ipaOut + ' ...');
const rootFolder = this.config.payloadOnly ? 'Payload' : '.';
await tools.zip(this.config.outdir, ipaOut, rootFolder);
if (this.config.replaceipa) {
this.events.emit('message', 'mv into ' + ipaIn);
fs.rename(ipaOut, ipaIn);
}
}
setFile (name) {
fchk(arguments, ['string']);
this.config.file = path.resolve(name);
this.config.outdir = this.config.file + '.' + uuid.v4();
if (!this.config.outfile) {
this.config.outfile = getResignedFilename(this.config.file);
}
}
async unzipIPA (file, outdir) {
fchk(arguments, ['string', 'string']);
if (!file || !outdir) {
throw new Error('No output specified');
}
if (!outdir) {
throw new Error('Invalid output directory');
}
await this.cleanup();
this.events.emit('message', 'Unzipping ' + file);
return tools.unzip(file, outdir);
}
/* Event Wrapper API with cb support */
emit (ev, msg) {
this.events.emit(ev, msg);
}
on (ev, cb) {
this.events.on(ev, cb);
return this;
}
}
// helper functions
function getResignedFilename (input) {
if (!input) {
return null;
}
const pos = input.lastIndexOf(path.sep);
if (pos !== -1) {
const tmp = input.substring(pos + 1);
const dot = tmp.lastIndexOf('.');
input = (dot !== -1) ? tmp.substring(0, dot) : tmp;
} else {
const dot = input.lastIndexOf('.');
if (dot !== -1) {
input = input.substring(0, dot);
}
}
return input + '-resigned.ipa';
}
function getExecutable (appdir) {
if (!appdir) {
throw new Error('No application directory is provided');
}
const plistPath = path.join(appdir, 'Info.plist');
try {
const plistData = plist.readFileSync(plistPath);
const cfBundleExecutable = plistData.CFBundleExecutable;
if (cfBundleExecutable) {
return cfBundleExecutable;
}
} catch (e) {
// do nothing
}
const exename = path.basename(appdir);
const dotap = exename.indexOf('.app');
return (dotap === -1) ? exename : exename.substring(0, dotap);
}
async function insertLibrary (config) {
const appDir = config.appdir;
const targetLib = config.insertLibrary;
const libraryName = path.basename(targetLib);
try {
fs.mkdirSync(path.join(appDir, 'Frameworks'));
} catch (_) {
}
const outputLib = path.join(appDir, 'Frameworks', libraryName);
await insertLibraryLL(outputLib, targetLib, config);
}
function insertLibraryLL (outputLib, targetLib, config) {
return new Promise((resolve, reject) => {
try {
const writeStream = fs.createWriteStream(outputLib);
writeStream.on('finish', () => {
fs.chmodSync(outputLib, 0x1ed); // 0755
/* XXX: if binary doesnt contains an LC_RPATH load command this will not work */
const insertedLibraryName = '@rpath/' + path.basename(targetLib);
/* Just copy the library via USB on the DCIM directory */
// const insertedLibraryName = '/var/mobile/Media/DCIM/' + path.basename(targetLib);
/* useful on jailbroken devices where we can write in /usr/lib */
// const insertedLibraryName = '/usr/lib/' + path.basename(targetLib);
/* forbidden in iOS */
// const insertedLibraryName = '@executable_path/Frameworks/' + path.basename(targetLib);
tools.insertLibrary(insertedLibraryName, config.appbin, outputLib).then(resolve).catch(reject);
});
fs.createReadStream(targetLib).pipe(writeStream);
} catch (e) {
reject(e);
}
});
}
function parentDirectory (root) {
return path.normalize(path.join(root, '..'));
}
function getOutputPath (cwd, ofile) {
return ofile.startsWith(path.sep) ? ofile : path.join(parentDirectory(cwd), ofile);
}
function runScriptSync (script, session) {
if (script.endsWith('.js')) {
try {
const s = require(script);
return s(session);
} catch (e) {
console.error(e);
return false;
}
} else {
process.env.APPLESIGN_DIRECTORY = session.config.appdir;
process.env.APPLESIGN_MAINBIN = session.config.appbin;
process.env.APPLESIGN_OUTFILE = session.config.outfile;
process.env.APPLESIGN_OUTDIR = session.config.outdir;
process.env.APPLESIGN_FILE = session.config.file;
try {
const res = execSync(script);
console.error(res.toString());
} catch (e) {
console.error(e.toString());
return false;
}
}
return true;
}
function nestedApp (file) {
const dotApp = file.indexOf('.app/');
if (dotApp !== -1) {
const subApp = file.substring(dotApp + 4).indexOf('.app/');
if (subApp !== -1) {
return file.substring(0, dotApp + 4 + subApp + 4);
}
}
return false;
}
function getAppDirectory (ipadir) {
if (!ipadir) {
ipadir = path.join(this.config.outdir, 'Payload');
}
if (!tools.isDirectory(ipadir)) {
throw new Error('Not a directory ' + ipadir);
}
if (ipadir.endsWith('.app')) {
this.config.appdir = ipadir;
} else {
const files = fs.readdirSync(ipadir).filter((x) => {
return x.endsWith('.app');
});
if (files.length !== 1) {
throw new Error('Invalid IPA: ' + ipadir);
}
return path.join(ipadir, files[0]);
}
if (ipadir.endsWith('/')) {
ipadir = ipadir.substring(0, ipadir.length - 1);
}
return ipadir;
}
async function enumerateTestFiles (dir) {
let tests = [];
if (fs.existsSync(dir)) {
tests = (await fs.readdir(dir)).filter((x) => {
return x.indexOf('.xctest') !== -1;
});
}
return tests;
}
async function moveFiles (files, sourceDir, destDir) {
await fs.mkdir(destDir, { recursive: true });
for (const f of files) {
const oldName = path.join(sourceDir, f);
const newName = path.join(destDir, f);
await fs.rename(oldName, newName);
}
}
module.exports = Applesign;