Merge pull request #19109 from atom/use-random-socketname

Add authentication/encryption when using pipes for IPC
This commit is contained in:
Rafael Oleza
2019-04-05 21:34:38 +02:00
committed by GitHub
2 changed files with 159 additions and 38 deletions

View File

@@ -939,6 +939,49 @@ describe('AtomApplication', function () {
})
}
it('reuses the main process between invocations', async () => {
const tempDirPath1 = makeTempDir()
const tempDirPath2 = makeTempDir()
const options = {
pathsToOpen: [tempDirPath1]
}
// Open the main application
const originalApplication = buildAtomApplication(options)
await originalApplication.initialize(options)
// Wait until the first window gets opened
await conditionPromise(
() => originalApplication.getAllWindows().length === 1
)
// Open another instance of the application on a different path.
AtomApplication.open({
resourcePath: ATOM_RESOURCE_PATH,
atomHomeDirPath: process.env.ATOM_HOME,
pathsToOpen: [tempDirPath2]
})
await conditionPromise(
() => originalApplication.getAllWindows().length === 2
)
// Check that the original application now has the two opened windows.
assert.notEqual(
originalApplication.getAllWindows().find(
window => window.loadSettings.initialPaths[0] === tempDirPath1
),
undefined
)
assert.notEqual(
originalApplication.getAllWindows().find(
window => window.loadSettings.initialPaths[0] === tempDirPath2
),
undefined
)
})
function buildAtomApplication (params = {}) {
const atomApplication = new AtomApplication(
Object.assign(

View File

@@ -34,6 +34,81 @@ const getDefaultPath = () => {
}
}
const getSocketSecretPath = (atomVersion) => {
const {username} = os.userInfo()
const atomHome = path.resolve(process.env.ATOM_HOME)
return path.join(atomHome, `.atom-socket-secret-${username}-${atomVersion}`)
}
const getSocketPath = (socketSecret) => {
if (!socketSecret) {
return null
}
// Hash the secret to create the socket name to not expose it.
const socketName = crypto
.createHmac('sha256', socketSecret)
.update('socketName')
.digest('hex')
.substr(0, 12)
if (process.platform === 'win32') {
return `\\\\.\\pipe\\atom-${socketName}-sock`
} else {
return path.join(os.tmpdir(), `atom-${socketName}.sock`)
}
}
const getExistingSocketSecret = (atomVersion) => {
const socketSecretPath = getSocketSecretPath(atomVersion)
if (!fs.existsSync(socketSecretPath)) {
return null
}
return fs.readFileSync(socketSecretPath, 'utf8')
}
const createSocketSecret = (atomVersion) => {
const socketSecret = crypto.randomBytes(16).toString('hex')
fs.writeFileSync(getSocketSecretPath(atomVersion), socketSecret, {encoding: 'utf8', mode: 0o600})
return socketSecret
}
const encryptOptions = (options, secret) => {
const message = JSON.stringify(options)
const initVector = crypto.randomBytes(16)
const cipher = crypto.createCipheriv('aes-256-gcm', secret, initVector)
let content = cipher.update(message, 'utf8', 'hex')
content += cipher.final('hex')
const authTag = cipher.getAuthTag().toString('hex')
return JSON.stringify({
authTag,
content,
initVector: initVector.toString('hex')
})
}
const decryptOptions = (optionsMessage, secret) => {
const {authTag, content, initVector} = JSON.parse(optionsMessage)
const decipher = crypto.createDecipheriv('aes-256-gcm', secret, Buffer.from(initVector, 'hex'))
decipher.setAuthTag(Buffer.from(authTag, 'hex'))
let message = decipher.update(content, 'hex', 'utf8')
message += decipher.final('utf8')
return JSON.parse(message)
}
// The application's singleton class.
//
// It's the entry point into the Atom application and maintains the global state
@@ -43,50 +118,23 @@ module.exports =
class AtomApplication extends EventEmitter {
// Public: The entry point into the Atom application.
static open (options) {
if (!options.socketPath) {
const {username} = os.userInfo()
// Lowercasing the ATOM_HOME to make sure that we don't get multiple sockets
// on case-insensitive filesystems due to arbitrary case differences in paths.
const atomHomeUnique = path.resolve(process.env.ATOM_HOME).toLowerCase()
const hash = crypto
.createHash('sha1')
.update(options.version)
.update('|')
.update(process.arch)
.update('|')
.update(username || '')
.update('|')
.update(atomHomeUnique)
// We only keep the first 12 characters of the hash as not to have excessively long
// socket file. Note that macOS/BSD limit the length of socket file paths (see #15081).
// The replace calls convert the digest into "URL and Filename Safe" encoding (see RFC 4648).
const atomInstanceDigest = hash
.digest('base64')
.substring(0, 12)
.replace(/\+/g, '-')
.replace(/\//g, '_')
if (process.platform === 'win32') {
options.socketPath = `\\\\.\\pipe\\atom-${atomInstanceDigest}-sock`
} else {
options.socketPath = path.join(os.tmpdir(), `atom-${atomInstanceDigest}.sock`)
}
}
const socketSecret = getExistingSocketSecret(options.version)
const socketPath = getSocketPath(socketSecret)
// FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely
// take a few seconds to trigger 'error' event, it could be a bug of node
// or electron, before it's fixed we check the existence of socketPath to
// speedup startup.
if ((process.platform !== 'win32' && !fs.existsSync(options.socketPath)) ||
options.test || options.benchmark || options.benchmarkTest) {
if (
!socketPath || options.test || options.benchmark || options.benchmarkTest ||
(process.platform !== 'win32' && !fs.existsSync(socketPath))
) {
new AtomApplication(options).initialize(options)
return
}
const client = net.connect({path: options.socketPath}, () => {
client.write(JSON.stringify(options), () => {
const client = net.connect({path: socketPath}, () => {
client.write(encryptOptions(options, socketSecret), () => {
client.end()
app.quit()
})
@@ -110,11 +158,14 @@ class AtomApplication extends EventEmitter {
this.version = options.version
this.devMode = options.devMode
this.safeMode = options.safeMode
this.socketPath = options.socketPath
this.logFile = options.logFile
this.userDataDir = options.userDataDir
this._killProcess = options.killProcess || process.kill.bind(process)
if (options.test || options.benchmark || options.benchmarkTest) this.socketPath = null
if (!options.test && !options.benchmark && !options.benchmarkTest) {
this.socketSecret = createSocketSecret(this.version)
this.socketPath = getSocketPath(this.socketSecret)
}
this.waitSessionsByWindow = new Map()
this.windowStack = new WindowStack()
@@ -351,7 +402,15 @@ class AtomApplication extends EventEmitter {
const server = net.createServer(connection => {
let data = ''
connection.on('data', chunk => { data += chunk })
connection.on('end', () => this.openWithOptions(JSON.parse(data)))
connection.on('end', () => {
try {
const options = decryptOptions(data, this.socketSecret)
this.openWithOptions(options)
} catch (e) {
// Error while parsing/decrypting the options passed by the client.
// We cannot trust the client, aborting.
}
})
})
server.listen(this.socketPath)
@@ -373,6 +432,24 @@ class AtomApplication extends EventEmitter {
}
}
deleteSocketSecretFile () {
if (!this.socketSecret) {
return
}
const socketSecretPath = getSocketSecretPath(this.version)
if (fs.existsSync(socketSecretPath)) {
try {
fs.unlinkSync(socketSecretPath)
} catch (error) {
// Ignore ENOENT errors in case the file was deleted between the exists
// check and the call to unlink sync.
if (error.code !== 'ENOENT') throw error
}
}
}
// Registers basic application commands, non-idempotent.
handleEvents () {
const getLoadSettings = () => {
@@ -480,6 +557,7 @@ class AtomApplication extends EventEmitter {
this.disposable.add(ipcHelpers.on(app, 'will-quit', () => {
this.killAllProcesses()
this.deleteSocketFile()
this.deleteSocketSecretFile()
}))
this.disposable.add(ipcHelpers.on(app, 'open-file', (event, pathToOpen) => {