├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── CODEOWNERS ├── LICENSE ├── README.linux.md ├── README.md ├── bin └── applesign.ts ├── example.ts ├── index.ts ├── lib ├── appdir.ts ├── bin.ts ├── config.ts ├── depsolver.ts ├── entitlements.ts ├── fchk.ts ├── idprov.ts ├── info-plist.ts ├── tools.ts ├── types.d.ts └── version.ts ├── package-lock.json ├── package.json ├── scripts ├── dist.sh └── update-version.cjs ├── test ├── ipa │ └── .empty └── test.ts └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | runs-on: macos-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: ["18", "20"] 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - run: npm install 24 | - run: npm run indent-check 25 | - run: npm test 26 | - run: npm run dist 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @trufae @dki 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 NowSecure 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.linux.md: -------------------------------------------------------------------------------- 1 | # Running applesign on Linux 2 | 3 | It is possible to use applesign outside the Apple ecosystem, but this requires 4 | the `rcodesign` tool to be installed: 5 | 6 | ``` 7 | $ cargo install apple-codesign 8 | ``` 9 | 10 | ## Self Signed certificates 11 | 12 | You can read more about rcodesign and certificates in: 13 | 14 | - https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_certificate_management.html#apple-codesign-certificate-management 15 | 16 | ```sh 17 | $ rcodesign generate-self-signed-certificate --person-name pancake > a.pem 18 | $ rcodesign analyze-certificate --pem-source a.pem 19 | ``` 20 | 21 | With this `a.pem` file you can now sign a binary like this: 22 | 23 | ```sh 24 | $ rcodesign sign --pem-source a.pem --code-signature-flags runtime /path/to/binary 25 | ``` 26 | 27 | ## Codesign Requirements 28 | 29 | Apple requires a csreq to be signed inside the binary. this is an evaluated 30 | expression that defines the conditions that must 31 | 32 | - https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/RequirementLang/RequirementLang.html 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-applesign 2 | 3 | NodeJS module and commandline utility for re-signing iOS applications (IPA 4 | files). 5 | 6 | ## Author 7 | 8 | Sergi Alvarez Capilla aka pancake @ nowsecure.com 9 | 10 | ## Program Dependencies 11 | 12 | - zip - re-create IPA 13 | - unzip - decompress IPA (see `npm run unzip-lzfse`) 14 | - codesign - sign and verify binary with new entitlements and identity 15 | - security - get entitlements from mobileprovision 16 | - insert_dylib - only if you want to use the -I,--insert flag 17 | 18 | ## Usage 19 | 20 | When running without arguments we get a short help message 21 | 22 | ``` 23 | $ bin/applesign.js 24 | Usage: 25 | 26 | applesign [--options ...] [target.ipa | Payload/Target.app] 27 | 28 | -a, --all Resign all binaries, even it unrelated to the app 29 | -b, --bundleid [BUNDLEID] Change the bundleid when repackaging 30 | -c, --clone-entitlements Clone the entitlements from the provisioning to the bin 31 | -f, --force-family Force UIDeviceFamily in Info.plist to be iPhone 32 | -h, --help Show verbose help message 33 | -H, --allow-http Add NSAppTransportSecurity.NSAllowsArbitraryLoads in plist 34 | -i, --identity [1C4D1A..] Specify hash-id of the identity to use 35 | -L, --identities List local codesign identities 36 | -m, --mobileprovision [FILE] Specify the mobileprovision file to use 37 | -o, --output [APP.IPA] Path to the output IPA filename 38 | -O, --osversion 9.0 Force specific OSVersion if any in Info.plist 39 | -p, --without-plugins Remove plugins (excluding XCTests) from the resigned IPA 40 | -w, --without-watchapp Remove the WatchApp from the IPA before resigning 41 | -x, --without-xctests Remove the XCTests from the resigned IPA 42 | 43 | Example: 44 | 45 | $ applesign -w -c -m embedded.mobileprovision target.ipa 46 | ``` 47 | 48 | The full help is displayed when passing the `--help` flag. 49 | 50 | ``` 51 | $ bin/applesign.js --help 52 | Usage: 53 | 54 | applesign [--options ...] [input-ipafile] 55 | 56 | Packaging: 57 | -7, --use-7zip Use 7zip instead of unzip 58 | -A, --all-dirs Archive all directories, not just Payload/ 59 | -I, --insert [frida.dylib] Insert a dynamic library to the main executable 60 | -l, --lipo [arm64|armv7] Lipo -thin all bins inside the IPA for the given architecture 61 | -n, --noclean keep temporary files when signing error happens 62 | -o, --output [APP.IPA] Path to the output IPA filename 63 | -P, --parallel Run layered signing dependencies in parallel (EXPERIMENTAL) 64 | -r, --replace Replace the input IPA file with the resigned one 65 | -u, --unfair Resign encrypted applications 66 | -z, --ignore-zip-errors Ignore unzip/7z uncompressing errors 67 | 68 | Stripping: 69 | -p, --without-plugins Remove plugins (excluding XCTests) from the resigned IPA 70 | -w, --without-watchapp Remove the WatchApp from the IPA before resigning 71 | -x, --without-xctests Remove the XCTests from the resigned IPA 72 | 73 | Signing: 74 | --use-openssl Use OpenSSL cms instead of Apple's security tool 75 | -a, --all Resign all binaries, even it unrelated to the app 76 | -i, --identity [1C4D1A..] Specify hash-id of the identity to use 77 | -k, --keychain [KEYCHAIN] Specify alternative keychain file 78 | -K, --add-access-group [NAME] Add $(TeamIdentifier).NAME to keychain-access-groups 79 | -L, --identities List local codesign identities 80 | -m, --mobileprovision [FILE] Specify the mobileprovision file to use 81 | -s, --single Sign a single file instead of an IPA 82 | -S, --self-sign-provision Self-sign mobile provisioning (EXPERIMENTAL) 83 | -v, --verify Verify all the signed files at the end 84 | -V, --verify-twice Verify after signing every file and at the end 85 | 86 | Info.plist 87 | -b, --bundleid [BUNDLEID] Change the bundleid when repackaging 88 | -B, --bundleid-access-group Add $(TeamIdentifier).bundleid to keychain-access-groups 89 | -f, --force-family Force UIDeviceFamily in Info.plist to be iPhone 90 | -H, --allow-http Add NSAppTransportSecurity.NSAllowsArbitraryLoads in plist 91 | -O, --osversion 9.0 Force specific OSVersion if any in Info.plist 92 | 93 | Entitlements: 94 | -c, --clone-entitlements Clone the entitlements from the provisioning to the bin 95 | -e, --entitlements [ENTITL] Specify entitlements file (EXPERIMENTAL) 96 | -E, --entry-entitlement Use generic entitlement (EXPERIMENTAL) 97 | -M, --massage-entitlements Massage entitlements to remove privileged ones 98 | -t, --without-get-task-allow Do not set the get-task-allow entitlement (EXPERIMENTAL) 99 | -C, --no-entitlements-file Do not create .entitlements file in the IPA 100 | 101 | -h, --help Show this help message 102 | --version Show applesign version 103 | [input-ipafile] Path to the IPA file to resign 104 | 105 | Examples: 106 | 107 | $ applesign -L # enumerate codesign identities, grab one and use it with -i 108 | $ applesign -m embedded.mobileprovision target.ipa 109 | $ applesign -i AD71EB42BC289A2B9FD3C2D5C9F02D923495A23C target.ipa 110 | $ applesign -m a.mobileprovision -c --lipo arm64 -w target.ipa 111 | 112 | Installing in the device: 113 | 114 | $ ideviceinstaller -i target-resigned.ipa 115 | $ ios-deploy -b target-resigned.ipa 116 | ``` 117 | 118 | List local codesign identities: 119 | 120 | $ bin/applesign -L 121 | 122 | Resign an IPA with a specific identity: 123 | 124 | $ bin/applesign -i 1C4D1A442A623A91E6656F74D170A711CB1D257A foo.ipa 125 | 126 | Change bundleid: 127 | 128 | $ bin/applesign -b org.nowsecure.testapp path/to/ipa 129 | 130 | ## Signing methods 131 | 132 | There are different ways to sign an IPA file with applesign for experimental 133 | reasons. 134 | 135 | You may want to check the following options: 136 | 137 | **-c, --clone-entitlements** 138 | 139 | put the entitlements embedded inside the signed mobileprovisioning file provided 140 | by the user as the default ones to sign all the binaries 141 | 142 | **-S, --self-sign-provision** 143 | 144 | creates a custom mobileprovisioning (unsigned for now). installd complains 145 | 146 | **-E, --entry-entitlement** 147 | 148 | use the default entitlements plist. useful when troubleshooting 149 | 150 | The default signing method does as follow: 151 | 152 | - Grab entitlements from binary 153 | - Remove problematic entitlements 154 | - Grab entitlements from the provisioning 155 | - Adjust application-id and team-id of the binary with the provisioning ones 156 | - Copy the original mobileprovisioning inside the IPA 157 | - Creates ${AppName}.entitlements and signs all the mach0s 158 | 159 | After some testing we will probably go for having -c or -E as default. 160 | 161 | In addition, for performance reasons, applesign supports -p for parallel 162 | signing. The order of signing the binaries inside an IPA matters, so applesign 163 | creates a dependency list of all the bins and signs them in order. The parallel 164 | signing aims to run in parallel as much tasks as possible without breaking the 165 | dependency list. 166 | 167 | ## Mangling 168 | 169 | It is possible with `--force-family` to remove the UISupportedDevices from the 170 | Info.plist and replace the entitlement information found in the 171 | mobileprovisioning and then carefully massage the rest of entitlements to drop 172 | the privileged ones (`--massage-entitlements`). 173 | 174 | Other interesting manipulations that can be done in the IPA are: 175 | 176 | **-I, --insert [frida.dylib]** 177 | 178 | Allows to insert a dynamic library in the main executable. This is how Frida can 179 | be injected to introspect iOS applications without jailbreak. 180 | 181 | **-l, --lipo [arm64|armv7]** 182 | 183 | Thinifies an IPA by removing all fatmach0s to only contain binaries for one 184 | specified architecture. Also this is helpful to identify non-arm binaries 185 | embedded inside IPA that can be leaked from development or pre-production 186 | environments. 187 | 188 | In order to thinify the final IPA even more, applesign allows to drop the 189 | watchapp extensions which would not be necessary for non Apple Watch users. 190 | 191 | ## Performance 192 | 193 | Sometimes the time required to run the codesigning step matters, so applesign 194 | allows to skip some steps and speedup the process. 195 | 196 | See `--dont-verify` and `--parallel` commandline flags. 197 | 198 | Enabling those options can result on a 35% speedup on ~60MB IPAs. 199 | 200 | ## API usage 201 | 202 | Here's a simple program that resigns an IPA: 203 | 204 | ```js 205 | const Applesign = require("applesign"); 206 | 207 | const as = new Applesign({ 208 | identity: "81A24300FE2A8EAA99A9601FDA3EA811CD80526A", 209 | mobileprovision: "/path/to/dev.mobileprovision", 210 | withoutWatchapp: true, 211 | }); 212 | as.events.on("warning", (msg) => { 213 | console.log("WARNING", msg); 214 | }) 215 | .on("message", (msg) => { 216 | console.log("msg", msg); 217 | }); 218 | 219 | as.signIPA("/path/to/app.ipa") 220 | .then((_) => { 221 | console.log("ios-deploy -b", as.config.outfile); 222 | }) 223 | .catch((e) => { 224 | console.error(e); 225 | process.exitCode = 1; 226 | }); 227 | ``` 228 | 229 | To list the developer identities available in the system: 230 | 231 | ```js 232 | try { 233 | const ids = await as.getIdentities(); 234 | ids.forEach((id) => { 235 | console.log(id.hash, id.name); 236 | }); 237 | } catch (err) { 238 | console.error(err, ids); 239 | } 240 | ``` 241 | 242 | Bear in mind that the Applesign object can tuned to use different configuration 243 | options: 244 | 245 | ```js 246 | const options = { 247 | file: "/path/to/app.ipa", 248 | outfile: "/path/to/app-resigned.ipa", 249 | entitlement: "/path/to/entitlement", 250 | bundleid: "app.company.bundleid", 251 | identity: "hash id of the developer", 252 | mobileprovision: "/path/to/mobileprovision file", 253 | ignoreVerificationErrors: true, 254 | withoutWatchapp: true, 255 | }; 256 | ``` 257 | 258 | ## Further reading 259 | 260 | See the Wiki: https://github.com/nowsecure/node-applesign/wiki 261 | 262 | - https://github.com/maciekish/iReSign 263 | - https://github.com/saucelabs/isign 264 | - https://github.com/phonegap/ios-deploy 265 | 266 | Pre iOS9 devices will require a developer account: 267 | 268 | - http://dev.mlsdigital.net/posts/how-to-resign-an-ios-app-from-external-developers/ 269 | -------------------------------------------------------------------------------- /bin/applesign.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from "node:fs"; 3 | import pkgVersion from "../lib/version.js"; 4 | import { join } from "node:path"; 5 | import * as tools from "../lib/tools.js"; 6 | import * as config from "../lib/config.js"; 7 | import colors from "colors"; 8 | import Applesign from "../index.js"; 9 | 10 | colors.setTheme({ 11 | error: "red", 12 | msg: "yellow", 13 | warning: "green", 14 | }); 15 | 16 | // Removed unused SpawnOptions import (no longer required) 17 | /** 18 | * Main entry point for applesign CLI. 19 | * @param argv Command-line arguments (process.argv) 20 | */ 21 | async function main(argv: string[]): Promise { 22 | const conf = config.parse(argv); 23 | const options = config.compile(conf); 24 | const as = new Applesign(options); 25 | // initialize 26 | if (conf.identities || conf.L) { 27 | const ids = await as.getIdentities(); 28 | ids.forEach((id: any) => { 29 | console.log(id.hash, id.name); 30 | }); 31 | } else if (conf.version) { 32 | console.log(pkgVersion); 33 | } else if (conf.h || conf.help) { 34 | console.error(config.helpMessage); 35 | } else if (conf._.length === 0) { 36 | console.error(config.shortHelpMessage); 37 | } else { 38 | const singleMode = Boolean(conf.s || conf.single); 39 | const target = getTargetMethod(options.file, singleMode); 40 | if (!target) { 41 | throw new Error("Cannot open file"); 42 | } 43 | // Subscribe to signing events 44 | as.events 45 | .on("message", (msg: string) => console.log(colors.msg(msg))) 46 | .on( 47 | "warning", 48 | (msg: string) => console.error(colors.warning("warning"), msg), 49 | ) 50 | .on("error", (msg: string) => console.error(colors.msg(msg))); 51 | if (!options.file) { 52 | throw new Error("No file provided"); 53 | } 54 | try { 55 | await as[target](options.file); 56 | const outfile = as.config.outfile || options.file; 57 | console.log(`Target is now signed: ${outfile}`); 58 | } catch (err) { 59 | process.exitCode = 1; 60 | console.error(err); 61 | } finally { 62 | if (!options.noclean) { 63 | await as.cleanupTmp(); 64 | await as.cleanup(); 65 | } 66 | } 67 | if (as.config.debug) { 68 | const data = JSON.stringify(as.debugObject); 69 | fs.writeFileSync(as.config.debug, data); 70 | console.error(`Debug: json file saved: ${as.config.debug}`); 71 | } 72 | } 73 | } 74 | 75 | // Invoke main with proper typing 76 | main(process.argv) 77 | .catch((err: Error) => { 78 | console.error(err); 79 | process.exitCode = 1; 80 | }); 81 | 82 | /** 83 | * Determine which signing method to use based on file type and mode. 84 | * @param file Path to target file or directory 85 | * @param single Whether to sign a single file instead of an IPA 86 | * @returns Signing method name or undefined if unsupported 87 | */ 88 | type TargetMethod = "signAppDirectory" | "signFile" | "signIPA"; 89 | function getTargetMethod( 90 | file: string | undefined, 91 | single: boolean, 92 | ): TargetMethod | undefined { 93 | if (!file) return undefined; 94 | try { 95 | return tools.isDirectory(file) 96 | ? "signAppDirectory" 97 | : single 98 | ? "signFile" 99 | : "signIPA"; 100 | } catch { 101 | return undefined; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /example.ts: -------------------------------------------------------------------------------- 1 | import Applesign from "./index.js"; 2 | 3 | const as = new Applesign({ 4 | /* bin/applesign -L to list all available identities in your system */ 5 | identity: "67CF8DCD3BA1E7241FFCFCE66FA6C0F58D17F795", 6 | /* clone the entitlements from the mobile provisioning */ 7 | cloneEntitlements: false, 8 | mobileProvisioning: "/tmp/embedded.mobileprovision", 9 | }); 10 | 11 | if (process.argv.length < 3) { 12 | console.error("Usage: example.js [path/to/ipa]"); 13 | process.exit(1); 14 | } 15 | 16 | as.events.on("message", (msg: any) => { 17 | console.log("message", msg); 18 | }).on("warning", (msg: any) => { 19 | console.error("warning", msg); 20 | }); 21 | as.signIPA!(process.argv[2]!).then((_) => { 22 | console.log("ios-deploy -b", as.config.outfile); 23 | }); 24 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import * as tools from "./lib/tools.js"; 2 | import * as config from "./lib/config.js"; 3 | import idprov from "./lib/idprov.js"; 4 | import { EventEmitter } from "node:events"; 5 | import path from "node:path"; 6 | import { execSync } from "node:child_process"; 7 | import * as uuid from "uuid"; 8 | import fs from "fs-extra"; 9 | import walk from "fs-walk"; 10 | import plist from "simple-plist"; 11 | import fchk from "./lib/fchk.js"; 12 | import { homedir, tmpdir } from "node:os"; 13 | import { AppDirectory } from "./lib/appdir.js"; 14 | import depSolver from "./lib/depsolver.js"; 15 | import adjustInfoPlist from "./lib/info-plist.js"; 16 | import defaultEntitlements from "./lib/entitlements.js"; 17 | // import { build as plistBuild } from 'plist'; 18 | import * as bin from "./lib/bin.js"; 19 | 20 | import pkg from "plist"; 21 | const { build: plistBuild } = pkg; 22 | 23 | class Applesign { 24 | config: config.ConfigOptions; 25 | debugObject: Record; 26 | events: EventEmitter; 27 | nested: any[]; 28 | tmpDir: string; 29 | constructor(options: any) { 30 | this.config = config.fromOptions(options || {}); 31 | this.events = new EventEmitter(); 32 | this.nested = []; 33 | this.debugObject = {}; 34 | this.tmpDir = this._makeTmpDir(); 35 | } 36 | 37 | _makeTmpDir() { 38 | const tmp = tmpdir(); 39 | const base = path.join(tmp, "applesign"); 40 | const result = path.join(base, uuid.v4()); 41 | fs.mkdirSync(result, { recursive: true }); 42 | return result; 43 | } 44 | 45 | _fullPathInTmp(filePath: string) { 46 | const dirname = path.dirname(filePath); 47 | const dirnameInTmp = path.join(this.tmpDir, dirname); 48 | fs.mkdirpSync(dirnameInTmp); 49 | return path.join(this.tmpDir, filePath); 50 | } 51 | 52 | async getDeviceProvision() { 53 | const installedProvisions = await tools.ideviceprovision("list"); 54 | const pd = path.join( 55 | homedir(), 56 | "Library", 57 | "MobileDevice", 58 | "Provisioning Profiles", 59 | ); 60 | for (const ip of installedProvisions) { 61 | const absPath = path.join(pd, ip + ".mobileprovision"); 62 | if (fs.existsSync(absPath)) { 63 | return absPath; 64 | } 65 | } 66 | throw new Error( 67 | "Cannot find provisioning file automatically. Please use -m", 68 | ); 69 | } 70 | 71 | async signXCarchive(file: string) { 72 | fchk(arguments, ["string"]); 73 | const ipaFile = file + ".ipa"; 74 | await tools.xcaToIpa(file, ipaFile); 75 | await this.signIPA(ipaFile); 76 | } 77 | 78 | async getIdentities() { 79 | fchk(arguments, []); 80 | return tools.getIdentities(); 81 | } 82 | 83 | async signIPA(file: string) { 84 | fchk(arguments, ["string"]); 85 | if (typeof file === "string") { 86 | this.setFile(file); 87 | } 88 | tools.setOptions({ 89 | use7zip: this.config.use7zip, 90 | useOpenSSL: this.config.useOpenSSL, 91 | }); 92 | // unsure, but this.config.file shouldnt be undefined 93 | if (!this.config.file) { 94 | this.config.file = file; 95 | } 96 | await this._pullMobileProvision(); 97 | this.emit("message", "File: " + this.config.file); 98 | this.emit("message", "Outdir: " + this.config.outdir); 99 | if (tools.isDirectory(this.config.file as string)) { 100 | throw new Error("This is a directory"); 101 | } 102 | try { 103 | await this.unzipIPA(this.config.file, this.config.outdir); 104 | const appDirectory = path.join(this.config.outdir, "/Payload"); 105 | this.config.appdir = getAppDirectory(appDirectory); 106 | if (this.config.debug) { 107 | this.debugObject = {}; 108 | } 109 | const tasks = []; 110 | if (this.config.withoutWatchapp) { 111 | tasks.push(this.removeWatchApp()); 112 | } 113 | // TODO: this .withoutSigningFiles option doesnt exist yet 114 | if (this.config.withoutSigningFiles) { 115 | tasks.push(this.removeSigningFiles()); 116 | } 117 | if (this.config.withoutPlugins) { 118 | tasks.push(this.removePlugins()); 119 | } 120 | if (this.config.withoutXCTests) { 121 | tasks.push(this.removeXCTests()); 122 | } 123 | if (tasks.length > 0) { 124 | await Promise.all(tasks); 125 | } 126 | await this.signAppDirectory(appDirectory); 127 | await this.zipIPA(); 128 | } finally { 129 | await this.cleanup(); 130 | } 131 | return this; 132 | } 133 | 134 | async _pullMobileProvision() { 135 | if (this.config.deviceProvision === true) { 136 | this.config.mobileprovision = await this.getDeviceProvision(); 137 | this.config.mobileprovisions = [this.config.mobileprovision]; 138 | this.config.identity = idprov(this.config.mobileprovision); 139 | } 140 | this.config.mobileprovision = this.config.mobileprovisions[0]; 141 | if (this.config.mobileprovisions.length > 1) { 142 | this.config.mobileprovisions.slice(1); 143 | } 144 | } 145 | 146 | async signAppDirectoryInternal(ipadir: string, skipNested: boolean) { 147 | fchk(arguments, ["string", "boolean"]); 148 | await this._pullMobileProvision(); 149 | if (this.config.run) { 150 | runScriptSync(this.config.run, this); 151 | } 152 | if (this.config.appdir === undefined) { 153 | this.config.appdir = ipadir; 154 | } 155 | const binname = getExecutable(this.config.appdir); 156 | this.emit("msg", "Main Executable Name: " + binname); 157 | this.config.appbin = path.join(this.config.appdir, binname); 158 | if (!fs.lstatSync(this.config.appbin).isFile()) { 159 | throw new Error("This was supposed to be a file"); 160 | } 161 | if (bin.isBitcode(this.config.appbin)) { 162 | throw new Error( 163 | "This IPA contains only bitcode. Must be transpiled for the target device to run.", 164 | ); 165 | } 166 | if (bin.isEncrypted(this.config.appbin)) { 167 | if (!this.config.unfairPlay) { 168 | throw new Error("This IPA is encrypted"); 169 | } 170 | this.emit("warning", "Main IPA executable is encrypted"); 171 | } else { 172 | this.emit("message", "Main IPA executable is not encrypted"); 173 | } 174 | if (this.config.insertLibrary !== undefined) { 175 | await injectLibrary(this.config); 176 | } 177 | const infoPlistPath = path.join(this.config.appdir, "Info.plist"); 178 | adjustInfoPlist(infoPlistPath, this.config, this.emit.bind(this)); 179 | if (!this.config.pseudoSign) { 180 | if (!this.config.mobileprovision) { 181 | throw new Error("warning: No mobile provisioning file provided"); 182 | } 183 | await this.checkProvision( 184 | this.config.appdir, 185 | this.config.mobileprovision, 186 | ); 187 | } 188 | await this.adjustEntitlements(this.config.appbin); 189 | await this.signLibraries(this.config.appbin, this.config.appdir); 190 | 191 | if (skipNested !== true) { 192 | for (const nest of this.nested) { 193 | if (tools.isDirectory(nest)) { 194 | await this.signAppDirectoryInternal(nest, true); 195 | } else { 196 | this.emit("warning", "Cannot find " + nest); 197 | } 198 | } 199 | } 200 | } 201 | 202 | async signAppDirectory(ipadir: string) { 203 | fchk(arguments, ["string"]); 204 | return this.signAppDirectoryInternal(ipadir, false); 205 | } 206 | 207 | async removeWatchApp() { 208 | fchk(arguments, []); 209 | const watchdir = path.join(this.config.appdir, "Watch"); 210 | this.emit("message", "Stripping out the WatchApp at " + watchdir); 211 | await tools.asyncRimraf(watchdir); 212 | 213 | const placeholderdir = path.join( 214 | this.config.appdir, 215 | "com.apple.WatchPlaceholder", 216 | ); 217 | this.emit("message", "Stripping out the WatchApp at " + placeholderdir); 218 | await tools.asyncRimraf(placeholderdir); 219 | } 220 | 221 | // XXX some directory leftovers 222 | async removeXCTests() { 223 | fchk(arguments, []); 224 | const dir = this.config.appdir; 225 | walk.walkSync(dir, (basedir: string, filename: string, stat: any) => { 226 | const target = path.join(basedir, filename); 227 | // if (target.toLowerCase().indexOf('/xct') !== -1) 228 | if (target.toLowerCase().indexOf("xctest") !== -1) { 229 | this.emit("message", "Deleting " + target); 230 | fs.unlinkSync(target); 231 | } 232 | }); 233 | } 234 | 235 | async removeSigningFiles() { 236 | fchk(arguments, []); 237 | const dir = this.config.appdir; 238 | walk.walkSync(dir, (basedir: string, filename: string, stat: any) => { 239 | if ( 240 | filename.endsWith(".entitlements") || 241 | filename.endsWith(".mobileprovision") 242 | ) { 243 | const target = path.join(basedir, filename); 244 | this.emit("message", "Deleting " + target); 245 | fs.unlinkSync(target); 246 | } 247 | }); 248 | } 249 | 250 | async removePlugins() { 251 | fchk(arguments, []); 252 | const plugdir = path.join(this.config.appdir, "PlugIns"); 253 | const tmpdir = path.join(this.config.appdir, "applesign_xctest_tmp"); 254 | this.emit("message", "Stripping out the PlugIns at " + plugdir); 255 | let tests: string[] = []; 256 | if (!this.config.withoutXCTests) { 257 | tests = await enumerateTestFiles(plugdir); 258 | if (tests.length > 0) { 259 | await moveFiles(tests, plugdir, tmpdir); 260 | } 261 | } 262 | 263 | await tools.asyncRimraf(plugdir); 264 | if (tests.length > 0) { 265 | await moveFiles(tests, tmpdir, plugdir); 266 | await fs.rmdir(tmpdir); 267 | } 268 | } 269 | 270 | findProvisioningsSync() { 271 | fchk(arguments, []); 272 | const files: string[] = []; 273 | walk.walkSync( 274 | this.config.appdir, 275 | (basedir: string, filename: string, stat: any) => { 276 | const file = path.join(basedir, filename); 277 | // only walk on files. Symlinks and other special files are forbidden 278 | if (!fs.lstatSync(file).isFile()) { 279 | return; 280 | } 281 | if (filename === "embedded.mobileprovision") { 282 | files.push(file); 283 | } 284 | }, 285 | ); 286 | return files; 287 | } 288 | 289 | /* 290 | TODO: verify is mobileprovision app-id glob string matches the bundleid 291 | read provision file in raw 292 | search for application-identifier and ... 293 | check if prefix matches and last dot separated word is an asterisk 294 | const identifierInProvisioning = 'x' 295 | Read the one in Info.plist and compare with bundleid 296 | */ 297 | async checkProvision(appdir: string, file: string) { 298 | fchk(arguments, ["string", "string"]); 299 | /* Deletes the embedded.mobileprovision from the ipa? */ 300 | const withoutMobileProvision = false; 301 | if (withoutMobileProvision) { 302 | const files = this.findProvisioningsSync(); 303 | files.forEach((file: string) => { 304 | console.error("Deleting ", file); 305 | fs.unlinkSync(file); 306 | }); 307 | } 308 | if (appdir && file && !withoutMobileProvision) { 309 | this.emit("message", "Embedding new mobileprovision"); 310 | const mobileProvision = path.join(appdir, "embedded.mobileprovision"); 311 | if (this.config.selfSignedProvision) { 312 | /* update entitlements */ 313 | const data = await tools.getMobileProvisionPlist( 314 | this.config.mobileprovision, 315 | ); 316 | const mainBin = path.join(appdir, getExecutable(appdir)); 317 | let ent = bin.entitlements(mainBin); 318 | if (ent === null) { 319 | this.emit( 320 | "warning", 321 | "Cannot find entitlements in binary. Using defaults", 322 | ); 323 | const entMobProv = data.Entitlements; 324 | const teamId = entMobProv["com.apple.developer.team-identifier"]; 325 | const appId = entMobProv["application-identifier"]; 326 | ent = defaultEntitlements(appId, teamId); 327 | } 328 | data.Entitlements = plist.parse(ent.toString().trim()); 329 | fs.writeFileSync(mobileProvision, plistBuild(data).toString()); 330 | /* TODO: self-sign mobile provisioning */ 331 | } 332 | return fs.copySync(file, mobileProvision); 333 | } 334 | } 335 | 336 | debugInfo(path: string, key: any, val: any) { 337 | if (!val) { 338 | return; 339 | } 340 | const f = path.replace(this.config.outdir + "/", ""); 341 | if (!this.debugObject) { 342 | this.debugObject = {}; 343 | } 344 | if (this.debugObject[f] === undefined) { 345 | this.debugObject[f] = {}; 346 | } 347 | this.debugObject[f][key] = val; 348 | } 349 | 350 | addEntitlementsSync(orig: any) { 351 | if (this.config.addEntitlements === undefined) { 352 | return orig; 353 | } 354 | this.emit("message", "Adding entitlements from file"); 355 | const addEnt = plist.readFileSync(this.config.addEntitlements); 356 | // TODO: deepmerge 357 | return Object.assign(orig, addEnt); 358 | } 359 | 360 | adjustEntitlementsSync(file: string, entMobProv: any) { 361 | if (this.config.pseudoSign) { 362 | const ent = bin.entitlements(file); 363 | if (ent === null) { 364 | return; 365 | } 366 | let entMacho = plist.parse(ent.toString().trim()); 367 | entMacho = this.addEntitlementsSync(entMacho); 368 | // TODO: merge additional entitlements here 369 | const newEntitlements = plistBuild(entMacho).toString(); 370 | const newEntitlementsFile = file + ".entitlements"; 371 | const tmpEntitlementsFile = this._fullPathInTmp(newEntitlementsFile); 372 | fs.writeFileSync(tmpEntitlementsFile, newEntitlements); 373 | this.config.entitlement = tmpEntitlementsFile; 374 | return; 375 | } 376 | fchk(arguments, ["string", "object"]); 377 | this.debugInfo(file, "before", entMobProv); 378 | const teamId = entMobProv["com.apple.developer.team-identifier"]; 379 | const appId = entMobProv["application-identifier"]; 380 | let ent = bin.entitlements(file); 381 | if (ent === null && !this.config.cloneEntitlements) { 382 | console.error("Cannot find entitlements in binary. Using defaults"); 383 | ent = defaultEntitlements(appId, teamId); 384 | } 385 | let entMacho: any; 386 | if (ent !== null) { 387 | entMacho = plist.parse(ent.toString().trim()); 388 | entMacho = this.addEntitlementsSync(entMacho); 389 | this.debugInfo(file, "fullPath", file); 390 | this.debugInfo(file, "oldEntitlements", entMacho || "TODO"); 391 | if (this.config.selfSignedProvision) { 392 | this.emit("message", "Using an unsigned provisioning"); 393 | const newEntitlementsFile = file + ".entitlements"; 394 | const newEntitlements = plistBuild(entMacho).toString(); 395 | const tmpEntitlementsFile = this._fullPathInTmp(newEntitlementsFile); 396 | fs.writeFileSync(tmpEntitlementsFile, newEntitlements); 397 | this.config.entitlement = tmpEntitlementsFile; 398 | if (!this.config.noEntitlementsFile) { 399 | fs.writeFileSync(newEntitlementsFile, tmpEntitlementsFile); 400 | } 401 | this.debugInfo(file, "newEntitlements", plist.parse(newEntitlements)); 402 | return; 403 | } 404 | } 405 | let changed = false; 406 | if (this.config.cloneEntitlements) { 407 | this.emit("message", "Cloning entitlements"); 408 | entMacho = entMobProv; 409 | changed = true; 410 | } else { 411 | const k = "com.apple.developer.icloud-container-identifiers"; 412 | if (entMacho[k]) { 413 | entMacho[k] = "iCloud." + appId; 414 | } 415 | ["application-identifier", "com.apple.developer.team-identifier"].forEach( 416 | (id) => { 417 | if (entMacho[id] !== entMobProv[id]) { 418 | changed = true; 419 | entMacho[id] = entMobProv[id]; 420 | } 421 | }, 422 | ); 423 | if (this.config.massageEntitlements === true) { 424 | if (typeof entMacho["keychain-access-groups"] === "object") { 425 | changed = true; 426 | // keychain access groups makes the resigning fail with -M 427 | delete entMacho["keychain-access-groups"]; 428 | // entMacho['keychain-access-groups'][0] = appId; 429 | } 430 | [ 431 | "com.apple.developer.ubiquity-kvstore-identifier", 432 | "com.apple.developer.ubiquity-container-identifiers", 433 | "com.apple.developer.icloud-container-identifiers", 434 | "com.apple.developer.icloud-container-environment", 435 | "com.apple.developer.icloud-services", 436 | "com.apple.developer.payment-pass-provisioning", 437 | "com.apple.developer.default-data-protection", 438 | "com.apple.networking.vpn.configuration", 439 | "com.apple.developer.associated-domains", 440 | "com.apple.security.application-groups", 441 | "com.apple.developer.in-app-payments", 442 | "com.apple.developer.siri", 443 | "beta-reports-active", /* our entitlements doesnt support beta */ 444 | "aps-environment", 445 | ].forEach((id) => { 446 | if (typeof entMacho[id] !== "undefined") { 447 | delete entMacho[id]; 448 | changed = true; 449 | } 450 | }); 451 | } else if (!this.config.cloneEntitlements) { 452 | delete entMacho["com.apple.developer.icloud-container-identifiers"]; 453 | delete entMacho["com.apple.developer.icloud-container-environment"]; 454 | delete entMacho["com.apple.developer.ubiquity-kvstore-identifier"]; 455 | delete entMacho["com.apple.developer.icloud-services"]; 456 | delete entMacho["com.apple.developer.siri"]; 457 | delete entMacho["com.apple.developer.in-app-payments"]; 458 | delete entMacho["aps-environment"]; 459 | delete entMacho["com.apple.security.application-groups"]; 460 | delete entMacho["com.apple.developer.associated-domains"]; 461 | delete entMacho["keychain-access-groups"]; 462 | changed = true; 463 | } 464 | } 465 | 466 | if (typeof this.config.withGetTaskAllow !== "undefined") { 467 | this.emit( 468 | "message", 469 | "get-task-allow set to " + this.config.withGetTaskAllow, 470 | ); 471 | entMacho["get-task-allow"] = this.config.withGetTaskAllow; 472 | changed = true; 473 | } 474 | 475 | const additionalKeychainGroups = []; 476 | if (typeof this.config.customKeychainGroup === "string") { 477 | additionalKeychainGroups.push(this.config.customKeychainGroup); 478 | } 479 | const infoPlist = path.join(this.config.appdir, "Info.plist"); 480 | const plistData = plist.readFileSync(infoPlist); 481 | if (this.config.bundleIdKeychainGroup) { 482 | if (typeof this.config.bundleid === "string") { 483 | additionalKeychainGroups.push(this.config.bundleid); 484 | } else { 485 | const bundleid = plistData.CFBundleIdentifier; 486 | additionalKeychainGroups.push(bundleid); 487 | } 488 | } 489 | if (this.config.osversion !== undefined) { 490 | // DTPlatformVersion 491 | plistData.MinimumOSVersion = this.config.osversion; 492 | plist.writeFileSync(infoPlist, plistData); 493 | } 494 | if (additionalKeychainGroups.length > 0) { 495 | const newGroups = additionalKeychainGroups.map( 496 | (group) => `${teamId}.${group}`, 497 | ); 498 | const groups = entMacho["keychain-access-groups"]; 499 | if (typeof groups === "undefined") { 500 | entMacho["keychain-access-groups"] = newGroups; 501 | } else { 502 | groups.push(...newGroups); 503 | } 504 | changed = true; 505 | } 506 | if (changed || this.config.entry) { 507 | const newEntitlementsFile = file + ".entitlements"; 508 | let newEntitlements = appId && teamId && this.config.entry 509 | ? defaultEntitlements(appId, teamId) 510 | : this.config.entitlement 511 | ? fs.readFileSync(this.config.entitlement).toString() 512 | : plistBuild(entMacho).toString(); 513 | const ent = plist.parse(newEntitlements.trim()); 514 | const shouldRenameGroups = !this.config.mobileprovision && 515 | !this.config.cloneEntitlements; 516 | if (shouldRenameGroups && ent["com.apple.security.application-groups"]) { 517 | const ids = appId.split("."); 518 | ids.shift(); 519 | const id = ids.join("."); 520 | const groups = []; 521 | for (const group of ent["com.apple.security.application-groups"]) { 522 | const cols = group.split("."); 523 | if (cols.length === 4) { 524 | groups.push("group." + id); 525 | } else { 526 | groups.push("group." + id + "." + cols.pop()); 527 | } 528 | } 529 | ent["com.apple.security.application-groups"] = groups; 530 | } 531 | delete ent[ 532 | "beta-reports-active" 533 | ]; /* our entitlements doesnt support beta */ 534 | if (this.config.massageEntitlements === true) { 535 | delete ent["com.apple.developer.ubiquity-container-identifiers"]; // TODO should be massaged 536 | } 537 | newEntitlements = plistBuild(ent).toString(); 538 | 539 | this.debugInfo(file, "newEntitlements", ent); 540 | 541 | const tmpEntitlementsFile = this._fullPathInTmp(newEntitlementsFile); 542 | fs.writeFileSync(tmpEntitlementsFile, newEntitlements); 543 | this.config.entitlement = tmpEntitlementsFile; 544 | if (!this.config.noEntitlementsFile) { 545 | fs.writeFileSync(tmpEntitlementsFile, newEntitlements); 546 | this.emit( 547 | "message", 548 | "Updated binary entitlements" + tmpEntitlementsFile, 549 | ); 550 | } 551 | this.debugInfo(file, "after", newEntitlements); 552 | } else { 553 | this.debugInfo(file, "nothing-changed", true); 554 | } 555 | } 556 | 557 | async adjustEntitlements(file: string) { 558 | fchk(arguments, ["string"]); 559 | let newEntitlements = null; 560 | if (!this.config.pseudoSign) { 561 | const mp = this.config.mobileprovision 562 | ? this.config.mobileprovision 563 | : path.join(this.config.appdir, "embedded.mobileprovision"); 564 | newEntitlements = await tools.getEntitlementsFromMobileProvision(mp); 565 | this.emit("message", JSON.stringify(newEntitlements)); 566 | } 567 | this.adjustEntitlementsSync(file, newEntitlements); 568 | } 569 | 570 | async signFile(file: string) { 571 | const config = this.config; 572 | function customOptions(config: any, file: string) { 573 | if ( 574 | typeof config.json === "object" && 575 | typeof config.json.custom === "object" 576 | ) { 577 | for (const c of config.json.custom) { 578 | if (!c.filematch) { 579 | continue; 580 | } 581 | const re = new RegExp(c.filematch); 582 | if (re.test(file)) { 583 | // console.error('Debug: '+ JSON.stringify(c, null, 2)) 584 | return c; 585 | } 586 | } 587 | } 588 | return false; 589 | } 590 | const custom = customOptions(config, file); 591 | function getKeychain() { 592 | return custom !== false && custom.keychain !== undefined 593 | ? custom.keychain 594 | : config.keychain; 595 | } 596 | function getIdentity() { 597 | return custom !== false && custom.identity !== undefined 598 | ? custom.identity 599 | : config.identity; 600 | } 601 | function getEntitlements() { 602 | return custom !== false && custom.entitlements !== undefined 603 | ? custom.entitlements 604 | : config.entitlement; 605 | } 606 | 607 | fchk(arguments, ["string"]); 608 | if (this.config.lipoArch !== undefined) { 609 | this.emit("message", "[lipo] " + this.config.lipoArch + " " + file); 610 | try { 611 | await tools.lipoFile(file, this.config.lipoArch); 612 | } catch (ignored) {} 613 | } 614 | function codesignHasFailed(config: any, error: any, errmsg: string) { 615 | if (error && errmsg.indexOf("Error:") !== -1) { 616 | throw error; 617 | } 618 | return ( 619 | (errmsg && errmsg.indexOf("no identity found") !== -1) || 620 | !config.ignoreCodesignErrors 621 | ); 622 | } 623 | const identity = getIdentity(); 624 | let entitlements = ""; 625 | if (this.config.cloneEntitlements) { 626 | const mp = await tools.getMobileProvisionPlist( 627 | this.config.mobileprovision, 628 | ); 629 | const newEntitlementsFile = file + ".entitlements"; 630 | const tmpEntitlementsFile = this._fullPathInTmp(newEntitlementsFile); 631 | const entstr = plistBuild(mp.Entitlements, { 632 | pretty: true, 633 | allowEmpty: false, 634 | }).toString(); 635 | fs.writeFileSync(tmpEntitlementsFile, entstr); 636 | entitlements = tmpEntitlementsFile; 637 | } else { 638 | entitlements = getEntitlements(); 639 | } 640 | let res: any; 641 | if (this.config.pseudoSign) { 642 | const newEntitlementsFile = file + ".entitlements"; 643 | const tmpEntitlementsFile = this._fullPathInTmp(newEntitlementsFile); 644 | const entitlements = fs.existsSync(tmpEntitlementsFile) 645 | ? tmpEntitlementsFile 646 | : null; 647 | res = await tools.pseudoSign(entitlements, file); 648 | } else { 649 | const keychain = getKeychain(); 650 | res = await tools.codesign(identity, entitlements, keychain, file); 651 | if (res.code !== 0 && codesignHasFailed(config, res.code, res.stderr)) { 652 | return this.emit("end", res.stderr); 653 | } 654 | } 655 | this.emit("message", "Signed " + file); 656 | if (config.verifyTwice) { 657 | this.emit("message", "Verify " + file); 658 | const res = await tools.verifyCodesign(file, config.keychain); 659 | if (res.code !== 0) { 660 | const type = config.ignoreVerificationErrors ? "warning" : "error"; 661 | return this.emit(type, res.stderr); 662 | } 663 | } 664 | return this; 665 | } 666 | 667 | filterLibraries(libraries: any) { 668 | fchk(arguments, ["object"]); 669 | return libraries.filter((library: any) => { 670 | // Resign all frameworks. even if not referenced :? 671 | if (library.indexOf("Frameworks/") !== -1) { 672 | return true; 673 | } 674 | if (this.config.all) { 675 | return true; 676 | } 677 | // check if there's a Plist to inform us which is the right executable 678 | const exe = getExecutable(path.dirname(library)); 679 | if (path.basename(library) !== exe) { 680 | this.emit("warning", "Not signing " + library); 681 | return false; 682 | } 683 | return true; 684 | }); 685 | } 686 | 687 | findLibrariesSync() { 688 | fchk(arguments, []); 689 | const libraries: any = []; 690 | const nested: any = []; 691 | const exe = path.sep + getExecutable(this.config.appdir); 692 | const folders = this.config.appbin.split(path.sep); 693 | const exe2 = path.sep + folders[folders.length - 1]; 694 | 695 | let found = false; 696 | walk.walkSync( 697 | this.config.appdir, 698 | (basedir: any, filename: any, stat: any) => { 699 | const file = path.join(basedir, filename); 700 | // only walk on files. Symlinks and other special files are forbidden 701 | if (!fs.lstatSync(file).isFile()) { 702 | return; 703 | } 704 | if (file.endsWith(exe) || file.endsWith(exe2)) { 705 | this.emit("message", "Executable found at " + file); 706 | libraries.push(file); 707 | found = true; 708 | return; 709 | } 710 | 711 | const nest = nestedApp(file); 712 | if (nest !== false) { 713 | if (nested.indexOf(nest) === -1) { 714 | nested.push(nest); 715 | } 716 | return; 717 | } 718 | if (bin.isMacho(file)) { 719 | libraries.push(file); 720 | } 721 | }, 722 | ); 723 | if (!found) { 724 | throw new Error("Cannot find any MACH0 binary to sign"); 725 | } 726 | console.error("Found nested", nested); 727 | this.nested = nested; 728 | // return this.filterLibraries(libraries); 729 | 730 | return libraries; 731 | } 732 | 733 | async signLibraries(bpath: string, appdir: string) { 734 | fchk(arguments, ["string", "string"]); 735 | this.emit("message", "Signing libraries and frameworks"); 736 | 737 | const parallelVerify = async (libs: any) => { 738 | if (!this.config.verify) { 739 | return; 740 | } 741 | this.emit("message", "Verifying " + libs); 742 | const promises = libs.map((lib: any) => tools.verifyCodesign); 743 | return Promise.all(promises); 744 | }; 745 | 746 | const layeredSigning = async (libs: any) => { 747 | const libsCopy = libs.slice(0).reverse(); 748 | for (const deps of libsCopy) { 749 | const promises = deps.map((dep: any) => { 750 | return this.signFile(dep); 751 | }); 752 | await Promise.all(promises); 753 | } 754 | await parallelVerify(libs); 755 | }; 756 | 757 | const serialSigning = async (libs: any) => { 758 | const libsCopy = libs.slice(0).reverse(); 759 | for (const lib of libsCopy) { 760 | await this.signFile(lib); 761 | if (this.config.verify) { 762 | this.emit("message", "Verifying " + lib); 763 | await tools.verifyCodesign(lib); 764 | } 765 | } 766 | }; 767 | 768 | this.emit("message", "Resolving signing order using layered list"); 769 | let libs = []; 770 | const ls = new AppDirectory(); 771 | await ls.loadFromDirectory(appdir); 772 | if (this.config.parallel) { 773 | // known to be buggy in some situations, must use AppDirectory 774 | const libraries = this.findLibrariesSync(); 775 | libs = await depSolver(bpath, libraries, true); 776 | 777 | for (const appex of ls.appexs) { 778 | libs.push([appex]); 779 | } 780 | } else { 781 | for (const appex of ls.appexs) { 782 | await this.adjustEntitlements(appex); 783 | await this.signFile(appex); 784 | } 785 | 786 | this.emit( 787 | "message", 788 | "Nested: " + JSON.stringify(ls.nestedApplications()), 789 | ); 790 | this.emit( 791 | "message", 792 | "SystemLibraries: " + JSON.stringify(ls.systemLibraries()), 793 | ); 794 | this.emit( 795 | "message", 796 | "DiskLibraries: " + JSON.stringify(ls.diskLibraries()), 797 | ); 798 | this.emit( 799 | "message", 800 | "UnavailableLibraries: " + JSON.stringify(ls.unavailableLibraries()), 801 | ); 802 | this.emit( 803 | "message", 804 | "AppLibraries: " + JSON.stringify(ls.appLibraries()), 805 | ); 806 | this.emit("message", "Orphan: " + JSON.stringify(ls.orphanedLibraries())); 807 | const libraries = ls.appLibraries(); 808 | if (this.config.all) { 809 | libraries.push(...ls.orphanedLibraries()); 810 | } else { 811 | for (const ol of ls.orphanedLibraries()) { 812 | console.error("Warning: Orphaned library not signed, try -a: " + ol); 813 | } 814 | } 815 | this.debugInfo("analysis", "orphan", ls.orphanedLibraries()); 816 | // const libraries = ls.diskLibraries (); 817 | libs = libraries.filter((library: any) => !ls.appexs.includes(library)); // remove already-signed appexs 818 | } 819 | if (libs.length === 0) { 820 | libs.push(bpath); 821 | } 822 | return typeof libs[0] === "object" 823 | ? layeredSigning(libs) 824 | : serialSigning(libs); 825 | } 826 | 827 | async cleanup() { 828 | fchk(arguments, []); 829 | if (this.config.noclean) { 830 | return; 831 | } 832 | const outdir = this.config.outdir; 833 | this.emit("message", "Cleaning up " + outdir); 834 | // await tools.asyncRimraf(this.config.outfile); 835 | return tools.asyncRimraf(outdir); 836 | } 837 | 838 | async cleanupTmp() { 839 | this.emit("message", "Cleaning up temp dir " + this.tmpDir); 840 | await tools.asyncRimraf(this.tmpDir); 841 | } 842 | 843 | async zipIPA() { 844 | fchk(arguments, []); 845 | if (!this.config.file) { 846 | console.error("Missing input file to zip"); 847 | return; 848 | } 849 | const ipaIn = this.config.file as string; 850 | const ipaOut = getOutputPath(this.config.outdir, this.config.outfile!); 851 | try { 852 | fs.unlinkSync(ipaOut); // await for it 853 | } catch (e) { 854 | /* do nothing */ 855 | } 856 | this.events.emit("message", "Zipifying into " + ipaOut + " ..."); 857 | const rootFolder = this.config.payloadOnly ? "Payload" : "."; 858 | await tools.zip(this.config.outdir, ipaOut, rootFolder); 859 | if (this.config.replaceipa) { 860 | this.events.emit("message", "mv into " + ipaIn); 861 | fs.rename(ipaOut, ipaIn); 862 | } 863 | } 864 | 865 | setFile(name: any) { 866 | fchk(arguments, ["string"]); 867 | this.config.file = path.resolve(name); 868 | this.config.outdir = this.config.file + "." + uuid.v4(); 869 | if (!this.config.outfile) { 870 | this.config.outfile = getResignedFilename(this.config.file); 871 | } 872 | } 873 | 874 | async unzipIPA(file: any, outdir: any): Promise { 875 | fchk(arguments, ["string", "string"]); 876 | if (!file || !outdir) { 877 | throw new Error("No output specified"); 878 | } 879 | if (!outdir) { 880 | throw new Error("Invalid output directory"); 881 | } 882 | await this.cleanup(); 883 | this.events.emit("message", "Unzipping " + file); 884 | return tools.unzip(file, outdir); 885 | } 886 | 887 | /* Event Wrapper API with cb support */ 888 | emit(ev: any, msg: any) { 889 | this.events.emit(ev, msg); 890 | } 891 | 892 | on(ev: any, cb: any) { 893 | this.events.on(ev, cb); 894 | return this; 895 | } 896 | } 897 | 898 | // helper functions 899 | 900 | function getResignedFilename(input: string): string | null { 901 | if (!input) { 902 | return null; 903 | } 904 | const pos = input.lastIndexOf(path.sep); 905 | if (pos !== -1) { 906 | const tmp = input.substring(pos + 1); 907 | const dot = tmp.lastIndexOf("."); 908 | input = dot !== -1 ? tmp.substring(0, dot) : tmp; 909 | } else { 910 | const dot = input.lastIndexOf("."); 911 | if (dot !== -1) { 912 | input = input.substring(0, dot); 913 | } 914 | } 915 | return input + "-resigned.ipa"; 916 | } 917 | 918 | function getExecutable(appdir: string) { 919 | if (!appdir) { 920 | throw new Error("No application directory is provided"); 921 | } 922 | const plistPath = path.join(appdir, "Info.plist"); 923 | try { 924 | const plistData = plist.readFileSync(plistPath); 925 | const cfBundleExecutable = plistData.CFBundleExecutable; 926 | if (cfBundleExecutable) { 927 | return cfBundleExecutable; 928 | } 929 | } catch (e) { 930 | // do nothing 931 | } 932 | const exename = path.basename(appdir); 933 | const dotap = exename.indexOf(".app"); 934 | return dotap === -1 ? exename : exename.substring(0, dotap); 935 | } 936 | 937 | async function injectLibrary(config: any) { 938 | const appDir = config.appdir; 939 | const targetLib = config.insertLibrary; 940 | const libraryName = path.basename(targetLib); 941 | try { 942 | fs.mkdirSync(path.join(appDir, "Frameworks")); 943 | } catch (_) {} 944 | const outputLib = path.join(appDir, "Frameworks", libraryName); 945 | await insertLibraryLL(outputLib, targetLib, config); 946 | } 947 | 948 | function insertLibraryLL(outputLib: any, targetLib: string, config: any) { 949 | return new Promise((resolve, reject) => { 950 | try { 951 | const writeStream = fs.createWriteStream(outputLib); 952 | writeStream.on("finish", () => { 953 | /* XXX: if binary doesnt contains an LC_RPATH load command this will not work */ 954 | const insertedLibraryName = "@rpath/" + path.basename(targetLib); 955 | fs.chmodSync(outputLib, 0x1ed); // 0755 956 | /* Just copy the library via USB on the DCIM directory */ 957 | // const insertedLibraryName = '/var/mobile/Media/DCIM/' + path.basename(targetLib); 958 | /* useful on jailbroken devices where we can write in /usr/lib */ 959 | // const insertedLibraryName = '/usr/lib/' + path.basename(targetLib); 960 | /* forbidden in iOS */ 961 | // const insertedLibraryName = '@executable_path/Frameworks/' + path.basename(targetLib); 962 | tools 963 | .insertLibrary(insertedLibraryName, config.appbin, outputLib) 964 | .then(resolve) 965 | .catch(reject); 966 | }); 967 | fs.createReadStream(targetLib).pipe(writeStream); 968 | } catch (e) { 969 | reject(e); 970 | } 971 | }); 972 | } 973 | 974 | function parentDirectory(root: string) { 975 | return path.normalize(path.join(root, "..")); 976 | } 977 | 978 | function getOutputPath(cwd: string, ofile: string) { 979 | return ofile.startsWith(path.sep) 980 | ? ofile 981 | : path.join(parentDirectory(cwd), ofile); 982 | } 983 | 984 | function runScriptSync(script: string, session: any) { 985 | if (script.endsWith(".js")) { 986 | try { 987 | const s = require(script); 988 | return s(session); 989 | } catch (e: any) { 990 | console.error(e); 991 | return false; 992 | } 993 | } else { 994 | process.env.APPLESIGN_DIRECTORY = session.config.appdir; 995 | process.env.APPLESIGN_MAINBIN = session.config.appbin; 996 | process.env.APPLESIGN_OUTFILE = session.config.outfile; 997 | process.env.APPLESIGN_OUTDIR = session.config.outdir; 998 | process.env.APPLESIGN_FILE = session.config.file; 999 | try { 1000 | const res = execSync(script); 1001 | console.error(res.toString()); 1002 | } catch (e: any) { 1003 | console.error(e.toString()); 1004 | return false; 1005 | } 1006 | } 1007 | return true; 1008 | } 1009 | 1010 | function nestedApp(file: string) { 1011 | const dotApp = file.indexOf(".app/"); 1012 | if (dotApp !== -1) { 1013 | const subApp = file.substring(dotApp + 4).indexOf(".app/"); 1014 | if (subApp !== -1) { 1015 | return file.substring(0, dotApp + 4 + subApp + 4); 1016 | } 1017 | } 1018 | return false; 1019 | } 1020 | 1021 | function getAppDirectory(this: any, ipadir: string) { 1022 | if (!ipadir) { 1023 | ipadir = path.join(this.config.outdir, "Payload"); 1024 | } 1025 | if (!tools.isDirectory(ipadir)) { 1026 | throw new Error("Not a directory " + ipadir); 1027 | } 1028 | if (ipadir.endsWith(".app")) { 1029 | this.config.appdir = ipadir; 1030 | } else { 1031 | const files = fs.readdirSync(ipadir).filter((x: string) => { 1032 | return x.endsWith(".app"); 1033 | }); 1034 | if (files.length !== 1) { 1035 | throw new Error("Invalid IPA: " + ipadir); 1036 | } 1037 | return path.join(ipadir, files[0]); 1038 | } 1039 | if (ipadir.endsWith("/")) { 1040 | ipadir = ipadir.substring(0, ipadir.length - 1); 1041 | } 1042 | return ipadir; 1043 | } 1044 | 1045 | async function enumerateTestFiles(dir: string): Promise { 1046 | let tests: string[] = []; 1047 | if (fs.existsSync(dir)) { 1048 | tests = (await fs.readdir(dir)).filter((x: string) => { 1049 | return x.indexOf(".xctest") !== -1; 1050 | }); 1051 | } 1052 | return tests; 1053 | } 1054 | 1055 | async function moveFiles(files: string[], sourceDir: string, destDir: string) { 1056 | await fs.mkdir(destDir, { recursive: true }); 1057 | for (const f of files) { 1058 | const oldName = path.join(sourceDir, f); 1059 | const newName = path.join(destDir, f); 1060 | await fs.rename(oldName, newName); 1061 | } 1062 | } 1063 | export default Applesign; 1064 | -------------------------------------------------------------------------------- /lib/appdir.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import walk from "fs-walk"; 3 | import plist from "simple-plist"; 4 | import * as bin from "./bin.js"; 5 | import fs from "fs"; 6 | import { resolvePath } from "./depsolver.js"; 7 | 8 | export class AppDirectory { 9 | appbin: string = ""; 10 | appdir: string = ""; 11 | appexs: string[] = []; 12 | applibs: string[] = []; 13 | disklibs: string[] = []; 14 | exebin: string | null = null; 15 | nested: string[] = []; 16 | notlibs: string[] = []; 17 | orphan: string[] = []; 18 | syslibs: string[] = []; 19 | constructor() { 20 | this.nested = []; 21 | } 22 | 23 | async loadFromDirectory(appdir: string) { 24 | this.exebin = _getAppExecutable(appdir); 25 | this.appdir = appdir; 26 | this.appbin = path.join(this.appdir, this.exebin); 27 | this.nested = _findNested(this.appdir); 28 | this.disklibs = _findBinaries(this.appdir); 29 | this.appexs = _getAppExtensions(appdir); 30 | const applibs = _findLibraries( 31 | this.appdir, 32 | this.exebin, 33 | this.appexs, 34 | this.disklibs, 35 | ); 36 | this.notlibs = applibs.filter((l) => l[0] === "@"); 37 | this.applibs = applibs.filter((l) => l[0] !== "@"); 38 | this.syslibs = _findSystemLibraries(this.applibs); 39 | this.orphan = orphanedLibraries(this.applibs, this.disklibs); 40 | } 41 | 42 | appLibraries(): string[] { 43 | return this.applibs; 44 | } 45 | 46 | diskLibraries(): string[] { 47 | return this.disklibs; 48 | } 49 | 50 | systemLibraries(): string[] { 51 | return this.syslibs; 52 | } 53 | 54 | unavailableLibraries(): string[] { 55 | return this.notlibs; 56 | } 57 | 58 | orphanedLibraries(): string[] { 59 | return this.orphan; 60 | } 61 | 62 | nestedApplications(): string[] { 63 | return this.nested; 64 | } 65 | 66 | appExtensions(): string[] { 67 | return this.appexs; 68 | } 69 | } 70 | 71 | // internal functions // 72 | /** 73 | * Finds libraries that are present in the application bundle but not 74 | * referenced by the main binary or any of its dependencies. 75 | * 76 | * @param src - Array of libraries that are referenced by the main binary and its dependencies 77 | * @param dst - Array of all libraries found in the application bundle 78 | * @returns Array of library paths that exist in the application but aren't referenced 79 | */ 80 | function orphanedLibraries(src: string[], dst: string[]): string[] { 81 | return dst.filter((lib) => !src.includes(lib)); 82 | } 83 | 84 | function _findSystemLibraries(applibs: string[]): string[] { 85 | const syslibs: string[] = []; 86 | for (const lib of applibs) { 87 | const res = binSysLibs(lib).filter((l: any) => syslibs.indexOf(l) === -1); 88 | syslibs.push(...res); 89 | } 90 | return syslibs; 91 | } 92 | 93 | function _getAppExecutable(appdir: string): string { 94 | if (!appdir) { 95 | throw new Error("No application directory is provided"); 96 | } 97 | const plistPath = path.join(appdir, "Info.plist"); 98 | try { 99 | const plistData = plist.readFileSync(plistPath); 100 | const cfBundleExecutable = plistData.CFBundleExecutable; 101 | if (cfBundleExecutable) { 102 | return cfBundleExecutable; 103 | } 104 | } catch (e) { 105 | // do nothing 106 | console.error(e); 107 | } 108 | const exename = path.basename(appdir); 109 | const dotap = exename.indexOf(".app"); 110 | return dotap === -1 ? exename : exename.substring(0, dotap); 111 | } 112 | 113 | function _getAppExtensions(appdir: string): string[] { 114 | const d = path.join(appdir, "PlugIns"); 115 | const apexBins: string[] = []; 116 | try { 117 | if (!fs.existsSync(d)) { 118 | return apexBins; 119 | } 120 | const files = fs.readdirSync(d); 121 | for (const file of files) { 122 | const apexDir = path.join(d, file); 123 | const apexPlist = path.join(apexDir, "Info.plist"); 124 | if (fs.existsSync(apexPlist)) { 125 | const apexInfo = plist.readFileSync(apexPlist); 126 | if (apexInfo.CFBundleExecutable) { 127 | const apexBin = path.join(apexDir, apexInfo.CFBundleExecutable); 128 | if (fs.existsSync(apexBin)) { 129 | apexBins.push(apexBin); 130 | } 131 | } 132 | } 133 | } 134 | console.error(apexBins); 135 | } catch (e) { 136 | console.error(e); 137 | return []; 138 | } 139 | return apexBins; 140 | } 141 | 142 | /** return an array of strings with the absolute paths of the sub-apps found inside appdir */ 143 | function _findNested(d: string): string[] { 144 | const nested: string[] = []; 145 | walk.walkSync(d, (basedir: any, filename: any, stat: any) => { 146 | const file = path.join(basedir, filename); 147 | if (file.indexOf(".app/Info.plist") !== -1) { 148 | const nest = file.lastIndexOf(".app/"); 149 | nested.push(file.substring(0, nest + 4)); 150 | } 151 | }); 152 | return nested; 153 | } 154 | 155 | function _findBinaries(appdir: string): string[] { 156 | const libraries: string[] = []; 157 | walk.walkSync(appdir, (basedir: any, filename: any, stat: any) => { 158 | const file = path.join(basedir, filename); 159 | // only walk on files. Symlinks and other special files are forbidden 160 | if (!fs.lstatSync(file).isFile()) { 161 | return; 162 | } 163 | if (bin.isMacho(file)) { 164 | libraries.push(file); 165 | } 166 | }); 167 | return libraries; 168 | } 169 | 170 | /** 171 | * Finds all libraries with absolute paths that the file depends on 172 | * 173 | * @param file - The macho file to be analyzed 174 | * @returns Array of absolute paths to the discovered binary files 175 | */ 176 | function binSysLibs(file: string): string[] { 177 | try { 178 | return bin 179 | .enumerateLibraries(file) 180 | .filter((l: string) => l.startsWith("/")); 181 | } catch (e) { 182 | console.error("Warning: missing file:", file); 183 | return []; 184 | } 185 | } 186 | 187 | // return a list of the libs that must be inside the app 188 | function binAbsLibs(sourceFile: string, targetPaths: any): string[] { 189 | try { 190 | return bin 191 | .enumerateLibraries(sourceFile) 192 | .filter((libraryPath: string) => { 193 | return !libraryPath.startsWith("/"); 194 | }) 195 | .map((libraryPath: string) => { 196 | if (libraryPath.startsWith("@")) { 197 | const resolvedPath = resolvePath( 198 | targetPaths.exe, 199 | sourceFile, 200 | libraryPath, 201 | targetPaths.libs, 202 | ); 203 | if (resolvedPath) { 204 | libraryPath = resolvedPath; 205 | } else { 206 | console.error( 207 | "Warning: Cannot resolve dependency library: " + sourceFile, 208 | ); 209 | } 210 | } 211 | return libraryPath; 212 | }); 213 | } catch (error) { 214 | console.error("Warning: missing file:", sourceFile); 215 | return []; 216 | } 217 | } 218 | 219 | // get all dependencies from appbin recursively 220 | function _findLibraries( 221 | appdir: string, 222 | appbin: string, 223 | appexs: string[], 224 | disklibs: string[], 225 | ): string[] { 226 | const exe = path.join(appdir, appbin); 227 | 228 | const targets = { 229 | exe, 230 | lib: exe, 231 | libs: disklibs, 232 | }; 233 | const libraries: any = []; 234 | const pending = [exe, ...appexs]; 235 | while (pending.length > 0) { 236 | const target = pending.shift() as string; 237 | if (libraries.indexOf(target) === -1) { 238 | libraries.push(target); 239 | } 240 | const res = binAbsLibs(target, targets); 241 | const unexplored = res.filter((l: string) => libraries.indexOf(l) === -1); 242 | pending.push( 243 | ...unexplored.filter((l: string) => pending.indexOf(l) === -1), 244 | ); 245 | libraries.push(...unexplored); 246 | } 247 | return libraries; 248 | } 249 | -------------------------------------------------------------------------------- /lib/bin.ts: -------------------------------------------------------------------------------- 1 | import isEncryptedSync from "macho-is-encrypted"; 2 | import fatmacho from "fatmacho"; 3 | import macho from "macho"; 4 | import fs from "fs"; 5 | import machoEntitlements from "macho-entitlements"; 6 | 7 | const MACH0_MIN_SIZE = 1024 * 4; 8 | const MH_EXECUTE = 2; 9 | const MH_DYLIB = 6; 10 | const MH_BUNDLE = 8; 11 | const CSSLOT_CODEDIRECTORY = 0; 12 | 13 | function isMacho(filePath: string): boolean { 14 | if (typeof filePath !== "string") { 15 | throw new Error("Expected a string"); 16 | } 17 | // read file headers and read the magic and filetype 18 | if (!fs.lstatSync(filePath).isFile()) { 19 | return false; 20 | } 21 | const fd = fs.openSync(filePath, "r"); 22 | if (fd < 1) { 23 | return false; 24 | } 25 | const machoMagic = Buffer.alloc(4); 26 | if (fs.readSync(fd, machoMagic, { position: 0, length: 4 }) !== 4) { 27 | return false; 28 | } 29 | const machoType = Buffer.alloc(4); 30 | if (fs.readSync(fd, machoType, { position: 0xc, length: 4 }) !== 4) { 31 | return false; 32 | } 33 | fs.close(fd); 34 | // is this a fatmacho? 35 | 36 | if (!machoMagic.compare(Buffer.from([0xca, 0xfe, 0xba, 0xbe]))) { 37 | try { 38 | const data = fs.readFileSync(filePath); 39 | const butter = fatmacho.parse(data); 40 | for (const slice of butter) { 41 | const mm = slice.data.slice(0, 4); 42 | const mt = slice.data.slice(0xc, 0xc + 4); 43 | if (isValidMacho(mm, mt)) { 44 | return true; 45 | } 46 | } 47 | } catch (_) { 48 | // nothing to see 49 | } 50 | return false; 51 | } 52 | return isValidMacho(machoMagic, machoType); 53 | } 54 | 55 | function isValidMacho(machoMagic: any, machoType: any): boolean { 56 | // verify this file have enough magic 57 | const magics = [ 58 | [0xce, 0xfa, 0xed, 0xfe], // 32bit 59 | [0xcf, 0xfa, 0xed, 0xfe], // 64bit 60 | ]; 61 | for (const a of magics) { 62 | if (!machoMagic.slice(0, 4).compare(Buffer.from(a))) { 63 | // ensure the macho type is supported by ldid2 64 | const fileType = machoType[0]; 65 | switch (fileType) { 66 | case MH_EXECUTE: 67 | case MH_DYLIB: 68 | case MH_BUNDLE: 69 | return true; 70 | } 71 | return false; 72 | } 73 | } 74 | return false; 75 | } 76 | 77 | function isBitcodeMacho(cmds: any): boolean { 78 | let haveBitcode = false; 79 | let haveNative = false; 80 | for (const cmd of cmds) { 81 | if (cmd.type === "segment" || cmd.type === "segment_64") { 82 | if (cmd.name === "__TEXT" && cmd.sections.length > 0) { 83 | haveNative = cmd.vmsize > 0; 84 | } 85 | if (cmd.name === "__LLVM" && cmd.sections.length > 0) { 86 | const section = cmd.sections[0]; 87 | if (section.sectname === "__bundle" && section.size > 0) { 88 | haveBitcode = true; 89 | } 90 | } 91 | } 92 | } 93 | return haveBitcode && !haveNative; 94 | } 95 | 96 | function isEncrypted(fileName: string): boolean { 97 | if (typeof fileName !== "string") { 98 | throw new Error("invalid argument for isEncryptedSync"); 99 | } 100 | return isEncryptedSync.data(fs.readFileSync(fileName)); 101 | } 102 | 103 | function isBitcode(data: any): boolean { 104 | if (typeof data === "string") { 105 | data = fs.readFileSync(data); 106 | } 107 | try { 108 | const exec = macho.parse(data); 109 | return isBitcodeMacho(exec.cmds); 110 | } catch (e) { 111 | const fat = fatmacho.parse(data); 112 | for (const bin of fat) { 113 | const exec = macho.parse(bin.data); 114 | if (isBitcodeMacho(exec.cmds)) { 115 | return true; 116 | } 117 | } 118 | } 119 | return false; 120 | } 121 | 122 | function isTruncated(data: any): boolean { 123 | if (typeof data === "string") { 124 | data = fs.readFileSync(data); 125 | } 126 | if (data.length < MACH0_MIN_SIZE) { 127 | return true; 128 | } 129 | const diskMacho = macho.parse(data); 130 | for (const cmd of diskMacho.cmds) { 131 | switch (cmd.type) { 132 | case "segment": 133 | case "segment_64": 134 | { 135 | const end = cmd.fileoff + cmd.filesize; 136 | if (end > data.length) { 137 | return true; 138 | } 139 | } 140 | break; 141 | } 142 | } 143 | return false; 144 | } 145 | 146 | function parseMacho(data: any): any { 147 | try { 148 | return macho.parse(data); 149 | } catch (e: unknown) { 150 | const fat = fatmacho.parse(data); // throws 151 | // we get the first slice, assuming it contains the same libs as the others 152 | return parseMacho(fat[0].data); 153 | } 154 | } 155 | 156 | function parseMachoAndGetData(data: any): [any, any] { 157 | try { 158 | return [macho.parse(data), data]; 159 | } catch (e) { 160 | const fat = fatmacho.parse(data); // throws 161 | const slice = fat[0].data; 162 | return [parseMacho(slice), slice]; 163 | } 164 | } 165 | 166 | function enumerateLibraries(data: any) { 167 | if (typeof data === "string") { 168 | data = fs.readFileSync(data); 169 | } 170 | const exec = parseMacho(data); 171 | return exec.cmds 172 | .filter((x: any) => x.type === "load_dylib" || x.type === "load_weak_dylib") 173 | .map((x: any) => x.name); 174 | } 175 | 176 | function entitlements(file: any) { 177 | return machoEntitlements.parseFile(file); 178 | } 179 | 180 | function getIdentifier(path: any) { 181 | const rawData = fs.readFileSync(path); 182 | const [bin, data] = parseMachoAndGetData(rawData); 183 | for (const cmd of bin.cmds) { 184 | if (cmd.type === "code_signature") { 185 | return parseIdentifier(data.slice(cmd.dataoff)); 186 | } 187 | } 188 | return null; 189 | } 190 | 191 | function parseIdentifier(data: any) { 192 | const count = data.readUInt32BE(8); 193 | for (let i = 0; i < count; i++) { 194 | const base = 8 * i; 195 | const type = data.readUInt32BE(base + 12); 196 | const blob = data.readUInt32BE(base + 16); 197 | if (type === CSSLOT_CODEDIRECTORY) { 198 | const size = data.readUInt32BE(blob + 4); 199 | const directory = data.slice(blob + 8, blob + size); 200 | const identOffset = directory.readUInt32BE(12); 201 | const identifier = []; 202 | let cursor = identOffset; 203 | while (cursor < size) { 204 | const charCode = data.readUInt8(blob + cursor); 205 | if (charCode === 0) { 206 | break; 207 | } 208 | identifier.push(String.fromCharCode(charCode)); 209 | cursor++; 210 | } 211 | return identifier.join(""); 212 | } 213 | } 214 | return null; 215 | } 216 | 217 | export { 218 | entitlements, 219 | enumerateLibraries, 220 | getIdentifier, 221 | isBitcode, 222 | isEncrypted, 223 | isMacho, 224 | isTruncated, 225 | }; 226 | -------------------------------------------------------------------------------- /lib/config.ts: -------------------------------------------------------------------------------- 1 | // Converted to ES module 2 | 3 | import path from "path"; 4 | import idprov from "./idprov.js"; 5 | import minimist from "minimist"; 6 | 7 | const shortHelpMessage = `Usage: 8 | 9 | applesign [--options ...] [target.ipa | Payload/Target.app] 10 | 11 | -a, --all Resign all binaries, even it unrelated to the app 12 | -b, --bundleid [BUNDLEID] Change the bundleid when repackaging 13 | -c, --clone-entitlements Clone the entitlements from the provisioning to the bin 14 | -D, --device-provision Automatically find the mobileprovision for the available device 15 | -f, --force-family Force UIDeviceFamily in Info.plist to be iPhone 16 | -h, --help Show verbose help message 17 | -H, --allow-http Add NSAppTransportSecurity.NSAllowsArbitraryLoads in plist 18 | -i, --identity [1C4D1A..] Specify hash-id of the identity to use 19 | -L, --identities List local codesign identities 20 | -m, --mobileprovision [FILE] Specify the mobileprovision file to use 21 | -o, --output [APP.IPA] Path to the output IPA filename 22 | -O, --osversion 9.0 Force specific OSVersion if any in Info.plist 23 | -p, --without-plugins Remove plugins (excluding XCTests) from the resigned IPA 24 | -w, --without-watchapp Remove the WatchApp from the IPA before resigning 25 | -x, --without-xctests Remove the XCTests from the resigned IPA 26 | 27 | Example: 28 | 29 | $ applesign -w -c -m embedded.mobileprovision target.ipa 30 | `; 31 | 32 | const helpMessage = `Usage: 33 | 34 | applesign [--options ...] [input-ipafile] 35 | 36 | Packaging: 37 | -7, --use-7zip Use 7zip instead of unzip 38 | -A, --all-dirs Archive all directories, not just Payload/ 39 | -I, --insert [frida.dylib] Insert a dynamic library to the main executable 40 | -l, --lipo [arm64|armv7] Lipo -thin all bins inside the IPA for the given architecture 41 | -n, --noclean keep temporary files when signing error happens 42 | -o, --output [APP.IPA] Path to the output IPA filename 43 | -P, --parallel Run layered signing dependencies in parallel (EXPERIMENTAL) 44 | -r, --replace Replace the input IPA file with the resigned one 45 | -u, --unfair Resign encrypted applications 46 | -z, --ignore-zip-errors Ignore unzip/7z uncompressing errors 47 | 48 | Stripping: 49 | -F, --without-signing-files Remove signing related files 50 | -p, --without-plugins Remove plugins (excluding XCTests) from the resigned IPA 51 | -w, --without-watchapp Remove the WatchApp from the IPA before resigning 52 | -x, --without-xctests Remove the XCTests from the resigned IPA 53 | 54 | Signing: 55 | --use-openssl Use OpenSSL cms instead of Apple's security tool (EXPERIMENTAL) 56 | -a, --all Resign all binaries, even it unrelated to the app 57 | -d, --debug [file] Create debug file with all the signing process 58 | -D, --device-provision Automatically find the mobileprovision for the available device 59 | -i, --identity [1C4D1A..] Specify hash-id of the identity to use 60 | -j, --json '{}' Set the alternative JSON for signing files with custom entitlements 61 | -k, --keychain [KEYCHAIN] Specify custom keychain file 62 | -K, --add-access-group [NAME] Add $(TeamIdentifier).NAME to keychain-access-groups 63 | -L, --identities List local codesign identities 64 | -m, --mobileprovision [FILE] Specify the mobileprovision file to use 65 | -s, --single Sign a single file instead of an IPA 66 | -S, --self-sign-provision Self-sign mobile provisioning (EXPERIMENTAL) 67 | -v, --verify Verify all the signed files at the end 68 | -V, --verify-twice Verify after signing every file and at the end 69 | -Z, --pseudo-sign Pseudo sign the binaries (EXPERIMENTAL) 70 | 71 | Info.plist 72 | -b, --bundleid [BUNDLEID] Change the bundleid when repackaging 73 | -B, --bundleid-access-group Add $(TeamIdentifier).bundleid to keychain-access-groups 74 | -f, --force-family Force UIDeviceFamily in Info.plist to be iPhone 75 | -H, --allow-http Add NSAppTransportSecurity.NSAllowsArbitraryLoads in plist 76 | -O, --osversion 9.0 Force specific OSVersion if any in Info.plist 77 | 78 | Entitlements: 79 | -c, --clone-entitlements Clone the entitlements from the provisioning to the bin 80 | -e, --entitlements [ENTITL] Specify entitlements file (EXPERIMENTAL) 81 | -E, --entry-entitlement Use generic entitlement (EXPERIMENTAL) 82 | -N, --add-entitlements [FILE] Append entitlements from file (EXPERIMENTAL) 83 | -M, --massage-entitlements Massage entitlements to remove privileged ones 84 | -t, --without-get-task-allow Do not set the get-task-allow entitlement (EXPERIMENTAL) 85 | -C, --no-entitlements-file Do not create .entitlements file in the IPA 86 | 87 | -h, --help Show this help message 88 | --version Show applesign version 89 | [input-ipafile] Path to the IPA file to resign 90 | 91 | Examples: 92 | 93 | $ applesign -L # enumerate codesign identities, grab one and use it with -i 94 | $ applesign -m embedded.mobileprovision target.ipa 95 | $ applesign -i AD71EB42BC289A2B9FD3C2D5C9F02D923495A23C target.ipa 96 | $ applesign -m a.mobileprovision -c --lipo arm64 -w target.ipa 97 | $ applesign -m a.mobileprovision -j '{"custom":[{"filematch":"ShareExtension$","entitlements":"/tmp/foo.ent"}]}' target.ipa 98 | 99 | Installing in the device: 100 | 101 | $ ideviceinstaller -i target-resigned.ipa 102 | $ ios-deploy -b target-resigned.ipa 103 | `; 104 | 105 | /* 106 | // Expected format: 107 | // ---------------- 108 | 109 | { 110 | "custom": [ 111 | { 112 | "filematch": "ShareExtension$", 113 | "entitlements": "/tmp/share.entitlements", 114 | "__identity": "83498489489X", 115 | } 116 | ] 117 | } 118 | 119 | */ 120 | export interface ConfigOptions { 121 | appdir: any; 122 | appbin: any; 123 | all: boolean; 124 | allDirs: boolean; 125 | allowHttp: boolean; 126 | addEntitlements: string | null; 127 | bundleIdKeychainGroup: string | false; 128 | bundleid: string | undefined; 129 | cloneEntitlements: boolean; 130 | customKeychainGroup: string | undefined; 131 | debug: any; // opt.d || opt.debug || "" 132 | deviceProvision: any; // opt.D || opt.deviceProvision || false 133 | entitlement: string | undefined; 134 | entry: any; // opt.entry || undefined 135 | file: string | undefined; 136 | forceFamily: boolean; 137 | identity: string | undefined; 138 | ignoreCodesignErrors: boolean; 139 | ignoreVerificationErrors: boolean; 140 | ignoreZipErrors: boolean; 141 | insertLibrary: any; // opt.insertLibrary || undefined, 142 | json: any; // JSON.parse(opt.json || "{}"), 143 | keychain: any; 144 | lipoArch: any; 145 | massageEntitlements: boolean; 146 | mobileprovision: any; 147 | mobileprovisions: any; 148 | noEntitlementsFile: any; 149 | payloadOnly: boolean; 150 | noclean: boolean; 151 | osversion: any; // opt.osversion || undefined, 152 | outdir: any; 153 | outfile: string | null; 154 | parallel: boolean; 155 | pseudoSign: boolean; 156 | replaceipa: boolean; 157 | run: any; 158 | selfSignedProvision: boolean; 159 | unfairPlay: boolean; 160 | use7zip: boolean; 161 | useOpenSSL: boolean; 162 | verify: boolean; 163 | verifyTwice: boolean; 164 | withGetTaskAllow: boolean; 165 | withoutPlugins: boolean; 166 | withoutSigningFiles: boolean; 167 | withoutWatchapp: boolean; 168 | withoutXCTests: boolean; 169 | } 170 | 171 | const fromOptions = function (opt: any): ConfigOptions { 172 | if (typeof opt !== "object") { 173 | opt = {}; 174 | } 175 | if (opt.osversion !== undefined) { 176 | if (isNaN(+opt.osversion)) { 177 | throw new Error("Version passed to -O must be numeric"); 178 | } 179 | } 180 | if (opt.mobileprovision) { 181 | if (Array.isArray(opt.mobileprovision)) { 182 | opt.mobileprovisions = opt.mobileprovision; 183 | opt.mobileprovision = opt.mobileprovision[0]; 184 | // throw new Error('Multiple mobile provisionings not yet supported'); 185 | } 186 | const mp = opt.mobileprovision; 187 | opt.mobileprovisions = [mp]; 188 | if (opt.identity) { 189 | const id0 = idprov(mp); 190 | const id1 = opt.identity; 191 | if (id0 !== id1) { 192 | // throw new Error('MobileProvisioningVersion doesn\'t match the given identity (' + id0 + ' vs ' + id1 + ')'); 193 | } 194 | } else { 195 | opt.identity = idprov(mp); 196 | } 197 | } else { 198 | opt.mobileprovision = undefined; 199 | opt.mobileprovisions = []; 200 | } 201 | return { 202 | all: opt.all || false, 203 | allDirs: opt.allDirs || true, 204 | allowHttp: opt.allowHttp || false, 205 | addEntitlements: opt.addEntitlements || undefined, 206 | bundleIdKeychainGroup: opt.bundleIdKeychainGroup || false, 207 | bundleid: opt.bundleid || undefined, 208 | cloneEntitlements: opt.cloneEntitlements || false, 209 | customKeychainGroup: opt.customKeychainGroup || undefined, 210 | debug: opt.d || opt.debug || "", 211 | deviceProvision: opt.D || opt.deviceProvision || false, 212 | entitlement: opt.entitlement || undefined, 213 | entry: opt.entry || undefined, 214 | file: opt.file ? path.resolve(opt.file) : undefined, 215 | forceFamily: opt.forceFamily || false, 216 | identity: opt.identity || undefined, 217 | ignoreCodesignErrors: true, 218 | ignoreVerificationErrors: true, 219 | ignoreZipErrors: opt.ignoreZipErrors || false, 220 | insertLibrary: opt.insertLibrary || undefined, 221 | json: JSON.parse(opt.json || "{}"), 222 | keychain: opt.keychain, 223 | lipoArch: opt.lipoArch || undefined, 224 | massageEntitlements: opt.massageEntitlements || false, 225 | mobileprovision: opt.mobileprovision, 226 | mobileprovisions: opt.mobileprovisions, 227 | noEntitlementsFile: opt.noEntitlementsFile || false, 228 | noclean: opt.noclean || false, 229 | osversion: opt.osversion || undefined, 230 | appbin: undefined, 231 | appdir: undefined, 232 | outdir: undefined, 233 | payloadOnly: false, 234 | outfile: opt.outfile, 235 | parallel: opt.parallel || false, 236 | pseudoSign: opt.pseudoSign || false, 237 | replaceipa: opt.replaceipa || false, 238 | run: opt.run, 239 | selfSignedProvision: opt.selfSignedProvision || false, 240 | unfairPlay: opt.unfairPlay || false, 241 | use7zip: opt.use7zip === true, 242 | useOpenSSL: opt.useOpenSSL === true, 243 | verify: opt.verify || false, 244 | verifyTwice: opt.verifyTwice || false, 245 | withGetTaskAllow: opt.withGetTaskAllow, 246 | withoutPlugins: opt.withoutPlugins || false, 247 | withoutSigningFiles: opt.withoutSigningFiles || false, 248 | withoutWatchapp: opt.withoutWatchapp || false, 249 | withoutXCTests: opt.withoutXCTests || false, 250 | }; 251 | }; 252 | 253 | const fromState = function (state: any) { 254 | return JSON.parse(JSON.stringify(state)); 255 | }; 256 | 257 | function parse(argv: any) { 258 | return minimist(argv.slice(2), { 259 | string: [ 260 | "d", 261 | "debug", 262 | "j", 263 | "json", 264 | "i", 265 | "identity", 266 | "O", 267 | "osversion", 268 | "R", 269 | "run", 270 | ], 271 | boolean: [ 272 | "7", 273 | "use-7zip", 274 | "A", 275 | "all-dirs", 276 | "B", 277 | "bundleid-access-group", 278 | "C", 279 | "no-entitlements-file", 280 | "D", 281 | "device-provision", 282 | "E", 283 | "entry-entitlement", 284 | "F", 285 | "without-signing-files", 286 | "H", 287 | "allow-http", 288 | "L", 289 | "identities", 290 | "M", 291 | "massage-entitlements", 292 | "P", 293 | "parallel", 294 | "S", 295 | "self-signed-provision", 296 | "V", 297 | "verify-twice", 298 | "Z", 299 | "pseudo-sign", 300 | "a", 301 | "all", 302 | "c", 303 | "clone-entitlements", 304 | "f", 305 | "force-family", 306 | "n", 307 | "noclean", 308 | "p", 309 | "without-plugins", 310 | "r", 311 | "replace", 312 | "s", 313 | "single", 314 | "t", 315 | "without-get-task-allow", 316 | "u", 317 | "unfair", 318 | "u", 319 | "unsigned-provision", 320 | "v", 321 | "verify", 322 | "w", 323 | "without-watchapp", 324 | "x", 325 | "without-xctests", 326 | "z", 327 | "ignore-zip-errors", 328 | ], 329 | }); 330 | } 331 | 332 | function compile(conf: any) { 333 | const options = { 334 | all: conf.a || conf.all || false, 335 | allDirs: conf["all-dirs"] || conf.A, 336 | allowHttp: conf["allow-http"] || conf.H, 337 | addEntitlements: conf["add-entitlements"] || conf.N, 338 | bundleIdKeychainGroup: conf.B || conf["bundleid-access-group"], 339 | bundleid: conf.bundleid || conf.b, 340 | cloneEntitlements: conf.c || conf["clone-entitlements"], 341 | customKeychainGroup: conf.K || conf["add-access-group"], 342 | debug: conf.debug || conf.d || "", 343 | deviceProvision: conf.D || conf.deviceProvision || false, 344 | entitlement: conf.entitlement || conf.e, 345 | entry: conf["entry-entitlement"] || conf.E, 346 | file: conf._[0] || undefined, 347 | forceFamily: conf["force-family"] || conf.f, 348 | identity: conf.identity || conf.i, 349 | ignoreZipErrors: conf.z || conf["ignore-zip-errors"], 350 | insertLibrary: conf.I || conf.insert, 351 | json: conf.json || conf.j, 352 | keychain: conf.keychain || conf.k, 353 | lipoArch: conf.lipo || conf.l, 354 | massageEntitlements: conf["massage-entitlements"] || conf.M, 355 | mobileprovision: conf.mobileprovision || conf.m, 356 | noEntitlementsFile: conf.C || conf["no-entitlements-file"] || 357 | conf.noEntitlementsFile, 358 | noclean: conf.n || conf.noclean, 359 | osversion: conf.osversion || conf.O, 360 | outfile: (conf.output || conf.o) ? path.resolve(conf.output || conf.o) : "", 361 | parallel: conf.parallel || conf.P, 362 | pseudoSign: conf.Z || conf["pseudo-sign"], 363 | replaceipa: conf.replace || conf.r, 364 | run: conf.R || conf.run, 365 | selfSignedProvision: conf.S || conf["self-signed-provision"], 366 | single: conf.single || conf.s, 367 | unfairPlay: conf.unfair || conf.u, 368 | use7zip: conf["7"] || conf["use-7zip"], 369 | useOpenSSL: conf["use-openssl"], 370 | verify: conf.v || conf.V || conf.verify || conf["verify-twice"], 371 | verifyTwice: conf.V || conf["verify-twice"], 372 | withGetTaskAllow: !(conf["without-get-task-allow"] || conf.t), 373 | withoutPlugins: !!conf["without-plugins"] || !!conf.p, 374 | withoutSigningFiles: !!conf["without-signing-files"] || !!conf.F, 375 | withoutWatchapp: !!conf["without-watchapp"] || !!conf.w, 376 | withoutXCTests: !!conf["without-xctests"] || !!conf.x, 377 | }; 378 | return options; 379 | } 380 | 381 | export { 382 | compile, 383 | fromOptions, 384 | fromState, 385 | helpMessage, 386 | parse, 387 | shortHelpMessage, 388 | }; 389 | -------------------------------------------------------------------------------- /lib/depsolver.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import * as bin from "./bin.js"; 3 | 4 | function resolveRpath( 5 | libs: string[], 6 | file: string, 7 | lib: string, 8 | ): string | null { 9 | const libName = lib.substring(6); /* chop @rpath */ 10 | const rpaths = libs.filter((x: string) => { 11 | return x.indexOf(libName) !== -1; 12 | }); 13 | if (rpaths.length > 0) { 14 | return rpaths[0]; 15 | } 16 | // throw new Error('Cannot resolve rpath: ' + libName); 17 | console.error("Cannot resolve rpath for:", lib, "from", file); 18 | return null; 19 | } 20 | 21 | function resolvePathDirective( 22 | file: string, 23 | lib: string, 24 | directive: string, 25 | ): string { 26 | const slash = file.lastIndexOf("/"); 27 | const rpath = slash !== -1 ? file.substring(0, slash) : ""; 28 | return lib.replace(directive, rpath); 29 | } 30 | 31 | export function resolvePath( 32 | executable: string, 33 | file: string, 34 | lib: string, 35 | libs: string[], 36 | ) { 37 | if (lib.startsWith("/")) { 38 | return null; 39 | } 40 | if (lib.startsWith("@rpath")) { 41 | return resolveRpath(libs, file, lib); 42 | } 43 | if (lib.startsWith("@executable_path")) { 44 | return resolvePathDirective(executable, lib, "@executable_path"); 45 | } 46 | if (lib.startsWith("@loader_path")) { 47 | return resolvePathDirective(executable, lib, "@loader_path"); 48 | } 49 | throw new Error("Cannot resolve: " + file); 50 | } 51 | 52 | function layerize(state: any) { 53 | let currentLayer = 0; 54 | const result: string[][] = []; 55 | let processing = false; 56 | do { 57 | result[currentLayer] = []; 58 | for (const lib of Object.keys(state)) { 59 | const deps = state[lib].deps; 60 | if (deps.length === 0) { 61 | if (state[lib].layer === -1) { 62 | result[currentLayer].push(lib); 63 | state[lib].layer = 0; 64 | } 65 | } 66 | let allDepsSolved = true; 67 | for (const dep of deps) { 68 | const depLayer = state[dep] ? state[dep].layer : 0; 69 | if (depLayer === -1 || depLayer === currentLayer) { 70 | allDepsSolved = false; 71 | break; 72 | } 73 | } 74 | processing = true; 75 | if (allDepsSolved) { 76 | if (state[lib].layer === -1) { 77 | result[currentLayer].push(lib); 78 | state[lib].layer = currentLayer; 79 | } 80 | processing = false; 81 | } 82 | } 83 | currentLayer++; 84 | } while (processing); 85 | 86 | return result; 87 | } 88 | 89 | export default function depSolver( 90 | executable: string, 91 | libs: string[], 92 | parallel: boolean, 93 | ): Promise { 94 | return new Promise((resolve, reject) => { 95 | if (libs.length === 0) { 96 | return resolve([]); 97 | } 98 | const state: Record = {}; 99 | const peekableLibs = libs.slice(0); 100 | const peek = () => { 101 | const target = peekableLibs.pop() as string; 102 | const macholibs = bin.enumerateLibraries(target); 103 | state[target] = { 104 | layer: -1, 105 | deps: [], 106 | }; 107 | for (const r of macholibs) { 108 | if (!r.startsWith("/")) { 109 | const realPath = resolvePath(executable, target!, r, libs); 110 | if (realPath !== null) { 111 | try { 112 | fs.statSync(realPath); 113 | state[target].deps.push(realPath); 114 | } catch (e) {} 115 | } 116 | } 117 | } 118 | if (peekableLibs.length === 0) { 119 | const layers = layerize(state); 120 | if (parallel) { 121 | return resolve(layers); 122 | } 123 | const finalLibs: string[] = layers.flatMap((layer) => layer); 124 | 125 | if (libs.length !== finalLibs.length) { 126 | console.log("Orphaned libraries found"); 127 | const orphaned = libs.filter( 128 | (lib: string) => finalLibs.indexOf(lib) === -1, 129 | ); 130 | orphaned.forEach((lib: any) => { 131 | console.log(" *", lib); 132 | }); 133 | 134 | /* 135 | * sign those anyways, just ensure to 136 | * sign them before the app executable 137 | */ 138 | finalLibs.unshift(...orphaned); 139 | } 140 | return resolve(finalLibs); 141 | } 142 | peek(); 143 | }; 144 | peek(); 145 | }); 146 | } 147 | -------------------------------------------------------------------------------- /lib/entitlements.ts: -------------------------------------------------------------------------------- 1 | import plist from "simple-plist"; 2 | import plistPkg from "plist"; 3 | const { build: plistBuild } = plistPkg; 4 | 5 | const entitlementTemplate = ` 6 | 7 | 8 | 9 | 10 | application-identifier 11 | FILLME.APPID 12 | com.apple.developer.team-identifier 13 | FILLME 14 | get-task-allow 15 | 16 | keychain-access-groups 17 | 18 | FILLME.APPID 19 | 20 | 21 | 22 | `; 23 | 24 | export default function defaultEntitlements(appid: any, devid: any): string { 25 | const ent = plist.parse(entitlementTemplate.trim()); 26 | ent["application-identifier"] = appid; 27 | ent["com.apple.developer.team-identifier"] = devid; 28 | ent["keychain-access-groups"] = [appid]; 29 | ent["com.apple.developer.ubiquity-kvstore-identifier"] = appid; 30 | delete ent["aps-environment"]; 31 | ent["com.apple.developer.icloud-container-identifiers"] = "iCloud." + devid; 32 | return plistBuild(ent, { pretty: true, allowEmpty: false }).toString(); 33 | } 34 | -------------------------------------------------------------------------------- /lib/fchk.ts: -------------------------------------------------------------------------------- 1 | // TODO: remove this runtime function check helper function when the typescript port is complete 2 | export default function fchk(args: any, types: any): void { 3 | if (args.length !== types.length) { 4 | throw new Error("Incorrect arguments count"); 5 | } 6 | const stack = [...args].reverse(); 7 | for (const t of types) { 8 | const arg = typeof stack.pop(); 9 | if (t && arg !== t) { 10 | throw new Error("Invalid argument type"); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/idprov.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import fs from "fs"; 4 | import plist from "plist"; 5 | import * as tools from "./tools.js"; 6 | 7 | export default function findIdentityFromProvisionSync(file: any): string { 8 | let data = fs.readFileSync(file).toString(); 9 | const b = data.indexOf(""); 15 | if (e === -1) { 16 | throw new Error("Cannot find end of plist inside " + file); 17 | } 18 | const cert = plist.parse(data.substring(0, e + 8)).DeveloperCertificates 19 | .toString(); 20 | const res = tools.getIdentitiesSync(); 21 | for (const id of res) { 22 | if (cert.indexOf(id.name) !== -1) { 23 | return id.hash; 24 | } 25 | } 26 | throw new Error("Cannot find an identity in " + file); 27 | } 28 | -------------------------------------------------------------------------------- /lib/info-plist.ts: -------------------------------------------------------------------------------- 1 | import plist from "simple-plist"; 2 | import { ConfigOptions } from "./config"; 3 | 4 | interface AppleDevices { 5 | iPhone: string[]; 6 | iPad: string[]; 7 | AppleTV: string[]; 8 | AppleWatch: string[]; 9 | } 10 | const appleDeviceNames = ["iPhone", "iPad", "AppleTV", "AppleWatch"]; 11 | /** 12 | * Creates an object with keys from the input array, each mapping to an empty array. 13 | */ 14 | function createEmptyArraysObject(keys: string[]): Record { 15 | return Object.fromEntries(keys.map((key) => [key, []])); 16 | } 17 | 18 | export default function fix( 19 | file: string, 20 | options: ConfigOptions, 21 | emit: any, 22 | ): void { 23 | if (!options.appdir) { 24 | throw new Error("Invalid parameters for fixPlist"); 25 | } 26 | let changed = false; 27 | const data = plist.readFileSync(file); 28 | delete data[""]; 29 | if (options.allowHttp) { 30 | emit("message", "Adding NSAllowArbitraryLoads"); 31 | if ( 32 | !data.NSAppTransportSecurity || 33 | data.NSAppTransportSecurity.constructor !== Object 34 | ) { 35 | data.NSAppTransportSecurity = {}; 36 | } 37 | data.NSAppTransportSecurity.NSAllowsArbitraryLoads = true; 38 | changed = true; 39 | } 40 | if (options.forceFamily) { 41 | if (performForceFamily(data, emit)) { 42 | changed = true; 43 | } 44 | } 45 | if (options.bundleid) { 46 | setBundleId(data, options.bundleid); 47 | changed = true; 48 | } 49 | if (changed) { 50 | plist.writeFileSync(file, data); 51 | } 52 | } 53 | 54 | function setBundleId(data: any, bundleid: any) { 55 | const oldBundleId = data.CFBundleIdentifier; 56 | if (oldBundleId) { 57 | data.CFBundleIdentifier = bundleid; 58 | } 59 | if (data.basebundleidentifier) { 60 | data.basebundleidentifier = bundleid; 61 | } 62 | try { 63 | data.CFBundleURLTypes[0].CFBundleURLName = bundleid; 64 | } catch (e) { 65 | /* do nothing */ 66 | } 67 | } 68 | 69 | function performForceFamily(data: any, emit: Function | undefined) { 70 | if (emit === undefined) { 71 | emit = console.error; 72 | } 73 | const have = supportedDevices(data); 74 | const df = []; 75 | if (have.iPhone && have.iPhone.length > 0) { 76 | df.push(1); 77 | } 78 | if (have.iPad && have.iPad.length > 0) { 79 | df.push(2); 80 | } 81 | let changes = false; 82 | if (data.UISupportedDevices) { 83 | delete data.UISupportedDevices; 84 | changes = true; 85 | } 86 | if ( 87 | (have.AppleWatch && have.AppleWatch.length > 0) || 88 | (have.AppleTV && have.AppleTV.length > 0) 89 | ) { 90 | emit("message", "Apple{TV/Watch} apps do not require to be re-familied"); 91 | return changes; 92 | } 93 | if (df.length === 0) { 94 | emit("message", "UIDeviceFamily forced to iPhone/iPod"); 95 | df.push(1); 96 | } 97 | if (df.length === 2) { 98 | emit("message", "No UIDeviceFamily changes required"); 99 | return changes; 100 | } 101 | emit("message", "UIDeviceFamily set to " + JSON.stringify(df)); 102 | data.UIDeviceFamily = df; 103 | return true; 104 | } 105 | 106 | function supportedDevices(data: any) { 107 | const have = createEmptyArraysObject(appleDeviceNames); 108 | const sd = data.UISupportedDevices; 109 | if (Array.isArray(sd)) { 110 | sd.forEach((model) => { 111 | for (const type of appleDeviceNames) { 112 | if (model.indexOf(type) !== -1) { 113 | if (!have[type]) { 114 | have[type] = []; 115 | } 116 | have[type].push(model); 117 | break; 118 | } 119 | } 120 | }); 121 | } else if (sd !== undefined) { 122 | console.error("Warning: Invalid UISupportedDevices in Info.plist?"); 123 | } 124 | const df = data.UIDeviceFamily; 125 | if (Array.isArray(df)) { 126 | df.forEach((family) => { 127 | const families = ["Any", ...appleDeviceNames]; 128 | const fam = families[family]; 129 | if (fam) { 130 | if (have[fam] === undefined) { 131 | have[fam] = []; 132 | } 133 | have[fam].push(fam); 134 | } 135 | }); 136 | } 137 | return have; 138 | } 139 | -------------------------------------------------------------------------------- /lib/tools.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import { promisify } from "node:util"; 3 | import { execSync, spawn } from "node:child_process"; 4 | const unlinkAsync = promisify(fs.unlink); 5 | const renameAsync = promisify(fs.rename); 6 | import plist from "simple-plist"; 7 | import path from "node:path"; 8 | import which from "which"; 9 | import rimraf from "rimraf"; 10 | import * as bin from "./bin.js"; 11 | // import { ConfigOptions } from "../dist/lib/config.js"; 12 | import { ConfigOptions } from "./config.js"; 13 | 14 | // TODO: remove globals 15 | let use7zip = false; 16 | let useOpenSSL = false; 17 | 18 | const cmdSpec = { 19 | "7z": "/usr/local/bin/7z", 20 | codesign: "/usr/bin/codesign", 21 | insert_dylib: "insert_dylib", 22 | lipo: "/usr/bin/lipo", 23 | /* only when useOpenSSL is true */ 24 | openssl: "/usr/local/bin/openssl", 25 | security: "/usr/bin/security", 26 | unzip: "/usr/bin/unzip", 27 | xcodebuild: "/usr/bin/xcodebuild", 28 | ideviceprovision: "/usr/local/bin/ideviceprovision", 29 | zip: "/usr/bin/zip", 30 | ldid2: "ldid2", 31 | }; 32 | 33 | const cmd: Record = {}; 34 | let cmdInited = false; 35 | 36 | /** 37 | * Result of executing a child process. 38 | */ 39 | interface ExecResult { 40 | stdout: string; 41 | stderr: string; 42 | code: number; 43 | } 44 | /** 45 | * Execute a program and capture stdout, stderr, and exit code. 46 | * @param cmdPath Path to executable 47 | * @param args Array of string arguments 48 | * @param options Spawn options 49 | * @returns Promise resolving to execution result 50 | */ 51 | /** 52 | * Options for spawning child processes used by execProgram. 53 | */ 54 | type ExecOptions = { 55 | cwd?: string; 56 | env?: { [key: string]: string | undefined }; 57 | stdio?: any; 58 | }; 59 | /** 60 | * Execute a program and capture stdout, stderr, and exit code. 61 | * @param cmdPath Path to executable 62 | * @param args Arguments array 63 | * @param options Spawn options 64 | * @returns Execution result 65 | */ 66 | async function execProgram( 67 | cmdPath: string, 68 | args: string[], 69 | options?: ExecOptions, 70 | ): Promise { 71 | return new Promise((resolve, reject) => { 72 | let _out = Buffer.alloc(0); 73 | let _err = Buffer.alloc(0); 74 | const child = spawn(cmdPath, args, options || {}); 75 | child.stdout.on("data", (data: Buffer) => { 76 | _out = Buffer.concat([_out, data]); 77 | }); 78 | child.stderr.on("data", (data: Buffer) => { 79 | _err = Buffer.concat([_err, data]); 80 | }); 81 | child.stdin.end(); 82 | child.on("close", (code: number) => { 83 | if (code !== 0) { 84 | let msg = `stdout: ${_out.toString("utf8")}`; 85 | msg += `\nstderr: ${_err.toString("utf8")}`; 86 | msg += `\ncommand: ${cmdPath} ${args.join(" ")}`; 87 | msg += `\ncode: ${code}`; 88 | return reject(new Error(msg)); 89 | } 90 | resolve({ 91 | stdout: _out.toString(), 92 | stderr: _err.toString(), 93 | code, 94 | }); 95 | }); 96 | }); 97 | } 98 | 99 | /* public */ 100 | function findInPath() { 101 | if (cmdInited) { 102 | return; 103 | } 104 | cmdInited = true; 105 | const keys = Object.keys(cmdSpec); 106 | for (const key of keys) { 107 | try { 108 | cmd[key] = which.sync(key); 109 | } catch { 110 | // ignore missing tools 111 | } 112 | } 113 | } 114 | 115 | /** 116 | * Get the path to a tool executable, or throw if not found. 117 | * @param tool Name of the tool 118 | */ 119 | function getTool(tool: string): string { 120 | findInPath(); 121 | if (!(tool in cmd)) { 122 | throw new Error(`tools.findInPath: not found: ${tool}`); 123 | } 124 | return cmd[tool]; 125 | } 126 | 127 | async function ideviceprovision(action: any, optarg?: any) { 128 | if (action === "list") { 129 | const res = await execProgram(getTool("ideviceprovision")!, ["list"]); 130 | return res.stdout 131 | .split("\n") 132 | .filter((line: any) => line.indexOf("-") !== -1) 133 | .map((line: any) => line.split(" ")[0]); 134 | } else { 135 | throw new Error("unsupported ideviceprovision action"); 136 | } 137 | } 138 | 139 | async function codesign( 140 | identity: string, 141 | entitlement: string | undefined, 142 | keychain: string | undefined, 143 | file: string, 144 | ) { 145 | if (identity === undefined) { 146 | // XXX: typescript can ensure this at compile time 147 | throw new Error("--identity is required to sign"); 148 | } 149 | /* use the --no-strict to avoid the "resource envelope is obsolete" error */ 150 | const args = ["--no-strict"]; // http://stackoverflow.com/a/26204757 151 | args.push("-fs", identity); 152 | // args.push('-v'); 153 | // args.push('--deep'); 154 | if (typeof entitlement === "string") { 155 | args.push("--entitlements=" + entitlement); 156 | } 157 | if (typeof keychain === "string") { 158 | args.push("--keychain=" + keychain); 159 | } 160 | args.push("--generate-entitlement-der"); 161 | args.push(file); 162 | return execProgram(getTool("codesign")!, args); 163 | } 164 | 165 | async function pseudoSign(entitlement: any, file: string): Promise { 166 | const args = []; 167 | if (typeof entitlement === "string") { 168 | args.push("-S" + entitlement); 169 | } else { 170 | args.push("-S"); 171 | } 172 | const identifier = bin.getIdentifier(file); 173 | if (identifier !== null && identifier !== "") { 174 | args.push("-I" + identifier); 175 | } 176 | args.push(file); 177 | return execProgram(getTool("ldid2")!, args); 178 | } 179 | 180 | async function verifyCodesign( 181 | file: string, 182 | keychain?: string, 183 | ): Promise { 184 | const args = ["-v", "--no-strict"]; 185 | if (typeof keychain === "string") { 186 | args.push("--keychain=" + keychain); 187 | } 188 | args.push(file); 189 | return execProgram(getTool("codesign")!, args); 190 | } 191 | 192 | async function getMobileProvisionPlist(file: string) { 193 | let res; 194 | if (file === undefined) { 195 | throw new Error("No mobile provisioning file available."); 196 | } 197 | if (useOpenSSL === true) { 198 | /* portable using openssl */ 199 | const args = ["cms", "-in", file, "-inform", "der", "-verify"]; 200 | res = await execProgram(getTool("openssl")!, args); 201 | } else { 202 | /* OSX specific using security */ 203 | const args = ["cms", "-D", "-i", file]; 204 | res = await execProgram(getTool("security")!, args); 205 | } 206 | return plist.parse(res.stdout); 207 | } 208 | 209 | async function getEntitlementsFromMobileProvision( 210 | file: string, 211 | cb?: any, 212 | ): Promise { 213 | const res = await getMobileProvisionPlist(file); 214 | return res.Entitlements; 215 | } 216 | 217 | async function zip(cwd: string, ofile: string, src: string) { 218 | try { 219 | await unlinkAsync(ofile); 220 | } catch (ignored) {} 221 | const ofilePath = path.dirname(ofile); 222 | fs.mkdirSync(ofilePath, { recursive: true }); 223 | if (use7zip) { 224 | const zipFile = ofile + ".zip"; 225 | const args = ["a", zipFile, src]; 226 | await execProgram(getTool("7z")!, args, { cwd }); 227 | await renameAsync(zipFile, ofile); 228 | } else { 229 | const args = ["-qry", ofile, src]; 230 | await execProgram(getTool("zip")!, args, { cwd }); 231 | } 232 | } 233 | 234 | async function unzip(ifile: string, odir: string) { 235 | if (use7zip) { 236 | const args = ["x", "-y", "-o" + odir, ifile]; 237 | return execProgram(getTool("7z")!, args); 238 | } 239 | if (process.env.UNZIP !== undefined) { 240 | cmd.unzip = process.env.UNZIP; 241 | delete process.env.UNZIP; 242 | } 243 | const args = ["-o", ifile, "-d", odir]; 244 | return execProgram(getTool("unzip")!, args); 245 | } 246 | 247 | async function xcaToIpa(ifile: string, odir: string) { 248 | const args = [ 249 | "-exportArchive", 250 | "-exportFormat", 251 | "ipa", 252 | "-archivePath", 253 | ifile, 254 | "-exportPath", 255 | odir, 256 | ]; 257 | return execProgram(getTool("xcodebuild")!, args); 258 | } 259 | 260 | // XXX: the out parameter is never used. therfor the caller doesnt works well 261 | async function insertLibrary(lib: string, bin: string, out: string) { 262 | let error = null; 263 | try { 264 | const machoMangle = require("macho-mangle"); 265 | try { 266 | let src = fs.readFileSync(bin); 267 | if (lib.indexOf("@rpath") === 0) { 268 | src = machoMangle(src, { 269 | type: "rpath", 270 | name: "@executable_path/Frameworks", 271 | }); 272 | } 273 | const dst = machoMangle(src, { 274 | type: "load_dylib", 275 | name: lib, 276 | version: { 277 | current: "1.0.0", 278 | compat: "0.0.0", 279 | }, 280 | }); 281 | fs.writeFileSync(bin, dst); 282 | console.log("Library inserted"); 283 | } catch (e) { 284 | error = e; 285 | } 286 | } catch (e) { 287 | if (getTool("insert_dylib") !== null) { 288 | const args = ["--strip-codesig", "--all-yes", lib, bin, bin]; 289 | const res = await execProgram(getTool("insert_dylib")!, args); 290 | console.error(JSON.stringify(res)); 291 | } else { 292 | error = new Error("Cannot find insert_dylib or macho-mangle"); 293 | } 294 | } 295 | if (error) { 296 | throw error; 297 | } 298 | } 299 | 300 | export interface Identity { 301 | hash: string; 302 | name: string; 303 | } 304 | 305 | function getIdentitiesFromString(stdout: any): Identity[] { 306 | const lines = stdout.split("\n"); 307 | lines.pop(); // remove last line 308 | const ids: Identity[] = []; 309 | lines 310 | .filter((entry: string) => { 311 | return entry.indexOf("CSSMERR_TP_CERT_REVOKED") === -1; 312 | }) 313 | .forEach((line: string) => { 314 | const tok = line.indexOf(") "); 315 | if (tok !== -1) { 316 | const msg = line.substring(tok + 2).trim(); 317 | const tok2 = msg.indexOf(" "); 318 | if (tok2 !== -1) { 319 | ids.push({ 320 | hash: msg.substring(0, tok2), 321 | name: msg 322 | .substring(tok2 + 1) 323 | .replace(/^"/, "") 324 | .replace(/"$/, ""), 325 | }); 326 | } 327 | } 328 | }); 329 | return ids; 330 | } 331 | 332 | function getIdentitiesSync(): Identity[] { 333 | const command = [ 334 | getTool("security"), 335 | "find-identity", 336 | "-v", 337 | "-p", 338 | "codesigning", 339 | ]; 340 | return getIdentitiesFromString(execSync(command.join(" ")).toString()); 341 | } 342 | 343 | async function getIdentities(): Promise { 344 | const args = ["find-identity", "-v", "-p", "codesigning"]; 345 | const res = await execProgram(getTool("security")!, args); 346 | return getIdentitiesFromString(res.stdout); 347 | } 348 | 349 | async function lipoFile(file: string, arch: string): Promise { 350 | const args = [file, "-thin", arch, "-output", file]; 351 | return execProgram(getTool("lipo")!, args); 352 | } 353 | 354 | function isDirectory(filePath: string): boolean { 355 | try { 356 | return fs.lstatSync(filePath).isDirectory(); 357 | } catch (error) { 358 | return false; 359 | } 360 | } 361 | 362 | export interface GlobalOptions { 363 | use7zip: boolean; 364 | useOpenSSL: boolean; 365 | } 366 | 367 | function setGlobalOptions(obj: GlobalOptions): void { 368 | if (typeof obj.use7zip === "boolean") { 369 | use7zip = obj.use7zip; 370 | } 371 | if (typeof obj.useOpenSSL === "boolean") { 372 | useOpenSSL = obj.useOpenSSL; 373 | } 374 | } 375 | 376 | function asyncRimraf(dir: any) { 377 | return new Promise((resolve, reject) => { 378 | if (dir === undefined) { 379 | resolve(undefined); 380 | } 381 | rimraf(dir, (err: any, res: any) => { 382 | return err ? reject(err) : resolve(res); 383 | }); 384 | }); 385 | } 386 | 387 | export { 388 | asyncRimraf, 389 | codesign, 390 | getEntitlementsFromMobileProvision, 391 | getIdentities, 392 | getIdentitiesSync, 393 | getMobileProvisionPlist, 394 | ideviceprovision, 395 | insertLibrary, 396 | isDirectory, 397 | lipoFile, 398 | pseudoSign, 399 | setGlobalOptions as setOptions, 400 | unzip, 401 | verifyCodesign, 402 | xcaToIpa, 403 | zip, 404 | }; 405 | -------------------------------------------------------------------------------- /lib/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "colors"; 2 | declare module "simple-plist"; 3 | declare module "plist"; 4 | declare module "macho-entitlements"; 5 | declare module "macho-is-encrypted"; 6 | declare module "fatmacho"; 7 | declare module "macho"; 8 | declare module "which"; 9 | declare module "rimraf"; 10 | 11 | declare module "fs-walk" { 12 | function walkSync( 13 | appdir, 14 | cb: (basedir: string, filename: string, stat: number) => void, 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /lib/version.ts: -------------------------------------------------------------------------------- 1 | const version = "5.0.1"; 2 | export default version; 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "author": { 4 | "name": "Sergi Alvarez" 5 | }, 6 | "bin": { 7 | "applesign": "bin/applesign.js" 8 | }, 9 | "engines": { 10 | "node": ">=20", 11 | "npm": ">=10" 12 | }, 13 | "scripts": { 14 | "indent-check": "deno fmt --check", 15 | "indent": "deno fmt", 16 | "unzip-lzfse": "git clone https://github.com/sskaje/unzip-lzfse ; cd unzip-lzfse ; chmod +x unix/configure ; LZFSE_PATH=/usr/local make -f unix/Makefile CF='-DUSE_LZFSE=1 -c -O3 -Wall -DBSD -I. -Ibzip2 -DUNIX' LF2=-llzfse all", 17 | "test": "npm run build && chmod +x dist/bin/applesign.js && node --loader ts-node/esm node_modules/.bin/mocha test/test.ts", 18 | "dist": "./scripts/dist.sh", 19 | "dist-all": "./scripts/dist.sh macos linux freebsd alpine win", 20 | "prebuild": "node scripts/update-version.cjs", 21 | "build": "tsc" 22 | }, 23 | "devDependencies": { 24 | "@types/mocha": "^10.0.10", 25 | "@types/node": "^22.15.29", 26 | "@types/fs-extra": "^11.0.4", 27 | "@types/minimist": "^1.2.5", 28 | "@types/uuid": "^8.3.4", 29 | "mocha": "^11.5.0", 30 | "npm": "^10.0.0", 31 | "pkg": "5.6.0", 32 | "ts-node": "^10.9.2", 33 | "typescript": "^5.8.3", 34 | "deno": "^2.3.5" 35 | }, 36 | "dependencies": { 37 | "colors": "1.4.0", 38 | "fatmacho": "0.1.2", 39 | "fs-extra": "^11.3.0", 40 | "fs-walk": "github:trufae/fs-walk#patch-1", 41 | "macho": "^1.4.0", 42 | "macho-entitlements": "^0.2.3", 43 | "macho-is-encrypted": "^0.1.2", 44 | "minimist": "^1.2.8", 45 | "plist": "github:TooTallNate/plist.js#e17373ef96510a606b62553bd28845842133ba12", 46 | "rimraf": "^3.0.2", 47 | "simple-plist": "^1.3.1", 48 | "uniq": "1.0.1", 49 | "uuid": "8.2.0", 50 | "which": "2.0.2" 51 | }, 52 | "overrides": { 53 | "plist": "github:TooTallNate/plist.js#e17373ef96510a606b62553bd28845842133ba12" 54 | }, 55 | "description": "API to resign IPA files", 56 | "homepage": "https://www.nowsecure.com", 57 | "repository": { 58 | "type": "git", 59 | "url": "git+https://github.com/nowsecure/node-applesign.git" 60 | }, 61 | "keywords": [ 62 | "binary", 63 | "iphone", 64 | "codesign", 65 | "ios", 66 | "osx" 67 | ], 68 | "license": "MIT", 69 | "main": "dist/index.js", 70 | "types": "dist/index.d.ts", 71 | "files": [ 72 | "/dist/**/*.js", 73 | "/dist/index.d.ts" 74 | ], 75 | "maintainers": [ 76 | { 77 | "name": "Sergi Alvarez", 78 | "email": "pancake@nowsecure.com" 79 | } 80 | ], 81 | "name": "applesign", 82 | "version": "5.0.1" 83 | } 84 | -------------------------------------------------------------------------------- /scripts/dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$1" = "-h" ]; then 4 | echo "Usage: dist.sh [linux macos win alpine]" 5 | echo "See https://github.com/zeit/pkg-fetch/releases for a complete list" 6 | echo "Generate a distribution zip for the given targets" 7 | exit 0 8 | fi 9 | 10 | PKG=node_modules/.bin/pkg 11 | if [ ! -x $PKG ]; then 12 | npm i 13 | exec $@ 14 | fi 15 | 16 | V=$(node -e 'console.log(require("./package.json").version)') 17 | 18 | echo "Version: $V" 19 | 20 | # $PKG bin/applesign.js || exit 1 21 | 22 | if [ -z "$*" ]; then 23 | TARGETS=macos 24 | else 25 | TARGETS=$* 26 | fi 27 | # TARGETS=linux macos win.exe 28 | echo "Targets: $TARGETS" 29 | 30 | for a in ${TARGETS}; do 31 | echo "Packaging for $a ..." 32 | if [ "$a" = "win" ]; then 33 | E=$V.exe 34 | else 35 | E=$V 36 | fi 37 | $PKG -t node10-$a-x64 -o applesign-$E bin/applesign.js 38 | rm -f applesign-$V-$a.zip 39 | echo "Compressing applesign-$V-$a.zip" 40 | zip -9 applesign-$V-$a.zip applesign-$E 41 | done 42 | 43 | echo Done 44 | -------------------------------------------------------------------------------- /scripts/update-version.cjs: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | const pkg = require("../package.json"); 5 | const versionFile = path.join(__dirname, "../lib/version.ts"); 6 | 7 | fs.writeFileSync( 8 | versionFile, 9 | `const version = "${pkg.version}";\nexport default version;\n`, 10 | ); 11 | console.log(`✅ Updated version.ts to ${pkg.version}`); 12 | -------------------------------------------------------------------------------- /test/ipa/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nowsecure/node-applesign/910965dafd20f768c73c5bbb7299f1212547b2b5/test/ipa/.empty -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "node:assert"; 2 | import { spawn } from "node:child_process"; 3 | import * as path from "node:path"; 4 | import * as fs from "node:fs"; 5 | import { describe, it } from "mocha"; 6 | 7 | const mochaTimeout = 15000; 8 | const developerCertificate = process.env.DEVCERT; 9 | const ipaDir = "test/ipa"; 10 | 11 | /* 12 | // cant await import or require because.. mocha/esm 13 | describe("API", () => { 14 | describe("require", () => { 15 | it("require works", async () => { 16 | try { 17 | // const index = await import('./dist/index.js') as any; 18 | assert.strictEqual(0, 0); 19 | } catch (e) { 20 | console.error(e); 21 | assert.fail("require failed"); 22 | } 23 | }); 24 | }); 25 | }); 26 | */ 27 | 28 | describe("Commandline", () => { 29 | describe("dist/bin/applesign.js", () => { 30 | it("should fail when applesign cannot be executed", (done) => { 31 | let data = ""; 32 | const ipaResign = spawn("dist/bin/applesign.js"); 33 | ipaResign.stdout.on("data", (text) => { 34 | data += text; 35 | }); 36 | ipaResign.on("close", (code) => { 37 | assert.strictEqual(data, ""); 38 | assert.strictEqual(code, 0); 39 | done(); 40 | }); 41 | }); 42 | }); 43 | 44 | describe("dist/bin/applesign.js missing.ipa", () => { 45 | it("should fail when passing a nonexistent IPA", (done) => { 46 | const ipaResign = spawn("dist/bin/applesign.js", ["missing.ipa"]); 47 | ipaResign.on("close", (code) => { 48 | assert.strictEqual(code, 1); 49 | done(); 50 | }); 51 | }); 52 | }); 53 | 54 | /* 55 | describe("bin/applesign.js -L", () => { 56 | it("checking for developer certificates", (done) => { 57 | let data = ""; 58 | const ipaResign = spawn("bin/applesign.js", ["-L"]); 59 | ipaResign.stdout.on("data", (text) => { 60 | if (!developerCertificate) { 61 | developerCertificate = text.toString().split(" ")[0]; 62 | } 63 | data += text; 64 | }); 65 | ipaResign.on("close", (code) => { 66 | assert.notStrictEqual(data, ""); 67 | assert.strictEqual(code, 0); 68 | done(); 69 | }); 70 | }); 71 | }); 72 | */ 73 | }); 74 | 75 | function grabIPAs(file: string): boolean { 76 | return !file.includes("resigned") && file.endsWith(".ipa"); 77 | } 78 | 79 | function processIPA(file: string, parallel: boolean) { 80 | describe(`${parallel ? "Parallel" : "Serial"} signing`, function () { 81 | this.timeout(mochaTimeout); 82 | it(file, (done) => { 83 | let hasData = false; 84 | const ipaFile = path.resolve(path.join(ipaDir, file)); 85 | const args = parallel 86 | ? ["-p", "-i", developerCertificate!, ipaFile] 87 | : ["-i", developerCertificate!, ipaFile]; 88 | const ipaResign = spawn("dist/bin/applesign.js", args); 89 | 90 | ipaResign.stdout.on("data", () => { 91 | hasData = true; 92 | }); 93 | 94 | ipaResign.stderr.on("data", (text) => { 95 | console.error(text.toString()); 96 | }); 97 | 98 | ipaResign.on("close", (code) => { 99 | assert.strictEqual(hasData, true); 100 | assert.strictEqual(code, 0); 101 | done(); 102 | }); 103 | }); 104 | }); 105 | } 106 | 107 | function deployIPA(file: string) { 108 | describe(`Deploy ${file}`, function () { 109 | this.timeout(mochaTimeout); 110 | it("deploying", (done) => { 111 | let hasData = false; 112 | const ipaResign = spawn("ios-deploy", ["-b", path.join(ipaDir, file)]); 113 | ipaResign.stdout.on("data", () => { 114 | hasData = true; 115 | }); 116 | 117 | ipaResign.stderr.on("data", (text) => { 118 | console.error(text.toString()); 119 | }); 120 | 121 | ipaResign.on("close", (code) => { 122 | assert.strictEqual(hasData, true); 123 | assert.strictEqual(code, 0); 124 | done(); 125 | }); 126 | }); 127 | }); 128 | } 129 | 130 | describe("Commandline IPA signing", function () { 131 | this.timeout(30000); // in case reading directory is slow 132 | const files = fs.readdirSync(ipaDir); 133 | describe("Processing", () => { 134 | files.filter(grabIPAs).forEach((file) => { 135 | describe(file, () => { 136 | processIPA(file, false); 137 | processIPA(file, true); 138 | deployIPA(file); 139 | }); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "declaration": true, 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "rootDir": ".", 8 | "moduleDetection": "force", 9 | "outDir": "dist", 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "skipLibCheck": true, 14 | "allowSyntheticDefaultImports": true 15 | }, 16 | "ts-node": { 17 | "esm": true 18 | }, 19 | "include": [ 20 | // include both JS and TS sources for migration 21 | "lib/**/*", 22 | "bin/**/*", 23 | "index.*", 24 | "example.*" 25 | ], 26 | "exclude": ["node_modules", "dist", "test"] 27 | } 28 | --------------------------------------------------------------------------------