This commit is contained in:
2022-09-30 05:39:11 +00:00
parent 41ee9463ae
commit 4687fa49bc
11418 changed files with 1312504 additions and 0 deletions

View File

@ -0,0 +1,23 @@
Copyright (c) 2015-2016 Zhuo Lu, Jason Hinkle, et al.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
</dict>
</plist>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
</dict>
</plist>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.inherit</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,149 @@
/**
* @module flat
*/
'use strict'
const path = require('path')
const util = require('./util')
const debuglog = util.debuglog
const debugwarn = util.debugwarn
const execFileAsync = util.execFileAsync
const validateOptsAppAsync = util.validateOptsAppAsync
const validateOptsPlatformAsync = util.validateOptsPlatformAsync
const Identity = require('./util-identities').findIdentitiesAsync
const findIdentitiesAsync = require('./util-identities').findIdentitiesAsync
/**
* This function returns a promise validating all options passed in opts.
* @function
* @param {Object} opts - Options.
* @returns {Promise} Promise.
*/
function validateFlatOptsAsync (opts) {
if (opts.pkg) {
if (typeof opts.pkg !== 'string') return Promise.reject(new Error('`pkg` must be a string.'))
if (path.extname(opts.pkg) !== '.pkg') return Promise.reject(new Error('Extension of output package must be `.pkg`.'))
} else {
debugwarn('No `pkg` passed in arguments, will fallback to default inferred from the given application.')
opts.pkg = path.join(path.dirname(opts.app), path.basename(opts.app, '.app') + '.pkg')
}
if (opts.install) {
if (typeof opts.install !== 'string') return Promise.reject(new Error('`install` must be a string.'))
} else {
debugwarn('No `install` passed in arguments, will fallback to default `/Applications`.')
opts.install = '/Applications'
}
return Promise.all([
validateOptsAppAsync(opts),
validateOptsPlatformAsync(opts),
])
}
/**
* This function returns a promise flattening the application.
* @function
* @param {Object} opts - Options.
* @returns {Promise} Promise.
*/
function flatApplicationAsync (opts) {
const args = [
'--component', opts.app, opts.install,
'--sign', opts.identity.name,
opts.pkg
]
if (opts.keychain) {
args.unshift('--keychain', opts.keychain)
}
if (opts.scripts) {
args.unshift('--scripts', opts.scripts)
}
debuglog('Flattening... ' + opts.app)
return execFileAsync('productbuild', args)
.thenReturn(undefined)
}
/**
* This function is exported and returns a promise flattening the application.
* @function
* @param {Object} opts - Options.
* @returns {Promise} Promise.
*/
module.exports.flatAsync = function (opts) {
return validateFlatOptsAsync(opts)
.then(function () {
let promise
if (opts.identity) {
debuglog('`identity` passed in arguments.')
if (opts['identity-validation'] === false || opts.identity instanceof Identity) {
return Promise.resolve()
}
promise = findIdentitiesAsync(opts, opts.identity)
} else {
debugwarn('No `identity` passed in arguments...')
if (opts.platform === 'mas') {
debuglog('Finding `3rd Party Mac Developer Installer` certificate for flattening app distribution in the Mac App Store...')
promise = findIdentitiesAsync(opts, '3rd Party Mac Developer Installer:')
} else {
debuglog('Finding `Developer ID Application` certificate for distribution outside the Mac App Store...')
promise = findIdentitiesAsync(opts, 'Developer ID Installer:')
}
}
return promise
.then(function (identities) {
if (identities.length > 0) {
// Provisioning profile(s) found
if (identities.length > 1) {
debugwarn('Multiple identities found, will use the first discovered.')
} else {
debuglog('Found 1 identity.')
}
opts.identity = identities[0]
} else {
// No identity found
return Promise.reject(new Error('No identity found for signing.'))
}
})
})
.then(function () {
// Pre-flat operations
})
.then(function () {
debuglog('Flattening application...', '\n',
'> Application:', opts.app, '\n',
'> Package output:', opts.pkg, '\n',
'> Install path:', opts.install, '\n',
'> Identity:', opts.identity, '\n',
'> Scripts:', opts.scripts)
return flatApplicationAsync(opts)
})
.then(function () {
// Post-flat operations
debuglog('Application flattened.')
})
}
/**
* This function is exported with normal callback implementation.
* @function
* @param {Object} opts - Options.
* @param {RequestCallback} cb - Callback.
*/
module.exports.flat = function (opts, cb) {
module.exports.flatAsync(opts)
.then(function () {
debuglog('Application flattened, saved to: ' + opts.app)
if (cb) cb()
})
.catch(function (err) {
debuglog('Flat failed:')
if (err.message) debuglog(err.message)
else if (err.stack) debuglog(err.stack)
else debuglog(err)
if (cb) cb(err)
})
}

View File

@ -0,0 +1,39 @@
interface BaseSignOptions {
app: string;
identity?: string;
platform?: string;
keychain?: string;
}
interface SignOptions extends BaseSignOptions {
binaries?: string[];
entitlements?: string;
'entitlements-inherit'?: string;
'entitlements-loginhelper'?: string;
'gatekeeper-assess'?: boolean;
hardenedRuntime?: boolean;
'identity-validation'?: boolean;
ignore?: string;
'pre-auto-entitlements'?: boolean;
'pre-embed-provisioning-profile'?: boolean;
'provisioning-profile'?: string;
'requirements'?: string;
'signature-size'?: number;
type?: string;
version?: string;
}
export function sign(opts: SignOptions, callback: (error: Error) => void): void;
export function signAsync(opts: SignOptions): Promise<any>;
interface FlatOptions extends BaseSignOptions {
'identity-validation'?: boolean;
install?: string;
pkg?: string;
scripts?: string;
}
export function flat(opts: FlatOptions, callback: (error: Error) => void): void;
export function flatAsync(opts: FlatOptions): Promise<any>;

View File

@ -0,0 +1,47 @@
/**
* @module electron-osx-sign
*/
'use strict'
const sign = require('./sign')
const flat = require('./flat')
/**
* This function is a normal callback implementation.
* @param {Object} opts - Options.
* @param {RequestCallback} cb - Callback.
*/
module.exports = sign.sign // Aliasing
/**
* This function is a normal callback implementation.
* @function
* @param {Object} opts - Options.
* @param {RequestCallback} cb - Callback.
*/
module.exports.sign = sign.sign
/**
* This function returns a promise signing the application.
* @function
* @param {mixed} opts - Options.
* @returns {Promise} Promise.
*/
module.exports.signAsync = sign.signAsync
/**
* This function is exported with normal callback implementation.
* @function
* @param {Object} opts - Options.
* @param {RequestCallback} cb - Callback.
*/
module.exports.flat = flat.flat
/**
* This function is exported and returns a promise flattening the application.
* @function
* @param {Object} opts - Options.
* @returns {Promise} Promise.
*/
module.exports.flatAsync = flat.flatAsync

View File

@ -0,0 +1,413 @@
/**
* @module sign
*/
'use strict'
const path = require('path')
const semver = require('semver')
const util = require('./util')
const debuglog = util.debuglog
const debugwarn = util.debugwarn
const getAppContentsPath = util.getAppContentsPath
const execFileAsync = util.execFileAsync
const validateOptsAppAsync = util.validateOptsAppAsync
const validateOptsPlatformAsync = util.validateOptsPlatformAsync
const walkAsync = util.walkAsync
const Identity = require('./util-identities').Identity
const findIdentitiesAsync = require('./util-identities').findIdentitiesAsync
const ProvisioningProfile = require('./util-provisioning-profiles').ProvisioningProfile
const preEmbedProvisioningProfile = require('./util-provisioning-profiles').preEmbedProvisioningProfile
const preAutoEntitlements = require('./util-entitlements').preAutoEntitlements
const osRelease = require('os').release()
/**
* This function returns a promise validating opts.binaries, the additional binaries to be signed along with the discovered enclosed components.
* @function
* @param {Object} opts - Options.
* @returns {Promise} Promise.
*/
function validateOptsBinariesAsync (opts) {
return new Promise(function (resolve, reject) {
if (opts.binaries) {
if (!Array.isArray(opts.binaries)) {
reject(new Error('Additional binaries should be an Array.'))
return
}
// TODO: Presence check for binary files, reject if any does not exist
}
resolve()
})
}
/**
* This function returns a promise validating all options passed in opts.
* @function
* @param {Object} opts - Options.
* @returns {Promise} Promise.
*/
function validateSignOptsAsync (opts) {
if (opts.ignore && !(opts.ignore instanceof Array)) {
opts.ignore = [opts.ignore]
}
if (opts['provisioning-profile']) {
if (typeof opts['provisioning-profile'] !== 'string' && !(opts['provisioning-profile'] instanceof ProvisioningProfile)) return Promise.reject(new Error('Path to provisioning profile should be a string or a ProvisioningProfile object.'))
}
if (opts['type']) {
if (opts['type'] !== 'development' && opts['type'] !== 'distribution') return Promise.reject(new Error('Type must be either `development` or `distribution`.'))
} else {
opts['type'] = 'distribution'
}
return Promise.all([
validateOptsAppAsync(opts),
validateOptsPlatformAsync(opts),
validateOptsBinariesAsync(opts),
])
}
/**
* This function returns a promise verifying the code sign of application bundle.
* @function
* @param {Object} opts - Options.
* @returns {Promise} Promise resolving output.
*/
async function verifySignApplicationAsync (opts) {
// Verify with codesign
const semver = require('semver')
debuglog('Verifying application bundle with codesign...')
await execFileAsync('codesign', [
'--verify',
'--deep'
]
.concat(
opts['strict-verify'] !== false &&
semver.gte(osRelease, '15.0.0') >= 0 // Strict flag since darwin 15.0.0 --> OS X 10.11.0 El Capitan
? ['--strict' +
(opts['strict-verify']
? '=' + opts['strict-verify'] // Array should be converted to a comma separated string
: '')]
: [],
['--verbose=2', opts.app]))
// Additionally test Gatekeeper acceptance for darwin platform
if (opts.platform === 'darwin' && opts['gatekeeper-assess'] !== false) {
debuglog('Verifying Gatekeeper acceptance for darwin platform...')
await execFileAsync('spctl', [
'--assess',
'--type', 'execute',
'--verbose',
'--ignore-cache',
'--no-cache',
opts.app
])
}
}
/**
* This function returns a promise codesigning only.
* @function
* @param {Object} opts - Options.
* @returns {Promise} Promise.
*/
function signApplicationAsync (opts) {
return walkAsync(getAppContentsPath(opts))
.then(async function (childPaths) {
/**
* Sort the child paths by how deep they are in the file tree. Some arcane apple
* logic expects the deeper files to be signed first otherwise strange errors get
* thrown our way
*/
childPaths = childPaths.sort((a, b) => {
const aDepth = a.split(path.sep).length
const bDepth = b.split(path.sep).length
return bDepth - aDepth
})
function ignoreFilePath (opts, filePath) {
if (opts.ignore) {
return opts.ignore.some(function (ignore) {
if (typeof ignore === 'function') {
return ignore(filePath)
}
return filePath.match(ignore)
})
}
return false
}
if (opts.binaries) childPaths = childPaths.concat(opts.binaries)
const args = [
'--sign', opts.identity.hash || opts.identity.name,
'--force'
]
if (opts.keychain) {
args.push('--keychain', opts.keychain)
}
if (opts.requirements) {
args.push('--requirements', opts.requirements)
}
if (opts.timestamp) {
args.push('--timestamp=' + opts.timestamp)
} else {
args.push('--timestamp')
}
if (opts['signature-size']) {
if (Number.isInteger(opts['signature-size']) && opts['signature-size'] > 0) {
args.push('--signature-size', opts['signature-size'])
} else {
debugwarn(`Invalid value provided for --signature-size (${opts['signature-size']}). Must be a positive integer.`)
}
}
let optionsArguments = []
if (opts['signature-flags']) {
if (Array.isArray(opts['signature-flags'])) {
optionsArguments = [...opts['signature-flags']]
} else {
const flags = opts['signature-flags'].split(',').map(function (flag) { return flag.trim() })
optionsArguments = [...flags]
}
}
if (opts.hardenedRuntime || opts['hardened-runtime'] || optionsArguments.includes('runtime')) {
// Hardened runtime since darwin 17.7.0 --> macOS 10.13.6
if (semver.gte(osRelease, '17.7.0') >= 0) {
optionsArguments.push('runtime')
} else {
// Remove runtime if passed in with --signature-flags
debuglog('Not enabling hardened runtime, current macOS version too low, requires 10.13.6 and higher')
optionsArguments = optionsArguments.filter(function (element, index) { return element !== 'runtime' })
}
}
if (opts['restrict']) {
optionsArguments.push('restrict')
debugwarn('This flag is to be deprecated, consider using --signature-flags=restrict instead')
}
if (optionsArguments.length) {
args.push('--options', [...new Set(optionsArguments)].join(','))
}
if (opts.entitlements) {
// Sign with entitlements
for (const filePath of childPaths) {
if (ignoreFilePath(opts, filePath)) {
debuglog('Skipped... ' + filePath)
continue
}
debuglog('Signing... ' + filePath)
let entitlementsFile = opts['entitlements-inherit']
if (filePath.includes('Library/LoginItems')) {
entitlementsFile = opts['entitlements-loginhelper']
}
await execFileAsync('codesign', args.concat('--entitlements', entitlementsFile, filePath))
}
debuglog('Signing... ' + opts.app)
await execFileAsync('codesign', args.concat('--entitlements', opts.entitlements, opts.app))
} else {
for (const filePath of childPaths) {
if (ignoreFilePath(opts, filePath)) {
debuglog('Skipped... ' + filePath)
continue
}
debuglog('Signing... ' + filePath)
await execFileAsync('codesign', args.concat(filePath))
}
debuglog('Signing... ' + opts.app)
await execFileAsync('codesign', args.concat(opts.app))
}
// Verify code sign
debuglog('Verifying...')
await verifySignApplicationAsync(opts)
debuglog('Verified.')
// Check entitlements if applicable
if (opts.entitlements) {
debuglog('Displaying entitlements...')
const result = await execFileAsync('codesign', [
'--display',
'--entitlements', ':-', // Write to standard output and strip off the blob header
opts.app
])
debuglog('Entitlements:', '\n', result)
}
})
}
/**
* This function returns a promise signing the application.
* @function
* @param {mixed} opts - Options.
* @returns {Promise} Promise.
*/
const signAsync = module.exports.signAsync = function (opts) {
return validateSignOptsAsync(opts)
.then(function () {
// Determine identity for signing
let promise
if (opts.identity) {
debuglog('`identity` passed in arguments.')
if (opts['identity-validation'] === false) {
if (!(opts.identity instanceof Identity)) {
opts.identity = new Identity(opts.identity)
}
return Promise.resolve()
}
promise = findIdentitiesAsync(opts, opts.identity)
} else {
debugwarn('No `identity` passed in arguments...')
if (opts.platform === 'mas') {
if (opts.type === 'distribution') {
debuglog('Finding `3rd Party Mac Developer Application` certificate for signing app distribution in the Mac App Store...')
promise = findIdentitiesAsync(opts, '3rd Party Mac Developer Application:')
} else {
debuglog('Finding `Mac Developer` certificate for signing app in development for the Mac App Store signing...')
promise = findIdentitiesAsync(opts, 'Mac Developer:')
}
} else {
debuglog('Finding `Developer ID Application` certificate for distribution outside the Mac App Store...')
promise = findIdentitiesAsync(opts, 'Developer ID Application:')
}
}
return promise
.then(function (identities) {
if (identities.length > 0) {
// Identity(/ies) found
if (identities.length > 1) {
debugwarn('Multiple identities found, will use the first discovered.')
} else {
debuglog('Found 1 identity.')
}
opts.identity = identities[0]
} else {
// No identity found
return Promise.reject(new Error('No identity found for signing.'))
}
})
})
.then(function () {
// Determine entitlements for code signing
let filePath
if (opts.platform === 'mas') {
// To sign apps for Mac App Store, an entitlements file is required, especially for app sandboxing (as well some other services).
// Fallback entitlements for sandboxing by default: Note this may cause troubles while running an signed app due to missing keys special to the project.
// Further reading: https://developer.apple.com/library/mac/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/EnablingAppSandbox.html
if (!opts.entitlements) {
filePath = path.join(__dirname, 'default.entitlements.mas.plist')
debugwarn('No `entitlements` passed in arguments:', '\n',
'* Sandbox entitlements are required for Mac App Store distribution, your codesign entitlements file is default to:', filePath)
opts.entitlements = filePath
}
if (!opts['entitlements-inherit']) {
filePath = path.join(__dirname, 'default.entitlements.mas.inherit.plist')
debugwarn('No `entitlements-inherit` passed in arguments:', '\n',
'* Sandbox entitlements file for enclosing app files is default to:', filePath)
opts['entitlements-inherit'] = filePath
}
} else {
// Not necessary to have entitlements for non Mac App Store distribution
if (!opts.entitlements) {
debugwarn('No `entitlements` passed in arguments:', '\n',
'* Provide `entitlements` to specify entitlements file for codesign.')
} else {
// If entitlements is provided as a flag, fallback to default
if (opts.entitlements === true) {
filePath = path.join(__dirname, 'default.entitlements.darwin.plist')
debugwarn('`entitlements` not specified in arguments:', '\n',
'* Provide `entitlements` to specify entitlements file for codesign.', '\n',
'* Sandbox entitlements file for enclosing app files is default to:', filePath)
opts.entitlements = filePath
}
if (!opts['entitlements-inherit']) {
filePath = path.join(__dirname, 'default.entitlements.darwin.inherit.plist')
debugwarn('No `entitlements-inherit` passed in arguments:', '\n',
'* Sandbox entitlements file for enclosing app files is default to:', filePath)
opts['entitlements-inherit'] = filePath
}
}
}
if (!opts['entitlements-loginhelper']) {
filePath = opts.entitlements
debugwarn('No `entitlements-loginhelper` passed in arguments:', '\n',
'* Sandbox entitlements file for login helper is default to:', filePath)
opts['entitlements-loginhelper'] = filePath
}
})
.then(async function () {
// Pre-sign operations
const preSignOperations = []
if (opts['pre-embed-provisioning-profile'] === false) {
debugwarn('Pre-sign operation disabled for provisioning profile embedding:', '\n',
'* Enable by setting `pre-embed-provisioning-profile` to `true`.')
} else {
debuglog('Pre-sign operation enabled for provisioning profile:', '\n',
'* Disable by setting `pre-embed-provisioning-profile` to `false`.')
preSignOperations.push(preEmbedProvisioningProfile)
}
if (opts['pre-auto-entitlements'] === false) {
debugwarn('Pre-sign operation disabled for entitlements automation.')
} else {
debuglog('Pre-sign operation enabled for entitlements automation with versions >= `1.1.1`:', '\n',
'* Disable by setting `pre-auto-entitlements` to `false`.')
if (opts.entitlements && (!opts.version || semver.gte(opts.version, '1.1.1') >= 0)) {
// Enable Mac App Store sandboxing without using temporary-exception, introduced in Electron v1.1.1. Relates to electron#5601
preSignOperations.push(preAutoEntitlements)
}
}
for (const preSignOperation of preSignOperations) {
await preSignOperation(opts)
}
})
.then(function () {
debuglog('Signing application...', '\n',
'> Application:', opts.app, '\n',
'> Platform:', opts.platform, '\n',
'> Entitlements:', opts.entitlements, '\n',
'> Child entitlements:', opts['entitlements-inherit'], '\n',
'> Additional binaries:', opts.binaries, '\n',
'> Identity:', opts.identity)
return signApplicationAsync(opts)
})
.then(function () {
// Post-sign operations
debuglog('Application signed.')
})
}
/**
* This function is a normal callback implementation.
* @function
* @param {Object} opts - Options.
* @param {RequestCallback} cb - Callback.
*/
module.exports.sign = function (opts, cb) {
signAsync(opts)
.then(function () {
debuglog('Application signed: ' + opts.app)
if (cb) cb()
})
.catch(function (err) {
debuglog('Sign failed:')
if (err.message) debuglog(err.message)
else if (err.stack) debuglog(err.stack)
else debuglog(err)
if (cb) cb(err)
})
}

View File

@ -0,0 +1,91 @@
/**
* @module util-entitlements
*/
'use strict'
const { executeAppBuilderAsJson, executeAppBuilderAndWriteJson } = require("../out/util/appBuilder")
const os = require('os')
const path = require('path')
const util = require('./util')
const debuglog = util.debuglog
const getAppContentsPath = util.getAppContentsPath
let tmpFileCounter = 0
/**
* This function returns a promise completing the entitlements automation: The process includes checking in `Info.plist` for `ElectronTeamID` or setting parsed value from identity, and checking in entitlements file for `com.apple.security.application-groups` or inserting new into array. A temporary entitlements file may be created to replace the input for any changes introduced.
* @function
* @param {Object} opts - Options.
* @returns {Promise} Promise.
*/
async function preAutoEntitlements(opts) {
// If entitlements file not provided, default will be used. Fixes #41
const appInfoPath = path.join(getAppContentsPath(opts), 'Info.plist')
debuglog('Automating entitlement app group...', '\n',
'> Info.plist:', appInfoPath, '\n',
'> Entitlements:', opts.entitlements)
const plistContent = await executeAppBuilderAsJson(["decode-plist", "-f", opts.entitlements, "-f", appInfoPath])
let entitlements = plistContent[0]
if (!entitlements['com.apple.security.app-sandbox']) {
// Only automate when app sandbox enabled by user
return
}
const appInfo = plistContent[1]
// Use ElectronTeamID in Info.plist if already specified
if (appInfo.ElectronTeamID) {
debuglog('`ElectronTeamID` found in `Info.plist`: ' + appInfo.ElectronTeamID)
} else {
// The team identifier in signing identity should not be trusted
if (opts['provisioning-profile']) {
appInfo.ElectronTeamID = opts['provisioning-profile'].message.Entitlements['com.apple.developer.team-identifier']
debuglog('`ElectronTeamID` not found in `Info.plist`, use parsed from provisioning profile: ' + appInfo.ElectronTeamID)
} else {
appInfo.ElectronTeamID = opts.identity.name.substring(opts.identity.name.indexOf('(') + 1, opts.identity.name.lastIndexOf(')'))
debuglog('`ElectronTeamID` not found in `Info.plist`, use parsed from signing identity: ' + appInfo.ElectronTeamID)
}
await executeAppBuilderAndWriteJson(["encode-plist"], {[appInfoPath]: appInfo})
debuglog('`Info.plist` updated:', '\n', '> Info.plist:', appInfoPath)
}
const appIdentifier = appInfo.ElectronTeamID + '.' + appInfo.CFBundleIdentifier
// Insert application identifier if not exists
if (entitlements['com.apple.application-identifier']) {
debuglog('`com.apple.application-identifier` found in entitlements file: ' + entitlements['com.apple.application-identifier'])
} else {
debuglog('`com.apple.application-identifier` not found in entitlements file, new inserted: ' + appIdentifier)
entitlements['com.apple.application-identifier'] = appIdentifier
}
// Insert developer team identifier if not exists
if (entitlements['com.apple.developer.team-identifier']) {
debuglog('`com.apple.developer.team-identifier` found in entitlements file: ' + entitlements['com.apple.developer.team-identifier'])
} else {
debuglog('`com.apple.developer.team-identifier` not found in entitlements file, new inserted: ' + appInfo.ElectronTeamID)
entitlements['com.apple.developer.team-identifier'] = appInfo.ElectronTeamID
}
// Init entitlements app group key to array if not exists
if (!entitlements['com.apple.security.application-groups']) {
entitlements['com.apple.security.application-groups'] = []
}
// Insert app group if not exists
if (Array.isArray(entitlements['com.apple.security.application-groups']) && entitlements['com.apple.security.application-groups'].indexOf(appIdentifier) === -1) {
debuglog('`com.apple.security.application-groups` not found in entitlements file, new inserted: ' + appIdentifier)
entitlements['com.apple.security.application-groups'].push(appIdentifier)
} else {
debuglog('`com.apple.security.application-groups` found in entitlements file: ' + appIdentifier)
}
// Create temporary entitlements file
const entitlementsPath = path.join(os.tmpdir(), `tmp-entitlements-${process.pid.toString(16)}-${(tmpFileCounter++).toString(16)}.plist`)
opts.entitlements = entitlementsPath
await executeAppBuilderAndWriteJson(["encode-plist"], {[entitlementsPath]: entitlements})
debuglog('Entitlements file updated:', '\n', '> Entitlements:', entitlementsPath)
}
module.exports.preAutoEntitlements = preAutoEntitlements

View File

@ -0,0 +1,55 @@
/**
* @module util-identities
*/
'use strict'
const util = require('./util')
const debuglog = util.debuglog
const flatList = util.flatList
const execFileAsync = util.execFileAsync
/**
* @constructor
* @param {string} name - Name of the signing identity.
* @param {String} hash - SHA-1 hash of the identity.
*/
var Identity = module.exports.Identity = function (name, hash) {
this.name = name
this.hash = hash
}
/**
* This function returns a promise checking the indentity proposed and updates the identity option to a exact finding from results.
* @function
* @param {Object} opts - Options.
* @param {string} identity - The proposed identity.
* @returns {Promise} Promise.
*/
module.exports.findIdentitiesAsync = function (opts, identity) {
// Only to look for valid identities, excluding those flagged with
// CSSMERR_TP_CERT_EXPIRED or CSSMERR_TP_NOT_TRUSTED. Fixes #9
var args = [
'find-identity',
'-v'
]
if (opts.keychain) {
args.push(opts.keychain)
}
return execFileAsync('security', args)
.then(function (result) {
return result.split('\n').map(function (line) {
if (line.indexOf(identity) >= 0) {
var identityFound = line.substring(line.indexOf('"') + 1, line.lastIndexOf('"'))
var identityHashFound = line.substring(line.indexOf(')') + 2, line.indexOf('"') - 1)
debuglog('Identity:', '\n',
'> Name:', identityFound, '\n',
'> Hash:', identityHashFound)
return new Identity(identityFound, identityHashFound)
}
})
})
.then(flatList)
}

View File

@ -0,0 +1,178 @@
/**
* @module util-provisioning-profiles
*/
'use strict'
const path = require('path')
const fs = require("fs-extra")
const os = require('os')
const { executeAppBuilderAsJson } = require("../out/util/appBuilder")
const util = require('./util')
const debuglog = util.debuglog
const debugwarn = util.debugwarn
const getAppContentsPath = util.getAppContentsPath
const copyFileAsync = util.copyFileAsync
const execFileAsync = util.execFileAsync
/**
* @constructor
* @param {string} filePath - Path to provisioning profile.
* @param {Object} message - Decoded message in provisioning profile.
*/
let ProvisioningProfile = module.exports.ProvisioningProfile = function (filePath, message) {
this.filePath = filePath
this.message = message
}
Object.defineProperty(ProvisioningProfile.prototype, 'name', {
get: function () {
return this.message['Name']
}
})
Object.defineProperty(ProvisioningProfile.prototype, 'platforms', {
get: function () {
if ('ProvisionsAllDevices' in this.message) return ['darwin'] // Developer ID
else if (this.type === 'distribution') return ['mas'] // Mac App Store
else return ['darwin', 'mas'] // Mac App Development
}
})
Object.defineProperty(ProvisioningProfile.prototype, 'type', {
get: function () {
if ('ProvisionedDevices' in this.message) return 'development' // Mac App Development
else return 'distribution' // Developer ID or Mac App Store
}
})
/**
* Returns a promise resolving to a ProvisioningProfile instance based on file.
* @function
* @param {string} filePath - Path to provisioning profile.
* @param {string} keychain - Keychain to use when unlocking provisioning profile.
* @returns {Promise} Promise.
*/
function getProvisioningProfileAsync(filePath, keychain) {
const securityArgs = [
'cms',
'-D', // Decode a CMS message
'-i', filePath // Use infile as source of data
]
if (keychain != null) {
securityArgs.push('-k', keychain)
}
return util.execFileAsync('security', securityArgs)
.then(async function (result) {
// make filename unique so it doesn't create issues with parallel method calls
const timestamp = process.hrtime.bigint
? process.hrtime.bigint().toString()
: process.hrtime().join('')
// todo read directly
const tempFile = path.join(os.tmpdir(), `${require('crypto').createHash('sha1').update(filePath).update(timestamp).digest('hex')}.plist`)
await fs.outputFile(tempFile, result)
const plistContent = await executeAppBuilderAsJson(["decode-plist", "-f", tempFile])
await fs.unlink(tempFile)
const provisioningProfile = new ProvisioningProfile(filePath, plistContent[0])
debuglog('Provisioning profile:', '\n',
'> Name:', provisioningProfile.name, '\n',
'> Platforms:', provisioningProfile.platforms, '\n',
'> Type:', provisioningProfile.type, '\n',
'> Path:', provisioningProfile.filePath, '\n',
'> Message:', provisioningProfile.message)
return provisioningProfile
})
}
/**
* Returns a promise resolving to a list of suitable provisioning profile within the current working directory.
* @function
* @param {Object} opts - Options.
* @returns {Promise} Promise.
*/
async function findProvisioningProfilesAsync(opts) {
const dirPath = process.cwd()
const dirContent = await Promise.all((await fs.readdir(dirPath))
.filter(it => it.endsWith(".provisionprofile"))
.map(async function (name) {
const filePath = path.join(dirPath, name)
const stat = await fs.lstat(filePath)
return stat.isFile() ? filePath : undefined
}))
return util.flatList(await Promise.all(util.flatList(dirContent).map(filePath => {
return getProvisioningProfileAsync(filePath)
.then((provisioningProfile) => {
if (provisioningProfile.platforms.indexOf(opts.platform) >= 0 && provisioningProfile.type === opts.type) {
return provisioningProfile
}
debugwarn('Provisioning profile above ignored, not for ' + opts.platform + ' ' + opts.type + '.')
return undefined
})
})))
}
/**
* Returns a promise embedding the provisioning profile in the app Contents folder.
* @function
* @param {Object} opts - Options.
* @returns {Promise} Promise.
*/
module.exports.preEmbedProvisioningProfile = function (opts) {
async function embedProvisioningProfile () {
if (!opts['provisioning-profile']) {
return
}
debuglog('Looking for existing provisioning profile...')
let embeddedFilePath = path.join(getAppContentsPath(opts), 'embedded.provisionprofile')
try {
await fs.lstat(embeddedFilePath)
debuglog('Found embedded provisioning profile:', '\n',
'* Please manually remove the existing file if not wanted.', '\n',
'* Current file at:', embeddedFilePath)
} catch (err) {
if (err.code === 'ENOENT') {
// File does not exist
debuglog('Embedding provisioning profile...')
return copyFileAsync(opts['provisioning-profile'].filePath, embeddedFilePath)
} else throw err
}
}
if (opts['provisioning-profile']) {
// User input provisioning profile
debuglog('`provisioning-profile` passed in arguments.')
if (opts['provisioning-profile'] instanceof ProvisioningProfile) {
return embedProvisioningProfile()
} else {
return getProvisioningProfileAsync(opts['provisioning-profile'], opts['keychain'])
.then(function (provisioningProfile) {
opts['provisioning-profile'] = provisioningProfile
})
.then(embedProvisioningProfile)
}
} else {
// Discover provisioning profile
debuglog('No `provisioning-profile` passed in arguments, will find in current working directory and in user library...')
return findProvisioningProfilesAsync(opts)
.then(function (provisioningProfiles) {
if (provisioningProfiles.length > 0) {
// Provisioning profile(s) found
if (provisioningProfiles.length > 1) {
debuglog('Multiple provisioning profiles found, will use the first discovered.')
} else {
debuglog('Found 1 provisioning profile.')
}
opts['provisioning-profile'] = provisioningProfiles[0]
} else {
// No provisioning profile found
debuglog('No provisioning profile found, will not embed profile in app contents.')
}
})
.then(embedProvisioningProfile)
}
}

View File

@ -0,0 +1,236 @@
/**
* @module util
*/
'use strict'
const child = require('child_process')
const fs = require('fs-extra')
const path = require('path')
const debug = require('debug')
/**
* This callback is used across signing and flattening.
* @callback RequestCallback
* @param {?Error} err
*/
/** @function */
const debuglog = module.exports.debuglog = debug('electron-osx-sign')
debuglog.log = console.log.bind(console)
/** @function */
const debugwarn = module.exports.debugwarn = debug('electron-osx-sign:warn')
debugwarn.log = console.warn.bind(console)
/** @function */
const removePassword = function (input) {
return input.replace(/(-P |pass:|\/p|-pass )([^ ]+)/, function (match, p1, p2) {
return `${p1}***`
})
}
/** @function */
module.exports.execFileAsync = function (file, args, options) {
if (debuglog.enabled) {
debuglog('Executing...', file, args && Array.isArray(args) ? removePassword(args.join(' ')) : '')
}
return new Promise(function (resolve, reject) {
child.execFile(file, args, options, function (err, stdout, stderr) {
if (err) {
debuglog('Error executing file:', '\n',
'> Stdout:', stdout, '\n',
'> Stderr:', stderr)
reject(err)
return
}
resolve(stdout)
})
})
}
/**
* This function returns a flattened list of elements from an array of lists.
* @function
* @param {*} list - List.
* @returns Flattened list.
*/
module.exports.flatList = function (list) {
function populateResult(list) {
if (!Array.isArray(list)) {
result.push(list)
} else if (list.length > 0) {
for (let item of list) if (item) populateResult(item)
}
}
let result = []
populateResult(list)
return result
}
/**
* This function returns the path to app contents.
* @function
* @param {Object} opts - Options.
* @returns {string} App contents path.
*/
var getAppContentsPath = module.exports.getAppContentsPath = function (opts) {
return path.join(opts.app, 'Contents')
}
/**
* This function returns the path to app frameworks within contents.
* @function
* @param {Object} opts - Options.
* @returns {string} App frameworks path.
*/
var getAppFrameworksPath = module.exports.getAppFrameworksPath = function (opts) {
return path.join(getAppContentsPath(opts), 'Frameworks')
}
/**
* This function returns a promise copying a file from the source to the target.
* @function
* @param {string} source - Source path.
* @param {string} target - Target path.
* @returns {Promise} Promise.
*/
module.exports.copyFileAsync = function (source, target) {
debuglog('Copying file...', '\n',
'> Source:', source, '\n',
'> Target:', target)
return new Promise(function (resolve, reject) {
var readStream = fs.createReadStream(source)
readStream.on('error', reject)
var writeStream = fs.createWriteStream(target)
writeStream.on('error', reject)
writeStream.on('close', resolve)
readStream.pipe(writeStream)
})
}
/**
* This function returns a promise with platform resolved.
* @function
* @param {Object} opts - Options.
* @returns {Promise} Promise resolving platform.
*/
var detectElectronPlatformAsync = module.exports.detectElectronPlatformAsync = function (opts) {
return new Promise(function (resolve) {
var appFrameworksPath = getAppFrameworksPath(opts)
// The presence of Squirrel.framework identifies a Mac App Store build as used in https://github.com/atom/electron/blob/master/docs/tutorial/mac-app-store-submission-guide.md
return fs.lstat(path.join(appFrameworksPath, 'Squirrel.framework'))
.then(function () {
resolve('darwin')
})
.catch(function () {
resolve('mas')
})
})
}
const isBinaryFile = require("isbinaryfile").isBinaryFile;
/**
* This function returns a promise resolving the file path if file binary.
* @function
* @param {string} filePath - Path to file.
* @returns {Promise} Promise resolving file path or undefined.
*/
const getFilePathIfBinaryAsync = module.exports.getFilePathIfBinaryAsync = function (filePath) {
return isBinaryFile(filePath)
.then(function (isBinary) {
return isBinary ? filePath : undefined
})
}
/**
* This function returns a promise validating opts.app, the application to be signed or flattened.
* @function
* @param {Object} opts - Options.
* @returns {Promise} Promise.
*/
module.exports.validateOptsAppAsync = async function (opts) {
if (!opts.app) {
throw new Error('Path to aplication must be specified.')
}
if (path.extname(opts.app) !== '.app') {
throw new Error('Extension of application must be `.app`.')
}
await fs.lstat(opts.app)
}
/**
* This function returns a promise validating opts.platform, the platform of Electron build. It allows auto-discovery if no opts.platform is specified.
* @function
* @param {Object} opts - Options.
* @returns {Promise} Promise.
*/
module.exports.validateOptsPlatformAsync = function (opts) {
if (opts.platform) {
if (opts.platform === 'mas' || opts.platform === 'darwin') {
return Promise.resolve()
} else {
debugwarn('`platform` passed in arguments not supported, checking Electron platform...')
}
} else {
debugwarn('No `platform` passed in arguments, checking Electron platform...')
}
return detectElectronPlatformAsync(opts)
.then(function (platform) {
opts.platform = platform
})
}
/**
* This function returns a promise resolving all child paths within the directory specified.
* @function
* @param {string} dirPath - Path to directory.
* @returns {Promise} Promise resolving child paths needing signing in order.
*/
module.exports.walkAsync = async function (dirPath) {
debuglog('Walking... ' + dirPath)
async function _walkAsync(dirPath) {
const names = await fs.readdir(dirPath)
return await Promise.all(names.map(async (name) => {
let filePath = path.join(dirPath, name)
const stat = await fs.lstat(filePath)
if (stat.isFile()) {
switch (path.extname(filePath)) {
case '': // Binary
if (path.basename(filePath)[0] !== '.') {
return getFilePathIfBinaryAsync(filePath)
} // Else reject hidden file
break
case '.dylib': // Dynamic library
case '.node': // Native node addon
return filePath
case '.cstemp': // Temporary file generated from past codesign
debuglog('Removing... ' + filePath)
await fs.unlink(filePath)
return
default:
if (path.extname(filePath).indexOf(' ') >= 0) {
// Still consider the file as binary if extension seems invalid
return getFilePathIfBinaryAsync(filePath)
}
}
} else if (stat.isDirectory() && !stat.isSymbolicLink()) {
const result = await _walkAsync(filePath)
switch (path.extname(filePath)) {
case '.app': // Application
case '.framework': // Framework
result.push(filePath)
}
return result
}
}))
}
return module.exports.flatList(await _walkAsync(dirPath))
}