From 3e28ae28a0cf68adc0d1e6f809eae98d98e4fa5f Mon Sep 17 00:00:00 2001 From: develar Date: Fri, 2 Jun 2017 18:34:33 +0200 Subject: [PATCH] feat: asar integrity (macos only for now) --- package.json | 14 ++++---- packages/asar-integrity/package.json | 18 ++++++++++ packages/asar-integrity/src/asarIntegrity.ts | 34 +++++++++++++++++++ packages/asar-integrity/tsconfig.json | 10 ++++++ packages/electron-builder/package.json | 5 +-- packages/electron-builder/src/packager/mac.ts | 6 +++- .../electron-builder/src/platformPackager.ts | 5 +-- .../src/publish/PublishManager.ts | 25 +++----------- packages/electron-publisher-s3/package.json | 2 +- .../mac/__snapshots__/macPackagerTest.js.snap | 1 + test/src/helpers/packTester.ts | 10 ++++++ 11 files changed, 96 insertions(+), 34 deletions(-) create mode 100644 packages/asar-integrity/package.json create mode 100644 packages/asar-integrity/src/asarIntegrity.ts create mode 100644 packages/asar-integrity/tsconfig.json diff --git a/package.json b/package.json index 2cc9ff8e3d5..3427f06f5d3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": true, "license": "MIT", "scripts": { - "compile": "ts-babel packages/electron-builder-http packages/electron-builder-core packages/electron-builder-util packages/electron-publish packages/electron-builder packages/electron-builder-squirrel-windows packages/electron-updater packages/electron-publisher-s3 test && node ./test/vendor/yarn.js schema", + "compile": "ts-babel packages/asar-integrity packages/electron-builder-http packages/electron-builder-core packages/electron-builder-util packages/electron-publish packages/electron-builder packages/electron-builder-squirrel-windows packages/electron-updater packages/electron-publisher-s3 test && node ./test/vendor/yarn.js schema", "lint": "node test/out/helpers/lint.js", "pretest": "node ./test/vendor/yarn.js compile && node ./test/vendor/yarn.js lint && node ./test/vendor/yarn.js lint-deps", "lint-deps": "node ./test/out/helpers/checkDeps.js", @@ -15,7 +15,7 @@ "whitespace": "whitespace 'src/**/*.ts'", "docker-images": "docker/build.sh", "test-deps-mac": "brew install rpm dpkg mono lzip gnu-tar graphicsmagick xz && brew install wine --without-x11", - "update-deps": "lerna exec -- npm-check-updates --reject 'electron-builder-http,electron-builder-util,electron-builder-core,electron-publish,electron-forge-maker-appimage,electron-forge-maker-nsis,electron-forge-maker-snap' -a", + "update-deps": "node ./packages/update-deps.js", "set-versions": "node test/out/helpers/setVersions.js", "npm-publish": "yarn set-versions && yarn compile && ./packages/npm-publish.sh && conventional-changelog -p angular -i CHANGELOG.md -s", "schema": "typescript-json-schema packages/electron-builder/tsconfig.json Config --out packages/electron-builder/scheme.json --noExtraProps --useTypeOfKeyword --strictNullChecks --titles", @@ -31,7 +31,7 @@ "ajv": "^5.1.5", "ajv-keywords": "^2.1.0", "archiver": "^1.3.0", - "aws-sdk": "^2.59.0", + "aws-sdk": "^2.61.0", "bluebird-lst": "^1.0.2", "chalk": "^1.1.3", "chromium-pickle-js": "^0.2.0", @@ -39,7 +39,7 @@ "debug": "^2.6.8", "electron-download-tf": "4.3.1", "electron-is-dev": "^0.1.2", - "electron-osx-sign": "0.4.5", + "electron-osx-sign": "0.4.6", "fs-extra-p": "^4.3.0", "hosted-git-info": "^2.4.2", "ini": "^1.3.4", @@ -66,10 +66,10 @@ }, "devDependencies": { "@types/ini": "^1.3.29", - "@types/jest": "^19.2.3", + "@types/jest": "^19.2.4", "@types/js-yaml": "^3.5.30", "@types/node-forge": "^0.6.9", - "@types/source-map-support": "^0.2.28", + "@types/source-map-support": "^0.4.0", "@types/xml2js": "^0.0.33", "babel-plugin-array-includes": "^2.0.3", "babel-plugin-transform-async-to-module-method": "^6.24.1", @@ -90,7 +90,7 @@ "path-sort": "^0.1.0", "source-map-support": "^0.4.15", "ts-babel": "^3.0.1", - "tslint": "^5.3.2", + "tslint": "^5.4.2", "typescript": "^2.3.4", "whitespace": "^2.1.0", "xml2js": "^0.4.17" diff --git a/packages/asar-integrity/package.json b/packages/asar-integrity/package.json new file mode 100644 index 00000000000..e90b0b6f2ea --- /dev/null +++ b/packages/asar-integrity/package.json @@ -0,0 +1,18 @@ +{ + "name": "asar-integrity", + "version": "0.0.0-semantic-release", + "main": "out/asarIntegrity.js", + "author": "Vladimir Krivosheev", + "license": "MIT", + "repository": "electron-userland/electron-builder", + "bugs": "https://github.com/electron-userland/electron-builder/issues", + "homepage": "https://github.com/electron-userland/electron-builder", + "files": [ + "out" + ], + "dependencies": { + "bluebird-lst": "^1.0.2", + "fs-extra-p": "^4.3.0" + }, + "typings": "./out/asar-integrity.d.ts" +} diff --git a/packages/asar-integrity/src/asarIntegrity.ts b/packages/asar-integrity/src/asarIntegrity.ts new file mode 100644 index 00000000000..ebf647da10e --- /dev/null +++ b/packages/asar-integrity/src/asarIntegrity.ts @@ -0,0 +1,34 @@ +import BluebirdPromise from "bluebird-lst" +import { createHash } from "crypto" +import { createReadStream } from "fs" +import { readdir } from "fs-extra-p" +import * as path from "path" + +export async function computeData(resourcesPath: string): Promise<{ [key: string]: string; }> { + // sort to produce constant result + const names = (await readdir(resourcesPath)).filter(it => it.endsWith(".asar")).sort() + const checksums = await BluebirdPromise.map(names, it => hashFile(path.join(resourcesPath, it), "sha512")) + + const result: { [key: string]: string; } = {} + for (let i = 0; i < names.length; i++) { + result[names[i]] = checksums[i] + } + return result +} + +export function hashFile(file: string, algorithm: string) { + return new BluebirdPromise((resolve, reject) => { + const hash = createHash(algorithm) + hash + .on("error", reject) + .setEncoding("hex") + + createReadStream(file) + .on("error", reject) + .on("end", () => { + hash.end() + resolve(hash.read()) + }) + .pipe(hash, {end: false}) + }) +} \ No newline at end of file diff --git a/packages/asar-integrity/tsconfig.json b/packages/asar-integrity/tsconfig.json new file mode 100644 index 00000000000..ddce4b78028 --- /dev/null +++ b/packages/asar-integrity/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig-base.json", + "compilerOptions": { + "outDir": "out" + }, + "declaration": true, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/electron-builder/package.json b/packages/electron-builder/package.json index 7a14f222e55..4f8fa1239e3 100644 --- a/packages/electron-builder/package.json +++ b/packages/electron-builder/package.json @@ -56,7 +56,7 @@ "electron-builder-http": "0.0.0-semantic-release", "electron-builder-util": "0.0.0-semantic-release", "electron-download-tf": "4.3.1", - "electron-osx-sign": "0.4.5", + "electron-osx-sign": "0.4.6", "electron-publish": "0.0.0-semantic-release", "fs-extra-p": "^4.3.0", "hosted-git-info": "^2.4.2", @@ -74,7 +74,8 @@ "update-notifier": "^2.1.0", "uuid-1345": "^0.99.6", "yargs": "^8.0.1", - "debug": "2.6.8" + "debug": "2.6.8", + "asar-integrity": "0.0.0-semantic-release" }, "typings": "./out/electron-builder.d.ts", "publishConfig": { diff --git a/packages/electron-builder/src/packager/mac.ts b/packages/electron-builder/src/packager/mac.ts index 5849258784a..5706e7e58c6 100644 --- a/packages/electron-builder/src/packager/mac.ts +++ b/packages/electron-builder/src/packager/mac.ts @@ -25,7 +25,7 @@ export function filterCFBundleIdentifier(identifier: string) { return identifier.replace(/ /g, "-").replace(/[^a-zA-Z0-9.-]/g, "") } -export async function createApp(packager: PlatformPackager, appOutDir: string) { +export async function createApp(packager: PlatformPackager, appOutDir: string, checksums: { [key: string]: string; }) { const appInfo = packager.appInfo const appFilename = appInfo.productFilename @@ -135,6 +135,10 @@ export async function createApp(packager: PlatformPackager, appOutDir: stri use(packager.platformSpecificBuildOptions.category || (buildMetadata).category, it => appPlist.LSApplicationCategoryType = it) appPlist.NSHumanReadableCopyright = appInfo.copyright + if (checksums != null) { + appPlist.AsarChecksums = JSON.stringify(checksums) + } + const promises: Array> = [ writeFile(appPlistFilename, buildPlist(appPlist)), writeFile(helperPlistFilename, buildPlist(helperPlist)), diff --git a/packages/electron-builder/src/platformPackager.ts b/packages/electron-builder/src/platformPackager.ts index 343db49b419..c83000d4ef5 100644 --- a/packages/electron-builder/src/platformPackager.ts +++ b/packages/electron-builder/src/platformPackager.ts @@ -15,6 +15,7 @@ import { Config } from "./metadata" import { unpackElectron, unpackMuon } from "./packager/dirPackager" import { BuildInfo, PackagerOptions } from "./packagerApi" import { readInstalled } from "./readInstalled" +import { computeData } from "asar-integrity" export abstract class PlatformPackager { readonly packagerOptions: PackagerOptions @@ -208,7 +209,7 @@ export abstract class PlatformPackager await BluebirdPromise.all(promises) if (platformName === "darwin" || platformName === "mas") { - await (require("./packager/mac")).createApp(this, appOutDir) + await (require("./packager/mac")).createApp(this, appOutDir, asarOptions == null ? null : await computeData(resourcesPath)) } await copyFiles(extraResourceMatchers) @@ -363,7 +364,7 @@ export abstract class PlatformPackager } const appInfo = this.appInfo - return pattern.replace(/\$\{([_a-zA-Z./*]+)\}/g, (match, p1): string => { + return pattern.replace(/\${([_a-zA-Z./*]+)}/g, (match, p1): string => { switch (p1) { case "productName": return appInfo.productFilename diff --git a/packages/electron-builder/src/publish/PublishManager.ts b/packages/electron-builder/src/publish/PublishManager.ts index 049fbf7acf5..497a45bf490 100644 --- a/packages/electron-builder/src/publish/PublishManager.ts +++ b/packages/electron-builder/src/publish/PublishManager.ts @@ -1,5 +1,4 @@ import BluebirdPromise from "bluebird-lst" -import { createHash } from "crypto" import { Arch, Platform, PlatformSpecificBuildOptions, Target } from "electron-builder-core" import { CancellationToken } from "electron-builder-http/out/CancellationToken" import { BintrayOptions, GenericServerOptions, GithubOptions, githubUrl, PublishConfiguration, PublishProvider, S3Options, s3Url, UpdateInfo, VersionInfo } from "electron-builder-http/out/publishOptions" @@ -10,7 +9,7 @@ import { HttpPublisher, PublishContext, Publisher, PublishOptions } from "electr import { BintrayPublisher } from "electron-publish/out/BintrayPublisher" import { GitHubPublisher } from "electron-publish/out/gitHubPublisher" import { MultiProgress } from "electron-publish/out/multiProgress" -import { createReadStream, ensureDir, outputJson, writeFile } from "fs-extra-p" +import { ensureDir, outputJson, writeFile } from "fs-extra-p" import isCi from "is-ci" import { safeDump } from "js-yaml" import * as path from "path" @@ -21,6 +20,7 @@ import { Packager } from "../packager" import { ArtifactCreated, BuildInfo } from "../packagerApi" import { PlatformPackager } from "../platformPackager" import { WinPackager } from "../winPackager" +import { hashFile } from "asar-integrity" const publishForPrWarning = "There are serious security concerns with PUBLISH_FOR_PULL_REQUEST=true (see the CircleCI documentation (https://circleci.com/docs/1.0/fork-pr-builds/) for details)" + "\nIf you have SSH keys, sensitive env vars or AWS credentials stored in your project settings and untrusted forks can make pull requests against your repo, then this option isn't for you." @@ -247,8 +247,8 @@ async function writeUpdateInfo(event: ArtifactCreated, _publishConfigs: Array hash(event.file!, "sha256")) - let sha512 = new Lazy(() => hash(event.file!, "sha512")) + let sha2 = new Lazy(() => hashFile(event.file!, "sha256")) + let sha512 = new Lazy(() => hashFile(event.file!, "sha512")) const isMac = packager.platform === Platform.MAC for (const publishConfig of publishConfigs) { @@ -421,23 +421,6 @@ export async function getPublishConfigs(packager: PlatformPackager, targetS return await (>>BluebirdPromise.map(asArray(publishers), it => getResolvedPublishConfig(packager, typeof it === "string" ? {provider: it} : it, arch))) } -function hash(file: string, algorithm: string) { - return new BluebirdPromise((resolve, reject) => { - const hash = createHash(algorithm) - hash - .on("error", reject) - .setEncoding("hex") - - createReadStream(file) - .on("error", reject) - .on("end", () => { - hash.end() - resolve(hash.read()) - }) - .pipe(hash, {end: false}) - }) -} - function isSuitableWindowsTarget(target: Target) { return target.name === "nsis" || target.name.startsWith("nsis-") } diff --git a/packages/electron-publisher-s3/package.json b/packages/electron-publisher-s3/package.json index 9a70c154e43..8818fda308d 100644 --- a/packages/electron-publisher-s3/package.json +++ b/packages/electron-publisher-s3/package.json @@ -12,7 +12,7 @@ ], "dependencies": { "fs-extra-p": "^4.3.0", - "aws-sdk": "^2.60.0", + "aws-sdk": "^2.61.0", "mime": "^1.3.6", "electron-publish": "~0.0.0-semantic-release", "electron-builder-util": "~0.0.0-semantic-release" diff --git a/test/out/mac/__snapshots__/macPackagerTest.js.snap b/test/out/mac/__snapshots__/macPackagerTest.js.snap index 65615edc964..217dc0b1764 100644 --- a/test/out/mac/__snapshots__/macPackagerTest.js.snap +++ b/test/out/mac/__snapshots__/macPackagerTest.js.snap @@ -29,6 +29,7 @@ Object { exports[`one-package 2`] = ` Object { + "AsarChecksums": "{\\"app.asar\\":\\"hash\\",\\"electron.asar\\":\\"hash\\"}", "BuildMachineOSBuild": "16E195", "CFBundleDisplayName": "Test App ßW", "CFBundleDocumentTypes": Array [ diff --git a/test/src/helpers/packTester.ts b/test/src/helpers/packTester.ts index bbb00027640..72e1ebddcd0 100644 --- a/test/src/helpers/packTester.ts +++ b/test/src/helpers/packTester.ts @@ -265,6 +265,16 @@ async function checkMacResult(packager: Packager, packagerOptions: PackagerOptio // checked manually, remove to avoid mismatch on CI server (where TRAVIS_BUILD_NUMBER is defined and different on each test run) delete info.CFBundleVersion delete info.NSHumanReadableCopyright + + const checksumData = info.AsarChecksums + if (checksumData != null) { + const checksums = JSON.parse(checksumData) + for (const name of Object.keys(checksums)) { + checksums[name] = "hash" + } + info.AsarChecksums = JSON.stringify(checksums) + } + if (checkOptions.checkMacApp != null) { await checkOptions.checkMacApp(packedAppDir, info) }